diff --git a/.agents/skills/uloop-screenshot/SKILL.md b/.agents/skills/uloop-screenshot/SKILL.md index 422a7ec90f..116b4af479 100644 --- a/.agents/skills/uloop-screenshot/SKILL.md +++ b/.agents/skills/uloop-screenshot/SKILL.md @@ -23,7 +23,7 @@ uloop screenshot [--window-name ] [--resolution-scale ] [--match-mo | `--match-mode` | enum | `exact` | Window name matching mode: `exact`, `prefix`, or `contains`. Ignored when the resolved capture mode is rendering (auto during Play Mode resolves to rendering). | | `--capture-mode` | enum | `auto` | `auto` - rendering in PlayMode and window otherwise, `window` - capture EditorWindow including toolbar, `rendering` - capture game rendering only (PlayMode required), `GameView` - alias for `rendering`. Rendering screenshots return `ScreenshotToInputFormula` for converting raw image pixels before calling simulate-mouse-input (including `--dry-run`) or simulate-mouse-ui. | | `--output-directory` | string | `""` | Output directory path for saving screenshots. When empty, uses default path (.uloop/outputs/Screenshots/). Accepts absolute paths. | -| `--annotate-elements` | flag | - | Annotate interactive UI elements with index labels and interaction hints (A / CLICK, B / DRAG, ...). The response includes an `AnnotatedElements` array with element metadata sorted by `SortingOrder` then `SiblingIndex` (an approximation of z-order, not the guaranteed draw order). Only works when the resolved capture mode is rendering (auto during Play Mode resolves to rendering) in PlayMode. | +| `--annotate-elements` | flag | - | Annotate interactive uGUI elements and UI Toolkit controls on screen-space panels with index labels and interaction hints (A / CLICK, B / DRAG, ...). The response includes an `AnnotatedElements` array with element metadata sorted by `SortingOrder` then `SiblingIndex` (an approximation of z-order, not the guaranteed draw order). Only works when the resolved capture mode is rendering (auto during Play Mode resolves to rendering) in PlayMode. | | `--annotate-raycast-grid` | flag | - | Annotate clustered 3D physics collider candidates as `PhysicsCollider` entries in `AnnotatedElements`. Uses `Camera.main` visibility and the same top-left Game View coordinates as `simulate-mouse-input`. Only works when the resolved capture mode is rendering (auto during Play Mode resolves to rendering) in PlayMode. | | `--raycast-layer-mask` | string | `""` | Comma-separated physics layer names to narrow which layers `--annotate-raycast-grid` clusters. Hits are limited to layers also visible to `Camera.main.cullingMask`. When omitted, clusters against `Physics.DefaultRaycastLayers`. | | `--elements-only` | flag | - | Return only annotated element JSON without capturing a screenshot image. Requires `--annotate-elements` or `--annotate-raycast-grid`, and the resolved capture mode to be rendering (auto during Play Mode resolves to rendering) in PlayMode. | diff --git a/.agents/skills/uloop-screenshot/references/annotated-elements.md b/.agents/skills/uloop-screenshot/references/annotated-elements.md index 1ef3f771da..69272a2bb2 100644 --- a/.agents/skills/uloop-screenshot/references/annotated-elements.md +++ b/.agents/skills/uloop-screenshot/references/annotated-elements.md @@ -10,15 +10,16 @@ Device Simulator is supported for this flow: prefer `--capture-mode rendering` ( - `Label`: Index label in JSON (`A` = first in that sort, `B` = next, ...). Treat it as a stable handle, not as proof of what is actually in front: when overlapping elements cross parents, pick the target by `Path` and confirm the result with `simulate-mouse-ui`'s `HitGameObjectName` — the handler that received the events, resolved up the hierarchy from the raycast hit (or from `--target-path` in bypass mode), so it can name an ancestor of the element you clicked. Screenshot labels also include the interaction hint, such as `A / CLICK` or `B / DRAG`. - `Name`: Element name -- `Path`: Hierarchy path from the scene root, for example `Canvas/Panel/Button`. Use this as `simulate-mouse-ui --target-path` when bypassing raycast blockers. +- `Path`: Hierarchy path from the scene root, for example `Canvas/Panel/Button`. Use this as `simulate-mouse-ui --target-path` when bypassing raycast blockers. UI Toolkit entries use a different form that `--target-path` does not accept; click them at `SimX`/`SimY` instead. See [UI Toolkit Elements](#ui-toolkit-elements). - `Type`: Element type (`Button`, `Toggle`, `Slider`, `Dropdown`, `InputField`, `Scrollbar`, `Draggable`, `DropTarget`, `Selectable`, `PhysicsCollider`) - `Interaction`: Derived interaction category (`Click`, `Drag`, `Drop`, `Text`) or `Raycast` for clustered physics collider entries. Use this to choose between `simulate-mouse-ui --action Click`, drag actions, or `simulate-mouse-input` (including `--dry-run`). +- `UiSystem`: UI system the entry came from: `UGUI`, `UIToolkit`, or empty for `PhysicsCollider` entries. - `Layer`: Physics layer name for `PhysicsCollider` entries. Empty for UI entries. - `Components`: Collider and MonoBehaviour component type names from the hit GameObject for `PhysicsCollider` entries. Empty for UI entries. - `SimX`, `SimY`: Target click position in top-left Game View coordinates. For UI entries this is the element center when that center is raycast-reachable; otherwise it is a reachable probe point inside the bounds (center, then four interior quarter points). For `PhysicsCollider` entries this is a representative sampled hit. Use these directly with `simulate-mouse-ui --x/--y` or `simulate-mouse-input --x/--y` (`--dry-run` optional). - `BoundsMinX`, `BoundsMinY`, `BoundsMaxX`, `BoundsMaxY`: Bounding box in the same coordinates as `SimX/SimY`. For `PhysicsCollider` entries, this is the axis-aligned sampled-cell coverage box from reachable raycast hits, not a guarantee that every interior point is clickable. -- `SortingOrder`: Canvas sorting order. Higher values are in front. -- `SiblingIndex`: Transform sibling index under the element's direct parent. Do not use it as a reliable z-order signal across nested UI hierarchies; the frontmost-first sort above can then disagree with draw order. +- `SortingOrder`: Canvas sorting order (for UI Toolkit, the panel's `PanelSettings.sortingOrder` rounded down). Higher values are in front. +- `SiblingIndex`: Transform sibling index under the element's direct parent (for UI Toolkit, the index in the parent visual element). Do not use it as a reliable z-order signal across nested UI hierarchies; the frontmost-first sort above can then disagree with draw order. ## Exclusion rules (by design) @@ -29,6 +30,33 @@ The annotator lists elements that share the same EventSystem raycast path as `si - Elements whose center and four interior quarter probe points are all covered by another raycast hit are not listed. - Selectables that are not interactable (`interactable = false`, or a parent `CanvasGroup` with `interactable = false`) ignore clicks and drags, so they are not listed. A separate pointer handler script on the same GameObject is still listed as that handler. +## UI Toolkit Elements + +Entries with `UiSystem` `UIToolkit` come from the `UIDocument`s on screen-space panels shown in the Game View. They use the same fields and top-left Game View coordinates as uGUI entries. + +- `Type`: `Button` (`Button`, `RepeatButton`), `Toggle` (`Toggle` and other boolean fields), `Slider` (`Slider`, `SliderInt`, `MinMaxSlider`), `Scrollbar` (`Scroller`, including the scrollers of a `ScrollView`), `InputField` (`TextField`), `Dropdown` (`DropdownField`, `EnumField`). `Interaction` follows from `Type` as for uGUI. +- `Path`: `#`, for example `UI/Menu#panel/play`. The chain starts below the document root and joins element names with `/`; an element without a name appears as its type name. + +Omitted on purpose: + +- Disabled elements, `visibility: hidden`, `display: none` (with everything under it), and zero-size elements. +- Elements covered at the center and every interior quarter probe point by a panel with a higher sorting order or by another element of the same panel, including parts clipped away by a parent with `overflow: hidden`. +- Panels that render into a RenderTexture, panels on a display other than the Game View's, world-space panels (Unity 6), and panels whose screen-to-panel mapping is not a plain scale and offset. +- Parts inside a control (the slider and buttons of a `Scroller`, for example) and any element that is not one of the types above. A custom element with only a `Clickable` manipulator is not listed. + +Limits: + +- When uGUI and UI Toolkit overlap, which one is in front is not checked; both may be listed. +- Panels with the same sorting order are not checked against each other. +- `Bounds` is the whole element rectangle and can include parts clipped away by a parent. +- Unity 2022.3 with both Device Simulator and Game View open lays UI Toolkit panels out at the Game View size and stretches them onto the simulated screen. Bounds and `SimX`/`SimY` then match where clicks land but not the PNG, and `set-game-view-size` cannot change the hidden Game View while Simulator is shown. Closing the Game View while using Simulator makes them match. + +Clicking: + +- `simulate-mouse-ui --action Click --x --y ` (or `LongPress`, or a drag from that point) reaches UI Toolkit controls, with or without an EventSystem. Its `--target-path` does not accept UI Toolkit `Path` values. +- `simulate-mouse-input --action Click --x --y ` also reaches them when the scene has an EventSystem with an Input System UI input module. +- Without an EventSystem the `simulate-mouse-input` click does not reach UI Toolkit controls. + ### PhysicsCollider Entries Per Closed Region When a single `PhysicsCollider` GameObject's reachable raycast samples form multiple 4-connected regions on screen — typically because UI occlusion splits them apart — each region becomes its own `AnnotatedElements` entry. `Path`, `Name`, `Layer`, and `Components` are identical across those entries (they describe the same GameObject), while `Label`, `Bounds`, `SimX`, `SimY`, and `RaycastOutlineSegments` are independent per region so each closed area is separately addressable. diff --git a/.agents/skills/uloop-simulate-mouse-input/SKILL.md b/.agents/skills/uloop-simulate-mouse-input/SKILL.md index b4e63a2c51..41723aa257 100644 --- a/.agents/skills/uloop-simulate-mouse-input/SKILL.md +++ b/.agents/skills/uloop-simulate-mouse-input/SKILL.md @@ -67,7 +67,7 @@ All rows below assume the New Input System is installed. | Scenario | Tool | |----------|------| -| Click a Unity UI Button (IPointerClickHandler) | `simulate-mouse-ui` | +| Click a uGUI or UI Toolkit button | `simulate-mouse-ui` | | Runtime logic reads `Mouse.current.leftButton` | `simulate-mouse-input` | | Runtime logic reads right-click | `simulate-mouse-input --button Right` | | Drag a UI slider | `simulate-mouse-ui --action Drag` | @@ -110,7 +110,7 @@ uloop simulate-mouse-input --action SmoothDelta --delta-x 300 --delta-y 0 --dura ## Coordinate System `--x`/`--y` use top-left Game View coordinates: pass `AnnotatedElements[].SimX/SimY` -directly, convert raw screenshot pixels with `ScreenshotToInputFormula`, and never flip Y +directly (UI Toolkit entries need an EventSystem with an Input System UI module here; `simulate-mouse-ui` reaches them without one), convert raw screenshot pixels with `ScreenshotToInputFormula`, and never flip Y in the caller (the tool converts internally). Conversion formula, Device Simulator notes, and how the injected position reads back inside Unity are in `references/output-and-coordinates.md`. diff --git a/.agents/skills/uloop-simulate-mouse-input/references/output-and-coordinates.md b/.agents/skills/uloop-simulate-mouse-input/references/output-and-coordinates.md index ac7a1299f2..979f721cb7 100644 --- a/.agents/skills/uloop-simulate-mouse-input/references/output-and-coordinates.md +++ b/.agents/skills/uloop-simulate-mouse-input/references/output-and-coordinates.md @@ -30,7 +30,7 @@ Verify visual outcome with a follow-up screenshot. - `--x` / `--y` use **top-left Game View coordinates**. - Raw image pixels from `uloop screenshot --capture-mode rendering` must be converted with `ScreenshotToInputFormula`. -- `AnnotatedElements[].SimX/SimY` can be passed directly to this tool. +- `AnnotatedElements[].SimX/SimY` can be passed directly to this tool. For UI Toolkit entries (`UiSystem` `UIToolkit`), a `Click` reaches the control only when the scene has an EventSystem with an Input System UI input module; without one, nothing receives the click. `simulate-mouse-ui` reaches them without an EventSystem. - Do not flip Y in the caller. The tool converts internally for Unity Input System: ```text diff --git a/.agents/skills/uloop-simulate-mouse-ui/SKILL.md b/.agents/skills/uloop-simulate-mouse-ui/SKILL.md index 88af8895ab..3d67862bbd 100644 --- a/.agents/skills/uloop-simulate-mouse-ui/SKILL.md +++ b/.agents/skills/uloop-simulate-mouse-ui/SKILL.md @@ -1,7 +1,7 @@ --- name: uloop-simulate-mouse-ui toolName: simulate-mouse-ui -description: "Simulate PlayMode EventSystem UI mouse actions using screen coordinates. Use for UI clicks, long-presses, or drags from annotated screenshots." +description: "Simulate PlayMode UI mouse actions on uGUI and UI Toolkit using screen coordinates. Use for UI clicks, long-presses, or drags from annotated screenshots." --- # Task @@ -64,6 +64,7 @@ uloop simulate-mouse-ui --action --x --y [options] - Get coordinates from `AnnotatedElements` JSON (`SimX`/`SimY`) — do NOT look up GameObject positions - `--bypass-raycast` still uses coordinates for pointer event positions, but chooses the clicked, long-pressed, or dragged GameObject by `--target-path` - If `--target-path` or `--drop-target-path` matches multiple active GameObjects, the command fails instead of choosing an arbitrary duplicate +- UI Toolkit entries (`UiSystem` `UIToolkit`) can be clicked, long-pressed, and dragged at `SimX`/`SimY`; `--bypass-raycast`, `--target-path`, and `--drop-target-path` take GameObjects only. Details: `references/ui-toolkit.md`. - Device Simulator play view is supported. Prefer `uloop screenshot --capture-mode rendering --annotate-elements` for coordinates; they use the simulated device resolution (`Handles.GetMainGameViewSize()` / `Screen`), not the Simulator chrome scale. ## Pause Point Inspection (Standard for E2E) @@ -85,12 +86,9 @@ uloop simulate-mouse-ui --action Drag --from-x 400 --from-y 300 --x 600 --y 300 # Long-press a button for 3 seconds uloop simulate-mouse-ui --action LongPress --x 400 --y 300 --duration 3.0 -# One-shot drag (start to end in one call) +# One-shot drag (add --drag-speed 200 to slow it for visual inspection) uloop simulate-mouse-ui --action Drag --from-x 400 --from-y 300 --x 600 --y 300 -# Slow drag for visual inspection -uloop simulate-mouse-ui --action Drag --from-x 400 --from-y 300 --x 600 --y 300 --drag-speed 200 - # Split drag with hold (for inspection between steps) uloop simulate-mouse-ui --action DragStart --x 400 --y 300 uloop screenshot --window-name Game @@ -101,14 +99,14 @@ uloop simulate-mouse-ui --action DragEnd --x 600 --y 300 ## Prerequisites - Unity must be in **PlayMode** -- Target scene must have an **EventSystem** GameObject -- UI elements must have a **GraphicRaycaster** on their Canvas +- uGUI targets need an **EventSystem** and a **GraphicRaycaster** on their Canvas; UI Toolkit targets need neither - If you need runtime mouse input rather than UI pointer events, `simulate-mouse-input` assumes the project uses the New Input System; otherwise prefer `execute-dynamic-code` ## Output The response reports `Success`, `Message`, `Action`, `HitGameObjectName`, the used -coordinates, and `InterruptedByPausePoint`/`PausePointHits`. Two readings that matter: +coordinates, and `InterruptedByPausePoint`/`PausePointHits`; `UiSystem` and `HitPath` say +which UI system was hit and give its path in the annotation's form. Two readings that matter: `Click`/`LongPress` on empty space still return `Success = true` with `HitGameObjectName = null` (drag actions on empty space fail), and on a pause-point interruption `Message` states whether the pointer event fired before the pause. diff --git a/.agents/skills/uloop-simulate-mouse-ui/references/output.md b/.agents/skills/uloop-simulate-mouse-ui/references/output.md index 779111873e..03086035fa 100644 --- a/.agents/skills/uloop-simulate-mouse-ui/references/output.md +++ b/.agents/skills/uloop-simulate-mouse-ui/references/output.md @@ -5,7 +5,9 @@ Returns JSON with: - `Success`: Whether the operation succeeded - `Message`: Status message (e.g. "Hit element: ButtonStart" or "No UI element under (x, y)") - `Action`: Echoes which action was executed (`Click`, `Drag`, `DragStart`, `DragMove`, `DragEnd`, or `LongPress`) -- `HitGameObjectName`: Name of the GameObject that received the pointer events (nullable string; null if nothing was hit). It is the event-handler target resolved up the hierarchy from the raycast hit (the first `IPointerDownHandler`, else the first `IPointerClickHandler`, for `Click`/`LongPress`; the first `IDragHandler` for drags), so it can be an ancestor of the topmost raycast hit — a Button's child Text reports the Button. With `--bypass-raycast` it is the handler resolved the same way from `--target-path` (also possibly an ancestor), not the raycast hit and not necessarily the raw path GameObject itself +- `HitGameObjectName`: Name of the GameObject that received the pointer events (nullable string; null if nothing was hit). It is the event-handler target resolved up the hierarchy from the raycast hit (the first `IPointerDownHandler`, else the first `IPointerClickHandler`, for `Click`/`LongPress`; the first `IDragHandler` for drags), so it can be an ancestor of the topmost raycast hit — a Button's child Text reports the Button. With `--bypass-raycast` it is the handler resolved the same way from `--target-path` (also possibly an ancestor), not the raycast hit and not necessarily the raw path GameObject itself. For a UI Toolkit hit it is the name of the outermost control an annotation would list (the same as the annotation's `Name`); when the picked element is not inside such a control (a bare `Label` or `VisualElement`), it is that element's own `name`, or its type name when empty, and the element is not annotated. Unlike uGUI, which reports null when no handler is found, UI Toolkit reports the picked element whether or not it handles pointer events +- `UiSystem`: Which UI system received the press (nullable string; `Click`/`LongPress` and drags, null if nothing was hit): `UGUI` for a target under a Canvas, `UIToolkit` for a UI Toolkit control, and `""` for a non-UI target such as a physics object +- `HitPath`: Path of the hit target in the form `AnnotatedElements[].Path` uses (nullable string; `Click`/`LongPress` and drags): a hierarchy path for GameObjects, `Document/Path#element` for UI Toolkit controls. Null when nothing was hit, or when the hit element belongs to no document, such as the menu of an open dropdown - `PositionX`: Target X coordinate that was used - `PositionY`: Target Y coordinate that was used - `EndPositionX`: Drag end X coordinate (nullable float; populated for drag actions only) @@ -15,3 +17,7 @@ Returns JSON with: Verify the visual outcome with a follow-up `uloop screenshot --capture-mode rendering --annotate-elements`. Note: Click and LongPress on empty space (no UI element) still return `Success = true` with `HitGameObjectName = null`. Drag actions on empty space return `Success = false`. + +## UI Toolkit Elements + +`Click`, `LongPress`, and the drag actions reach UI Toolkit controls at their annotated `SimX`/`SimY` (`--from-x`/`--from-y` for `Drag`). How presses and drags are delivered and what is not supported: `ui-toolkit.md`. diff --git a/.agents/skills/uloop-simulate-mouse-ui/references/ui-toolkit.md b/.agents/skills/uloop-simulate-mouse-ui/references/ui-toolkit.md new file mode 100644 index 0000000000..373a3deee1 --- /dev/null +++ b/.agents/skills/uloop-simulate-mouse-ui/references/ui-toolkit.md @@ -0,0 +1,61 @@ +# simulate-mouse-ui and UI Toolkit + +`Click`, `LongPress`, and the drag actions reach UI Toolkit controls on screen-space panels — +the `AnnotatedElements` entries with `UiSystem` `UIToolkit`. Pass the entry's `SimX`/`SimY` as +`--x`/`--y` (`--from-x`/`--from-y` for `Drag`); the response reports `UiSystem` `UIToolkit`, the +control's `Name` as `HitGameObjectName`, and its annotation `Path` as `HitPath`. + +## How a press is delivered + +- The point is mapped to each panel the same way the screenshot annotation maps controls to + `SimX`/`SimY`, so an annotated point lands on the annotated control. It does not depend on + which Editor view was drawn last. +- Panels are searched front to back by their panel settings' `sortingOrder`; the first panel + with a control under the point gets the press. The reported control is the outermost one an + annotation would list, so a click on a scrollbar's thumb reports the scrollbar. +- Where a uGUI element and a UI Toolkit panel are both under the point, the uGUI element wins + only when its Canvas sorts above the panel (raycaster sort priority greater than the panel's + `sortingOrder`), the same rule the EventSystem applies to real input. A uGUI image without + any pointer handler in front still blocks the panel, as real input would. +- A full-screen container left at the default `pickingMode` (`Position`) counts as a hit, so + uGUI underneath it is not reached — real input behaves the same way. +- UI Toolkit presses do not need an EventSystem. With neither an EventSystem nor a UI Toolkit + control under the point, the command fails and says so. +- A disabled control receives the press but does not react, as with real input. + +## LongPress + +A UI Toolkit `Button` fires `clicked` once when the long press is released (UI Toolkit's +`Clickable` behaviour); a uGUI long press sends no click. A `RepeatButton` fires repeatedly +while held. + +## Drags + +- A drag whose start point resolves to a UI Toolkit control (by the rule above) presses it + with the left button, moves the pointer, and releases at the end, so a `Slider` or a + `Scroller` follows the drag. The start point is what counts: the drag stays with that panel + wherever it moves. +- A labelled slider's bounds include its label; start the drag on the track. +- `--drop-target-path` applies to uGUI drags only. A `Drag` with it fails before anything is + sent; a `DragEnd` with it fails and leaves the drag in progress, so call `DragEnd` again + without it. +- If the control's document is disabled or destroyed during a split drag, the next `DragMove` + or `DragEnd` fails and the drag ends. A one-shot `Drag` stops sending and still succeeds. +- If a UI Toolkit handler throws, the button is released and the exception ends the command. + A `DragMove` that throws leaves the drag in progress, as a cancelled `DragMove` does. + +## Not supported + +- `--bypass-raycast`, `--target-path`, and `--drop-target-path` take GameObject paths only; + a UI Toolkit `Path` (it contains `#`) is reported as not found. +- World-space panels (Unity 6), as in the annotation. +- Two panels with the same `sortingOrder` overlapping at the point: Unity orders them by + creation order, which is not exposed, so either one may be chosen. + +## Notes + +- Move the real mouse cursor out of the Game View while simulating, and keep it away between + `DragStart` and `DragEnd`. Real pointer input over the Game View reaches the same panels and + can interleave with the simulated press or move a held slider. +- Device Simulator has the same limits as the annotation: coordinates use the simulated device + resolution. diff --git a/.claude/skills/uloop-screenshot/SKILL.md b/.claude/skills/uloop-screenshot/SKILL.md index 422a7ec90f..116b4af479 100644 --- a/.claude/skills/uloop-screenshot/SKILL.md +++ b/.claude/skills/uloop-screenshot/SKILL.md @@ -23,7 +23,7 @@ uloop screenshot [--window-name ] [--resolution-scale ] [--match-mo | `--match-mode` | enum | `exact` | Window name matching mode: `exact`, `prefix`, or `contains`. Ignored when the resolved capture mode is rendering (auto during Play Mode resolves to rendering). | | `--capture-mode` | enum | `auto` | `auto` - rendering in PlayMode and window otherwise, `window` - capture EditorWindow including toolbar, `rendering` - capture game rendering only (PlayMode required), `GameView` - alias for `rendering`. Rendering screenshots return `ScreenshotToInputFormula` for converting raw image pixels before calling simulate-mouse-input (including `--dry-run`) or simulate-mouse-ui. | | `--output-directory` | string | `""` | Output directory path for saving screenshots. When empty, uses default path (.uloop/outputs/Screenshots/). Accepts absolute paths. | -| `--annotate-elements` | flag | - | Annotate interactive UI elements with index labels and interaction hints (A / CLICK, B / DRAG, ...). The response includes an `AnnotatedElements` array with element metadata sorted by `SortingOrder` then `SiblingIndex` (an approximation of z-order, not the guaranteed draw order). Only works when the resolved capture mode is rendering (auto during Play Mode resolves to rendering) in PlayMode. | +| `--annotate-elements` | flag | - | Annotate interactive uGUI elements and UI Toolkit controls on screen-space panels with index labels and interaction hints (A / CLICK, B / DRAG, ...). The response includes an `AnnotatedElements` array with element metadata sorted by `SortingOrder` then `SiblingIndex` (an approximation of z-order, not the guaranteed draw order). Only works when the resolved capture mode is rendering (auto during Play Mode resolves to rendering) in PlayMode. | | `--annotate-raycast-grid` | flag | - | Annotate clustered 3D physics collider candidates as `PhysicsCollider` entries in `AnnotatedElements`. Uses `Camera.main` visibility and the same top-left Game View coordinates as `simulate-mouse-input`. Only works when the resolved capture mode is rendering (auto during Play Mode resolves to rendering) in PlayMode. | | `--raycast-layer-mask` | string | `""` | Comma-separated physics layer names to narrow which layers `--annotate-raycast-grid` clusters. Hits are limited to layers also visible to `Camera.main.cullingMask`. When omitted, clusters against `Physics.DefaultRaycastLayers`. | | `--elements-only` | flag | - | Return only annotated element JSON without capturing a screenshot image. Requires `--annotate-elements` or `--annotate-raycast-grid`, and the resolved capture mode to be rendering (auto during Play Mode resolves to rendering) in PlayMode. | diff --git a/.claude/skills/uloop-screenshot/references/annotated-elements.md b/.claude/skills/uloop-screenshot/references/annotated-elements.md index 1ef3f771da..69272a2bb2 100644 --- a/.claude/skills/uloop-screenshot/references/annotated-elements.md +++ b/.claude/skills/uloop-screenshot/references/annotated-elements.md @@ -10,15 +10,16 @@ Device Simulator is supported for this flow: prefer `--capture-mode rendering` ( - `Label`: Index label in JSON (`A` = first in that sort, `B` = next, ...). Treat it as a stable handle, not as proof of what is actually in front: when overlapping elements cross parents, pick the target by `Path` and confirm the result with `simulate-mouse-ui`'s `HitGameObjectName` — the handler that received the events, resolved up the hierarchy from the raycast hit (or from `--target-path` in bypass mode), so it can name an ancestor of the element you clicked. Screenshot labels also include the interaction hint, such as `A / CLICK` or `B / DRAG`. - `Name`: Element name -- `Path`: Hierarchy path from the scene root, for example `Canvas/Panel/Button`. Use this as `simulate-mouse-ui --target-path` when bypassing raycast blockers. +- `Path`: Hierarchy path from the scene root, for example `Canvas/Panel/Button`. Use this as `simulate-mouse-ui --target-path` when bypassing raycast blockers. UI Toolkit entries use a different form that `--target-path` does not accept; click them at `SimX`/`SimY` instead. See [UI Toolkit Elements](#ui-toolkit-elements). - `Type`: Element type (`Button`, `Toggle`, `Slider`, `Dropdown`, `InputField`, `Scrollbar`, `Draggable`, `DropTarget`, `Selectable`, `PhysicsCollider`) - `Interaction`: Derived interaction category (`Click`, `Drag`, `Drop`, `Text`) or `Raycast` for clustered physics collider entries. Use this to choose between `simulate-mouse-ui --action Click`, drag actions, or `simulate-mouse-input` (including `--dry-run`). +- `UiSystem`: UI system the entry came from: `UGUI`, `UIToolkit`, or empty for `PhysicsCollider` entries. - `Layer`: Physics layer name for `PhysicsCollider` entries. Empty for UI entries. - `Components`: Collider and MonoBehaviour component type names from the hit GameObject for `PhysicsCollider` entries. Empty for UI entries. - `SimX`, `SimY`: Target click position in top-left Game View coordinates. For UI entries this is the element center when that center is raycast-reachable; otherwise it is a reachable probe point inside the bounds (center, then four interior quarter points). For `PhysicsCollider` entries this is a representative sampled hit. Use these directly with `simulate-mouse-ui --x/--y` or `simulate-mouse-input --x/--y` (`--dry-run` optional). - `BoundsMinX`, `BoundsMinY`, `BoundsMaxX`, `BoundsMaxY`: Bounding box in the same coordinates as `SimX/SimY`. For `PhysicsCollider` entries, this is the axis-aligned sampled-cell coverage box from reachable raycast hits, not a guarantee that every interior point is clickable. -- `SortingOrder`: Canvas sorting order. Higher values are in front. -- `SiblingIndex`: Transform sibling index under the element's direct parent. Do not use it as a reliable z-order signal across nested UI hierarchies; the frontmost-first sort above can then disagree with draw order. +- `SortingOrder`: Canvas sorting order (for UI Toolkit, the panel's `PanelSettings.sortingOrder` rounded down). Higher values are in front. +- `SiblingIndex`: Transform sibling index under the element's direct parent (for UI Toolkit, the index in the parent visual element). Do not use it as a reliable z-order signal across nested UI hierarchies; the frontmost-first sort above can then disagree with draw order. ## Exclusion rules (by design) @@ -29,6 +30,33 @@ The annotator lists elements that share the same EventSystem raycast path as `si - Elements whose center and four interior quarter probe points are all covered by another raycast hit are not listed. - Selectables that are not interactable (`interactable = false`, or a parent `CanvasGroup` with `interactable = false`) ignore clicks and drags, so they are not listed. A separate pointer handler script on the same GameObject is still listed as that handler. +## UI Toolkit Elements + +Entries with `UiSystem` `UIToolkit` come from the `UIDocument`s on screen-space panels shown in the Game View. They use the same fields and top-left Game View coordinates as uGUI entries. + +- `Type`: `Button` (`Button`, `RepeatButton`), `Toggle` (`Toggle` and other boolean fields), `Slider` (`Slider`, `SliderInt`, `MinMaxSlider`), `Scrollbar` (`Scroller`, including the scrollers of a `ScrollView`), `InputField` (`TextField`), `Dropdown` (`DropdownField`, `EnumField`). `Interaction` follows from `Type` as for uGUI. +- `Path`: `#`, for example `UI/Menu#panel/play`. The chain starts below the document root and joins element names with `/`; an element without a name appears as its type name. + +Omitted on purpose: + +- Disabled elements, `visibility: hidden`, `display: none` (with everything under it), and zero-size elements. +- Elements covered at the center and every interior quarter probe point by a panel with a higher sorting order or by another element of the same panel, including parts clipped away by a parent with `overflow: hidden`. +- Panels that render into a RenderTexture, panels on a display other than the Game View's, world-space panels (Unity 6), and panels whose screen-to-panel mapping is not a plain scale and offset. +- Parts inside a control (the slider and buttons of a `Scroller`, for example) and any element that is not one of the types above. A custom element with only a `Clickable` manipulator is not listed. + +Limits: + +- When uGUI and UI Toolkit overlap, which one is in front is not checked; both may be listed. +- Panels with the same sorting order are not checked against each other. +- `Bounds` is the whole element rectangle and can include parts clipped away by a parent. +- Unity 2022.3 with both Device Simulator and Game View open lays UI Toolkit panels out at the Game View size and stretches them onto the simulated screen. Bounds and `SimX`/`SimY` then match where clicks land but not the PNG, and `set-game-view-size` cannot change the hidden Game View while Simulator is shown. Closing the Game View while using Simulator makes them match. + +Clicking: + +- `simulate-mouse-ui --action Click --x --y ` (or `LongPress`, or a drag from that point) reaches UI Toolkit controls, with or without an EventSystem. Its `--target-path` does not accept UI Toolkit `Path` values. +- `simulate-mouse-input --action Click --x --y ` also reaches them when the scene has an EventSystem with an Input System UI input module. +- Without an EventSystem the `simulate-mouse-input` click does not reach UI Toolkit controls. + ### PhysicsCollider Entries Per Closed Region When a single `PhysicsCollider` GameObject's reachable raycast samples form multiple 4-connected regions on screen — typically because UI occlusion splits them apart — each region becomes its own `AnnotatedElements` entry. `Path`, `Name`, `Layer`, and `Components` are identical across those entries (they describe the same GameObject), while `Label`, `Bounds`, `SimX`, `SimY`, and `RaycastOutlineSegments` are independent per region so each closed area is separately addressable. diff --git a/.claude/skills/uloop-simulate-mouse-input/SKILL.md b/.claude/skills/uloop-simulate-mouse-input/SKILL.md index b4e63a2c51..41723aa257 100644 --- a/.claude/skills/uloop-simulate-mouse-input/SKILL.md +++ b/.claude/skills/uloop-simulate-mouse-input/SKILL.md @@ -67,7 +67,7 @@ All rows below assume the New Input System is installed. | Scenario | Tool | |----------|------| -| Click a Unity UI Button (IPointerClickHandler) | `simulate-mouse-ui` | +| Click a uGUI or UI Toolkit button | `simulate-mouse-ui` | | Runtime logic reads `Mouse.current.leftButton` | `simulate-mouse-input` | | Runtime logic reads right-click | `simulate-mouse-input --button Right` | | Drag a UI slider | `simulate-mouse-ui --action Drag` | @@ -110,7 +110,7 @@ uloop simulate-mouse-input --action SmoothDelta --delta-x 300 --delta-y 0 --dura ## Coordinate System `--x`/`--y` use top-left Game View coordinates: pass `AnnotatedElements[].SimX/SimY` -directly, convert raw screenshot pixels with `ScreenshotToInputFormula`, and never flip Y +directly (UI Toolkit entries need an EventSystem with an Input System UI module here; `simulate-mouse-ui` reaches them without one), convert raw screenshot pixels with `ScreenshotToInputFormula`, and never flip Y in the caller (the tool converts internally). Conversion formula, Device Simulator notes, and how the injected position reads back inside Unity are in `references/output-and-coordinates.md`. diff --git a/.claude/skills/uloop-simulate-mouse-input/references/output-and-coordinates.md b/.claude/skills/uloop-simulate-mouse-input/references/output-and-coordinates.md index ac7a1299f2..979f721cb7 100644 --- a/.claude/skills/uloop-simulate-mouse-input/references/output-and-coordinates.md +++ b/.claude/skills/uloop-simulate-mouse-input/references/output-and-coordinates.md @@ -30,7 +30,7 @@ Verify visual outcome with a follow-up screenshot. - `--x` / `--y` use **top-left Game View coordinates**. - Raw image pixels from `uloop screenshot --capture-mode rendering` must be converted with `ScreenshotToInputFormula`. -- `AnnotatedElements[].SimX/SimY` can be passed directly to this tool. +- `AnnotatedElements[].SimX/SimY` can be passed directly to this tool. For UI Toolkit entries (`UiSystem` `UIToolkit`), a `Click` reaches the control only when the scene has an EventSystem with an Input System UI input module; without one, nothing receives the click. `simulate-mouse-ui` reaches them without an EventSystem. - Do not flip Y in the caller. The tool converts internally for Unity Input System: ```text diff --git a/.claude/skills/uloop-simulate-mouse-ui/SKILL.md b/.claude/skills/uloop-simulate-mouse-ui/SKILL.md index 88af8895ab..3d67862bbd 100644 --- a/.claude/skills/uloop-simulate-mouse-ui/SKILL.md +++ b/.claude/skills/uloop-simulate-mouse-ui/SKILL.md @@ -1,7 +1,7 @@ --- name: uloop-simulate-mouse-ui toolName: simulate-mouse-ui -description: "Simulate PlayMode EventSystem UI mouse actions using screen coordinates. Use for UI clicks, long-presses, or drags from annotated screenshots." +description: "Simulate PlayMode UI mouse actions on uGUI and UI Toolkit using screen coordinates. Use for UI clicks, long-presses, or drags from annotated screenshots." --- # Task @@ -64,6 +64,7 @@ uloop simulate-mouse-ui --action --x --y [options] - Get coordinates from `AnnotatedElements` JSON (`SimX`/`SimY`) — do NOT look up GameObject positions - `--bypass-raycast` still uses coordinates for pointer event positions, but chooses the clicked, long-pressed, or dragged GameObject by `--target-path` - If `--target-path` or `--drop-target-path` matches multiple active GameObjects, the command fails instead of choosing an arbitrary duplicate +- UI Toolkit entries (`UiSystem` `UIToolkit`) can be clicked, long-pressed, and dragged at `SimX`/`SimY`; `--bypass-raycast`, `--target-path`, and `--drop-target-path` take GameObjects only. Details: `references/ui-toolkit.md`. - Device Simulator play view is supported. Prefer `uloop screenshot --capture-mode rendering --annotate-elements` for coordinates; they use the simulated device resolution (`Handles.GetMainGameViewSize()` / `Screen`), not the Simulator chrome scale. ## Pause Point Inspection (Standard for E2E) @@ -85,12 +86,9 @@ uloop simulate-mouse-ui --action Drag --from-x 400 --from-y 300 --x 600 --y 300 # Long-press a button for 3 seconds uloop simulate-mouse-ui --action LongPress --x 400 --y 300 --duration 3.0 -# One-shot drag (start to end in one call) +# One-shot drag (add --drag-speed 200 to slow it for visual inspection) uloop simulate-mouse-ui --action Drag --from-x 400 --from-y 300 --x 600 --y 300 -# Slow drag for visual inspection -uloop simulate-mouse-ui --action Drag --from-x 400 --from-y 300 --x 600 --y 300 --drag-speed 200 - # Split drag with hold (for inspection between steps) uloop simulate-mouse-ui --action DragStart --x 400 --y 300 uloop screenshot --window-name Game @@ -101,14 +99,14 @@ uloop simulate-mouse-ui --action DragEnd --x 600 --y 300 ## Prerequisites - Unity must be in **PlayMode** -- Target scene must have an **EventSystem** GameObject -- UI elements must have a **GraphicRaycaster** on their Canvas +- uGUI targets need an **EventSystem** and a **GraphicRaycaster** on their Canvas; UI Toolkit targets need neither - If you need runtime mouse input rather than UI pointer events, `simulate-mouse-input` assumes the project uses the New Input System; otherwise prefer `execute-dynamic-code` ## Output The response reports `Success`, `Message`, `Action`, `HitGameObjectName`, the used -coordinates, and `InterruptedByPausePoint`/`PausePointHits`. Two readings that matter: +coordinates, and `InterruptedByPausePoint`/`PausePointHits`; `UiSystem` and `HitPath` say +which UI system was hit and give its path in the annotation's form. Two readings that matter: `Click`/`LongPress` on empty space still return `Success = true` with `HitGameObjectName = null` (drag actions on empty space fail), and on a pause-point interruption `Message` states whether the pointer event fired before the pause. diff --git a/.claude/skills/uloop-simulate-mouse-ui/references/output.md b/.claude/skills/uloop-simulate-mouse-ui/references/output.md index 779111873e..03086035fa 100644 --- a/.claude/skills/uloop-simulate-mouse-ui/references/output.md +++ b/.claude/skills/uloop-simulate-mouse-ui/references/output.md @@ -5,7 +5,9 @@ Returns JSON with: - `Success`: Whether the operation succeeded - `Message`: Status message (e.g. "Hit element: ButtonStart" or "No UI element under (x, y)") - `Action`: Echoes which action was executed (`Click`, `Drag`, `DragStart`, `DragMove`, `DragEnd`, or `LongPress`) -- `HitGameObjectName`: Name of the GameObject that received the pointer events (nullable string; null if nothing was hit). It is the event-handler target resolved up the hierarchy from the raycast hit (the first `IPointerDownHandler`, else the first `IPointerClickHandler`, for `Click`/`LongPress`; the first `IDragHandler` for drags), so it can be an ancestor of the topmost raycast hit — a Button's child Text reports the Button. With `--bypass-raycast` it is the handler resolved the same way from `--target-path` (also possibly an ancestor), not the raycast hit and not necessarily the raw path GameObject itself +- `HitGameObjectName`: Name of the GameObject that received the pointer events (nullable string; null if nothing was hit). It is the event-handler target resolved up the hierarchy from the raycast hit (the first `IPointerDownHandler`, else the first `IPointerClickHandler`, for `Click`/`LongPress`; the first `IDragHandler` for drags), so it can be an ancestor of the topmost raycast hit — a Button's child Text reports the Button. With `--bypass-raycast` it is the handler resolved the same way from `--target-path` (also possibly an ancestor), not the raycast hit and not necessarily the raw path GameObject itself. For a UI Toolkit hit it is the name of the outermost control an annotation would list (the same as the annotation's `Name`); when the picked element is not inside such a control (a bare `Label` or `VisualElement`), it is that element's own `name`, or its type name when empty, and the element is not annotated. Unlike uGUI, which reports null when no handler is found, UI Toolkit reports the picked element whether or not it handles pointer events +- `UiSystem`: Which UI system received the press (nullable string; `Click`/`LongPress` and drags, null if nothing was hit): `UGUI` for a target under a Canvas, `UIToolkit` for a UI Toolkit control, and `""` for a non-UI target such as a physics object +- `HitPath`: Path of the hit target in the form `AnnotatedElements[].Path` uses (nullable string; `Click`/`LongPress` and drags): a hierarchy path for GameObjects, `Document/Path#element` for UI Toolkit controls. Null when nothing was hit, or when the hit element belongs to no document, such as the menu of an open dropdown - `PositionX`: Target X coordinate that was used - `PositionY`: Target Y coordinate that was used - `EndPositionX`: Drag end X coordinate (nullable float; populated for drag actions only) @@ -15,3 +17,7 @@ Returns JSON with: Verify the visual outcome with a follow-up `uloop screenshot --capture-mode rendering --annotate-elements`. Note: Click and LongPress on empty space (no UI element) still return `Success = true` with `HitGameObjectName = null`. Drag actions on empty space return `Success = false`. + +## UI Toolkit Elements + +`Click`, `LongPress`, and the drag actions reach UI Toolkit controls at their annotated `SimX`/`SimY` (`--from-x`/`--from-y` for `Drag`). How presses and drags are delivered and what is not supported: `ui-toolkit.md`. diff --git a/.claude/skills/uloop-simulate-mouse-ui/references/ui-toolkit.md b/.claude/skills/uloop-simulate-mouse-ui/references/ui-toolkit.md new file mode 100644 index 0000000000..373a3deee1 --- /dev/null +++ b/.claude/skills/uloop-simulate-mouse-ui/references/ui-toolkit.md @@ -0,0 +1,61 @@ +# simulate-mouse-ui and UI Toolkit + +`Click`, `LongPress`, and the drag actions reach UI Toolkit controls on screen-space panels — +the `AnnotatedElements` entries with `UiSystem` `UIToolkit`. Pass the entry's `SimX`/`SimY` as +`--x`/`--y` (`--from-x`/`--from-y` for `Drag`); the response reports `UiSystem` `UIToolkit`, the +control's `Name` as `HitGameObjectName`, and its annotation `Path` as `HitPath`. + +## How a press is delivered + +- The point is mapped to each panel the same way the screenshot annotation maps controls to + `SimX`/`SimY`, so an annotated point lands on the annotated control. It does not depend on + which Editor view was drawn last. +- Panels are searched front to back by their panel settings' `sortingOrder`; the first panel + with a control under the point gets the press. The reported control is the outermost one an + annotation would list, so a click on a scrollbar's thumb reports the scrollbar. +- Where a uGUI element and a UI Toolkit panel are both under the point, the uGUI element wins + only when its Canvas sorts above the panel (raycaster sort priority greater than the panel's + `sortingOrder`), the same rule the EventSystem applies to real input. A uGUI image without + any pointer handler in front still blocks the panel, as real input would. +- A full-screen container left at the default `pickingMode` (`Position`) counts as a hit, so + uGUI underneath it is not reached — real input behaves the same way. +- UI Toolkit presses do not need an EventSystem. With neither an EventSystem nor a UI Toolkit + control under the point, the command fails and says so. +- A disabled control receives the press but does not react, as with real input. + +## LongPress + +A UI Toolkit `Button` fires `clicked` once when the long press is released (UI Toolkit's +`Clickable` behaviour); a uGUI long press sends no click. A `RepeatButton` fires repeatedly +while held. + +## Drags + +- A drag whose start point resolves to a UI Toolkit control (by the rule above) presses it + with the left button, moves the pointer, and releases at the end, so a `Slider` or a + `Scroller` follows the drag. The start point is what counts: the drag stays with that panel + wherever it moves. +- A labelled slider's bounds include its label; start the drag on the track. +- `--drop-target-path` applies to uGUI drags only. A `Drag` with it fails before anything is + sent; a `DragEnd` with it fails and leaves the drag in progress, so call `DragEnd` again + without it. +- If the control's document is disabled or destroyed during a split drag, the next `DragMove` + or `DragEnd` fails and the drag ends. A one-shot `Drag` stops sending and still succeeds. +- If a UI Toolkit handler throws, the button is released and the exception ends the command. + A `DragMove` that throws leaves the drag in progress, as a cancelled `DragMove` does. + +## Not supported + +- `--bypass-raycast`, `--target-path`, and `--drop-target-path` take GameObject paths only; + a UI Toolkit `Path` (it contains `#`) is reported as not found. +- World-space panels (Unity 6), as in the annotation. +- Two panels with the same `sortingOrder` overlapping at the point: Unity orders them by + creation order, which is not exposed, so either one may be chosen. + +## Notes + +- Move the real mouse cursor out of the Game View while simulating, and keep it away between + `DragStart` and `DragEnd`. Real pointer input over the Game View reaches the same panels and + can interleave with the simulated press or move a held slider. +- Device Simulator has the same limits as the annotation: coordinates use the simulated device + resolution. diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index d87e245abc..c6a199d3ee 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -201,6 +201,7 @@ jobs: $null = [scriptblock]::Create((Get-Content -Raw -Encoding UTF8 scripts/test-check-release-installer.ps1)) $null = [scriptblock]::Create((Get-Content -Raw -Encoding UTF8 scripts/run-windows-e2e.ps1)) $null = [scriptblock]::Create((Get-Content -Raw -Encoding UTF8 scripts/test-simulate-mouse-demo.ps1)) + $null = [scriptblock]::Create((Get-Content -Raw -Encoding UTF8 scripts/test-screenshot-uitoolkit-annotation.ps1)) $null = [scriptblock]::Create((Get-Content -Raw -Encoding UTF8 Assets/Tests/Demo/scripts/verify-replay-via-cli.ps1)) $null = [scriptblock]::Create((Get-Content -Raw -Encoding UTF8 scripts/test-powershell51-native-argument-escaping.ps1)) $null = [scriptblock]::Create((Get-Content -Raw -Encoding UTF8 scripts/test-install-pin-resolution.ps1)) diff --git a/Assets/Scenes/UIToolkitAnnotationDemoScene.unity b/Assets/Scenes/UIToolkitAnnotationDemoScene.unity new file mode 100644 index 0000000000..39e854c0e5 --- /dev/null +++ b/Assets/Scenes/UIToolkitAnnotationDemoScene.unity @@ -0,0 +1,740 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!29 &1 +OcclusionCullingSettings: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_OcclusionBakeSettings: + smallestOccluder: 5 + smallestHole: 0.25 + backfaceThreshold: 100 + m_SceneGUID: 00000000000000000000000000000000 + m_OcclusionCullingData: {fileID: 0} +--- !u!104 &2 +RenderSettings: + m_ObjectHideFlags: 0 + serializedVersion: 9 + m_Fog: 0 + m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1} + m_FogMode: 3 + m_FogDensity: 0.01 + m_LinearFogStart: 0 + m_LinearFogEnd: 300 + m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1} + m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1} + m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1} + m_AmbientIntensity: 1 + m_AmbientMode: 0 + m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1} + m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0} + m_HaloStrength: 0.5 + m_FlareStrength: 1 + m_FlareFadeSpeed: 3 + m_HaloTexture: {fileID: 0} + m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0} + m_DefaultReflectionMode: 0 + m_DefaultReflectionResolution: 128 + m_ReflectionBounces: 1 + m_ReflectionIntensity: 1 + m_CustomReflection: {fileID: 0} + m_Sun: {fileID: 0} + m_UseRadianceAmbientProbe: 0 +--- !u!157 &3 +LightmapSettings: + m_ObjectHideFlags: 0 + serializedVersion: 12 + m_GIWorkflowMode: 1 + m_GISettings: + serializedVersion: 2 + m_BounceScale: 1 + m_IndirectOutputScale: 1 + m_AlbedoBoost: 1 + m_EnvironmentLightingMode: 0 + m_EnableBakedLightmaps: 1 + m_EnableRealtimeLightmaps: 0 + m_LightmapEditorSettings: + serializedVersion: 12 + m_Resolution: 2 + m_BakeResolution: 40 + m_AtlasSize: 1024 + m_AO: 0 + m_AOMaxDistance: 1 + m_CompAOExponent: 1 + m_CompAOExponentDirect: 0 + m_ExtractAmbientOcclusion: 0 + m_Padding: 2 + m_LightmapParameters: {fileID: 0} + m_LightmapsBakeMode: 1 + m_TextureCompression: 1 + m_FinalGather: 0 + m_FinalGatherFiltering: 1 + m_FinalGatherRayCount: 256 + m_ReflectionCompression: 2 + m_MixedBakeMode: 2 + m_BakeBackend: 1 + m_PVRSampling: 1 + m_PVRDirectSampleCount: 32 + m_PVRSampleCount: 512 + m_PVRBounces: 2 + m_PVREnvironmentSampleCount: 256 + m_PVREnvironmentReferencePointCount: 2048 + m_PVRFilteringMode: 1 + m_PVRDenoiserTypeDirect: 1 + m_PVRDenoiserTypeIndirect: 1 + m_PVRDenoiserTypeAO: 1 + m_PVRFilterTypeDirect: 0 + m_PVRFilterTypeIndirect: 0 + m_PVRFilterTypeAO: 0 + m_PVREnvironmentMIS: 1 + m_PVRCulling: 1 + m_PVRFilteringGaussRadiusDirect: 1 + m_PVRFilteringGaussRadiusIndirect: 5 + m_PVRFilteringGaussRadiusAO: 2 + m_PVRFilteringAtrousPositionSigmaDirect: 0.5 + m_PVRFilteringAtrousPositionSigmaIndirect: 2 + m_PVRFilteringAtrousPositionSigmaAO: 1 + m_ExportTrainingData: 0 + m_TrainingDataDestination: TrainingData + m_LightProbeSampleCountMultiplier: 4 + m_LightingDataAsset: {fileID: 0} + m_LightingSettings: {fileID: 0} +--- !u!196 &4 +NavMeshSettings: + serializedVersion: 2 + m_ObjectHideFlags: 0 + m_BuildSettings: + serializedVersion: 3 + agentTypeID: 0 + agentRadius: 0.5 + agentHeight: 2 + agentSlope: 45 + agentClimb: 0.4 + ledgeDropHeight: 0 + maxJumpAcrossDistance: 0 + minRegionArea: 2 + manualCellSize: 0 + cellSize: 0.16666667 + manualTileSize: 0 + tileSize: 256 + buildHeightMesh: 0 + maxJobWorkers: 0 + preserveTilesOutsideBounds: 0 + debug: + m_Flags: 0 + m_NavMeshData: {fileID: 0} +--- !u!1 &824351698 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 824351700} + - component: {fileID: 824351699} + m_Layer: 0 + m_Name: FrontDocument + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &824351699 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 824351698} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 19102, guid: 0000000000000000e000000000000000, type: 0} + m_Name: + m_EditorClassIdentifier: + m_PanelSettings: {fileID: 11400000, guid: 7ba283d20dd0f429f952f9b6bc6f1546, type: 2} + m_ParentUI: {fileID: 0} + sourceAsset: {fileID: 0} + m_SortingOrder: 0 +--- !u!4 &824351700 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 824351698} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 926978105} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &888756792 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 888756793} + - component: {fileID: 888756795} + - component: {fileID: 888756794} + m_Layer: 0 + m_Name: Text (Legacy) + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &888756793 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 888756792} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 1115416529} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &888756794 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 888756792} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 14 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 10 + m_MaxSize: 40 + m_Alignment: 4 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: Button +--- !u!222 &888756795 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 888756792} + m_CullTransparentMesh: 1 +--- !u!1 &926978103 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 926978105} + - component: {fileID: 926978104} + m_Layer: 0 + m_Name: UIToolkitAnnotationDemo + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &926978104 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 926978103} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: b288f11987e9b4656905feabfc18ef89, type: 3} + m_Name: + m_EditorClassIdentifier: + backDocument: {fileID: 1341402965} + frontDocument: {fileID: 824351699} +--- !u!4 &926978105 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 926978103} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 1341402966} + - {fileID: 824351700} + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &1115416528 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1115416529} + - component: {fileID: 1115416532} + - component: {fileID: 1115416531} + - component: {fileID: 1115416530} + m_Layer: 0 + m_Name: ugui-button + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1115416529 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1115416528} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 888756793} + m_Father: {fileID: 1546344952} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 1, y: 0} + m_AnchorMax: {x: 1, y: 0} + m_AnchoredPosition: {x: -40, y: 40} + m_SizeDelta: {x: 200, y: 50} + m_Pivot: {x: 1, y: 0} +--- !u!114 &1115416530 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1115416528} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Navigation: + m_Mode: 3 + m_WrapAround: 0 + m_SelectOnUp: {fileID: 0} + m_SelectOnDown: {fileID: 0} + m_SelectOnLeft: {fileID: 0} + m_SelectOnRight: {fileID: 0} + m_Transition: 1 + m_Colors: + m_NormalColor: {r: 1, g: 1, b: 1, a: 1} + m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} + m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608} + m_ColorMultiplier: 1 + m_FadeDuration: 0.1 + m_SpriteState: + m_HighlightedSprite: {fileID: 0} + m_PressedSprite: {fileID: 0} + m_SelectedSprite: {fileID: 0} + m_DisabledSprite: {fileID: 0} + m_AnimationTriggers: + m_NormalTrigger: Normal + m_HighlightedTrigger: Highlighted + m_PressedTrigger: Pressed + m_SelectedTrigger: Selected + m_DisabledTrigger: Disabled + m_Interactable: 1 + m_TargetGraphic: {fileID: 1115416531} + m_OnClick: + m_PersistentCalls: + m_Calls: [] +--- !u!114 &1115416531 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1115416528} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!222 &1115416532 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1115416528} + m_CullTransparentMesh: 1 +--- !u!1 &1341402964 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1341402966} + - component: {fileID: 1341402965} + m_Layer: 0 + m_Name: BackDocument + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &1341402965 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1341402964} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 19102, guid: 0000000000000000e000000000000000, type: 0} + m_Name: + m_EditorClassIdentifier: + m_PanelSettings: {fileID: 11400000, guid: 34868ecccc6b142e6a841a062a97a6bd, type: 2} + m_ParentUI: {fileID: 0} + sourceAsset: {fileID: 0} + m_SortingOrder: 0 +--- !u!4 &1341402966 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1341402964} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 926978105} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &1378620445 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1378620448} + - component: {fileID: 1378620447} + - component: {fileID: 1378620446} + m_Layer: 0 + m_Name: EventSystem + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &1378620446 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1378620445} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 01614664b831546d2ae94a42149d80ac, type: 3} + m_Name: + m_EditorClassIdentifier: + m_SendPointerHoverToParent: 1 + m_MoveRepeatDelay: 0.5 + m_MoveRepeatRate: 0.1 + m_XRTrackingOrigin: {fileID: 0} + m_ActionsAsset: {fileID: -944628639613478452, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} + m_PointAction: {fileID: -1654692200621890270, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} + m_MoveAction: {fileID: -8784545083839296357, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} + m_SubmitAction: {fileID: 392368643174621059, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} + m_CancelAction: {fileID: 7727032971491509709, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} + m_LeftClickAction: {fileID: 3001919216989983466, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} + m_MiddleClickAction: {fileID: -2185481485913320682, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} + m_RightClickAction: {fileID: -4090225696740746782, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} + m_ScrollWheelAction: {fileID: 6240969308177333660, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} + m_TrackedDevicePositionAction: {fileID: 6564999863303420839, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} + m_TrackedDeviceOrientationAction: {fileID: 7970375526676320489, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} + m_DeselectOnBackgroundClick: 1 + m_PointerBehavior: 0 + m_CursorLockBehavior: 0 + m_ScrollDeltaPerTick: 6 +--- !u!114 &1378620447 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1378620445} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 76c392e42b5098c458856cdf6ecaaaa1, type: 3} + m_Name: + m_EditorClassIdentifier: + m_FirstSelected: {fileID: 0} + m_sendNavigationEvents: 1 + m_DragThreshold: 10 +--- !u!4 &1378620448 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1378620445} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &1546344948 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1546344952} + - component: {fileID: 1546344951} + - component: {fileID: 1546344950} + - component: {fileID: 1546344949} + m_Layer: 0 + m_Name: Canvas + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &1546344949 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1546344948} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: dc42784cf147c0c48a680349fa168899, type: 3} + m_Name: + m_EditorClassIdentifier: + m_IgnoreReversedGraphics: 1 + m_BlockingObjects: 0 + m_BlockingMask: + serializedVersion: 2 + m_Bits: 4294967295 +--- !u!114 &1546344950 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1546344948} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 0cd44c1031e13a943bb63640046fad76, type: 3} + m_Name: + m_EditorClassIdentifier: + m_UiScaleMode: 0 + m_ReferencePixelsPerUnit: 100 + m_ScaleFactor: 1 + m_ReferenceResolution: {x: 800, y: 600} + m_ScreenMatchMode: 0 + m_MatchWidthOrHeight: 0 + m_PhysicalUnit: 3 + m_FallbackScreenDPI: 96 + m_DefaultSpriteDPI: 96 + m_DynamicPixelsPerUnit: 1 + m_PresetInfoIsWorld: 0 +--- !u!223 &1546344951 +Canvas: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1546344948} + m_Enabled: 1 + serializedVersion: 3 + m_RenderMode: 0 + m_Camera: {fileID: 0} + m_PlaneDistance: 100 + m_PixelPerfect: 0 + m_ReceivesEvents: 1 + m_OverrideSorting: 0 + m_OverridePixelPerfect: 0 + m_SortingBucketNormalizedSize: 0 + m_VertexColorAlwaysGammaSpace: 0 + m_AdditionalShaderChannelsFlag: 0 + m_UpdateRectTransformForStandalone: 0 + m_SortingLayerID: 0 + m_SortingOrder: 0 + m_TargetDisplay: 0 +--- !u!224 &1546344952 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1546344948} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 0, y: 0, z: 0} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 1115416529} + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0, y: 0} +--- !u!1 &1633613639 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1633613641} + - component: {fileID: 1633613640} + m_Layer: 0 + m_Name: Main Camera + m_TagString: MainCamera + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!20 &1633613640 +Camera: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1633613639} + m_Enabled: 1 + serializedVersion: 2 + m_ClearFlags: 2 + m_BackGroundColor: {r: 0.08, g: 0.09, b: 0.12, a: 1} + m_projectionMatrixMode: 1 + m_GateFitMode: 2 + m_FOVAxisMode: 0 + m_Iso: 200 + m_ShutterSpeed: 0.005 + m_Aperture: 16 + m_FocusDistance: 10 + m_FocalLength: 50 + m_BladeCount: 5 + m_Curvature: {x: 2, y: 11} + m_BarrelClipping: 0.25 + m_Anamorphism: 0 + m_SensorSize: {x: 36, y: 24} + m_LensShift: {x: 0, y: 0} + m_NormalizedViewPortRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 1 + height: 1 + near clip plane: 0.3 + far clip plane: 1000 + field of view: 60 + orthographic: 0 + orthographic size: 5 + m_Depth: 0 + m_CullingMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_RenderingPath: -1 + m_TargetTexture: {fileID: 0} + m_TargetDisplay: 0 + m_TargetEye: 3 + m_HDR: 1 + m_AllowMSAA: 1 + m_AllowDynamicResolution: 0 + m_ForceIntoRT: 0 + m_OcclusionCulling: 1 + m_StereoConvergence: 10 + m_StereoSeparation: 0.022 +--- !u!4 &1633613641 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1633613639} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 1, z: -10} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1660057539 &9223372036854775807 +SceneRoots: + m_ObjectHideFlags: 0 + m_Roots: + - {fileID: 1633613641} + - {fileID: 1378620448} + - {fileID: 926978105} + - {fileID: 1546344952} diff --git a/Assets/Scenes/UIToolkitAnnotationDemoScene.unity.meta b/Assets/Scenes/UIToolkitAnnotationDemoScene.unity.meta new file mode 100644 index 0000000000..4af61e3a45 --- /dev/null +++ b/Assets/Scenes/UIToolkitAnnotationDemoScene.unity.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: c80190a87ff2e40f58bc9fda7d981d9c +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/Demo/Editor/UIToolkitAnnotationDemoSceneBuilder.cs b/Assets/Tests/Demo/Editor/UIToolkitAnnotationDemoSceneBuilder.cs new file mode 100644 index 0000000000..0c79d40e3a --- /dev/null +++ b/Assets/Tests/Demo/Editor/UIToolkitAnnotationDemoSceneBuilder.cs @@ -0,0 +1,142 @@ +#if ULOOP_HAS_INPUT_SYSTEM +#nullable enable +using UnityEditor; +using UnityEditor.SceneManagement; +using UnityEngine; +using UnityEngine.EventSystems; +using UnityEngine.InputSystem.UI; +using UnityEngine.SceneManagement; +using UnityEngine.UI; +using UnityEngine.UIElements; + +using io.github.hatayama.UnityCliLoop.Tests.Demo; + +namespace io.github.hatayama.UnityCliLoop.Tests.Demo.Editor +{ + /// + /// Builds the scene the UI Toolkit screenshot annotation E2E runs against: two screen-space UI Toolkit + /// panels driven by UIToolkitAnnotationDemo, plus one uGUI button so both UI systems are annotated together. + /// + public static class UIToolkitAnnotationDemoSceneBuilder + { + private const string SCENE_PATH = "Assets/Scenes/UIToolkitAnnotationDemoScene.unity"; + private const string ASSET_FOLDER = "Assets/Tests/Demo/UIToolkitAnnotation"; + private const string BACK_PANEL_PATH = ASSET_FOLDER + "/UIToolkitAnnotationDemoBackPanel.asset"; + private const string FRONT_PANEL_PATH = ASSET_FOLDER + "/UIToolkitAnnotationDemoFrontPanel.asset"; + private const string THEME_PATH = "Assets/UI Toolkit/UnityThemes/UnityDefaultRuntimeTheme.tss"; + private const float BACK_SORTING_ORDER = 0f; + private const float FRONT_SORTING_ORDER = 10f; + + [MenuItem("UnityCliLoop/Build UI Toolkit Annotation Demo Scene")] + public static void Build() + { + PanelSettings backPanel = CreateOrUpdatePanelSettings(BACK_PANEL_PATH, BACK_SORTING_ORDER); + PanelSettings frontPanel = CreateOrUpdatePanelSettings(FRONT_PANEL_PATH, FRONT_SORTING_ORDER); + + Scene scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single); + CreateCamera(); + CreateEventSystem(); + CreateDemo(backPanel, frontPanel); + CreateUguiButton(); + + bool saved = EditorSceneManager.SaveScene(scene, SCENE_PATH); + if (!saved) + { + Debug.Assert(false, $"[UIToolkitAnnotationDemoSceneBuilder] Failed to save scene to {SCENE_PATH}"); + return; + } + Debug.Log($"[UIToolkitAnnotationDemoSceneBuilder] Scene saved to {SCENE_PATH}"); + } + + // The theme is assigned explicitly so the panels do not depend on which theme Unity finds first. + private static PanelSettings CreateOrUpdatePanelSettings(string path, float sortingOrder) + { + if (!AssetDatabase.IsValidFolder(ASSET_FOLDER)) + { + AssetDatabase.CreateFolder("Assets/Tests/Demo", "UIToolkitAnnotation"); + } + + PanelSettings? settings = AssetDatabase.LoadAssetAtPath(path); + if (settings == null) + { + settings = ScriptableObject.CreateInstance(); + AssetDatabase.CreateAsset(settings, path); + } + + ThemeStyleSheet theme = AssetDatabase.LoadAssetAtPath(THEME_PATH); + Debug.Assert(theme != null, $"[UIToolkitAnnotationDemoSceneBuilder] Missing theme at {THEME_PATH}"); + settings.themeStyleSheet = theme; + settings.scaleMode = PanelScaleMode.ConstantPixelSize; + settings.scale = 1f; + settings.sortingOrder = sortingOrder; + EditorUtility.SetDirty(settings); + AssetDatabase.SaveAssets(); + return settings; + } + + private static void CreateCamera() + { + GameObject cameraGo = new("Main Camera"); + cameraGo.tag = "MainCamera"; + Camera camera = cameraGo.AddComponent(); + camera.clearFlags = CameraClearFlags.SolidColor; + camera.backgroundColor = new Color(0.08f, 0.09f, 0.12f, 1f); + camera.transform.position = new Vector3(0f, 1f, -10f); + } + + private static void CreateEventSystem() + { + GameObject eventSystemGo = new("EventSystem"); + eventSystemGo.AddComponent(); + eventSystemGo.AddComponent(); + } + + // The two documents are siblings: a document under another document's GameObject would join its + // parent's panel instead of using its own panel settings. + private static void CreateDemo(PanelSettings backPanel, PanelSettings frontPanel) + { + GameObject demoGo = new("UIToolkitAnnotationDemo"); + UIDocument backDocument = CreateDocument("BackDocument", demoGo.transform, backPanel); + UIDocument frontDocument = CreateDocument("FrontDocument", demoGo.transform, frontPanel); + + UIToolkitAnnotationDemo demo = demoGo.AddComponent(); + SerializedObject serializedDemo = new(demo); + serializedDemo.FindProperty("backDocument").objectReferenceValue = backDocument; + serializedDemo.FindProperty("frontDocument").objectReferenceValue = frontDocument; + serializedDemo.ApplyModifiedPropertiesWithoutUndo(); + } + + private static UIDocument CreateDocument(string name, Transform parent, PanelSettings panelSettings) + { + GameObject documentGo = new(name); + documentGo.transform.SetParent(parent, false); + UIDocument document = documentGo.AddComponent(); + // Through the serialized field: assigning the property from a builder did not reach the saved scene. + SerializedObject serializedDocument = new(document); + serializedDocument.FindProperty("m_PanelSettings").objectReferenceValue = panelSettings; + serializedDocument.ApplyModifiedPropertiesWithoutUndo(); + return document; + } + + // Placed in the bottom-right corner, away from every UI Toolkit control. + private static void CreateUguiButton() + { + GameObject canvasGo = new("Canvas"); + Canvas canvas = canvasGo.AddComponent(); + canvas.renderMode = RenderMode.ScreenSpaceOverlay; + canvasGo.AddComponent(); + canvasGo.AddComponent(); + + GameObject buttonGo = DefaultControls.CreateButton(new DefaultControls.Resources()); + buttonGo.name = "ugui-button"; + buttonGo.transform.SetParent(canvasGo.transform, false); + RectTransform rectTransform = buttonGo.GetComponent(); + rectTransform.anchorMin = new Vector2(1f, 0f); + rectTransform.anchorMax = new Vector2(1f, 0f); + rectTransform.pivot = new Vector2(1f, 0f); + rectTransform.anchoredPosition = new Vector2(-40f, 40f); + rectTransform.sizeDelta = new Vector2(200f, 50f); + } + } +} +#endif diff --git a/Assets/Tests/Demo/Editor/UIToolkitAnnotationDemoSceneBuilder.cs.meta b/Assets/Tests/Demo/Editor/UIToolkitAnnotationDemoSceneBuilder.cs.meta new file mode 100644 index 0000000000..667b15063f --- /dev/null +++ b/Assets/Tests/Demo/Editor/UIToolkitAnnotationDemoSceneBuilder.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 37912b5d1be014e7ba283ec3fef44161 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/Demo/UIToolkitAnnotation.meta b/Assets/Tests/Demo/UIToolkitAnnotation.meta new file mode 100644 index 0000000000..57d86e4e1b --- /dev/null +++ b/Assets/Tests/Demo/UIToolkitAnnotation.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: f9e87819ed88e420f91d8c740b8e11f3 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/Demo/UIToolkitAnnotation/UIToolkitAnnotationDemoBackPanel.asset b/Assets/Tests/Demo/UIToolkitAnnotation/UIToolkitAnnotationDemoBackPanel.asset new file mode 100644 index 0000000000..66c8fe1689 --- /dev/null +++ b/Assets/Tests/Demo/UIToolkitAnnotation/UIToolkitAnnotationDemoBackPanel.asset @@ -0,0 +1,38 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 19101, guid: 0000000000000000e000000000000000, type: 0} + m_Name: UIToolkitAnnotationDemoBackPanel + m_EditorClassIdentifier: + themeUss: {fileID: -4733365628477956816, guid: 8fd404036dab64fb4bde32641268683b, type: 3} + m_TargetTexture: {fileID: 0} + m_ScaleMode: 0 + m_ReferenceSpritePixelsPerUnit: 100 + m_Scale: 1 + m_ReferenceDpi: 96 + m_FallbackDpi: 96 + m_ReferenceResolution: {x: 1200, y: 800} + m_ScreenMatchMode: 0 + m_Match: 0 + m_SortingOrder: 0 + m_TargetDisplay: 0 + m_ClearDepthStencil: 1 + m_ClearColor: 0 + m_ColorClearValue: {r: 0, g: 0, b: 0, a: 0} + m_DynamicAtlasSettings: + m_MinAtlasSize: 64 + m_MaxAtlasSize: 4096 + m_MaxSubTextureSize: 64 + m_ActiveFilters: 31 + m_AtlasBlitShader: {fileID: 9101, guid: 0000000000000000f000000000000000, type: 0} + m_RuntimeShader: {fileID: 9100, guid: 0000000000000000f000000000000000, type: 0} + m_RuntimeWorldShader: {fileID: 9102, guid: 0000000000000000f000000000000000, type: 0} + textSettings: {fileID: 0} diff --git a/Assets/Tests/Demo/UIToolkitAnnotation/UIToolkitAnnotationDemoBackPanel.asset.meta b/Assets/Tests/Demo/UIToolkitAnnotation/UIToolkitAnnotationDemoBackPanel.asset.meta new file mode 100644 index 0000000000..92fa4a5b8a --- /dev/null +++ b/Assets/Tests/Demo/UIToolkitAnnotation/UIToolkitAnnotationDemoBackPanel.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 34868ecccc6b142e6a841a062a97a6bd +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/Demo/UIToolkitAnnotation/UIToolkitAnnotationDemoFrontPanel.asset b/Assets/Tests/Demo/UIToolkitAnnotation/UIToolkitAnnotationDemoFrontPanel.asset new file mode 100644 index 0000000000..b60e59f071 --- /dev/null +++ b/Assets/Tests/Demo/UIToolkitAnnotation/UIToolkitAnnotationDemoFrontPanel.asset @@ -0,0 +1,38 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 19101, guid: 0000000000000000e000000000000000, type: 0} + m_Name: UIToolkitAnnotationDemoFrontPanel + m_EditorClassIdentifier: + themeUss: {fileID: -4733365628477956816, guid: 8fd404036dab64fb4bde32641268683b, type: 3} + m_TargetTexture: {fileID: 0} + m_ScaleMode: 0 + m_ReferenceSpritePixelsPerUnit: 100 + m_Scale: 1 + m_ReferenceDpi: 96 + m_FallbackDpi: 96 + m_ReferenceResolution: {x: 1200, y: 800} + m_ScreenMatchMode: 0 + m_Match: 0 + m_SortingOrder: 10 + m_TargetDisplay: 0 + m_ClearDepthStencil: 1 + m_ClearColor: 0 + m_ColorClearValue: {r: 0, g: 0, b: 0, a: 0} + m_DynamicAtlasSettings: + m_MinAtlasSize: 64 + m_MaxAtlasSize: 4096 + m_MaxSubTextureSize: 64 + m_ActiveFilters: 31 + m_AtlasBlitShader: {fileID: 9101, guid: 0000000000000000f000000000000000, type: 0} + m_RuntimeShader: {fileID: 9100, guid: 0000000000000000f000000000000000, type: 0} + m_RuntimeWorldShader: {fileID: 9102, guid: 0000000000000000f000000000000000, type: 0} + textSettings: {fileID: 0} diff --git a/Assets/Tests/Demo/UIToolkitAnnotation/UIToolkitAnnotationDemoFrontPanel.asset.meta b/Assets/Tests/Demo/UIToolkitAnnotation/UIToolkitAnnotationDemoFrontPanel.asset.meta new file mode 100644 index 0000000000..6e1a020c58 --- /dev/null +++ b/Assets/Tests/Demo/UIToolkitAnnotation/UIToolkitAnnotationDemoFrontPanel.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 7ba283d20dd0f429f952f9b6bc6f1546 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/Demo/UIToolkitAnnotationDemo.cs b/Assets/Tests/Demo/UIToolkitAnnotationDemo.cs new file mode 100644 index 0000000000..097387a995 --- /dev/null +++ b/Assets/Tests/Demo/UIToolkitAnnotationDemo.cs @@ -0,0 +1,101 @@ +#nullable enable +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.UIElements; + +namespace io.github.hatayama.UnityCliLoop.Tests.Demo +{ + /// + /// Builds the UI Toolkit controls the screenshot annotation E2E expects on two screen-space panels, and + /// counts button clicks so the E2E can check that a click reached a control. + /// + public sealed class UIToolkitAnnotationDemo : MonoBehaviour + { + [SerializeField] private UIDocument backDocument = null!; + [SerializeField] private UIDocument frontDocument = null!; + + private readonly Dictionary _clickCounts = new Dictionary(); + + public int GetClickCount(string elementName) + { + return _clickCounts.TryGetValue(elementName, out int count) ? count : 0; + } + + // NaN when the back panel has no slider by that name, so a missing control cannot pass as a value. + public float GetSliderValue(string elementName) + { + Slider? slider = backDocument.rootVisualElement?.Q(elementName); + return slider != null ? slider.value : float.NaN; + } + + // Start runs after every Awake and OnEnable, so both documents have created their roots by now + // regardless of component order. + private void Start() + { + BuildBackPanel(backDocument.rootVisualElement); + BuildFrontPanel(frontDocument.rootVisualElement); + } + + private void BuildBackPanel(VisualElement root) + { + AddPlaced(root, CreateCountingButton("play"), 100f, 100f, 200f, 50f); + AddPlaced(root, new Toggle("Sound") { name = "sound" }, 100f, 180f, 200f, 30f); + // No label, so the slider's bounds are its track and the E2E can drag between its edges. + AddPlaced(root, new Slider(0f, 1f) { name = "volume" }, 100f, 240f, 300f, 30f); + AddPlaced(root, new TextField("Player") { name = "player-name" }, 100f, 300f, 300f, 40f); + DropdownField quality = new DropdownField( + "Quality", new List { "Low", "Medium", "High" }, 1) { name = "quality" }; + AddPlaced(root, quality, 100f, 370f, 300f, 40f); + + Button locked = CreateCountingButton("locked"); + locked.SetEnabled(false); + AddPlaced(root, locked, 100f, 440f, 200f, 50f); + + Button ghost = CreateCountingButton("ghost"); + ghost.style.visibility = Visibility.Hidden; + AddPlaced(root, ghost, 100f, 510f, 200f, 50f); + + ScrollView list = new ScrollView { name = "list" }; + list.verticalScrollerVisibility = ScrollerVisibility.AlwaysVisible; + VisualElement content = new VisualElement { name = "list-content" }; + content.style.height = 1000f; + content.style.flexShrink = 0f; + list.Add(content); + AddPlaced(root, list, 500f, 100f, 200f, 200f); + + AddPlaced(root, CreateCountingButton("partly-covered"), 800f, 100f, 300f, 60f); + } + + // Covers the left 60% of partly-covered from a panel with a higher sorting order. + private static void BuildFrontPanel(VisualElement root) + { + VisualElement cover = new VisualElement { name = "cover" }; + cover.style.backgroundColor = new Color(0.2f, 0.2f, 0.25f, 1f); + AddPlaced(root, cover, 800f, 100f, 180f, 60f); + } + + private Button CreateCountingButton(string elementName) + { + Button button = new Button { name = elementName, text = elementName }; + button.clicked += () => _clickCounts[elementName] = GetClickCount(elementName) + 1; + return button; + } + + // Margins are zeroed because the theme gives controls margins that would shift their bounds away from + // the coordinates the E2E checks. + private static void AddPlaced( + VisualElement root, VisualElement element, float left, float top, float width, float height) + { + element.style.position = Position.Absolute; + element.style.left = left; + element.style.top = top; + element.style.width = width; + element.style.height = height; + element.style.marginLeft = 0f; + element.style.marginTop = 0f; + element.style.marginRight = 0f; + element.style.marginBottom = 0f; + root.Add(element); + } + } +} diff --git a/Assets/Tests/Demo/UIToolkitAnnotationDemo.cs.meta b/Assets/Tests/Demo/UIToolkitAnnotationDemo.cs.meta new file mode 100644 index 0000000000..9ec0ef24b1 --- /dev/null +++ b/Assets/Tests/Demo/UIToolkitAnnotationDemo.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b288f11987e9b4656905feabfc18ef89 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/Editor/MouseUiDragTargetResolverTests.cs b/Assets/Tests/Editor/MouseUiDragTargetResolverTests.cs index 104754fb03..53081d07fc 100644 --- a/Assets/Tests/Editor/MouseUiDragTargetResolverTests.cs +++ b/Assets/Tests/Editor/MouseUiDragTargetResolverTests.cs @@ -47,7 +47,7 @@ public void Resolve_WithBypassPathBelowDragHandler_ReturnsHandlerAndRawRaycast() GameObjectPathUtility.GetFullPath(rawTarget)); Vector2 position = new(10f, 20f); - (RaycastResult startRaycast, GameObject? target, SimulateMouseUiResponse? failureResponse) = + (MouseUiDragStartTarget start, SimulateMouseUiResponse? failureResponse) = MouseUiDragTargetResolver.Resolve( command, eventSystem, @@ -55,8 +55,8 @@ public void Resolve_WithBypassPathBelowDragHandler_ReturnsHandlerAndRawRaycast() position, position); - Assert.That(startRaycast.gameObject, Is.SameAs(rawTarget)); - Assert.That(target, Is.SameAs(expectedTarget)); + Assert.That(start.StartRaycast.gameObject, Is.SameAs(rawTarget)); + Assert.That(start.UguiTarget, Is.SameAs(expectedTarget)); Assert.That(failureResponse, Is.Null); } @@ -73,7 +73,7 @@ public void Resolve_WithBypassPathWithoutDragHandler_ReturnsRawRaycastWithoutFai GameObjectPathUtility.GetFullPath(rawTarget)); Vector2 position = new(30f, 40f); - (RaycastResult startRaycast, GameObject? target, SimulateMouseUiResponse? failureResponse) = + (MouseUiDragStartTarget start, SimulateMouseUiResponse? failureResponse) = MouseUiDragTargetResolver.Resolve( command, eventSystem, @@ -81,8 +81,8 @@ public void Resolve_WithBypassPathWithoutDragHandler_ReturnsRawRaycastWithoutFai position, position); - Assert.That(startRaycast.gameObject, Is.SameAs(rawTarget)); - Assert.That(target, Is.Null); + Assert.That(start.StartRaycast.gameObject, Is.SameAs(rawTarget)); + Assert.That(start.UguiTarget, Is.Null); Assert.That(failureResponse, Is.Null); } @@ -99,7 +99,7 @@ public void Resolve_WithMissingBypassPath_ReturnsPathFailure() targetPath); Vector2 position = new(50f, 60f); - (RaycastResult startRaycast, GameObject? target, SimulateMouseUiResponse? failureResponse) = + (MouseUiDragStartTarget start, SimulateMouseUiResponse? failureResponse) = MouseUiDragTargetResolver.Resolve( command, eventSystem, @@ -107,8 +107,8 @@ public void Resolve_WithMissingBypassPath_ReturnsPathFailure() position, position); - Assert.That(startRaycast.gameObject, Is.Null); - Assert.That(target, Is.Null); + Assert.That(start.StartRaycast.gameObject, Is.Null); + Assert.That(start.UguiTarget, Is.Null); Assert.That(failureResponse, Is.Not.Null); Assert.That(failureResponse!.Message, Is.EqualTo($"TargetPath '{targetPath}' was not found.")); Assert.That(failureResponse.Action, Is.EqualTo(MouseAction.DragStart.ToString())); diff --git a/Assets/Tests/Editor/MouseUiHitPriorityTests.cs b/Assets/Tests/Editor/MouseUiHitPriorityTests.cs new file mode 100644 index 0000000000..8120869ec4 --- /dev/null +++ b/Assets/Tests/Editor/MouseUiHitPriorityTests.cs @@ -0,0 +1,64 @@ +using NUnit.Framework; + +using io.github.hatayama.UnityCliLoop.FirstPartyTools; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor +{ + /// + /// Verifies which of an overlapping uGUI hit and UI Toolkit hit takes the input, following the EventSystem's + /// raycast ordering. + /// + public sealed class MouseUiHitPriorityTests + { + /// + /// Verifies a Canvas sorted above the panel puts uGUI in front. + /// + [Test] + public void IsUguiInFront_WhenCanvasSortsAbovePanel_ReturnsTrue() + { + Assert.That(MouseUiHitPriority.IsUguiInFront(11, 10f), Is.True); + } + + /// + /// Verifies equal sort priorities leave UI Toolkit in front, since the panel's render order priority is + /// nearly int.MaxValue. + /// + [TestCase(10, 10f)] + [TestCase(10, 10.5f)] + [TestCase(-1, -0.5f)] + public void IsUguiInFront_WhenSortPrioritiesAreEqual_ReturnsFalse(int uguiSortOrderPriority, float panelSortingOrder) + { + Assert.That(MouseUiHitPriority.IsUguiInFront(uguiSortOrderPriority, panelSortingOrder), Is.False); + } + + /// + /// Verifies a fractional panel sorting order is rounded down, as PanelRaycaster does, so a Canvas just + /// above the rounded value comes in front; truncating or rounding up or to nearest would not. + /// + [TestCase(0, -0.5f)] + [TestCase(10, 9.5f)] + public void IsUguiInFront_WhenPanelSortingOrderIsFractional_RoundsDown(int uguiSortOrderPriority, float panelSortingOrder) + { + Assert.That(MouseUiHitPriority.IsUguiInFront(uguiSortOrderPriority, panelSortingOrder), Is.True); + } + + /// + /// Verifies a panel sorted above the Canvas stays in front. + /// + [Test] + public void IsUguiInFront_WhenPanelSortsAboveCanvas_ReturnsFalse() + { + Assert.That(MouseUiHitPriority.IsUguiInFront(9, 10f), Is.False); + } + + /// + /// Verifies raycasters that keep the base priority, such as physics raycasters and non-overlay Canvases, + /// never come in front of a panel. + /// + [Test] + public void IsUguiInFront_WithBaseRaycasterPriority_ReturnsFalse() + { + Assert.That(MouseUiHitPriority.IsUguiInFront(int.MinValue, -1000f), Is.False); + } + } +} diff --git a/Assets/Tests/Editor/MouseUiHitPriorityTests.cs.meta b/Assets/Tests/Editor/MouseUiHitPriorityTests.cs.meta new file mode 100644 index 0000000000..f1d7b75805 --- /dev/null +++ b/Assets/Tests/Editor/MouseUiHitPriorityTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 493c169e2fe5f485f956471f9840227a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/Editor/MouseUiPointerTargetResolverTests.cs b/Assets/Tests/Editor/MouseUiPointerTargetResolverTests.cs index d3281f00f7..2f02091f9a 100644 --- a/Assets/Tests/Editor/MouseUiPointerTargetResolverTests.cs +++ b/Assets/Tests/Editor/MouseUiPointerTargetResolverTests.cs @@ -71,6 +71,28 @@ public void ResolveGameObjectPath_WithUniquePath_ReturnsTarget() Assert.That(failureResponse, Is.Null); } + /// + /// Verifies a GameObject whose name contains '#' still resolves by its path, so the UI Toolkit hint for + /// '#' paths only applies when nothing matches. + /// + [Test] + public void ResolveGameObjectPath_WithHashInGameObjectName_ReturnsTarget() + { + GameObject root = CreateGameObject("MouseUiTargetResolverTests_HashRoot"); + GameObject expectedTarget = CreateGameObject("Menu#Play", root.transform); + string targetPath = GameObjectPathUtility.GetFullPath(expectedTarget); + + (GameObject? target, SimulateMouseUiResponse? failureResponse) = + MouseUiPointerTargetResolver.ResolveGameObjectPath( + targetPath, + "TargetPath", + MouseAction.Click, + new Vector2(10f, 20f)); + + Assert.That(target, Is.SameAs(expectedTarget)); + Assert.That(failureResponse, Is.Null); + } + /// /// Verifies a missing hierarchy path returns the exact wire-visible failure response. /// diff --git a/Assets/Tests/Editor/MouseUiSimulationResponseFactoryTests.cs b/Assets/Tests/Editor/MouseUiSimulationResponseFactoryTests.cs index f7f9ab351b..22f357e89f 100644 --- a/Assets/Tests/Editor/MouseUiSimulationResponseFactoryTests.cs +++ b/Assets/Tests/Editor/MouseUiSimulationResponseFactoryTests.cs @@ -103,7 +103,7 @@ public void CreateClickResult_WithHitMode_MapsMessageAndFields( Vector2 position = new(10.25f, 20.75f); SimulateMouseUiResponse response = - MouseUiSimulationResponseFactory.CreateClickResult(command, position, targetName, hitTarget); + MouseUiSimulationResponseFactory.CreateClickResult(command, position, CreateHit(hitTarget, targetName)); Assert.That(response.Success, Is.True); Assert.That(response.Message, Is.EqualTo(expectedMessage)); @@ -136,7 +136,7 @@ public void CreateLongPressResult_WithHitMode_MapsMessageAndFields( Vector2 position = new(10.25f, 20.75f); SimulateMouseUiResponse response = - MouseUiSimulationResponseFactory.CreateLongPressResult(command, position, targetName, hitTarget); + MouseUiSimulationResponseFactory.CreateLongPressResult(command, position, CreateHit(hitTarget, targetName)); Assert.That(response.Success, Is.True); Assert.That(response.Message, Is.EqualTo(expectedMessage)); @@ -146,6 +146,52 @@ public void CreateLongPressResult_WithHitMode_MapsMessageAndFields( Assert.That(response.PositionY, Is.EqualTo(position.y)); } + /// + /// Verifies click and long-press results carry the hit's UI system and path, and name UI Toolkit hits as + /// such in the message. + /// + [Test] + public void CreatePressResults_WithUIToolkitHit_MapUiSystemAndPath() + { + MouseUiHitReport hit = new MouseUiHitReport("play", "Menu/Document#play", "UIToolkit"); + Vector2 position = new(200f, 125f); + + SimulateMouseUiResponse click = MouseUiSimulationResponseFactory.CreateClickResult( + CreateCommand(new SimulateMouseUiSchema { Action = UnityCliLoopMouseUiAction.Click }), position, hit); + SimulateMouseUiResponse longPress = MouseUiSimulationResponseFactory.CreateLongPressResult( + CreateCommand(new SimulateMouseUiSchema { Action = UnityCliLoopMouseUiAction.LongPress, Duration = 0.5f }), position, hit); + + Assert.That(click.Message, Is.EqualTo("Clicked UI Toolkit element 'play' at (200.0, 125.0)")); + Assert.That(longPress.Message, Is.EqualTo("Long-pressed UI Toolkit element 'play' at (200.0, 125.0) for 0.5s")); + foreach (SimulateMouseUiResponse response in new[] { click, longPress }) + { + Assert.That(response.HitGameObjectName, Is.EqualTo("play")); + Assert.That(response.UiSystem, Is.EqualTo("UIToolkit")); + Assert.That(response.HitPath, Is.EqualTo("Menu/Document#play")); + } + } + + /// + /// Verifies a uGUI hit keeps its message and carries its UI system and path, and no hit leaves both null. + /// + [Test] + public void CreateClickResult_WithUguiHitOrNoHit_MapsUiSystemAndPath() + { + MouseUiSimulationCommand command = CreateCommand(new SimulateMouseUiSchema { Action = UnityCliLoopMouseUiAction.Click }); + Vector2 position = new(10f, 20f); + + SimulateMouseUiResponse ugui = MouseUiSimulationResponseFactory.CreateClickResult( + command, position, new MouseUiHitReport("Ok", "Canvas/Ok", "UGUI")); + SimulateMouseUiResponse none = MouseUiSimulationResponseFactory.CreateClickResult( + command, position, MouseUiHitReport.None); + + Assert.That(ugui.Message, Is.EqualTo("Clicked 'Ok' at (10.0, 20.0)")); + Assert.That(ugui.UiSystem, Is.EqualTo("UGUI")); + Assert.That(ugui.HitPath, Is.EqualTo("Canvas/Ok")); + Assert.That(none.UiSystem, Is.Null); + Assert.That(none.HitPath, Is.Null); + } + /// /// Verifies one-shot drag results retain normal and bypass message contracts. /// @@ -170,12 +216,14 @@ public void CreateDragResult_WithBypassMode_MapsMessageAndFields( command, startPosition, endPosition, - "DragTarget"); + new MouseUiHitReport("DragTarget", "Canvas/DragTarget", "UGUI")); Assert.That(response.Success, Is.True); Assert.That(response.Message, Is.EqualTo(expectedMessage)); Assert.That(response.Action, Is.EqualTo(MouseAction.Drag.ToString())); Assert.That(response.HitGameObjectName, Is.EqualTo("DragTarget")); + Assert.That(response.UiSystem, Is.EqualTo("UGUI")); + Assert.That(response.HitPath, Is.EqualTo("Canvas/DragTarget")); Assert.That(response.PositionX, Is.EqualTo(startPosition.x)); Assert.That(response.PositionY, Is.EqualTo(startPosition.y)); Assert.That(response.EndPositionX, Is.EqualTo(endPosition.x)); @@ -198,18 +246,44 @@ public void CreateDragEndResult_WithFinalPosition_MapsMessageAndFields() SimulateMouseUiResponse response = MouseUiSimulationResponseFactory.CreateDragEndResult( command, endPosition, - "DragTarget"); + new MouseUiHitReport("DragTarget", "Canvas/DragTarget", "UGUI")); Assert.That(response.Success, Is.True); Assert.That( response.Message, Is.EqualTo("Drag ended on 'DragTarget' at (30.5, 40.3) at 125 px/s")); + Assert.That(response.UiSystem, Is.EqualTo("UGUI")); + Assert.That(response.HitPath, Is.EqualTo("Canvas/DragTarget")); Assert.That(response.Action, Is.EqualTo(MouseAction.DragEnd.ToString())); Assert.That(response.HitGameObjectName, Is.EqualTo("DragTarget")); Assert.That(response.PositionX, Is.EqualTo(endPosition.x)); Assert.That(response.PositionY, Is.EqualTo(endPosition.y)); } + /// + /// Verifies drag results name a UI Toolkit start element as such and carry its UI system and path. + /// + [Test] + public void CreateDragResults_WithUIToolkitHit_NameUIToolkitElement() + { + MouseUiHitReport hit = new MouseUiHitReport("volume", "Menu/Document#volume", "UIToolkit"); + Vector2 start = new(100f, 200f); + Vector2 end = new(300f, 200f); + + SimulateMouseUiResponse drag = MouseUiSimulationResponseFactory.CreateDragResult( + CreateCommand(new SimulateMouseUiSchema { Action = UnityCliLoopMouseUiAction.Drag, DragSpeed = 100f }), start, end, hit); + SimulateMouseUiResponse dragEnd = MouseUiSimulationResponseFactory.CreateDragEndResult( + CreateCommand(new SimulateMouseUiSchema { Action = UnityCliLoopMouseUiAction.DragEnd, DragSpeed = 100f }), end, hit); + + Assert.That(drag.Message, Is.EqualTo("Dragged UI Toolkit element 'volume' from (100.0, 200.0) to (300.0, 200.0) at 100 px/s")); + Assert.That(dragEnd.Message, Is.EqualTo("Drag ended on UI Toolkit element 'volume' at (300.0, 200.0) at 100 px/s")); + foreach (SimulateMouseUiResponse response in new[] { drag, dragEnd }) + { + Assert.That(response.UiSystem, Is.EqualTo("UIToolkit")); + Assert.That(response.HitPath, Is.EqualTo("Menu/Document#volume")); + } + } + private static MouseUiSimulationCommand CreateCommand(SimulateMouseUiSchema schema) { (MouseUiSimulationCommand? command, string? errorMessage) = @@ -218,5 +292,10 @@ private static MouseUiSimulationCommand CreateCommand(SimulateMouseUiSchema sche Assert.That(command, Is.Not.Null); return command!; } + + private static MouseUiHitReport CreateHit(bool hitTarget, string? targetName) + { + return hitTarget ? new MouseUiHitReport(targetName, null, null) : MouseUiHitReport.None; + } } } diff --git a/Assets/Tests/Editor/StaticFacadeStateGuardTests.cs b/Assets/Tests/Editor/StaticFacadeStateGuardTests.cs index bbc231a349..56e2406c97 100644 --- a/Assets/Tests/Editor/StaticFacadeStateGuardTests.cs +++ b/Assets/Tests/Editor/StaticFacadeStateGuardTests.cs @@ -355,14 +355,19 @@ public void ExecuteDynamicCodeUseCase_WhenAwaitingWorkflow_DoesNotCaptureEditorS [Test] public void MouseUiPressActionExecutor_WhenReturningAfterTimeout_UsesCapturedUnityObjectState() { - // Tests that timeout result branches use plain captured bools instead of UnityEngine.Object null checks. + // Tests that timeout result branches use the hit report captured before the first await, and that + // the release decision is a captured bool, instead of UnityEngine.Object null checks after awaits. string source = ReadSourceFile( "Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiPressActionExecutor.cs"); - - Assert.That(source, Does.Contain("bool hitTarget = resolvedTargets.Target != null;")); - Assert.That(source, Does.Contain("bool shouldReleasePointer = resolvedTargets.RawTarget != null && resolvedTargets.Target != null;")); - Assert.That(source, Does.Not.Contain("CreateClickResult(parameters, inputPos, targetName, resolvedTargets.Target != null)")); - Assert.That(source, Does.Not.Contain("CreateLongPressResult(parameters, inputPos, targetName, resolvedTargets.Target != null)")); + string uguiTargetSource = ReadSourceFile( + "Packages/src/Editor/FirstPartyTools/SimulateMouseUi/UguiPressTarget.cs"); + + Assert.That(source, Does.Contain("MouseUiHitReport report = pressTarget.Report;")); + Assert.That(uguiTargetSource, Does.Contain("bool shouldReleasePointer = resolvedTargets.RawTarget != null && resolvedTargets.Target != null;")); + Assert.That(uguiTargetSource, Does.Contain("if (!_shouldReleasePointer)")); + Assert.That(source, Does.Not.Contain("CreateClickResult(parameters, inputPos, pressTarget.Report)")); + Assert.That(source, Does.Not.Contain("CreateLongPressResult(parameters, inputPos, pressTarget.Report)")); + Assert.That(source, Does.Not.Contain("resolvedTargets.Target")); } [Test] diff --git a/Assets/Tests/Editor/UIElementAnnotatorCollectTests.cs b/Assets/Tests/Editor/UIElementAnnotatorCollectTests.cs index be0e642510..f4d507d6e5 100644 --- a/Assets/Tests/Editor/UIElementAnnotatorCollectTests.cs +++ b/Assets/Tests/Editor/UIElementAnnotatorCollectTests.cs @@ -89,6 +89,26 @@ public void CollectInteractiveElements_WithEventHandlerScripts_ClassifiesThem() Assert.That(typesByPath[root + "PointerDown"], Is.EqualTo("Button")); } + /// + /// Verifies uGUI elements, both selectables and handler scripts, are tagged as coming from uGUI. + /// + [Test] + public void CollectInteractiveElements_WithUguiElements_TagsThemAsUgui() + { + Assume.That(EventSystem.current, Is.Null); + Transform canvas = CreateCanvas("UiSystem", RenderMode.ScreenSpaceOverlay, true, null); + CreateUiChild