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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions .claude/skills/editor-media-capture/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
---
name: editor-media-capture
description: Shoot Unity Editor screenshots and GIFs for this package's documentation without the user touching Unity — floating Inspectors, TypeSelectorWindow dropdowns, prefab-mode states, and ffmpeg GIF assembly. Use when a task asks for docs media (PNG/GIF) of editor UI under Documentation/Images, or when scripting EditorWindow interaction headlessly.
user-invocable: false
allowed-tools:
- Bash
---

# Editor media capture

Driving the Editor is the `unity-pipeline` skill's job; this file covers what it cannot do —
capturing *editor windows* (not the Game/Scene view) and scripting UI states.

For a plain Game or Scene frame, stop here and use
`unity command screenshot --view game|scene` instead. Everything below is for editor chrome.

## Capture without stealing focus

```bash
screencapture -x -o -l <windowID> out.png # specific window, works while Unity is inactive/occluded
```

- Find window IDs with a small Swift script over `CGWindowListCopyWindowInfo` (no `pyobjc` on this
machine). Re-query **before every shot** and pick the **highest** id: closed editor windows leave
blank native ghosts with the same size and title, which `screencapture` will happily capture
(~13 KB of nothing).
- `-o` drops the drop shadow. Crop the 28 pt (56 px @2x) macOS title bar with `ffmpeg`; `sips -c`
cannot crop with an offset.
- `screencapture -R x,y,w,h` takes **points** and outputs @2x Retina pixels. It composites by
z-order, so a floating `EditorWindow` sitting under the main window yields a gray rectangle.
`win.Focus()` raises it but activates the app once — avoid while the user is typing; window-ID
capture needs no focus.

## Floating windows

- `CreateInstance<InspectorWindow>()` + `Show()`, then set `position` a **second** time — the first
assignment is ignored, and the getter returns stale values. Read real bounds from the window list;
do not trust the setter (macOS cascades floating windows).
- Title bar ≈ 28–30 pt: the capture rect is the content rect grown upward.

## TypeSelectorWindow

- It **survives without app focus**, and
`EditorWindow.SendEvent(Event.KeyboardEvent("down"|"return"))` drives its keyboard navigation
headlessly — the full root → namespace → select flow works with zero `cliclick`.
- The root page auto-shows a **Recent** section that breaks scripted down/return navigation. Clear
`TypeSelectorPreferences.RecentsKey` first and restore the user's entries afterwards — scripted
selections pollute Recents. Keys: `Aspid.FastTools.TypeSelector.{Favorites|Recents}.<productGUID>`,
JSON `{"Entries":[aqn,...]}`.
- A non-empty `currentAqn` opens the window already inside that type's namespace page (flat list,
breadcrumbs); empty starts at root. Favorites/Recent exist only on the root page and open
collapsed (in-memory state, not persisted).
- Anchor `ShowAsDropDown(screenRect, size)` to the **exact field rect** — it drops flush under
`screenRect`'s bottom, and a hand-guessed anchor leaves a visible gap. In a 430 pt floating
Inspector (GO header + Transform + component): value column `x+176`, row `y+154` from content
top, `248×17`.
- A dropdown opened without OS focus dies non-deterministically 0.5–4 s after opening. Capture each
state in its **own short eval** (Show → SendEvents fast → hold ≤2.5 s) with an outside burst loop
(5 shots @ 0.45 s), then composite the states onto the inspector capture with `ffmpeg overlay`.
`ShowPopup()` clones do not auto-close but usually render **blank** when the app is inactive —
dead end.
- If you must use `cliclick`: a single synthetic `c:` click on a picker row only hovers, it does
**not** activate — use `dc:` (double-click). `kp:esc` closes the dropdown. A picker anchored under
a field overlaps the rows below it, so clicks aimed there land on the picker's search bar.

## Scene / asset staging

