Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
ba5fb23
feat: validate prefab screenshot parameters
hatayama Oct 22, 2026
dd65bc9
feat: classify and frame a prefab instance for screenshots
hatayama Oct 22, 2026
73f598a
test: stop pinning the nested Canvas exclusion that Unity makes unobs…
hatayama Oct 22, 2026
d13e906
test: add prefab fixtures for prefab screenshot tests
hatayama Oct 22, 2026
208361b
feat: render a prefab asset in a preview scene and save it as a PNG
hatayama Oct 22, 2026
9c34f90
feat: route screenshot requests with a prefab path to the prefab capture
hatayama Oct 22, 2026
1ebc223
docs: document screenshot --prefab-path and --prefab-background-color
hatayama Oct 22, 2026
275ace8
fix: reject prefabs whose renderers have empty bounds instead of retu…
hatayama Oct 22, 2026
e5715f4
fix: a UI part with a nested overlay Canvas is captured as the part, …
hatayama Oct 22, 2026
e5a6357
test: run the real prefab renderer in CI instead of only on a real GPU
hatayama Oct 22, 2026
ed5a926
docs: list prefab framing rules in the order they are applied
hatayama Oct 22, 2026
dce8af3
fix: prefab screenshots draw renderers and UI parts in pipelines that…
hatayama Oct 22, 2026
a465ccf
fix: prefab screenshots reject empty renders and fill an opaque backg…
hatayama Oct 22, 2026
39aeccb
fix: a scaled UI part is no longer captured one pixel too wide
hatayama Oct 22, 2026
2888200
docs: explain how prefab screenshots behave in render pipelines that …
hatayama Oct 22, 2026
2003d1b
feat: prefab renderer can measure whether the pipeline clears the cap…
hatayama Oct 22, 2026
960b62a
fix: prefab screenshots no longer double the background in pipelines …
hatayama Oct 22, 2026
1cf1c46
docs: explain prefab background handling per pipeline and what a pref…
hatayama Oct 22, 2026
42bb228
Merge remote-tracking branch 'origin/main' into feature/screenshot-pr…
hatayama Oct 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 7 additions & 8 deletions .agents/skills/uloop-screenshot/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Take a screenshot of any Unity EditorWindow by name and save as PNG.
## Usage

```bash
uloop screenshot [--window-name <name>] [--resolution-scale <scale>] [--match-mode <mode>] [--capture-mode <mode>] [--annotate-elements] [--annotate-raycast-grid] [--raycast-layer-mask <layers>] [--elements-only] [--output-directory <path>]
uloop screenshot [--window-name <name>] [--resolution-scale <scale>] [--match-mode <mode>] [--capture-mode <mode>] [--annotate-elements] [--annotate-raycast-grid] [--raycast-layer-mask <layers>] [--elements-only] [--output-directory <path>] [--prefab-path <path>] [--prefab-background-color <color>]
```

## Parameters
Expand All @@ -27,14 +27,12 @@ uloop screenshot [--window-name <name>] [--resolution-scale <scale>] [--match-mo
| `--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. |
| `--prefab-path` | string | `""` | Prefab or model asset path to render alone in an isolated preview scene, in Edit or Play Mode. Open scenes, Prefab Mode, and the asset stay untouched. |
| `--prefab-background-color` | string | `""` | Background for `--prefab-path` as `#RRGGBB` or `#RRGGBBAA`. Empty uses `#808080`. |

## Match Modes

| Mode | Description | Example |
|------|-------------|---------|
| `exact` | Window name must match exactly (case-insensitive) | "Project" matches "Project" only |
| `prefix` | Window name must start with the input | "Project" matches "Project" and "Project Settings" |
| `contains` | Window name must contain the input anywhere | "set" matches "Project Settings" |
See [references/match-modes.md](references/match-modes.md) for exact / prefix / contains examples.

## Window Name

Expand All @@ -51,14 +49,14 @@ The window name is the text displayed in the window's title bar (tab). Common na
Returns JSON with:

- `ScreenshotCount`: Number of windows captured
- `ResolvedCaptureMode`: `"window"` or `"rendering"` — the mode actually used after resolving `auto`
- `ResolvedCaptureMode`: `"window"`, `"rendering"`, or `"prefab"` — the mode actually used after resolving `auto`
- `Warning`: Appears for window captures taken while Play Mode is running with at least one image, and for any image captured while Play Mode is paused (UGUI/canvas content may show the frame rendered before the pause; run `uloop control-play-mode --action Step` once and capture again to refresh it).
- `Screenshots`: Array of screenshot info, each containing:
- `ImagePath`: Absolute path to the saved PNG file. Empty (`""`) when `--elements-only` is used because no image file is written; `FileSizeBytes`, `Width`, and `Height` are then `0`, while `GameViewWidth`/`GameViewHeight` and `AnnotatedElements` still carry real values. When it is non-empty, always open the file named here — the output directory accumulates every past capture, so guessing the newest file with directory listing (`ls -t` or similar) can silently pick a stale screenshot from an earlier run.
- `FileSizeBytes`: Size of the saved file in bytes
- `Width`: Captured image width in pixels
- `Height`: Captured image height in pixels
- `ImageCoordinateSystem`: `"top-left-game-view"` or `"top-left-window"`
- `ImageCoordinateSystem`: `"top-left-game-view"`, `"top-left-window"`, or `"top-left-prefab-image"`
- `ResolutionScale`: Resolution scale used for capture
- `ImageToInputOffsetY`: Y offset used for top-left-game-view coordinate conversion
- `GameViewWidth`, `GameViewHeight`: Game View size in input coordinates at capture time — the basis for `SimX`/`SimY` and for the Y flip inside `simulate-mouse-input`. Populated for rendering captures, including `--elements-only`
Expand All @@ -75,3 +73,4 @@ When multiple windows match (e.g., multiple Inspector windows or when using `con
- Use `uloop focus-window` first if needed
- Target window must be open in Unity Editor
- Window name matching is always case-insensitive
- Before using --prefab-path, read [references/prefab-capture.md](references/prefab-capture.md).
7 changes: 7 additions & 0 deletions .agents/skills/uloop-screenshot/references/match-modes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Window Name Match Modes

| Mode | Description | Example |
|------|-------------|---------|
| `exact` | Window name must match exactly (case-insensitive) | "Project" matches "Project" only |
| `prefix` | Window name must start with the input | "Project" matches "Project" and "Project Settings" |
| `contains` | Window name must contain the input anywhere | "set" matches "Project Settings" |
42 changes: 42 additions & 0 deletions .agents/skills/uloop-screenshot/references/prefab-capture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Prefab Capture (`--prefab-path`)

`uloop screenshot --prefab-path <asset path>` instantiates the prefab or model asset in a temporary preview scene, renders it with a temporary camera, and returns one PNG. No window opens, and open scenes, Prefab Mode, and the asset itself are left untouched. It works in Edit Mode and Play Mode.

```bash
uloop screenshot --prefab-path Assets/UI/Panel.prefab
uloop screenshot --prefab-path Assets/UI/Panel.prefab --prefab-background-color "#00000000"
```

## How the prefab is framed

The first matching rule decides the image (the response `Message` says which one was used):

1. **UI part** — the root is a RectTransform with no enabled Canvas of its own, or its own Canvas is World Space. The image is sized to the union of all active RectTransforms, so children that overflow the root are included, and nested Canvases inside the part are drawn with it. Sizes are counted in UI units, so a world-space Canvas saved at a small scale keeps its pixel size. A stretched root fills the Game View size. The temporary Canvas that hosts the part sits on the built-in UI layer.
2. **Screen UI** — the root itself has an enabled Screen Space Canvas (Overlay or Camera), or the root is not a RectTransform and contains one. The outermost Canvases are switched to the capture camera and the image is the Game View size, so the layout matches the game. The prefab's own Canvases keep their layers, so a pipeline whose game cameras skip that layer draws nothing and the capture is rejected.
3. **Flat object** — content with no depth (sprites, quads, a lone world-space Canvas) is viewed straight on with an orthographic camera at the Game View size. A lone world-space Canvas is drawn on its own layer, so a pipeline whose game cameras skip that layer draws nothing and the capture is rejected.
4. **3D object** — anything else with an enabled Renderer is viewed from the front-left with a perspective camera at the Game View size, with the whole object in frame.

Prefabs with visible renderers (rules 3 and 4) are drawn with a preview camera, the kind Inspector previews use, so they appear even in pipelines whose game cameras draw only certain layers. Preview cameras do not draw uGUI, so in a prefab that mixes renderers and a world-space Canvas, the Canvas does not appear.

A prefab with nothing to render is rejected, including one whose renderers have empty bounds, such as a particle system that has not emitted. A render in which nothing appears (every pixel transparent or the background color) is rejected too; for a prefab that is a single flat color, pass a different `--prefab-background-color`. When a prefab has both Screen Space UI and 3D content, the UI rule wins and the 3D content may not appear.

The Game View size comes from the current Game View; change it with `uloop set-game-view-size`.

## Options

- `--window-name` and `--match-mode` are ignored.
- `--capture-mode` other than `auto`, `--annotate-elements`, `--annotate-raycast-grid`, `--raycast-layer-mask`, and `--elements-only` are rejected.
- `--resolution-scale` shrinks the image after it is rendered.
- `--prefab-background-color` takes `#RRGGBB` or `#RRGGBBAA`. The default `#808080` keeps both dark and white text readable. When the render pipeline clears the capture camera to the background color, the drawn colors are kept and only made opaque. When it does not, the background is composited after rendering under unpainted and translucent pixels. `#00000000` gives a transparent background with URP; transparent and translucent backgrounds depend on the render pipeline.
- `--prefab-background-color` without `--prefab-path` is rejected.

## What happens to the instance

- If the prefab has no enabled Light, one directional light is added to the preview scene.
- A root saved inactive is activated so it can be captured; children keep their saved state.
- Inside the preview scene, scripts other than `[ExecuteAlways]` ones do not run, even in Play Mode. uGUI draws normally.
- The prefab is rendered right after it is instantiated, so content that scripts assign later (asynchronous loads, Start, or Update) is missing, and post-processing such as bloom is not applied.

## Output

`ResolvedCaptureMode` is `"prefab"` and `ImageCoordinateSystem` is `"top-left-prefab-image"`. Prefab images are not Game View captures, so `ScreenshotToInputFormula` reports that mouse-input coordinates are unavailable.
15 changes: 7 additions & 8 deletions .claude/skills/uloop-screenshot/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Take a screenshot of any Unity EditorWindow by name and save as PNG.
## Usage

```bash
uloop screenshot [--window-name <name>] [--resolution-scale <scale>] [--match-mode <mode>] [--capture-mode <mode>] [--annotate-elements] [--annotate-raycast-grid] [--raycast-layer-mask <layers>] [--elements-only] [--output-directory <path>]
uloop screenshot [--window-name <name>] [--resolution-scale <scale>] [--match-mode <mode>] [--capture-mode <mode>] [--annotate-elements] [--annotate-raycast-grid] [--raycast-layer-mask <layers>] [--elements-only] [--output-directory <path>] [--prefab-path <path>] [--prefab-background-color <color>]
```

## Parameters
Expand All @@ -27,14 +27,12 @@ uloop screenshot [--window-name <name>] [--resolution-scale <scale>] [--match-mo
| `--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. |
| `--prefab-path` | string | `""` | Prefab or model asset path to render alone in an isolated preview scene, in Edit or Play Mode. Open scenes, Prefab Mode, and the asset stay untouched. |
| `--prefab-background-color` | string | `""` | Background for `--prefab-path` as `#RRGGBB` or `#RRGGBBAA`. Empty uses `#808080`. |

## Match Modes

| Mode | Description | Example |
|------|-------------|---------|
| `exact` | Window name must match exactly (case-insensitive) | "Project" matches "Project" only |
| `prefix` | Window name must start with the input | "Project" matches "Project" and "Project Settings" |
| `contains` | Window name must contain the input anywhere | "set" matches "Project Settings" |
See [references/match-modes.md](references/match-modes.md) for exact / prefix / contains examples.

## Window Name

Expand All @@ -51,14 +49,14 @@ The window name is the text displayed in the window's title bar (tab). Common na
Returns JSON with:

- `ScreenshotCount`: Number of windows captured
- `ResolvedCaptureMode`: `"window"` or `"rendering"` — the mode actually used after resolving `auto`
- `ResolvedCaptureMode`: `"window"`, `"rendering"`, or `"prefab"` — the mode actually used after resolving `auto`
- `Warning`: Appears for window captures taken while Play Mode is running with at least one image, and for any image captured while Play Mode is paused (UGUI/canvas content may show the frame rendered before the pause; run `uloop control-play-mode --action Step` once and capture again to refresh it).
- `Screenshots`: Array of screenshot info, each containing:
- `ImagePath`: Absolute path to the saved PNG file. Empty (`""`) when `--elements-only` is used because no image file is written; `FileSizeBytes`, `Width`, and `Height` are then `0`, while `GameViewWidth`/`GameViewHeight` and `AnnotatedElements` still carry real values. When it is non-empty, always open the file named here — the output directory accumulates every past capture, so guessing the newest file with directory listing (`ls -t` or similar) can silently pick a stale screenshot from an earlier run.
- `FileSizeBytes`: Size of the saved file in bytes
- `Width`: Captured image width in pixels
- `Height`: Captured image height in pixels
- `ImageCoordinateSystem`: `"top-left-game-view"` or `"top-left-window"`
- `ImageCoordinateSystem`: `"top-left-game-view"`, `"top-left-window"`, or `"top-left-prefab-image"`
- `ResolutionScale`: Resolution scale used for capture
- `ImageToInputOffsetY`: Y offset used for top-left-game-view coordinate conversion
- `GameViewWidth`, `GameViewHeight`: Game View size in input coordinates at capture time — the basis for `SimX`/`SimY` and for the Y flip inside `simulate-mouse-input`. Populated for rendering captures, including `--elements-only`
Expand All @@ -75,3 +73,4 @@ When multiple windows match (e.g., multiple Inspector windows or when using `con
- Use `uloop focus-window` first if needed
- Target window must be open in Unity Editor
- Window name matching is always case-insensitive
- Before using --prefab-path, read [references/prefab-capture.md](references/prefab-capture.md).
7 changes: 7 additions & 0 deletions .claude/skills/uloop-screenshot/references/match-modes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Window Name Match Modes

| Mode | Description | Example |
|------|-------------|---------|
| `exact` | Window name must match exactly (case-insensitive) | "Project" matches "Project" only |
| `prefix` | Window name must start with the input | "Project" matches "Project" and "Project Settings" |
| `contains` | Window name must contain the input anywhere | "set" matches "Project Settings" |
42 changes: 42 additions & 0 deletions .claude/skills/uloop-screenshot/references/prefab-capture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Prefab Capture (`--prefab-path`)

`uloop screenshot --prefab-path <asset path>` instantiates the prefab or model asset in a temporary preview scene, renders it with a temporary camera, and returns one PNG. No window opens, and open scenes, Prefab Mode, and the asset itself are left untouched. It works in Edit Mode and Play Mode.

```bash
uloop screenshot --prefab-path Assets/UI/Panel.prefab
uloop screenshot --prefab-path Assets/UI/Panel.prefab --prefab-background-color "#00000000"
```

## How the prefab is framed

The first matching rule decides the image (the response `Message` says which one was used):

1. **UI part** — the root is a RectTransform with no enabled Canvas of its own, or its own Canvas is World Space. The image is sized to the union of all active RectTransforms, so children that overflow the root are included, and nested Canvases inside the part are drawn with it. Sizes are counted in UI units, so a world-space Canvas saved at a small scale keeps its pixel size. A stretched root fills the Game View size. The temporary Canvas that hosts the part sits on the built-in UI layer.
2. **Screen UI** — the root itself has an enabled Screen Space Canvas (Overlay or Camera), or the root is not a RectTransform and contains one. The outermost Canvases are switched to the capture camera and the image is the Game View size, so the layout matches the game. The prefab's own Canvases keep their layers, so a pipeline whose game cameras skip that layer draws nothing and the capture is rejected.
3. **Flat object** — content with no depth (sprites, quads, a lone world-space Canvas) is viewed straight on with an orthographic camera at the Game View size. A lone world-space Canvas is drawn on its own layer, so a pipeline whose game cameras skip that layer draws nothing and the capture is rejected.
4. **3D object** — anything else with an enabled Renderer is viewed from the front-left with a perspective camera at the Game View size, with the whole object in frame.

Prefabs with visible renderers (rules 3 and 4) are drawn with a preview camera, the kind Inspector previews use, so they appear even in pipelines whose game cameras draw only certain layers. Preview cameras do not draw uGUI, so in a prefab that mixes renderers and a world-space Canvas, the Canvas does not appear.

A prefab with nothing to render is rejected, including one whose renderers have empty bounds, such as a particle system that has not emitted. A render in which nothing appears (every pixel transparent or the background color) is rejected too; for a prefab that is a single flat color, pass a different `--prefab-background-color`. When a prefab has both Screen Space UI and 3D content, the UI rule wins and the 3D content may not appear.

The Game View size comes from the current Game View; change it with `uloop set-game-view-size`.

## Options

- `--window-name` and `--match-mode` are ignored.
- `--capture-mode` other than `auto`, `--annotate-elements`, `--annotate-raycast-grid`, `--raycast-layer-mask`, and `--elements-only` are rejected.
- `--resolution-scale` shrinks the image after it is rendered.
- `--prefab-background-color` takes `#RRGGBB` or `#RRGGBBAA`. The default `#808080` keeps both dark and white text readable. When the render pipeline clears the capture camera to the background color, the drawn colors are kept and only made opaque. When it does not, the background is composited after rendering under unpainted and translucent pixels. `#00000000` gives a transparent background with URP; transparent and translucent backgrounds depend on the render pipeline.
- `--prefab-background-color` without `--prefab-path` is rejected.

## What happens to the instance

- If the prefab has no enabled Light, one directional light is added to the preview scene.
- A root saved inactive is activated so it can be captured; children keep their saved state.
- Inside the preview scene, scripts other than `[ExecuteAlways]` ones do not run, even in Play Mode. uGUI draws normally.
- The prefab is rendered right after it is instantiated, so content that scripts assign later (asynchronous loads, Start, or Update) is missing, and post-processing such as bloom is not applied.

## Output

`ResolvedCaptureMode` is `"prefab"` and `ImageCoordinateSystem` is `"top-left-prefab-image"`. Prefab images are not Game View captures, so `ScreenshotToInputFormula` reports that mouse-input coordinates are unavailable.
Loading
Loading