#nullable enable using System.Collections.Generic; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.UI; namespace io.github.hatayama.uLoopMCP { // Shared UI raycast logic used by SimulateMouseUiTool and InputReplayer. internal static class UiRaycastHelper { public static RaycastResult? RaycastUI(Vector2 screenPosition, EventSystem eventSystem) { PointerEventData pointerData = new PointerEventData(eventSystem) { position = screenPosition }; List results = new List(); eventSystem.RaycastAll(pointerData, results); RaycastResult? canvasSpaceHit = RaycastCanvasSpace(screenPosition); if (results.Count > 0) { RaycastResult firstHit = results[0]; if (canvasSpaceHit != null && ShouldPreferCanvasSpaceHit(canvasSpaceHit.Value, firstHit)) { return canvasSpaceHit; } return firstHit; } // EventSystem clips at Screen.width/height, which can be smaller than the // Canvas layout space (Game view target resolution). Fall back to manual hit testing. return canvasSpaceHit; } // Bypass EventSystem's Screen-bounds clipping by directly testing Graphic rects in Canvas space. // Only supports ScreenSpaceOverlay canvases where world positions equal Canvas-space positions. public static RaycastResult? RaycastCanvasSpace(Vector2 canvasPosition) { Canvas[] canvases = Object.FindObjectsByType(FindObjectsSortMode.None); RaycastResult? bestHit = null; foreach (Canvas canvas in canvases) { if (!canvas.gameObject.activeInHierarchy) { continue; } if (canvas.renderMode != RenderMode.ScreenSpaceOverlay) { continue; } GraphicRaycaster? raycaster = canvas.GetComponent(); if (raycaster == null || !raycaster.enabled) { continue; } Graphic[] graphics = canvas.GetComponentsInChildren(); foreach (Graphic graphic in graphics) { if (!IsRaycastCandidate(graphic, canvasPosition)) { continue; } RaycastResult candidate = new RaycastResult { gameObject = graphic.gameObject, module = raycaster, screenPosition = canvasPosition, sortingLayer = canvas.sortingLayerID, sortingOrder = canvas.sortingOrder, depth = graphic.depth }; if (bestHit == null || CompareRaycastPriority(candidate, bestHit.Value) > 0) { bestHit = candidate; } } } return bestHit; } // Respects CanvasGroup.blocksRaycasts, Mask, RectMask2D, and custom ICanvasRaycastFilter private static bool IsRaycastCandidate(Graphic graphic, Vector2 canvasPosition) { if (!graphic.gameObject.activeInHierarchy || !graphic.enabled) { return false; } if (!graphic.raycastTarget || graphic.depth == -1 || graphic.canvasRenderer.cull) { return false; } if (!RectTransformUtility.RectangleContainsScreenPoint( graphic.rectTransform, canvasPosition, null)) { return false; } return graphic.Raycast(canvasPosition, null); } private static bool ShouldPreferCanvasSpaceHit(RaycastResult canvasSpaceHit, RaycastResult eventSystemHit) { if (!IsGraphicRaycast(canvasSpaceHit)) { return false; } if (!IsGraphicRaycast(eventSystemHit)) { return true; } return CompareRaycastPriority(canvasSpaceHit, eventSystemHit) > 0; } private static bool IsGraphicRaycast(RaycastResult raycastResult) { return raycastResult.module is GraphicRaycaster; } private static int CompareRaycastPriority(RaycastResult left, RaycastResult right) { int sortOrderPriority = Compare(GetSortOrderPriority(left), GetSortOrderPriority(right)); if (sortOrderPriority != 0) { return sortOrderPriority; } int renderOrderPriority = Compare(GetRenderOrderPriority(left), GetRenderOrderPriority(right)); if (renderOrderPriority != 0) { return renderOrderPriority; } int sortingLayer = Compare(GetSortingLayerValue(left), GetSortingLayerValue(right)); if (sortingLayer != 0) { return sortingLayer; } int sortingOrder = Compare(left.sortingOrder, right.sortingOrder); if (sortingOrder != 0) { return sortingOrder; } return Compare(left.depth, right.depth); } private static int GetSortOrderPriority(RaycastResult result) { return result.module != null ? result.module.sortOrderPriority : 0; } private static int GetRenderOrderPriority(RaycastResult result) { return result.module != null ? result.module.renderOrderPriority : 0; } private static int GetSortingLayerValue(RaycastResult result) { return SortingLayer.GetLayerValueFromID(result.sortingLayer); } private static int Compare(int left, int right) { if (left > right) { return 1; } if (left < right) { return -1; } return 0; } } }