- `AssetDatabase.ImportAsset` does **not** recurse into folders (children get no `.meta`) — pass
`ImportAssetOptions.ImportRecursive | ForceUpdate`.
- A scene instance of a prefab with a missing managed-reference type shows `<None>`, not the notice.
Shoot missing-type UI in **Prefab Mode** (`PrefabStageUtility.OpenPrefab`).
- `SerializeReferenceSettings.ExcludedFolders` writes through to the **committed**
`ProjectSettings/SerializeReferenceSharedSettings.asset`. Restore it and check `git diff` after
using it to de-clutter Project References scans.
- Demo types for docs media live in `Aspid.FastTools/Assets/DevTests/Types/Scripts/DocsMedia/` under
the user-facing namespace `Game.Combat` — the namespace shows up in picker breadcrumbs, so no
`DevTests` naming there.

## GIF assembly

`ffmpeg` concat with a per-frame `duration`, then two-pass `palettegen`/`paletteuse`.
**Do not use `-fps_mode vfr`** — it silently collapsed a sequence into 6 identical frames.
141 changes: 141 additions & 0 deletions .claude/skills/unity-pipeline/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
---
name: unity-pipeline
description: Drive this repo's running Unity Editor from the shell via the official `unity` CLI and the `com.unity.pipeline` package — recompile/test loop, C# eval, console logs, Game/Scene screenshots, the `sr_gate` SerializeReference check, and authoring new `[CliCommand]` commands. Use whenever a task needs to compile, test, inspect, or script the live Editor instead of inferring behaviour from source.
user-invocable: false
allowed-tools:
- Bash
---

# Unity Pipeline (live Editor control)

Stack: `unity` CLI (`~/.unity/bin/unity`, normally already on `PATH`) plus the UPM package
`com.unity.pipeline`, pinned in `Aspid.FastTools/Packages/manifest.json`. The Editor exposes
~460 commands; the CLI just forwards to them, so no CLI update is needed when the package adds one.

Generic CLI surface (editors, licenses, headless `build`/`test`, `mcp`) is covered by the vendor
`unity-cli` skill. **This file covers only what is specific to Aspid.FastTools.**

## 1. Always target a project explicitly

Several Editors run at once here — the main checkout plus `.claude/worktrees/shared-*`. A bare
`unity command …` fails with `COMMAND_FAILED / Multiple Unity Editor instances found`.

```bash
P="$PWD/Aspid.FastTools" # from the repo (or worktree) root
unity command <name> --project-path "$P" --format json
```

## 2. Health check — `status`, not `pipeline list`

```bash
unity status --format json # port, project, version, PID, state per Editor
```

Reads the per-Editor lockfile instead of probing over HTTP: faster, and stale instances are
reported as `unreachable`. `unity pipeline list` is the wrong tool for this — a *closed* Editor
can still show `Running=true` there from a leftover lock. Use `pipeline list` only for package
install/version questions.

`unity list --project-path "$P"` prints every command the Editor exposes with its parameter
schema — use it instead of guessing argument names.

## 3. Edit → verify loop

```bash
unity command set_autotick --enable true --project-path "$P" # do this first
unity command recompile --project-path "$P"
unity command recompile_status --project-path "$P" # poll: idle|triggered|compiling|completed|up_to_date
unity command run_tests --mode editor --filter <Fixture> --project-path "$P"
```

- **`set_autotick` first.** Unfocused Unity throttles its update loop, so a recompile or test run
started from the shell can hang indefinitely without it.
- **The server goes silent during domain reload** — commands return empty. Poll with retries; the
connection restores itself afterwards (no reinstall needed).
- `run_tests` right after another command can return `result=null` (runner still busy) — retry.
- Long runs: `--async_tests true`, then poll `test_status`; abort with `cancel_tests`.

## 4. C# eval

```bash
unity command eval 'return UnityEditor.EditorUtility.scriptCompilationFailed ? "COMPILE_FAILED" : "OK";' \
--project-path "$P" --format json
```

