Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
736 changes: 736 additions & 0 deletions Assets/Tests/Editor/RaycastGridAnnotatorTests.cs

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions Assets/Tests/Editor/RaycastGridAnnotatorTests.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("UnityCLILoop.FirstPartyTools.Raycast.Editor")]
[assembly: InternalsVisibleTo("UnityCLILoop.FirstPartyTools.Screenshot.Editor")]
[assembly: InternalsVisibleTo("UnityCLILoop.Tests.Editor")]
[assembly: InternalsVisibleTo("UnityCLILoop.Tests.PlayMode")]
[assembly: InternalsVisibleTo("UnityCLILoop.Tests.Demo.Editor")]
114 changes: 85 additions & 29 deletions Packages/src/Editor/FirstPartyTools/Common/MouseUi/UiRaycastHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,37 +14,22 @@ internal static class UiRaycastHelper
{
public static RaycastResult? RaycastUI(Vector2 screenPosition, EventSystem eventSystem)
{
PointerEventData pointerData = new(eventSystem)
{
position = screenPosition
};
List<RaycastResult> results = new();
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;
RaycastContext context = new(eventSystem);
return context.Raycast(screenPosition);
}

// 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)
{
List<CanvasRaycastSource> canvasRaycastSources = CollectCanvasRaycastSources();
return RaycastCanvasSpaceFromSources(canvasPosition, canvasRaycastSources);
}

private static List<CanvasRaycastSource> CollectCanvasRaycastSources()
{
Canvas[] canvases = Object.FindObjectsByType<Canvas>(FindObjectsSortMode.None);
RaycastResult? bestHit = null;
List<CanvasRaycastSource> canvasRaycastSources = new List<CanvasRaycastSource>();

foreach (Canvas canvas in canvases)
{
Expand All @@ -65,20 +50,34 @@ internal static class UiRaycastHelper
}

Graphic[] graphics = canvas.GetComponentsInChildren<Graphic>();
foreach (Graphic graphic in graphics)
canvasRaycastSources.Add(new CanvasRaycastSource(canvas, raycaster, graphics));
}

return canvasRaycastSources;
}

private static RaycastResult? RaycastCanvasSpaceFromSources(
Vector2 canvasPosition,
List<CanvasRaycastSource> canvasRaycastSources)
{
RaycastResult? bestHit = null;

foreach (CanvasRaycastSource source in canvasRaycastSources)
{
foreach (Graphic graphic in source.Graphics)
{
if (!IsRaycastCandidate(graphic, canvas, raycaster, canvasPosition))
if (!IsRaycastCandidate(graphic, source.Canvas, source.Raycaster, canvasPosition))
{
continue;
}

RaycastResult candidate = new RaycastResult
{
gameObject = graphic.gameObject,
module = raycaster,
module = source.Raycaster,
screenPosition = canvasPosition,
sortingLayer = canvas.sortingLayerID,
sortingOrder = canvas.sortingOrder,
sortingLayer = source.Canvas.sortingLayerID,
sortingOrder = source.Canvas.sortingOrder,
depth = graphic.depth
};

Expand All @@ -92,6 +91,63 @@ internal static class UiRaycastHelper
return bestHit;
}

// Reuses EventSystem and Canvas-space raycast buffers for callers that test
// many positions in one frame, such as screenshot raycast-grid annotation collection.
// Keep this scoped to one frame because it snapshots Canvas and Graphic state.
internal sealed class RaycastContext
{
private readonly EventSystem _eventSystem;
private readonly PointerEventData _pointerData;
private readonly List<RaycastResult> _eventSystemResults = new();
private readonly List<CanvasRaycastSource> _canvasRaycastSources;

internal RaycastContext(EventSystem eventSystem)
{
Debug.Assert(eventSystem != null, "EventSystem is required for UI raycasting.");
_eventSystem = eventSystem!;
_pointerData = new PointerEventData(eventSystem!);
_canvasRaycastSources = CollectCanvasRaycastSources();
}

public RaycastResult? Raycast(Vector2 screenPosition)
{
_eventSystemResults.Clear();
_pointerData.position = screenPosition;
_eventSystem.RaycastAll(_pointerData, _eventSystemResults);
RaycastResult? canvasSpaceHit = RaycastCanvasSpaceFromSources(screenPosition, _canvasRaycastSources);

if (_eventSystemResults.Count > 0)
{
RaycastResult firstHit = _eventSystemResults[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;
}
}

private readonly struct CanvasRaycastSource
{
public Canvas Canvas { get; }
public GraphicRaycaster Raycaster { get; }
public Graphic[] Graphics { get; }

public CanvasRaycastSource(Canvas canvas, GraphicRaycaster raycaster, Graphic[] graphics)
{
Canvas = canvas;
Raycaster = raycaster;
Graphics = graphics;
}
}

// Respects CanvasGroup.blocksRaycasts, Mask, RectMask2D, and custom ICanvasRaycastFilter
private static bool IsRaycastCandidate(Graphic graphic, Canvas canvas, GraphicRaycaster raycaster, Vector2 canvasPosition)
{
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading