diff --git a/Assets/Tests/Editor/MouseUiPointerTargetResolverTests.cs b/Assets/Tests/Editor/MouseUiPointerTargetResolverTests.cs
new file mode 100644
index 0000000000..00e80320cc
--- /dev/null
+++ b/Assets/Tests/Editor/MouseUiPointerTargetResolverTests.cs
@@ -0,0 +1,350 @@
+#nullable enable
+using System.Collections.Generic;
+using NUnit.Framework;
+using UnityEngine;
+using UnityEngine.EventSystems;
+
+using io.github.hatayama.UnityCliLoop.FirstPartyTools;
+using io.github.hatayama.UnityCliLoop.Runtime;
+
+namespace io.github.hatayama.UnityCliLoop.Tests.Editor
+{
+ ///
+ /// Characterizes pointer target resolution before it is extracted from the mouse UI use case.
+ ///
+ [TestFixture]
+ public sealed class MouseUiPointerTargetResolverTests
+ {
+ private readonly List createdObjects = new();
+
+ [TearDown]
+ public void TearDown()
+ {
+ for (int index = createdObjects.Count - 1; index >= 0; index--)
+ {
+ GameObject gameObject = createdObjects[index];
+ if (gameObject != null)
+ {
+ Object.DestroyImmediate(gameObject);
+ }
+ }
+
+ createdObjects.Clear();
+ }
+
+ ///
+ /// Verifies pointer press data preserves position and runtime mouse button mapping.
+ ///
+ [Test]
+ public void CreatePointerPressData_WithRightButton_MapsPressState()
+ {
+ EventSystem eventSystem = CreateEventSystem();
+ Vector2 screenPosition = new(12.5f, 34.5f);
+
+ PointerEventData pointerData =
+ MouseUiPointerTargetResolver.CreatePointerPressData(eventSystem, screenPosition, MouseButton.Right);
+
+ Assert.That(pointerData.position, Is.EqualTo(screenPosition));
+ Assert.That(pointerData.pressPosition, Is.EqualTo(screenPosition));
+ Assert.That(pointerData.button, Is.EqualTo(PointerEventData.InputButton.Right));
+ }
+
+ ///
+ /// Verifies an exact unique hierarchy path resolves to its active GameObject.
+ ///
+ [Test]
+ public void TryResolveGameObjectPath_WithUniquePath_ReturnsTarget()
+ {
+ GameObject root = CreateGameObject("MouseUiTargetResolverTests_UniqueRoot");
+ GameObject expectedTarget = CreateGameObject("Target", root.transform);
+ string targetPath = GameObjectPathUtility.GetFullPath(expectedTarget);
+ Vector2 inputPosition = new(10f, 20f);
+
+ bool resolved = MouseUiPointerTargetResolver.TryResolveGameObjectPath(
+ targetPath,
+ "TargetPath",
+ MouseAction.Click,
+ inputPosition,
+ out GameObject? target,
+ out SimulateMouseUiResponse? failureResponse);
+
+ Assert.That(resolved, Is.True);
+ Assert.That(target, Is.SameAs(expectedTarget));
+ Assert.That(failureResponse, Is.Null);
+ }
+
+ ///
+ /// Verifies a missing hierarchy path returns the exact wire-visible failure response.
+ ///
+ [Test]
+ public void TryResolveGameObjectPath_WithMissingPath_ReturnsNotFoundFailure()
+ {
+ string targetPath = "MouseUiTargetResolverTests_MissingRoot/Target";
+ Vector2 inputPosition = new(10f, 20f);
+
+ bool resolved = MouseUiPointerTargetResolver.TryResolveGameObjectPath(
+ targetPath,
+ "TargetPath",
+ MouseAction.Click,
+ inputPosition,
+ out GameObject? target,
+ out SimulateMouseUiResponse? failureResponse);
+
+ Assert.That(resolved, Is.False);
+ Assert.That(target, Is.Null);
+ Assert.That(failureResponse, Is.Not.Null);
+ Assert.That(failureResponse!.Success, Is.False);
+ Assert.That(failureResponse.Message, Is.EqualTo($"TargetPath '{targetPath}' was not found."));
+ Assert.That(failureResponse.Action, Is.EqualTo(MouseAction.Click.ToString()));
+ Assert.That(failureResponse.PositionX, Is.EqualTo(inputPosition.x));
+ Assert.That(failureResponse.PositionY, Is.EqualTo(inputPosition.y));
+ }
+
+ ///
+ /// Verifies duplicate hierarchy paths return the exact ambiguity count and no target.
+ ///
+ [Test]
+ public void TryResolveGameObjectPath_WithDuplicatePath_ReturnsAmbiguousFailure()
+ {
+ GameObject firstRoot = CreateGameObject("MouseUiTargetResolverTests_DuplicateRoot");
+ GameObject firstTarget = CreateGameObject("Target", firstRoot.transform);
+ GameObject secondRoot = CreateGameObject("MouseUiTargetResolverTests_DuplicateRoot");
+ CreateGameObject("Target", secondRoot.transform);
+ string targetPath = GameObjectPathUtility.GetFullPath(firstTarget);
+ Vector2 inputPosition = new(10f, 20f);
+
+ bool resolved = MouseUiPointerTargetResolver.TryResolveGameObjectPath(
+ targetPath,
+ "TargetPath",
+ MouseAction.Click,
+ inputPosition,
+ out GameObject? target,
+ out SimulateMouseUiResponse? failureResponse);
+
+ Assert.That(resolved, Is.False);
+ Assert.That(target, Is.Null);
+ Assert.That(failureResponse, Is.Not.Null);
+ Assert.That(
+ failureResponse!.Message,
+ Is.EqualTo(
+ $"TargetPath '{targetPath}' matched 2 active GameObjects. Use a unique hierarchy path."));
+ }
+
+ ///
+ /// Verifies an empty drop path succeeds without resolving an explicit target.
+ ///
+ [Test]
+ public void TryResolveDropTargetPath_WithoutPath_ReturnsNoTarget()
+ {
+ MouseUiSimulationCommand command = CreateCommand(new SimulateMouseUiSchema
+ {
+ Action = UnityCliLoopMouseUiAction.Drag,
+ DropTargetPath = " "
+ });
+
+ bool resolved = MouseUiPointerTargetResolver.TryResolveDropTargetPath(
+ command,
+ MouseAction.Drag,
+ new Vector2(10f, 20f),
+ out GameObject? dropTarget,
+ out SimulateMouseUiResponse? failureResponse);
+
+ Assert.That(resolved, Is.True);
+ Assert.That(dropTarget, Is.Null);
+ Assert.That(failureResponse, Is.Null);
+ }
+
+ ///
+ /// Verifies an explicit drop target without a handler returns the exact failure response.
+ ///
+ [Test]
+ public void TryResolveDropTargetPath_WithoutDropHandler_ReturnsFailure()
+ {
+ GameObject root = CreateGameObject("MouseUiTargetResolverTests_DropRoot");
+ GameObject target = CreateGameObject("DropTarget", root.transform);
+ string targetPath = GameObjectPathUtility.GetFullPath(target);
+ MouseUiSimulationCommand command = CreateCommand(new SimulateMouseUiSchema
+ {
+ Action = UnityCliLoopMouseUiAction.Drag,
+ DropTargetPath = targetPath
+ });
+ Vector2 inputPosition = new(10f, 20f);
+
+ bool resolved = MouseUiPointerTargetResolver.TryResolveDropTargetPath(
+ command,
+ MouseAction.Drag,
+ inputPosition,
+ out GameObject? dropTarget,
+ out SimulateMouseUiResponse? failureResponse);
+
+ Assert.That(resolved, Is.False);
+ Assert.That(dropTarget, Is.Null);
+ Assert.That(failureResponse, Is.Not.Null);
+ Assert.That(
+ failureResponse!.Message,
+ Is.EqualTo($"DropTargetPath '{targetPath}' has no drop handler."));
+ Assert.That(failureResponse.Action, Is.EqualTo(MouseAction.Drag.ToString()));
+ Assert.That(failureResponse.PositionX, Is.EqualTo(inputPosition.x));
+ Assert.That(failureResponse.PositionY, Is.EqualTo(inputPosition.y));
+ }
+
+ ///
+ /// Verifies an explicit drop target with a hierarchy handler returns its raw target.
+ ///
+ [Test]
+ public void TryResolveDropTargetPath_WithDropHandler_ReturnsRawTarget()
+ {
+ GameObject root = CreateGameObject("MouseUiTargetResolverTests_DropHandlerRoot");
+ root.AddComponent();
+ GameObject expectedTarget = CreateGameObject("DropTarget", root.transform);
+ string targetPath = GameObjectPathUtility.GetFullPath(expectedTarget);
+ MouseUiSimulationCommand command = CreateCommand(new SimulateMouseUiSchema
+ {
+ Action = UnityCliLoopMouseUiAction.Drag,
+ DropTargetPath = targetPath
+ });
+
+ bool resolved = MouseUiPointerTargetResolver.TryResolveDropTargetPath(
+ command,
+ MouseAction.Drag,
+ new Vector2(10f, 20f),
+ out GameObject? dropTarget,
+ out SimulateMouseUiResponse? failureResponse);
+
+ Assert.That(resolved, Is.True);
+ Assert.That(dropTarget, Is.SameAs(expectedTarget));
+ Assert.That(failureResponse, Is.Null);
+ }
+
+ ///
+ /// Verifies bypass resolution preserves raw and hierarchy handlers in pointer state and result DTO.
+ ///
+ [Test]
+ public void ResolvePressablePointerTargets_WithBypassPath_MapsHierarchyHandlers()
+ {
+ EventSystem eventSystem = CreateEventSystem();
+ GameObject handlerTarget = CreateGameObject("MouseUiTargetResolverTests_PressHandler");
+ handlerTarget.AddComponent();
+ GameObject rawTarget = CreateGameObject("RawTarget", handlerTarget.transform);
+ string targetPath = GameObjectPathUtility.GetFullPath(rawTarget);
+ MouseUiSimulationCommand command = CreateCommand(new SimulateMouseUiSchema
+ {
+ Action = UnityCliLoopMouseUiAction.Click,
+ BypassRaycast = true,
+ TargetPath = targetPath
+ });
+ Vector2 position = new(10f, 20f);
+ PointerEventData pointerData =
+ MouseUiPointerTargetResolver.CreatePointerPressData(eventSystem, position, MouseButton.Left);
+
+ ResolvedPointerTargets resolvedTargets =
+ MouseUiPointerTargetResolver.ResolvePressablePointerTargets(
+ command,
+ eventSystem,
+ position,
+ position,
+ pointerData,
+ MouseAction.Click);
+
+ Assert.That(resolvedTargets.RawTarget, Is.SameAs(rawTarget));
+ Assert.That(resolvedTargets.PressTarget, Is.SameAs(handlerTarget));
+ Assert.That(resolvedTargets.ClickTarget, Is.SameAs(handlerTarget));
+ Assert.That(resolvedTargets.Target, Is.SameAs(handlerTarget));
+ Assert.That(resolvedTargets.FailureResponse, Is.Null);
+ Assert.That(pointerData.pointerCurrentRaycast.gameObject, Is.SameAs(rawTarget));
+ Assert.That(pointerData.pointerPressRaycast.gameObject, Is.SameAs(rawTarget));
+ Assert.That(pointerData.pointerPress, Is.SameAs(handlerTarget));
+ Assert.That(pointerData.rawPointerPress, Is.SameAs(rawTarget));
+ }
+
+ ///
+ /// Verifies non-bypass resolution returns an empty DTO when no raycaster reports a hit.
+ ///
+ [Test]
+ public void ResolvePressablePointerTargets_WithoutRaycastHit_ReturnsEmpty()
+ {
+ EventSystem eventSystem = CreateEventSystem();
+ MouseUiSimulationCommand command = CreateCommand(new SimulateMouseUiSchema
+ {
+ Action = UnityCliLoopMouseUiAction.Click
+ });
+ Vector2 position = new(10f, 20f);
+ PointerEventData pointerData =
+ MouseUiPointerTargetResolver.CreatePointerPressData(eventSystem, position, MouseButton.Left);
+
+ ResolvedPointerTargets resolvedTargets =
+ MouseUiPointerTargetResolver.ResolvePressablePointerTargets(
+ command,
+ eventSystem,
+ position,
+ position,
+ pointerData,
+ MouseAction.Click);
+
+ Assert.That(resolvedTargets.RawTarget, Is.Null);
+ Assert.That(resolvedTargets.PressTarget, Is.Null);
+ Assert.That(resolvedTargets.ClickTarget, Is.Null);
+ Assert.That(resolvedTargets.Target, Is.Null);
+ Assert.That(resolvedTargets.FailureResponse, Is.Null);
+ }
+
+ ///
+ /// Verifies direct raycast construction carries the supplied GameObject.
+ ///
+ [Test]
+ public void CreateDirectRaycastResult_WithTarget_MapsGameObject()
+ {
+ GameObject target = CreateGameObject("MouseUiTargetResolverTests_DirectRaycastTarget");
+
+ RaycastResult result = MouseUiPointerTargetResolver.CreateDirectRaycastResult(target);
+
+ Assert.That(result.gameObject, Is.SameAs(target));
+ }
+
+ private EventSystem CreateEventSystem()
+ {
+ GameObject gameObject = CreateGameObject("MouseUiTargetResolverTests_EventSystem");
+ return gameObject.AddComponent();
+ }
+
+ private GameObject CreateGameObject(string name, Transform? parent = null)
+ {
+ GameObject gameObject = new(name);
+ createdObjects.Add(gameObject);
+ if (parent != null)
+ {
+ gameObject.transform.SetParent(parent);
+ }
+
+ return gameObject;
+ }
+
+ private static MouseUiSimulationCommand CreateCommand(SimulateMouseUiSchema schema)
+ {
+ (MouseUiSimulationCommand? command, string? errorMessage) =
+ MouseUiSimulationCommand.TryFromSchema(schema);
+ Assert.That(errorMessage, Is.Null);
+ Assert.That(command, Is.Not.Null);
+ return command!;
+ }
+ }
+
+ internal sealed class MouseUiPointerTargetResolverTestHandler :
+ MonoBehaviour,
+ IPointerDownHandler,
+ IPointerClickHandler,
+ IDropHandler
+ {
+ public void OnPointerDown(PointerEventData eventData)
+ {
+ }
+
+ public void OnPointerClick(PointerEventData eventData)
+ {
+ }
+
+ public void OnDrop(PointerEventData eventData)
+ {
+ }
+ }
+}
diff --git a/Assets/Tests/Editor/MouseUiPointerTargetResolverTests.cs.meta b/Assets/Tests/Editor/MouseUiPointerTargetResolverTests.cs.meta
new file mode 100644
index 0000000000..3155526d9b
--- /dev/null
+++ b/Assets/Tests/Editor/MouseUiPointerTargetResolverTests.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 81d61bb5a1c24251a40a4de59570bebe
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiPointerTargetResolver.cs b/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiPointerTargetResolver.cs
new file mode 100644
index 0000000000..f2264cc4e0
--- /dev/null
+++ b/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiPointerTargetResolver.cs
@@ -0,0 +1,241 @@
+#nullable enable
+using System;
+using UnityEngine;
+using UnityEngine.EventSystems;
+
+using io.github.hatayama.UnityCliLoop.Runtime;
+
+namespace io.github.hatayama.UnityCliLoop.FirstPartyTools
+{
+ ///
+ /// Resolves pointer, hierarchy path, and drop targets for mouse UI simulation.
+ ///
+ internal static class MouseUiPointerTargetResolver
+ {
+ private static PointerEventData.InputButton ToInputButton(MouseButton button)
+ {
+ switch (button)
+ {
+ case MouseButton.Right:
+ return PointerEventData.InputButton.Right;
+ case MouseButton.Middle:
+ return PointerEventData.InputButton.Middle;
+ default:
+ return PointerEventData.InputButton.Left;
+ }
+ }
+
+ internal static PointerEventData CreatePointerPressData(
+ EventSystem eventSystem,
+ Vector2 screenPos,
+ MouseButton button)
+ {
+ return new PointerEventData(eventSystem)
+ {
+ position = screenPos,
+ pressPosition = screenPos,
+ button = ToInputButton(button)
+ };
+ }
+
+ internal static ResolvedPointerTargets ResolvePressablePointerTargets(
+ MouseUiSimulationCommand parameters,
+ EventSystem eventSystem,
+ Vector2 inputPos,
+ Vector2 screenPos,
+ PointerEventData pointerData,
+ MouseAction action)
+ {
+ if (parameters.BypassRaycast)
+ {
+ return ResolveBypassPressablePointerTargets(parameters, inputPos, pointerData, action);
+ }
+
+ RaycastResult? hit = UiRaycastHelper.RaycastUI(screenPos, eventSystem);
+ if (hit == null)
+ {
+ return ResolvedPointerTargets.Empty;
+ }
+
+ return ResolveRaycastPressablePointerTargets(hit.Value, pointerData);
+ }
+
+ private static ResolvedPointerTargets ResolveBypassPressablePointerTargets(
+ MouseUiSimulationCommand parameters,
+ Vector2 inputPos,
+ PointerEventData pointerData,
+ MouseAction action)
+ {
+ if (!TryResolveGameObjectPath(
+ parameters.TargetPath,
+ "TargetPath",
+ action,
+ inputPos,
+ out GameObject? rawTarget,
+ out SimulateMouseUiResponse? failureResponse))
+ {
+ return ResolvedPointerTargets.Failure(failureResponse);
+ }
+
+ RaycastResult directRaycast = CreateDirectRaycastResult(rawTarget!);
+ pointerData.pointerCurrentRaycast = directRaycast;
+ pointerData.pointerPressRaycast = directRaycast;
+
+ return CreateResolvedPressablePointerTargets(rawTarget!, pointerData);
+ }
+
+ private static ResolvedPointerTargets ResolveRaycastPressablePointerTargets(
+ RaycastResult hit,
+ PointerEventData pointerData)
+ {
+ GameObject rawTarget = hit.gameObject;
+ pointerData.pointerCurrentRaycast = hit;
+ pointerData.pointerPressRaycast = hit;
+
+ return CreateResolvedPressablePointerTargets(rawTarget, pointerData);
+ }
+
+ private static ResolvedPointerTargets CreateResolvedPressablePointerTargets(
+ GameObject rawTarget,
+ PointerEventData pointerData)
+ {
+ // Execute dispatches only to the exact target; composite controls need hierarchy traversal.
+ GameObject? pressTarget = ExecuteEvents.GetEventHandler(rawTarget);
+ GameObject? clickTarget = ExecuteEvents.GetEventHandler(rawTarget);
+ GameObject? target = pressTarget ?? clickTarget;
+ if (target != null)
+ {
+ pointerData.pointerPress = target;
+ pointerData.rawPointerPress = rawTarget;
+ }
+
+ return ResolvedPointerTargets.Success(rawTarget, pressTarget, clickTarget, target);
+ }
+
+ internal static bool TryResolveGameObjectPath(
+ string targetPath,
+ string parameterName,
+ MouseAction action,
+ Vector2 inputPosition,
+ out GameObject? target,
+ out SimulateMouseUiResponse? failureResponse)
+ {
+ TargetPathLookupResult lookupResult = FindActiveGameObjectByPath(targetPath);
+ target = lookupResult.Target;
+ if (target != null)
+ {
+ failureResponse = null;
+ return true;
+ }
+
+ string message = lookupResult.MatchCount == 0
+ ? $"{parameterName} '{targetPath}' was not found."
+ : $"{parameterName} '{targetPath}' matched {lookupResult.MatchCount} active GameObjects. Use a unique hierarchy path.";
+
+ failureResponse = new SimulateMouseUiResponse
+ {
+ Success = false,
+ Message = message,
+ Action = action.ToString(),
+ PositionX = inputPosition.x,
+ PositionY = inputPosition.y
+ };
+ return false;
+ }
+
+ internal static bool TryResolveDropTargetPath(
+ MouseUiSimulationCommand parameters,
+ MouseAction action,
+ Vector2 inputPosition,
+ out GameObject? dropTarget,
+ out SimulateMouseUiResponse? failureResponse)
+ {
+ dropTarget = null;
+ failureResponse = null;
+
+ if (string.IsNullOrWhiteSpace(parameters.DropTargetPath))
+ {
+ return true;
+ }
+
+ if (!TryResolveGameObjectPath(
+ parameters.DropTargetPath,
+ "DropTargetPath",
+ action,
+ inputPosition,
+ out GameObject? rawDropTarget,
+ out failureResponse))
+ {
+ return false;
+ }
+
+ GameObject? dropHandler = ExecuteEvents.GetEventHandler(rawDropTarget!);
+ if (dropHandler == null)
+ {
+ failureResponse = new SimulateMouseUiResponse
+ {
+ Success = false,
+ Message = $"DropTargetPath '{parameters.DropTargetPath}' has no drop handler.",
+ Action = action.ToString(),
+ PositionX = inputPosition.x,
+ PositionY = inputPosition.y
+ };
+ return false;
+ }
+
+ dropTarget = rawDropTarget;
+ return true;
+ }
+
+ private static TargetPathLookupResult FindActiveGameObjectByPath(string targetPath)
+ {
+ string normalizedPath = targetPath.Trim().Trim('/');
+ if (string.IsNullOrEmpty(normalizedPath))
+ {
+ return new TargetPathLookupResult(null, 0);
+ }
+
+ GameObject[] gameObjects = UnityEngine.Object.FindObjectsByType(FindObjectsSortMode.None);
+ GameObject? matchedTarget = null;
+ int matchCount = 0;
+
+ foreach (GameObject gameObject in gameObjects)
+ {
+ if (!string.Equals(
+ GameObjectPathUtility.GetFullPath(gameObject),
+ normalizedPath,
+ StringComparison.Ordinal))
+ {
+ continue;
+ }
+
+ matchCount++;
+ matchedTarget = gameObject;
+ }
+
+ return matchCount == 1
+ ? new TargetPathLookupResult(matchedTarget, matchCount)
+ : new TargetPathLookupResult(null, matchCount);
+ }
+
+ internal static RaycastResult CreateDirectRaycastResult(GameObject target)
+ {
+ return new RaycastResult
+ {
+ gameObject = target
+ };
+ }
+
+ private readonly struct TargetPathLookupResult
+ {
+ public TargetPathLookupResult(GameObject? target, int matchCount)
+ {
+ Target = target;
+ MatchCount = matchCount;
+ }
+
+ public GameObject? Target { get; }
+ public int MatchCount { get; }
+ }
+ }
+}
diff --git a/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiPointerTargetResolver.cs.meta b/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiPointerTargetResolver.cs.meta
new file mode 100644
index 0000000000..ea90b84ded
--- /dev/null
+++ b/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiPointerTargetResolver.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 49fa14da50334771a724084c34c05d5d
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/ResolvedPointerTargets.cs b/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/ResolvedPointerTargets.cs
new file mode 100644
index 0000000000..f823b39d00
--- /dev/null
+++ b/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/ResolvedPointerTargets.cs
@@ -0,0 +1,50 @@
+#nullable enable
+using UnityEngine;
+
+namespace io.github.hatayama.UnityCliLoop.FirstPartyTools
+{
+ ///
+ /// Carries raw and hierarchy-resolved pointer targets or a resolution failure.
+ ///
+ internal readonly struct ResolvedPointerTargets
+ {
+ private ResolvedPointerTargets(
+ GameObject? rawTarget,
+ GameObject? pressTarget,
+ GameObject? clickTarget,
+ GameObject? target,
+ SimulateMouseUiResponse? failureResponse)
+ {
+ RawTarget = rawTarget;
+ PressTarget = pressTarget;
+ ClickTarget = clickTarget;
+ Target = target;
+ FailureResponse = failureResponse;
+ }
+
+ public static ResolvedPointerTargets Empty { get; } =
+ new(null, null, null, null, null);
+
+ public GameObject? RawTarget { get; }
+ public GameObject? PressTarget { get; }
+ public GameObject? ClickTarget { get; }
+ public GameObject? Target { get; }
+ public SimulateMouseUiResponse? FailureResponse { get; }
+
+ public static ResolvedPointerTargets Success(
+ GameObject rawTarget,
+ GameObject? pressTarget,
+ GameObject? clickTarget,
+ GameObject? target)
+ {
+ return new ResolvedPointerTargets(rawTarget, pressTarget, clickTarget, target, null);
+ }
+
+ public static ResolvedPointerTargets Failure(
+ SimulateMouseUiResponse? failureResponse)
+ {
+ Debug.Assert(failureResponse != null, "Failure response must exist when target resolution fails.");
+ return new ResolvedPointerTargets(null, null, null, null, failureResponse);
+ }
+ }
+}
diff --git a/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/ResolvedPointerTargets.cs.meta b/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/ResolvedPointerTargets.cs.meta
new file mode 100644
index 0000000000..4520f4e776
--- /dev/null
+++ b/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/ResolvedPointerTargets.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 301801bfefc74357aecb54bea115374c
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/SimulateMouseUiUseCase.cs b/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/SimulateMouseUiUseCase.cs
index 0e2afe7b94..debd1d01c1 100644
--- a/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/SimulateMouseUiUseCase.cs
+++ b/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/SimulateMouseUiUseCase.cs
@@ -151,19 +151,6 @@ private static void EnsureOverlayExists()
OverlayCanvasFactory.EnsureExists();
}
- private static PointerEventData.InputButton ToInputButton(MouseButton button)
- {
- switch (button)
- {
- case MouseButton.Right:
- return PointerEventData.InputButton.Right;
- case MouseButton.Middle:
- return PointerEventData.InputButton.Middle;
- default:
- return PointerEventData.InputButton.Left;
- }
- }
-
// Input coordinates use top-left origin; Unity Screen space uses bottom-left origin.
// Handles.GetMainGameViewSize() returns the Game view's target resolution (e.g. 1920x1080),
// which matches the Canvas layout space — unlike Screen.height which returns the window pixel size.
@@ -184,9 +171,9 @@ private async Task ExecuteClick(
{
Vector2 inputPos = new(parameters.X, parameters.Y);
Vector2 screenPos = InputToScreen(inputPos);
- PointerEventData pointerData = CreatePointerPressData(eventSystem, screenPos, parameters.Button);
+ PointerEventData pointerData = MouseUiPointerTargetResolver.CreatePointerPressData(eventSystem, screenPos, parameters.Button);
ResolvedPointerTargets resolvedTargets =
- ResolvePressablePointerTargets(parameters, eventSystem, inputPos, screenPos, pointerData, MouseAction.Click);
+ MouseUiPointerTargetResolver.ResolvePressablePointerTargets(parameters, eventSystem, inputPos, screenPos, pointerData, MouseAction.Click);
if (resolvedTargets.FailureResponse != null)
{
return resolvedTargets.FailureResponse;
@@ -247,9 +234,9 @@ private async Task ExecuteLongPress(
Vector2 inputPos = new(parameters.X, parameters.Y);
Vector2 screenPos = InputToScreen(inputPos);
- PointerEventData pointerData = CreatePointerPressData(eventSystem, screenPos, parameters.Button);
+ PointerEventData pointerData = MouseUiPointerTargetResolver.CreatePointerPressData(eventSystem, screenPos, parameters.Button);
ResolvedPointerTargets resolvedTargets =
- ResolvePressablePointerTargets(parameters, eventSystem, inputPos, screenPos, pointerData, MouseAction.LongPress);
+ MouseUiPointerTargetResolver.ResolvePressablePointerTargets(parameters, eventSystem, inputPos, screenPos, pointerData, MouseAction.LongPress);
if (resolvedTargets.FailureResponse != null)
{
return resolvedTargets.FailureResponse;
@@ -324,93 +311,6 @@ private async Task ExecuteLongPress(
return MouseUiSimulationResponseFactory.CreateLongPressResult(parameters, inputPos, targetName, hitTarget);
}
- private static PointerEventData CreatePointerPressData(
- EventSystem eventSystem,
- Vector2 screenPos,
- MouseButton button)
- {
- return new PointerEventData(eventSystem)
- {
- position = screenPos,
- pressPosition = screenPos,
- button = ToInputButton(button)
- };
- }
-
- private static ResolvedPointerTargets ResolvePressablePointerTargets(
- MouseUiSimulationCommand parameters,
- EventSystem eventSystem,
- Vector2 inputPos,
- Vector2 screenPos,
- PointerEventData pointerData,
- MouseAction action)
- {
- if (parameters.BypassRaycast)
- {
- return ResolveBypassPressablePointerTargets(parameters, inputPos, pointerData, action);
- }
-
- RaycastResult? hit = RaycastUI(screenPos, eventSystem);
- if (hit == null)
- {
- return ResolvedPointerTargets.Empty;
- }
-
- return ResolveRaycastPressablePointerTargets(hit.Value, pointerData);
- }
-
- private static ResolvedPointerTargets ResolveBypassPressablePointerTargets(
- MouseUiSimulationCommand parameters,
- Vector2 inputPos,
- PointerEventData pointerData,
- MouseAction action)
- {
- if (!TryResolveGameObjectPath(
- parameters.TargetPath,
- "TargetPath",
- action,
- inputPos,
- out GameObject? rawTarget,
- out SimulateMouseUiResponse? failureResponse))
- {
- return ResolvedPointerTargets.Failure(failureResponse);
- }
-
- RaycastResult directRaycast = CreateDirectRaycastResult(rawTarget!);
- pointerData.pointerCurrentRaycast = directRaycast;
- pointerData.pointerPressRaycast = directRaycast;
-
- return CreateResolvedPressablePointerTargets(rawTarget!, pointerData);
- }
-
- private static ResolvedPointerTargets ResolveRaycastPressablePointerTargets(
- RaycastResult hit,
- PointerEventData pointerData)
- {
- GameObject rawTarget = hit.gameObject;
- pointerData.pointerCurrentRaycast = hit;
- pointerData.pointerPressRaycast = hit;
-
- return CreateResolvedPressablePointerTargets(rawTarget, pointerData);
- }
-
- private static ResolvedPointerTargets CreateResolvedPressablePointerTargets(
- GameObject rawTarget,
- PointerEventData pointerData)
- {
- // Execute dispatches only to the exact target; composite controls need hierarchy traversal.
- GameObject? pressTarget = ExecuteEvents.GetEventHandler(rawTarget);
- GameObject? clickTarget = ExecuteEvents.GetEventHandler(rawTarget);
- GameObject? target = pressTarget ?? clickTarget;
- if (target != null)
- {
- pointerData.pointerPress = target;
- pointerData.rawPointerPress = rawTarget;
- }
-
- return ResolvedPointerTargets.Success(rawTarget, pressTarget, clickTarget, target);
- }
-
private static void ExecutePointerClickEvents(
ResolvedPointerTargets resolvedTargets,
PointerEventData pointerData)
@@ -496,13 +396,13 @@ private async Task ExecuteDragOneShot(
Vector2 inputEnd = new(parameters.X, parameters.Y);
Vector2 screenStart = InputToScreen(inputStart);
Vector2 screenEnd = InputToScreen(inputEnd);
- RaycastResult? hit = parameters.BypassRaycast ? null : RaycastUI(screenStart, eventSystem);
+ RaycastResult? hit = parameters.BypassRaycast ? null : UiRaycastHelper.RaycastUI(screenStart, eventSystem);
RaycastResult startRaycast = new();
GameObject? rawTarget = null;
if (parameters.BypassRaycast)
{
- if (!TryResolveGameObjectPath(
+ if (!MouseUiPointerTargetResolver.TryResolveGameObjectPath(
parameters.TargetPath,
"TargetPath",
MouseAction.Drag,
@@ -513,7 +413,7 @@ private async Task ExecuteDragOneShot(
return failureResponse!;
}
- startRaycast = CreateDirectRaycastResult(rawTarget!);
+ startRaycast = MouseUiPointerTargetResolver.CreateDirectRaycastResult(rawTarget!);
}
else if (hit != null)
{
@@ -522,7 +422,7 @@ private async Task ExecuteDragOneShot(
}
GameObject? explicitDropTarget = null;
- if (!TryResolveDropTargetPath(
+ if (!MouseUiPointerTargetResolver.TryResolveDropTargetPath(
parameters,
MouseAction.Drag,
inputEnd,
@@ -631,7 +531,7 @@ private void FinalizeDrag(PointerEventData pointerData, GameObject target, GameO
{
if (explicitDropTarget != null)
{
- pointerData.pointerCurrentRaycast = CreateDirectRaycastResult(explicitDropTarget);
+ pointerData.pointerCurrentRaycast = MouseUiPointerTargetResolver.CreateDirectRaycastResult(explicitDropTarget);
}
else
{
@@ -737,13 +637,13 @@ private async Task ExecuteDragStart(
Vector2 inputPos = new(parameters.X, parameters.Y);
Vector2 screenPos = InputToScreen(inputPos);
- RaycastResult? hit = parameters.BypassRaycast ? null : RaycastUI(screenPos, eventSystem);
+ RaycastResult? hit = parameters.BypassRaycast ? null : UiRaycastHelper.RaycastUI(screenPos, eventSystem);
RaycastResult startRaycast = new();
GameObject? rawTarget = null;
if (parameters.BypassRaycast)
{
- if (!TryResolveGameObjectPath(
+ if (!MouseUiPointerTargetResolver.TryResolveGameObjectPath(
parameters.TargetPath,
"TargetPath",
MouseAction.DragStart,
@@ -754,7 +654,7 @@ private async Task ExecuteDragStart(
return failureResponse!;
}
- startRaycast = CreateDirectRaycastResult(rawTarget!);
+ startRaycast = MouseUiPointerTargetResolver.CreateDirectRaycastResult(rawTarget!);
}
else if (hit != null)
{
@@ -936,7 +836,7 @@ private async Task ExecuteDragEnd(
string targetName = target.name;
GameObject? explicitDropTarget = null;
- if (!TryResolveDropTargetPath(
+ if (!MouseUiPointerTargetResolver.TryResolveDropTargetPath(
parameters,
MouseAction.DragEnd,
inputEnd,
@@ -1128,178 +1028,5 @@ private void ExecuteCleanupOnMainThread(Action cleanup)
context.Post(_ => cleanup(), null);
}
- private static RaycastResult? RaycastUI(Vector2 screenPosition, EventSystem eventSystem)
- {
- return UiRaycastHelper.RaycastUI(screenPosition, eventSystem);
- }
-
- private static bool TryResolveGameObjectPath(
- string targetPath,
- string parameterName,
- MouseAction action,
- Vector2 inputPosition,
- out GameObject? target,
- out SimulateMouseUiResponse? failureResponse)
- {
- TargetPathLookupResult lookupResult = FindActiveGameObjectByPath(targetPath);
- target = lookupResult.Target;
- if (target != null)
- {
- failureResponse = null;
- return true;
- }
-
- string message = lookupResult.MatchCount == 0
- ? $"{parameterName} '{targetPath}' was not found."
- : $"{parameterName} '{targetPath}' matched {lookupResult.MatchCount} active GameObjects. Use a unique hierarchy path.";
-
- failureResponse = new SimulateMouseUiResponse
- {
- Success = false,
- Message = message,
- Action = action.ToString(),
- PositionX = inputPosition.x,
- PositionY = inputPosition.y
- };
- return false;
- }
-
- private static bool TryResolveDropTargetPath(
- MouseUiSimulationCommand parameters,
- MouseAction action,
- Vector2 inputPosition,
- out GameObject? dropTarget,
- out SimulateMouseUiResponse? failureResponse)
- {
- dropTarget = null;
- failureResponse = null;
-
- if (string.IsNullOrWhiteSpace(parameters.DropTargetPath))
- {
- return true;
- }
-
- if (!TryResolveGameObjectPath(
- parameters.DropTargetPath,
- "DropTargetPath",
- action,
- inputPosition,
- out GameObject? rawDropTarget,
- out failureResponse))
- {
- return false;
- }
-
- GameObject? dropHandler = ExecuteEvents.GetEventHandler(rawDropTarget!);
- if (dropHandler == null)
- {
- failureResponse = new SimulateMouseUiResponse
- {
- Success = false,
- Message = $"DropTargetPath '{parameters.DropTargetPath}' has no drop handler.",
- Action = action.ToString(),
- PositionX = inputPosition.x,
- PositionY = inputPosition.y
- };
- return false;
- }
-
- dropTarget = rawDropTarget;
- return true;
- }
-
- private static TargetPathLookupResult FindActiveGameObjectByPath(string targetPath)
- {
- string normalizedPath = targetPath.Trim().Trim('/');
- if (string.IsNullOrEmpty(normalizedPath))
- {
- return new TargetPathLookupResult(null, 0);
- }
-
- GameObject[] gameObjects = UnityEngine.Object.FindObjectsByType(FindObjectsSortMode.None);
- GameObject? matchedTarget = null;
- int matchCount = 0;
-
- foreach (GameObject gameObject in gameObjects)
- {
- if (!string.Equals(
- GameObjectPathUtility.GetFullPath(gameObject),
- normalizedPath,
- StringComparison.Ordinal))
- {
- continue;
- }
-
- matchCount++;
- matchedTarget = gameObject;
- }
-
- return matchCount == 1
- ? new TargetPathLookupResult(matchedTarget, matchCount)
- : new TargetPathLookupResult(null, matchCount);
- }
-
- private static RaycastResult CreateDirectRaycastResult(GameObject target)
- {
- return new RaycastResult
- {
- gameObject = target
- };
- }
-
- private readonly struct TargetPathLookupResult
- {
- public TargetPathLookupResult(GameObject? target, int matchCount)
- {
- Target = target;
- MatchCount = matchCount;
- }
-
- public GameObject? Target { get; }
- public int MatchCount { get; }
- }
-
- private readonly struct ResolvedPointerTargets
- {
- private ResolvedPointerTargets(
- GameObject? rawTarget,
- GameObject? pressTarget,
- GameObject? clickTarget,
- GameObject? target,
- SimulateMouseUiResponse? failureResponse)
- {
- RawTarget = rawTarget;
- PressTarget = pressTarget;
- ClickTarget = clickTarget;
- Target = target;
- FailureResponse = failureResponse;
- }
-
- public static ResolvedPointerTargets Empty { get; } =
- new(null, null, null, null, null);
-
- public GameObject? RawTarget { get; }
- public GameObject? PressTarget { get; }
- public GameObject? ClickTarget { get; }
- public GameObject? Target { get; }
- public SimulateMouseUiResponse? FailureResponse { get; }
-
- public static ResolvedPointerTargets Success(
- GameObject rawTarget,
- GameObject? pressTarget,
- GameObject? clickTarget,
- GameObject? target)
- {
- return new ResolvedPointerTargets(rawTarget, pressTarget, clickTarget, target, null);
- }
-
- public static ResolvedPointerTargets Failure(
- SimulateMouseUiResponse? failureResponse)
- {
- Debug.Assert(failureResponse != null, "Failure response must exist when target resolution fails.");
- return new ResolvedPointerTargets(null, null, null, null, failureResponse);
- }
- }
-
}
}