- **The trailing `;` is mandatory** — without it the snippet fails to compile with `CS1002`.
- **The JSON response echoes your source back** inside `parameters`. Grep only the result field
(`grep -E '"result": *"[^"]*"'`), otherwise a string literal in your own code matches and, for
the snippet above, "COMPILE_FAILED" is reported even on success.
- `eval_file <path>` runs a `.cs` file through the same path.

Compilation ground truth is `scriptCompilationFailed`, not the absence of console errors.

## 5. Console and screenshots

```bash
unity command get_console_logs --severity error --limit 50 --project-path "$P"
unity command screenshot --view game --output ./shot.png --project-path "$P"
```

`severity`: `all | log | warning | error`. Without `--output`, `screenshot` (and
`capture_game_view` / `capture_scene_view`) writes a timestamped PNG under
`<project>/Temp/pipeline-screenshots/`.

For docs media beyond a plain Game/Scene frame — floating Inspectors, `TypeSelectorWindow`
dropdowns, GIFs — use the `editor-media-capture` skill instead; those need window-ID capture and
focus tricks the pipeline commands do not cover.

## 6. `sr_gate` — SerializeReference gate without a batchmode relaunch

```bash
unity command sr_gate --scope full --project-path "$P" --format json
```

| Arg | Values | Default |
|---|---|---|
| `scope` | `missing` \| `required` \| `full` | `missing` |
| `warn_only` | force Warn severity — reports, `exitCode` 0 | `false` |
| `fail` | force Fail severity — `exitCode` 1 on violations | `false` |

Returns `{success, scope, severity, violationCount, exitCode, violations[]}`, each violation
carrying `kind`, `assetPath`, `fieldPath`, `storedType`, `fileId`, `rid`. Source:
`Aspid.FastTools/Assets/DevTests/CliCommands/Editor/SerializeReferenceGateCommands.cs` — a thin
wrapper over `SerializeReferenceGateScanner`, so it reflects the same rules as the CI gate.

## 7. Adding a command

```csharp
using Unity.Pipeline.Commands;

internal static class MyCommands
{
[CliCommand("my_command", "What this does")] // MainThreadRequired=true by default
internal static object Run(
[CliArg("text", "Input", Required = true)] string text,
[CliArg("count", "Repeat count")] int count = 1)
=> new { echoed = text, count }; // any object is serialised into `result`
}
```

- `[CliArg]` is optional (the C# parameter name is used); defaults come from the C# default value.
- Mutating commands take `confirm` + `dry_run` parameters by convention. Multi-field input goes
through a class implementing `IStructuredCommandInput`.
- `MainThreadRequired=false` only for thread-safe read-only work; `RuntimeOnly=true` marks a
Player-runtime command (hidden from the Editor listing).
- Discovery is via `TypeCache` — **a new command appears only after a recompile**.

**Placement rule:** never inside `Packages/tech.aspid.fasttools` — it would ship to users and drag
the experimental `com.unity.pipeline` dependency with it. Put it in
`Aspid.FastTools/Assets/DevTests/CliCommands/Editor/` (asmdef
`Aspid.FastTools.DevTests.CliCommands.Editor`, referencing `Unity.Pipeline` plus the package
assemblies it needs). Package internals are already opened to it via `InternalsVisibleTo`.

## Maintenance

```bash
unity upgrade --channel beta --check # CLI; --rollback restores the previous binary
unity pipeline list-versions --format json # package versions in the registry
unity pipeline upgrade --project-path "$P" # bumps manifest.json — a committed file, so review the diff
```

The package is experimental (`*-exp.*`); treat a version bump as a real change, not a chore.
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Types — Editor

Editor side of `SerializableType` and `[TypeSelector]`: the drawers, the picker window, and the
constraint resolution behind them. Runtime contracts live in `Unity/Runtime/Types/`.

## `[TypeSelector]` drives two different field shapes

| Field shape | Meaning | Drawn by |
|---|---|---|
| `string` | Assembly-qualified name; also what backs `SerializableType` | this folder (`Drawers/`) |
| `[SerializeReference]` managed reference | Picking a type **instantiates** it | `../SerializeReferences/` |

`TypeSelectorPropertyDrawer` dispatches on `SerializedProperty.propertyType`, so the same attribute
lands in two different code paths. **The managed-reference path is not in this folder** — look under
`Unity/Editor/Scripts/SerializeReferences/` for it.

The candidate list defaults to the field's declared type; a base type narrows it —
`[TypeSelector(typeof(IMelee))]`. Correct usage is enforced at compile time by the analyzer's
`AFT*` rules, so a wrong constraint is a build error, not a silent empty picker.

## Layout

```
Types/
├── TypeSelectorConstraintResolver.cs ← resolves the attribute's constraint to a candidate set
├── TypeSelectorHelpers.cs / TypeUtility.cs / SerializableTypeUtility.cs
├── Drawers/
│ ├── TypeSelectorPropertyDrawer.cs ← entry point, dispatches on propertyType
│ ├── SerializableTypePropertyDrawer.cs
│ ├── ComponentTypeSelectorPropertyDrawer.cs
│ ├── TypeIMGUIPropertyDrawer.cs ← static IMGUI body
│ └── TypeUIToolkitPropertyDrawer.cs ← static UIToolkit body
├── Selectors/
│ ├── TypeSelectorWindow.cs ← the picker window
│ ├── TypeSelectorView*.cs ← partials: View / Rows / Input / Navigation / Generics
│ ├── HierarchyBuilder.cs, NamespaceNode.cs, TreeNode.cs, NavigationController.cs
│ ├── GenericTypeResolver.cs, TypeSelectorFilter.cs, TypeSelectorIconResolver.cs
│ └── Settings/
│ ├── TypeSelectorSettings.cs ← project settings
│ ├── TypeSelectorPreferences.cs ← per-user EditorPrefs (favorites / recents)
│ └── TypeSelectorSettingsView.cs
├── VisualElements/ ← TypeField, InspectorTypeField
└── Extensions/ ← TypeExtensions
```

The `Drawers/` + `Selectors/` + `VisualElements/` split mirrors the sibling `Ids/` feature — see
`../Ids/CLAUDE.md`, which follows the same shape.

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

Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Internal editor components

Shared UIToolkit building blocks for this package's own editor UI. Everything here is `internal` —
in an `internal` class members are declared `internal` (or narrower), never `public`.

## Component convention

One subfolder per component under `Components/`, always the same four parts:

```
Components/AspidGradientButton/
├── AspidGradientButton.cs ← the VisualElement
├── AspidGradientButtonPreset.cs ← {Name}Preset
├── AspidGradientButtonExtensions.cs ← fluent extensions
└── Styles/ ← structs binding USS custom properties
```

- **Every component loads `AspidStyles.DefaultStyleSheet` first**, before its own sheet.
- Enums belong on their `Style` struct as a nested type named `Type` — not at namespace level.
- Styling goes in USS; code only calls `.AddClass()`. Class-name and `--aspid-*` variable grammar
lives in `../../../Resources/UI/CLAUDE.md` — read it before touching either.

## Shared pieces

| Path | What |
|---|---|
| `Styles/AspidStyles.cs` | default stylesheet + shared USS constants |
| `Styles/StatusStyle.cs`, `ThemeStyle.cs`, `InlineStyle<T>` | shared style helpers |
| `NavRing.cs`, `HoverSweep.cs` | keyboard nav ring and hover sweep, shared across window tabs |
| `DoubleClickTracker.cs` | double-click detection |

`ICustomStyleExtensions` is **not** here — it ships in runtime, at
`Unity/Runtime/VisualElements/Extensions/ICustomStyle/`.

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

Loading
Loading