@rig-c/review 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +242 -7
  2. package/docs/DESIGN.md +33 -5
  3. package/docs/REVIEW_PACKAGE.md +115 -0
  4. package/package.json +1 -1
  5. package/src/applied-context.ts +285 -0
  6. package/src/baseline.ts +175 -0
  7. package/src/bone-anchors.ts +123 -0
  8. package/src/bones-check.ts +105 -0
  9. package/src/build-route.ts +109 -0
  10. package/src/chest-landmarks.ts +122 -0
  11. package/src/chest-regions.ts +65 -6
  12. package/src/cli.ts +19 -8
  13. package/src/config-patch.ts +213 -0
  14. package/src/cut-edits.ts +184 -0
  15. package/src/environment.ts +226 -0
  16. package/src/feedback-export.ts +25 -1
  17. package/src/front.ts +11 -0
  18. package/src/handoff.ts +208 -0
  19. package/src/idle-tracks.ts +401 -0
  20. package/src/manifest.ts +15 -6
  21. package/src/mesh-edit.ts +317 -0
  22. package/src/mesh-entry.ts +351 -0
  23. package/src/open.ts +12 -5
  24. package/src/physics-tuning.ts +236 -0
  25. package/src/producer-files.ts +11 -0
  26. package/src/producer.ts +158 -0
  27. package/src/readings-run.ts +129 -0
  28. package/src/readings.ts +235 -0
  29. package/src/rebuild.ts +938 -0
  30. package/src/reference.ts +4 -3
  31. package/src/requirements-run.ts +160 -0
  32. package/src/requirements.ts +230 -0
  33. package/src/retarget.ts +211 -0
  34. package/src/retune-route.ts +52 -0
  35. package/src/rig-fields.ts +295 -0
  36. package/src/server.ts +59 -5
  37. package/src/session.ts +12 -0
  38. package/src/setup-mesh.ts +144 -0
  39. package/src/weights.ts +74 -0
  40. package/web/bones-panel.ts +230 -0
  41. package/web/chest-editor.ts +294 -0
  42. package/web/chest-regions-tool.ts +10 -4
  43. package/web/cuts-panel.ts +266 -0
  44. package/web/idle-panel.ts +280 -0
  45. package/web/index.html +2 -2
  46. package/web/inspector-blocks.ts +77 -0
  47. package/web/main.ts +813 -24
  48. package/web/mesh-editor.ts +303 -0
  49. package/web/mesh-panel.ts +273 -0
  50. package/web/part-selection.ts +13 -5
  51. package/web/part-thumbnails.ts +76 -0
  52. package/web/physics-panel.ts +151 -0
  53. package/web/readings-panel.ts +105 -0
  54. package/web/rebuild-client.ts +216 -0
  55. package/web/requirements-panel.ts +238 -0
  56. package/web/retarget-panel.ts +203 -0
  57. package/web/rig-fields-panel.ts +269 -0
  58. package/web/rig-overlays.ts +171 -14
  59. package/web/setup-pose.ts +30 -0
  60. package/web/styles.css +52 -5
  61. package/web/weights-view.ts +159 -0
  62. package/web/workspace-preferences.ts +17 -12
  63. package/web/workspace-ui.ts +5 -3
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  A reusable local workspace for a person to inspect an agent's rig output, compare source references, and export feedback ready to become an issue. The person receives a package or opens an existing output folder; the agent does not build a new viewer for each rig.
4
4
 
5
- The app provides one inspection workspace for setup geometry and motion, compiled-model playback through `@rig-c/play`, local image, video and pose-sequence references, checkpoints, audio audition, and saved structured feedback. **Motion is view, annotate and export only.** Navigation changes inspection time; it never retimes, authors keys, or writes `motion.json`. Modeling touch-up remains a design goal; this starter does not edit meshes, weights or source art.
5
+ The app provides one inspection workspace for setup geometry and motion, compiled-model playback through `@rig-c/play`, local image, video and pose-sequence references, checkpoints, audio audition, and saved structured feedback. **The producer writes motion in scratch; review never authors keys.** Navigation changes inspection time; it never retimes, and review never writes the package's `motion.json`: motion edits are data (config patches, pose maps, retarget flags) the installed producer turns into a scratch build. Meshes are edited in setup space and rebuilt by the producer (`rig-parts remesh --delta`, #100/#135); weights and source art are not edited.
6
6
 
7
7
  ## Place in the toolchain
8
8
 
@@ -155,8 +155,22 @@ rig-review open /absolute/output --reference /absolute/original.mp4 --animation
155
155
  rig-review feedback /absolute/output --reference /absolute/original.mp4
156
156
  rig-review open /absolute/a --compare /absolute/b --reference /absolute/original.mp4 --animation dance
157
157
  rig-review feedback /absolute/a --compare /absolute/b --reference /absolute/original.mp4 --animation dance
158
+ # Producer handoff (single review only): the config, parts and inspected rig an Apply rebuilds from
159
+ rig-review open /absolute/output --config /absolute/output/rig/config.json [--parts /absolute/output/rig/out] [--source <png> --full <dir> [--head <dir>]]
160
+ rig-review feedback /absolute/output --config /absolute/output/rig/config.json
158
161
  ```
159
162
 
163
+ `--config`, `--parts`, `--source`, `--full` and `--head` declare the producer
164
+ handoff (see `docs/REVIEW_PACKAGE.md`, *Producer handoff*). They win over the
165
+ review.json `producer` block, which wins over discovery (`config.json`, then
166
+ `rig/config.json` inside the opened folder, then `../config.json` beside an opened
167
+ `out/`, named as discovered outside the folder). Every path must sit inside the
168
+ package root; a path outside it or a symlink that leaves it is refused by name.
169
+ The handoff files are hashed into the reviewed identity, so `feedback` and
170
+ `control` take the same flags to retrieve the same session, and changing the
171
+ config bytes starts a new pending review. `--parts` and `--source`/`--full`/`--head`
172
+ need `--config`; `--source` and `--full` go together; compare takes none of them.
173
+
160
174
  `open` chooses an explicit `--animation` first, then the normalized report's
161
175
  `animation` after validating actual model membership, then a unique actual clip
162
176
  (or unique common clip in compare). Thus `rig-review open /absolute/output
@@ -223,7 +237,8 @@ stderr and exit code alone:
223
237
  - `OPEN_ANIMATION_UNKNOWN <name>`: the explicit or report-named clip (JSON-quoted) is not in the compiled model.
224
238
  - `OPEN_ANIMATION_AMBIGUOUS`: no flag or report selects one clip, or the A/B reports conflict.
225
239
  - `OPEN_ANIMATION_UNVALIDATABLE`: a clip was requested but no compiled model can validate it.
226
- - `OPEN_OPTION_INVALID <flag>`: an unknown, duplicate, empty or value-less option.
240
+ - `OPEN_OPTION_INVALID <flag>`: an unknown, duplicate, empty or value-less option, a handoff flag without `--config`, `--source` without `--full` (or the reverse), or a handoff flag with `--compare`.
241
+ - `OPEN_HANDOFF_INVALID <flag or path>`: a declared or discovered producer handoff path is refused — `HANDOFF_PATH_OUTSIDE_ROOT`, `HANDOFF_SYMLINK_ESCAPE`, `HANDOFF_INPUT_MISSING`, `HANDOFF_INPUT_INVALID` or `HANDOFF_PATH_INVALID`, named after the colon.
227
242
  4. Exit codes: 0 applied, 1 refused. (`control` keeps its own exit 2 for an
228
243
  unapplied command.)
229
244
 
@@ -382,6 +397,118 @@ body. A drawer keeps its title and close button in place while its body
382
397
  scrolls. The per-inspection **Repeat idle** toggle sits in the Animate
383
398
  transport row.
384
399
 
400
+ Animate's **Environment** block drives the player's physics while you
401
+ inspect: a **Wind** base (−100…100), **Breeze**, a **Gust** pulse
402
+ (peak 0…200, duration 0.1…20 s, **Repeat** every max(6, duration + 0.6) s)
403
+ and **Gravity** (0…4), applied through `player.environment` in the same frame
404
+ step that draws. The gust clock is the stepped player time, so pausing freezes
405
+ it. **Jolt** sends a translate-and-return impulse through `player.moved`;
406
+ with **Drag to trail** on, dragging the rig pane moves the rig's physics
407
+ instead of the camera (Space still pans). **Reset physics** returns the
408
+ runtime start values and re-poses from the current time, clearing the physics
409
+ history. Until a control is touched the player stays at the runtime start values. The
410
+ state is kept in the local draft and recorded in feedback as
411
+ `context.environment`; nothing is written to the model or motion. The compare
412
+ page does not have this block.
413
+
414
+ Below it, the **Physics tuning** panel (`#physics-panel`) edits the physics
415
+ constraints through the producer. Each row is a constraint of the inspected
416
+ model, matched to the handoff config's `constraints[]` entry of the same name
417
+ and kind; a row is editable only when every field the panel edits reads the
418
+ same on both sides. Rows the model has and the config does not declare (the
419
+ rig stage's `turn` twins), the config's transform constraints and any row that
420
+ does not map 1:1 are listed under **Not editable** with the reason. Picking a
421
+ row opens its editor: a slider and number field for each of `strength`,
422
+ `damping`, `inertia`, `mass`, `wind`, `gravity` and `mix` the config declares,
423
+ and toggles for the `x`, `y` and `rotate` components (0 is off). A **Chain**
424
+ (from bone parentage: links on parent bones that move the same components;
425
+ pick one from the list or with **Pick on stage**) takes a **Ramp**: one value
426
+ per link, linear from root to tip. Every edit is a config patch op at
427
+ `/constraints/<i>/<field>` that also records the constraint's `name`; the
428
+ pending ops are listed with a per-op **Revert** and kept in the local draft.
429
+ **Apply** sends them as one patch (`rig`, then `check`, about 20–50 s on a real
430
+ package); the scratch build reloads under the same wind, gust, jolt and gravity
431
+ state Apply was pressed in, and the readout shows the `ANIMATIONS_DO_NOT_FOLD`
432
+ and `TEXTURE_STRETCH` lines before and after. BEFORE for the inspected build
433
+ is its own check: once the baseline reproduces, review runs `rig-parts check`
434
+ on that baseline output in the background (about 16 s on a real package) and
435
+ stores the summary beside the baseline record, so a reload reads it; until it
436
+ finishes the line says "check running…", a producer refusal is shown verbatim,
437
+ and "not in check summary" remains only when the baseline did not reproduce. A
438
+ refusal is the producer's line verbatim (`CONFIG_CONSTRAINT_*`,
439
+ `CONFIG_NAME_RESOLVES`, rigc's `A23` for a mass of 0) and the stage is
440
+ unchanged. **Reset** returns to the inspected build and clears the pending ops.
441
+ Submit refuses a review written on an applied build whose check failed
442
+ (`EDIT_SUBMIT_UNCHECKED`). Live preview before Apply waits on play#147.
443
+
444
+ Below it, the **Idle tracks** panel (`#idle-tracks`) edits the idle through
445
+ the producer. It lists the handoff config's `motion.tracks`: a single track
446
+ opens amp, period, phase and base (base can be added or taken out); a chain
447
+ track opens one amp per link and lag, and draws a handle at each link on the
448
+ stage (drag up or down; arrow keys step 0.05°). Period is a select of the
449
+ divisors of `motion.duration` (`duration / 1 … 12`) — the only periods the
450
+ producer accepts — beside a field for typing one, which review's pre-check
451
+ refuses with the producer's own line (`CONFIG_PERIOD_DIVIDES_DURATION`). A
452
+ track is added by bone + property or by chain (it starts still: amplitude 0,
453
+ the whole idle's period) and removed with **Remove**; a second track on a
454
+ property already keyed (a chain link's `rotate`, a blink member) is refused
455
+ before Apply by the producer's `CONFIG_BONE_PROPERTY_KEYED_ONCE`. The blink's
456
+ `t` is a marker above the transport scrub, dragged along the timeline (arrow
457
+ keys step one 12 fps frame), with `squash` and `brow_drop` sliders. The
458
+ pending ops (`/motion/tracks/<i>/…`, `/motion/blink/…`) are listed with a
459
+ per-op **Revert** and kept in the local draft. While edits are pending the
460
+ **preview** drives the edited bones through the player's `adjust` with the
461
+ producer's sine on the bone the idle keys (`<bone>_ctl` under ctl keys, read
462
+ off the handoff rig), labelled "preview — Apply decides"; it is never
463
+ recorded or submitted, and a property the inspected idle does not key yet is
464
+ not previewed (`IDLE_PREVIEW_TARGET_UNKNOWN`). **Apply** clears the preview and
465
+ sends one patch (`rig --idle-keys <the inspected rig's>`, then `check`, about
466
+ 30 s on a real package); the built idle replaces the preview, and the
467
+ readout shows the `CHAIN_LAG`, `TIP_OVER_ROOT` and `BREATH_VISIBLE` lines and
468
+ check's `loop:` line before and after. **Reset** clears the preview and the
469
+ pending ops and returns to the inspected build.
470
+
471
+ Below that, the **Readings** block (`#readings`) runs the compiler's own
472
+ readings of the drawn animation: a **Folds** toggle, a **Pass-through**
473
+ cloth:leg pair and a **Seam** slot:slot pair (slot names suggested from the
474
+ model), an **Overlaps** toggle and an **Angle range** bone against a relative
475
+ bone. **Run readings** sends the chosen readings (names only) to
476
+ `POST /api/readings`; the server runs one `rigc render --animation <drawn>
477
+ --folds | --pass-through <cloth>:<leg> | --seam <a>:<b> | --overlaps |
478
+ --angle-range <bone>:<relative>` per reading on the inspected build and on the
479
+ latest scratch build of the session (if an Apply built one), into scratch
480
+ under the state directory (`readings/<package>/<build>/<key>/`; an identical
481
+ reading on the same build and compiler is reused; about 0.5–5 s each on a real
482
+ package). The frames each reading's JSON lists become ticks under the
483
+ transport scrub — inspected above, scratch below, in two colours — and
484
+ clicking a tick seeks to that frame's time (frame / fps). Each reading gets a
485
+ worst-frame line, `<build> · <reading> <pair>: worst at frame N — <figure> ·
486
+ <rigc's line>`, which also seeks; folds and overlaps list each folding mesh or
487
+ moving pair under it. `--angle-range` writes no file, so its ticks are the
488
+ lowest and highest frames its printed line names. An unknown slot or bone is
489
+ rigc's to refuse, and its line is shown verbatim. While the Environment
490
+ controls are applied, their current wind and gravity go along as a one-step
491
+ `--environment` schedule (only when the installed rigc's `render --help` lists
492
+ the flag). A reading is not a gate; nothing is written into the package. The
493
+ pairs of the last run are recorded in feedback as `context.readings`.
494
+
495
+ Below Readings, the **Requirements** block (`#requirements`) declares what the
496
+ scene asks of the motion (issue #113): pick a kind (contact, follow, aim,
497
+ range, stretch, seam), an animation and the subject — the slot picked on the
498
+ stage pre-fills the slot, its mesh attachment and its bone — then set each
499
+ bar on a slider whose track marks the last measured value. **Check** sends the
500
+ typed requirements (names and numbers only) to `POST /api/requirements`;
501
+ review writes them as a `spine-parts-requirements/1` file (`spec`, `fps`,
502
+ `requirements`; never `may_fold`, scene targets or the part exception lists)
503
+ into scratch and runs the installed producer's `rig-parts check --rig <rig>
504
+ --out out --parts <handoff parts> --requirements requirements.json` on the
505
+ inspected rig spec or the latest scratch build. Each requirement gets one
506
+ line with its worst frame (click to seek); `REQUIREMENTS_*`,
507
+ `CHECK_REQUIREMENT_MET` and `CHECK_REQUIREMENT_MEASURABLE` lines are shown
508
+ verbatim. A kind the rig spec cannot offer (no mesh slot, no ik or transform
509
+ constraint, fewer than two slots) is disabled with the reason. The file rides
510
+ submitted feedback by hash and content as `context.requirements`.
511
+
385
512
  The view bar is one row and keeps the layer toggles in reach in every section: **Images**
386
513
  (`I`), **Mesh edges** (`M`) and **Bone points** (`B`). The keys do nothing while
387
514
  a text field, textarea, select or editable text has focus, with Ctrl/Alt/Cmd
@@ -437,15 +564,123 @@ Source-bound links/drafts and explicit feedback preserve that side/slot plus exa
437
564
 
438
565
  This explorer is inspection. Producer-backed optimization and candidate Apply/undo/export remain separate work; the explorer does not generate mesh, rig or motion bytes.
439
566
 
440
- ## Chest regions (designation only)
567
+ ## Chest regions (designate, rebuild, watch)
568
+
569
+ In **Setup**, the view bar's **Chest regions** button (pressed state; hidden in Animate) opens the chest-region editor bar directly under the view bar and enables editing on the rig pane. The bar has an **R** and an **L** group, each with **X**, **Y**, **R** (core radius, 6–120) and **Band** (4–120) in crop pixels, x screen-right and y down; **Mirror R → L** copies R's y, core radius and band to L; **Download JSON**; and **Apply · Rebuild**.
570
+
571
+ On the rig pane each region is drawn as two circles: the core (full weight) and a dashed outer circle at core + band, R in one colour and L in another (two theme tokens per side), with a centre handle, a core handle on the right edge, a band handle on the top edge and a label `R (x,y) r:… b:…` under the outer circle. Dragging the centre moves the region, dragging the core edge sets r and dragging the band handle sets the band, all in crop pixels through the current camera scale. Each drag and each committed bar value is clamped live with the public example rebuild script's non-overlap rule: integer values, both cores plus 8 px must fit the centre distance minus a 2 px margin (both shrink by one factor) and the bands share what remains. A side the rule changed flashes its label and the footer says `CHEST_REGIONS_CLAMPED`. A handle drag never moves the camera; a drag anywhere else, or with Space held, still pans, and the wheel still zooms.
572
+
573
+ The bar opens on **Circles** (the R / L groups above); **Landmarks** beside it (#105) states the chest the way the producer's `meshes.<part>.contour.bust` does. Three clicks on the part place the **right apex**, the **left apex** and the **cleavage** (each pick button arms the point its click places, then the next unplaced one; a placed marker drags), with **X** / **Y** fields for each in rig pixels, an **α** slider (core ratio, the producer's default 0.22), a **Clearance** slider (rig px, default 3.8) and optional **Bone R** / **Bone L** (empty is the producer's default, `boob_r` / `boob_l`). The two circles are drawn live with the producer's own derivation (`@rig-c/parts` `deriveBustRegions`): with `d` the distance from an apex to the cleavage, `R = round(d − clearance)`, `r = round(alpha × R)` and `band = R − r`, centred on the apex, named `bust_r` / `bust_l`, spacing 14. Review pre-checks the producer's one geometric refusal on the same numbers — `CONFIG_BUST_CLEARANCE` when `clearance <= 0`, `R_r + R_l >= |apex_r − apex_l|` or a radius is not positive — draws both circles in the overlap colour, shows the producer's detail, and Apply sends nothing. Otherwise Apply · Rebuild sends `{part, bust, configHash}`: the part's entry with `contour.bust` in place of `contour.regions` (the hand-written regions are dropped; every other field as written) goes to the same `rig-parts remesh --mesh` run as Circles, and the producer derives the circles again and judges the bones (`CONFIG_NAME_RESOLVES`) and a region declared twice (`CONFIG_REGION_NAME_UNIQUE`), verbatim. The part is the contour part whose entry already states `contour.bust`, else the one the bones name, else the only contour part. A config entry that states `contour.bust` seeds an untouched Landmarks mode.
574
+
575
+ Rig pixels are the whole figure's frame, which is the crop frame the model's stage fixes (below), so a click maps through the same crop → world mapping. A part's own image is that frame less the part's `parts.json` box less the producer's 4 px pad: rig = part image + (box.x − 4, box.y − 4) (`src/chest-landmarks.ts` `partToRig` / `rigToPart`).
576
+
577
+ **Apply · Rebuild** runs the installed producer on the designated regions and reloads the result on the stage, so the loop is point → rebuild → watch → repeat → submit. Review builds no mesh itself and never runs a manifest command:
578
+
579
+ - **Producer.** `@rig-c/parts`' `rig-parts`, resolved the way `rigc review` resolves `rig-review`: `node_modules/@rig-c/parts/package.json` in the nearest directory walking up from the package, then from the directory `rig-review open` was run in; its `bin` entry is read from that manifest, never from `PATH`. The compiler is the `rig-c` that `rig-parts` itself finds walking up from its own package. Absent: `MODELING_PRODUCER_NOT_INSTALLED: … — bun add -d @rig-c/parts`; older than the first release with `remesh` (1.40.0): `MODELING_PRODUCER_TOO_OLD <found> 1.40.0`. Review never installs anything.
580
+ - **Inputs.** The producer handoff (`docs/REVIEW_PACKAGE.md`, *Producer handoff*): `open --config` (and `--parts`), else the review.json `producer` block, else `config.json`, then `rig/config.json` inside the opened folder, then `../config.json` beside an opened `out/`. The parts directory defaults to `out/` beside the config and holds `parts.json`, `parts/` and the inspected `rig/`. The editor bar shows, before any Apply, how the config was found (declared, discovered, or discovered outside the opened folder), its path and SHA-256, and the baseline. Without a handoff the bar shows `MODELING_INPUT_UNAVAILABLE: config.json — looked at …` and Apply refuses `MODELING_HANDOFF_MISSING`, naming `--config`. The request carries only `{part, regions, configHash}`; the server reads the config, replaces `meshes.<part>.contour.regions` with the designated regions (every other field as written) and runs `rig-parts remesh --rig <out>/rig --config <config> --parts <out> --part <part> --mesh mesh.json --out revision`, then `rigc build --profile rig-play --pack` with the revision's own `--page-edges` / `--pack-shape`, both with the scratch directory as working directory. A region row with equal semi-axes and no clip is sent as the config's circle, any other as the 32-sample polygon. An untouched editor starts from the config's own circle regions and contour numbers.
581
+ - **Baseline.** Once per session, when the handoff and the producer both resolve, review runs `rig-parts rig --config <config> --parts <parts> --out <state>/baseline/<key>/baseline` and compares every file it writes (rig.json, motion.json, mesh_report.json, sets.json, images/*) with the inspected rig directory: `baseline: reproduced — rig.json <hash> = inspected <hash>`, or `baseline: not reproduced — <first differing path>`. The key is the package hash, the handoff record and the producer versions (no clock), so a reopen reuses the result. Ops that rebuild the whole rig refuse `EDIT_BASELINE_MISMATCH <path>` unless the baseline reproduced; `remesh` keeps its own `REMESH_BASELINE_MISMATCH`. Once it reproduces, `rig-parts check` runs on the baseline output in the background (into `<state>/baseline/<key>/inspected-check/`, with Apply's default `--page-edges free --pack-shape polygon`) and its summary is stored beside the baseline record as `inspected-check.json`; that is the physics readout's BEFORE for the inspected build, and a reopen reads it instead of re-running.
582
+ - **Scratch.** `<state>/rebuild/<package source hash>/<designation hash>/` under `RIG_REVIEW_STATE_DIR` (refused inside the package), named from the package, the config bytes, the part, the entry and both tool versions — no clock. The same designation again reuses its build and says so; a new one replaces the previous scratch build. The inspected package's files are only read.
583
+ - **Watching.** The producer's lines stream into the editor bar (the latest line and a short log); a refusal shows the producer's first named line verbatim (for example `FAIL REMESH_REQUEST_HELD: …`) and the stage is unchanged. On success the compiled scratch build (served read-only under `/rebuild/<id>/`, its model and pages hashed) replaces the drawn rig in place with the same camera, animation, time and part set; playback resumes with physics when it was running. **Reset** returns the stage to the inspected build.
584
+ - **Record.** The last applied state (`rig-review-applied/1`: part, regions, for a Landmarks Apply also `bust` — the entry as sent, with `regions` its two derived circles — config hash, entry SHA-256, producer and compiler versions, the producer's line and the scratch model's SHA-256) stays in the inspection draft, and feedback added while that build is on the stage carries it as `context.applied`. The review's `reviewedSource` still names the inspected build.
585
+ - **Config patch.** The same loop takes any config edit as data: `POST /api/rebuild {patch}` with a `rig-review-config-patch/1` document (JSON Pointer ops over the handoff config, refused `EDIT_PATCH_STALE` when made against other config bytes) rebuilds the whole rig (`rig`) or one part (`remesh`) into scratch, compiles it and runs `rig-parts check` on it; the page's `window.rigReviewEditor.applyPatch(patch)` streams, reloads and resets like Apply, and the draft and `context.applied` carry `rig-review-applied/2` (the patch, its hash, the producer's line, check's summary and the diff of every document the producer writes, per file, with the images by hash). Schema and refusals: `docs/REVIEW_PACKAGE.md`, *Config patch and Apply*.
586
+
587
+ The editor's values are the fields of Setup's **Chest regions · detail** block (R is region 1, L is region 2), which also holds what the bar does not: per region a name, a bone chosen from the loaded model's actual bones, the two semi-axes, a spacing and an optional medial clip x, plus the contour settings (tolerance, margin, spacing, max edge, stray, bridges). Setting R in the bar writes both semi-axes. A region with equal semi-axes and no clip exports as the examples' circle, `{"name", "shape": "circle", "cx", "cy", "r", "bone", "spacing", "band"}`; unequal semi-axes or a clip export as the generator's 32-sample polygon on its 1/256-pixel grid and are drawn filled, without handles.
588
+
589
+ The overlay needs the loaded model to fix the crop: its declared stage must be the crop frame with the root at its bottom centre (`x = -width/2`, `y = 0`) and the drawn root bone must sit at the world origin, so crop `(x, y)` is world `(x - width/2, height - y)`. Otherwise the editor bar and the detail block show `CHEST_REGION_MAPPING_UNAVAILABLE: <what is missing>`, nothing is drawn, and the values still edit numerically and export from a crop size you enter.
590
+
591
+ **Download JSON** (and the detail block's **Download chest-regions.json**) saves two files in the browser: `chest-regions.json`, formatted exactly as the public example files, and `chest-regions.binding.json`, which names the package source hash, the rig and motion SHA-256 values, the export time, the viewer and runtime versions and the mapping used. An export holds 1–8 regions, the set Apply accepts (#118): a detail row without a centre x is not designated, so one row exports alone; two regions keep the public examples' text byte for byte. Export refuses `CHEST_REGIONS_COUNT`, `CHEST_REGION_NAME_DUPLICATE`, `CHEST_REGION_BONE_UNKNOWN <bone>`, `CHEST_REGION_OUT_OF_CROP <name>`, `CHEST_REGION_OFF_GRID` and `CHEST_REGIONS_OVERLAP <a> <b>` (every pair is tested). Two circles overlap when their centre distance minus both cores is less than the sum of their bands; any other pair is tested through polygons. The generator refuses per mesh vertex, so this check can refuse a pair it would accept, never the reverse.
592
+
593
+ Field values stay in the inspection's local draft. They never enter the review session's feedback, nothing is written into the package, and an export is review evidence, not an approval (see `docs/DESIGN.md`).
594
+
595
+ ## Mesh panel (density, regions, segments → remesh)
596
+
597
+ In **Setup**, under Chest regions · detail, the **Mesh** panel (`#mesh-panel`, #106) edits the selected part's `meshes.<part>` entry from the producer handoff's config — the entry `rig-parts remesh --mesh` takes — and rebuilds that one part. Select a mesh part in the parts list; the panel starts from the config's own entry (or the draft's pending entry for that part).
598
+
599
+ - **Modes.** The two modes whose fields the panel draws: **contour** (sliders for `tolerance`, `margin`, `spacing`, and optional `maxEdge`, `budget`, `stray`, each with a checkbox that declares or removes it; `bridges` as a checkbox) and **grid** (`grid`, optional top-level `stray`). In both: `r`, optional `exponent`, `rule` (absent / `distance` / `heat`), and the weight cut `influences.maxInfluences` and `influences.floor`. Each slider's range is review's drawing range only; the number field beside it takes any value and the producer judges it. An `auto` entry is shown with `MESH_PANEL_MODE` and no controls; an `influence` entry is not offered either, because `remesh --help` names grid, contour and auto entries and not influence (that is the producer's to document). The mode itself is not switched here.
600
+ - **Regions** (contour). **Add circle** adds a circle region (drawn like the chest editor's circles, in the mesh-region colour); **Draw polygon** arms the stage: each click places a point in rig px (whole pixels, so on the producer's 1/256 grid), **Enter** (or the button again) closes it with three or more points and **Escape** drops it. **Drag on stage** lets a polygon vertex or a circle centre be dragged; a drag never pans the camera. Each region row has its name, a bone chosen from the loaded model's bones, `spacing` and `band` (and `cx`, `cy`, `r` for a circle) and **Remove**. A `contour.bust` in the entry stays as written (the chest editor's Landmarks mode edits it).
601
+ - **Segments.** The entry's `segments` as written, each removable; **Add** appends a bone picked from the loaded model's bones. In contour mode each string segment (a chain) has a **ribs stations** field (empty: no ribs); removing a segment drops its ribs.
602
+ - **Pending entry.** The diff against the config's entry, one line per JSON Pointer (`/meshes/<part>/contour/spacing 28 → 20`). Review pre-checks the pending entry with the config loader's rules and names (`CONFIG_MESH_MODE`, `CONFIG_FIELD_PRESENT`, `CONFIG_FIELD_TYPE`, `CONFIG_REGION_NAME_UNIQUE`, `CONFIG_NAME_RESOLVES`, `CONFIG_RIBS_MODE`, `CONFIG_STRAY_MODE`) and Apply is disabled while one stands. What the outline refuses at the rig stage — a spacing whose keep radius snaps to 0 (`CONTOUR_PARAMETER`), a bridge no quad makes (`CONTOUR_BRIDGE`), a heat source (`RIG_HEAT_*`), a remesh rule (`REMESH_*`) — is sent and comes back as the producer's line.
603
+ - **Apply.** The pending entry becomes one config patch, `replace /meshes/<part>` with the whole entry and nothing else, run by `remesh --part <part>` through the patch runner (*Config patch* below): remesh into scratch, compile, check. On a build the revision replaces the drawn rig in place (same camera, animation, time and world), the **Mesh edges** layer turns on so the new triangulation is drawn, and the panel shows the part's vertex / triangle / hull counts before and after from the revision's `remesh.json`, one line comparing `remesh.json`'s after-count with the revision model's triangles for the slot and the triangles the overlay draws (`MESH_COUNTS_AGREE` / `MESH_COUNTS_DIFFER`, every figure named), and `TEXTURE_STRETCH` before (the build that was on the stage; for the inspected build its background check) and after. **Folds reading** runs the #108 folds reading of the drawn animation on the inspected build and on this revision and puts one worst-frame line per build in the status. A refusal leaves the stage and the pending entry as they were and the status line is the producer's line verbatim (for example `FAIL REMESH_CANDIDATE_REFUSED: … CONTOUR_PARAMETER: contour mesh "<part>", spacing — is 0.003; its keep radius … snaps to 0 grid units of 1/256 px …`).
604
+ - **Reset** returns the pending entry to the config's and the stage to the inspected build.
605
+ - **Record.** The draft keeps `#mesh-pending` (`{schema:"rig-review-mesh-entry/1", part, entry}`, only while it differs from the config). A built Apply's `context.applied` is `rig-review-applied/2` with `mesh` added: the part and its counts before and after.
606
+
607
+ ## Weights (setup pose, watch only)
608
+
609
+ In **Setup**, after the Mesh panel, the **Weights** block (`#weights`, #107; **Weights** in the block row) shows the selected slot's mesh weights as the compiled model document stores them. **Show weights** draws the slot's mesh at the setup pose over a veil that dims the rest of the rig: each vertex is a dot and each triangle is filled with the mean of its vertices' colours.
610
+
611
+ - **Bone weight.** Pick a bone (the slot's influencing bones first, then every other bone of the model): each vertex is coloured by that bone's stored weight on the weight ramp, 0 → 1 (`--rv-weight-0`, `--rv-weight-half`, `--rv-weight-1`); a bone that does not influence a vertex is 0 there. The legend shows the ramp and the bone.
612
+ - **Max influence.** Each vertex takes its dominant bone's colour (the largest stored weight; the first stored on a tie), from `--rv-weight-bone-1` … `-6` in the influencing bones' order; the legend names each bone with its colour.
613
+ - **Hover.** The vertex under the pointer (within 10 px) is ringed and labelled with its index, the bone and the weight drawn; the block's readout lists all of its stored influences. Values are printed as stored, never rounded or normalised.
614
+ - **Source.** The vertices and weights are the slot's attachment at the setup pose (`skins[].attachments.<slot>.<name>.vertices` of `rigc-compiled/3`: `{weighted:true, bindings}` — per vertex `{bone, x, y, weight}` — or `{weighted:false, xy}`). An unweighted mesh is its slot's bone at 1.0, named. A linked mesh reads its source mesh. The attachment is the one a never-played player resolves for the slot, and the positions are the stored bone-space coordinates through that player's setup-pose bones with the published player's skinning formula — the stage player is not touched, so the drawing can sit apart from an animated pose on the stage.
615
+ - **Refusals.** A package without a compiled model document refuses `WEIGHTS_MODEL_UNAVAILABLE`; no slot `WEIGHTS_SELECT_SLOT`; a slot without a mesh `WEIGHTS_NO_MESH <slot>`; a malformed attachment `WEIGHTS_MODEL_INVALID`.
616
+
617
+ Nothing here edits, applies or records a weight: the block adds nothing to the draft or to feedback, and weight editing is out of scope. With **Edit mesh** on, the weights layer is not drawn.
618
+
619
+ ## Bones (config anchors, check, rebuild)
620
+
621
+ In **Setup**, after Weights, the **Bones** block (`#bones-panel`, #109; **Bones** in the block row) edits where the producer config puts its bones. It reads the handoff config's `bones` as written (`GET /api/rebuild` returns them as `bones`): a single bone `{name, parent, at, tip?}` or a chain `{chain, parent, points, tip}` whose links are `<chain>0 … <chain>n` (rig-parts AUTHORING, `bones`).
622
+
623
+ - **Stage mode.** **Edit bones on stage** (`#bones-edit`, `aria-pressed`; it pauses playback and ends when Setup is left) draws every anchor on `#rig-overlay` at its config position: an origin (`at`) as a square, a chain point as a circle, a tip as a diamond, joined along the chain or from the bone to its tip. Positions are rig px, y down, mapped to the rig world through the same crop → world mapping the chest and weights views use (the model's stage). They are the config's setup positions; an animated stage pose can sit apart from them. Without that mapping no handle is drawn and the block says why.
624
+ - **Drag.** Drag a handle (within 9 px) to move it; the pointer's delta is snapped to whole rig px and added to the config's value, written with the decimals that value already has (an integer stays an integer). **Bone** (`#bones-select`) picks a bone, whose anchors can also be typed. Each move is one config patch op, `replace` at `/bones/<i>/at`, `/bones/<i>/points/<k>` or `/bones/<i>/tip`, recording the bone's name (a chain's `chain`) beside the index; the server refuses an index that no longer holds that name (`EDIT_PATCH_NAME_MISMATCH`). The pending ops are listed with their delta and a Revert each; the draft keeps them (`#bones-pending`, `{schema:"rig-review-bones-draft/1", edits}`, restored only while each anchor still holds the value it was made from).
625
+ - **Check.** `POST /api/bones-check` applies the ops to a scratch copy of the handoff config and runs the installed producer on it: `propose --from-config` prints its LINT lines, the LINT count line and one coverage line per bone (shown as printed; a LINT line blocks nothing — propose exits 1 on any, and Apply stays enabled), and `compare --left <handoff config> --right <patched>` gives each moved bone's origin (and tip) distance and the summary lines. `propose --from-config` reads the source painting: a handoff without one (declare it with `--source <png> --full <dir>`) shows `MODELING_INPUT_UNAVAILABLE: source painting — …` in place of the LINT lines, and compare still runs. Any change to the pending ops clears the last Check's lines.
626
+ - **Apply · Reset.** Apply sends the ops as one `rig` patch through the generic runner (`rig --config`, compile, `check`): a whole-rig op, so it runs only when the baseline reproduced the inspected rig (`EDIT_BASELINE_MISMATCH` otherwise). A producer or rigc refusal (`RIG_TIP_DISTINCT` for a tip dragged onto its origin, `CONFIG_NAME_RESOLVES`, rigc's `A51`, `RIG_HEAT_BONE_SOURCE`, …) is shown verbatim and leaves the stage as it was. On a build the re-weighted rig reloads in place, and the Weights block reads the revision. Reset returns to the inspected build and clears the pending ops.
627
+
628
+ Adding, removing or re-parenting bones is out of scope.
629
+
630
+ ## Rig fields (regions, still row, stops, inherit, turn)
631
+
632
+ In **Setup**, after Bones, the **Rig fields** block (`#rig-fields`, #110; **Fields** in the block row) edits the producer config's small fields. `GET /api/rebuild` returns them as written in `fields` (`regions`, `motion.blink`, `stops`, `bones`, `turn`); the block's state line counts what the config states. Every edit is one config patch op at the field's own pointer, listed with a Revert; the draft keeps the pending ops (`#rig-fields-pending`, `{schema:"rig-review-rig-fields-draft/1", edits}`, restored only while each field still holds the value the edit was made from).
633
+
634
+ - **`regions.<part>`** — the bone a rigid part rides. Pick the part (selecting a region part on the stage picks it) and its bone among the bones the config declares (chain links expanded, and `root`): `replace /regions/<part>`. A meshed part is not offered.
635
+ - **`motion.blink.still.<part>`** — a lash's still row, `{row, bone}`: rows above `row` (rig px, y down) are drawn by `<part>_still` on `bone` and do not blink. The block offers the region parts on a bone the blink's `eyes` names; **Add still row** starts at the eye bone's origin row on its parent. The row is drawn across the part on the stage and dragged in whole rig px (its number and bone can also be typed): `add /motion/blink/still/<part>` (with `add /motion/blink/still {}` first when the config states no `still`), **Remove** sends `remove`. It sits in Setup, beside the part it cuts, rather than in Animate's blink controls.
636
+ - **`stops[i]`** — a stop's inward and outward bounds (degrees). Picking a stop draws its two arcs on the stage at the first stopped link's origin, around the rest aim at the touch point (`at` in `against`'s frame, as rig-c's `expandStops` aims it); dragging an arc end sets that bound in whole degrees (never below 0). The op replaces the whole entry, every other key as written, and names its `chain`: `replace /stops/<i>`. Adding a stop is out of scope.
637
+ - **Inherit** — a single bone's `inherit`, or a chain link's entry in `link_inherit`, picked among rig-c's five modes (`normal`, `onlyTranslation`, `noRotationOrReflection`, `noScale`, `noScaleOrReflection`; absent is `normal`): `add` (or `remove`) at `/bones/<i>/inherit` or `/bones/<i>/link_inherit`, naming the bone.
638
+ - **`turn`** — the pivot and a part's depth, each a slider with a number: `replace /turn/pivot`, `replace /turn/depths/<part>`. The **dial** holds the drawn build's `turn` bone at x rig px through play's `adjust` (shared with the idle preview), labelled *preview — Apply decides*: it shows the build on the stage, not the pending depths, and it is never recorded. Apply and Reset set it back to 0. `turn.deform` is out of scope.
639
+
640
+ **Apply** sends the pending ops (checked against the block's scope, `RIG_FIELDS_OP_OUT_OF_SCOPE` otherwise) as one `rig` patch through the generic runner (`rig --config`, compile, `check`) — a whole-rig op, so it runs only when the baseline reproduced the inspected rig. rig-parts has no `retune` op yet; when it has, stops and inherit can move to it. The producer's refusal (`CONFIG_STILL_OFF_THE_BLINK`, `RIG_STILL_ROW_CLEAR`, `CONFIG_BONE_INHERIT`, `CONFIG_TURN_PIVOT_BEHIND_NEAREST` and the other `CONFIG_TURN_*`, `CONFIG_NAME_RESOLVES`, rigc's `RIG_STOP_BOUNDS`, …) is shown as printed and leaves the stage as it was. On a build the revision reloads in place, the status line names the per-file diff, and the readout shows check's `BLINK_NO_HOLE` and `FEET_STAY_LEVEL` lines before (the build that was on the stage, or the inspected build's stored check) and after. **Angle range** runs the angle-range reading (#108) on the inspected and the latest scratch build with one click: the inherit bone against its parent, else a re-boned part's new bone against the bone it rode, else the part's bone against its parent. **Reset** returns to the inspected build, clears the pending ops and sets the dial to 0.
641
+
642
+ ## Cuts (cut polygons, patch boxes, limb joints → rig-parts build)
643
+
644
+ In **Setup**, after Rig fields, the **Cuts** block (`#cuts-panel`, #115; **Cuts** in the block row) edits what the producer's assemble stage reads: the config's `assemble.cuts` (a plan part's layer cut by a polygon into a new part), `assemble.patches` (a part cut from the painting by a box) and `limbs` (a limb cut at its joints). `GET /api/rebuild` returns them as written in `cuts.config` (`assemble.plan`, `assemble.cuts`, `assemble.patches`, `limbs`) with `cuts.build`, the refusal that stops an Apply here (null when the handoff declares the painting and the runs). Everything is in rig px of the setup pose, the space of `parts.json`, mapped to the stage through the same crop → world mapping the chest, mesh and bones views use.
645
+
646
+ - **Stage mode.** **Edit cuts on stage** (`#cuts-edit`) draws each cut polygon and patch box with a handle on every vertex and corner, each limb joint (the lower segment's bone origin) as a diamond, and a declared `range` as an arc from the joint (min to max degrees from the lower bone's rest direction, counter-clockwise on screen, rigc's sense). Drag a handle (within 9 px); the pointer's delta snaps to whole rig px. **Entry** (`#cuts-select`) picks a cut, a patch or a joint, whose numbers can also be typed.
647
+ - **Ops.** A vertex is `replace /assemble/cuts/<i>/polygon/<k>` naming the cut by its `into`; a box is `replace /assemble/patches/<i>/box` naming the patch; a range is `add`, `replace` or `remove` at `/limbs/<limb>/segments/<k>/range` naming the segment's `part`; a joint is a bones op (`/bones/<i>/at` or `/bones/<i>/points/<k>`), since the joint is its bone's origin. The server checks the recorded name against the index (`EDIT_PATCH_NAME_MISMATCH`). **Place cut** / **Place patch** add a new part by clicking its polygon's points or its box's two corners: `add /assemble/cuts/-` (`{from, into, polygon, draw, overlap}`) or `add /assemble/patches/-` (`{name, box, alpha, draw}`), and `add /regions/<name>` with the bone typed beside it. Review refuses before sending only what is not the loader's type (`CUTS_VALUE_INVALID` for a negative or non-integer point, `CUTS_POLYGON_SHORT`); everything else is the producer's. The draft keeps the pending ops (`#cuts-pending`).
648
+ - **Apply · Reset.** Apply sends the ops as one `rig` patch. The server runs it as `rig-parts build` (*Config patch and Apply*, Routing) from the declared painting and runs into scratch — a whole-rig op behind the reproduced baseline — and the scratch build reloads in place. The producer's refusal is shown as printed (`ASSEMBLE_CUT_INSIDE`, `ASSEMBLE_CUT_OVERLAP`, `ASSEMBLE_CUT_PIXELS`, `ASSEMBLE_UNDER_SIZE`, `ASSEMBLE_UNDER_DRAW`, `CONFIG_LIMB_RANGE`, `CONFIG_LIMB_BONE`, `CONFIG_PART_UNIQUE`, …), and a check bar that is not met (`SEAM_OVER_RANGE`, `LIMB_ONE_BONE`, `CHECK_SEAM_WITHIN_BAR`) keeps the build with check's FAIL line. The readout shows the `cut:` / `limb cut:` / `limb cap:` lines, the recomposite figures before and after with the scratch build's `recomposite_error_rig.png`, the setup-pose `seam` and `seam pair` lines, which parts changed and the build's wall clock, which the applied record keeps. Reset returns to the inspected build and clears the pending ops.
649
+
650
+ Review paints no image: a new cut names no `under`, and a patch may only keep, move or drop an `under` or `cap` the config already names (`EDIT_PATCH_PATH_HELD`). Removing a cut or a patch is out of scope.
651
+
652
+ ## Mesh edit (typed delta, preview only)
653
+
654
+ In **Setup**, the view bar's **Edit mesh** button (pressed state; hidden in Animate) edits the selected slot's actual mesh attachment **in setup space**: the attachment's entry in the compiled model document (`skeleton.model.json`), its stored setup-pose vertices placed through the setup-pose bones of a never-played player — the same reader and placement the Weights block uses. A package without the model document (a Spine export alone, or the bundled demo) refuses `MESH_EDIT_MODEL_UNAVAILABLE` and the editor does not open. With **Whole rig** selected it refuses `MESH_EDIT_SELECT_SLOT`; a slot whose setup attachment is not a mesh refuses `MESH_EDIT_NO_MESH <slot>`, and so does a linked mesh (edit its source mesh). Entering pauses playback (the stage keeps the pose it held, under the veil; the mesh is drawn at the setup pose with the stage's own camera), turns the chest-region editor off and opens the mesh editor bar under the view bar. The rig pane dims except the slot's own triangles; the mesh is drawn at full opacity with every vertex as a handle, hull edges, interior triangulation edges and added constraint edges each in their own theme token.
655
+
656
+ The bar has four sub-modes, one pressed at a time, with single keys that avoid the layer keys `I` / `M` / `B`:
657
+
658
+ | Key | Sub-mode | Pointer |
659
+ | --- | --- | --- |
660
+ | `V` | Modify | drag a vertex, or an edge to move both ends; Shift-click adds to the selection; drag empty space to box-select; arrow keys nudge the selection 1 crop px (Shift 10) |
661
+ | `C` | Create | click empty space to add a vertex; click an edge to split it at the click point |
662
+ | `X` | Delete | click a vertex to remove it and its edges (a vertex that ends a constraint edge is refused until that edge is removed, `MESH_EDIT_CONSTRAINT_END`); click an added constraint edge to remove it (hull edges are refused, `MESH_EDIT_HULL_EDGE`; triangulation edges belong to the producer's retriangulation, `MESH_EDIT_TRIANGULATION_EDGE`) |
663
+ | `E` | Edges | click vertex A then vertex B to add a constraint edge; click a constraint edge to remove it; a hull edge is refused and an existing edge records nothing |
664
+
665
+ Hit-testing is in screen pixels through the current camera: a vertex within 6 px (8 on touch) wins over an edge within 4 px. In a sub-mode a press never pans; Space-drag still pans and the wheel still zooms. Escape leaves the sub-mode, a second Escape leaves mesh edit. **Undo** / **Redo** (Cmd/Ctrl-Z, Shift-Cmd/Ctrl-Z) step one action at a time — an edge or multi-selection drag is one action of several `move` ops. **Reset** drops the attachment's whole delta after a confirmation.
666
+
667
+ Everything is a typed delta over the loaded mesh, in setup-pose crop pixels of the rig canvas (x right, y down, the chest regions' crop mapping): `{schema:"rig-review-mesh-edit/1", slot, skin, attachment, source:{vertexCount, triangleCount, hullLength, hash}, units, basis:"setup", preview, ops}` with ordered ops `move {v, from, to}`, `add {at, onEdge?}`, `remove {v}`, `edge {a, b}` and `unedge {a, b}`. Source vertex ids are the attachment's indices; an added vertex takes the next id; a removed id is never reused. `hullLength` is the model's `hull` (the first `hull` vertices, in order, are the hull) and `hash` is the SHA-256 of `JSON.stringify` of the attachment's entry in the hash-verified model document. Source positions are not rounded (a `move`'s `from` is the vertex's exact setup position; `to` and `add.at` are rounded to 0.01 px). This is the input `rig-parts remesh --delta` reads as it is (see [docs/REVIEW_PACKAGE.md](docs/REVIEW_PACKAGE.md#mesh-edit-export)); the former last-drawn-pose basis is gone. The overlay previews moved, added and removed vertices and the constraint edges over the original triangulation; nothing is retriangulated in the browser (the status line says *preview only — the producer retriangulates*).
668
+
669
+ **Download JSON** saves `mesh-edit.json` and `mesh-edit.binding.json` (package source hash, rig and motion SHA-256, the attachment and its hash, export time, viewer and runtime versions and the mapping). **Apply · Rebuild** (#135) sends the delta to the installed producer as `POST /api/rebuild {part, delta, configHash}`: the server writes it as `mesh-edit.json` (the download's bytes) into the scratch directory and runs `rig-parts remesh --rig <handoff rig> --config <handoff config> --parts <handoff parts> --part <part> --delta mesh-edit.json --out rig`, then the shared tail (per-file diff, compile with remesh.json's pack flags, hash). The producer must read `--delta` (`@rig-c/parts` 1.43.0 or later, read from its own `package.json`), else `MODELING_PRODUCER_TOO_OLD <found> 1.43.0`. The delta's schema, basis, source counts and `source.hash` and every op are the producer's judgment: its `REMESH_DELTA_*` / `REMESH_*` line comes back verbatim and the stage keeps its model. A built revision loads in place and the editor re-reads that revision's setup mesh; `context.applied` becomes `rig-review-applied/3` (the delta by content, `deltaSha256` of its bytes, the producer's line, `op: "remesh"`, the per-file diff and the part's counts). While the revision is on the stage, Reset returns to the inspected build and the draft's delta is restored over it. A delta drawn over the revision's mesh is refused by the producer (`REMESH_DELTA_SOURCE`): its input is always the handoff rig. The delta rides in the inspection's local draft bound to the attachment hash (at most 300 ops per attachment); it never enters the review session and nothing is written into the package.
670
+
671
+ For inspection automation, `window.rigReviewEditor.meshEdit()` returns the open editor's source (setup-pose crop px per vertex, unrounded), its ops and each drawn vertex as page client pixels, or `null` when the editor is closed; it reads only.
672
+
673
+ ## Retarget (map, flags → rig-pose, watch)
441
674
 
442
- Setup's **Chest regions** tool records two chest deformation regions in the public generator's `family-chest-regions/1` schema: per region a name, a bone chosen from the loaded model's actual bones, a core centre and semi-axes, a spacing, a linear band and an optional medial clip x, plus the contour settings (tolerance, margin, spacing, max edge, stray, bridges). Values are crop pixels, x screen-right and y down. The fields are numeric only; there are no drag handles.
675
+ In **Animate**, the **Retarget** block (#114) puts a pose sequence the package declares onto the inspected rig through the installed `@rig-c/pose`. The sequence is a `rig-pose-sequence/1` pose reference the package declares (a `references` entry of type `pose` in `review.json`, or a `*pose*.json` in an opened folder); review takes no other file. The block's controls:
443
676
 
444
- Each core is drawn on the rig pane as the generator's 32-sample polygon on its 1/256-pixel grid. The overlay needs the loaded model to fix the crop: its declared stage must be the crop frame with the root at its bottom centre (`x = -width/2`, `y = 0`) and the drawn root bone must sit at the world origin, so crop `(x, y)` is world `(x - width/2, height - y)`. Otherwise the tool shows `CHEST_REGION_MAPPING_UNAVAILABLE: <what is missing>`, draws nothing and still exports from a crop size you enter.
677
+ - **Map** — a `rig-pose-map/1` editor. Start from **Body-18 chains** (trunk, both arms, both legs, no bone picked) or load a map file (read in the page and sent by content, never by path). Per chain: include it or leave it out, pick the rig bone that carries each segment (a name the rig lacks stays listed as *not in the rig*, so Apply shows the producer's own refusal), and its `bend`. The translated bone and the two rig points of `translate.scale` are picked the same way. **Map JSON** shows the whole document; aims, ranges, alternates and the rest are edited there and taken with **Use this JSON**. A bone not yet picked is refused before anything is sent (`RETARGET_MAP_INCOMPLETE`).
678
+ - **Clean** — clean first (default; a sequence that already carries clean's record starts with it off), `--loop`, `--legs`, `--rest`, the stillness hold and `--hips-inferred`. With clean off the sequence goes to retarget as the package holds it, and an uncleaned one is refused by the producer (`POSE_NOT_CLEANED`).
679
+ - **Retarget** — the new animation's name (`--animation`), `--flip`, `--short`, `--min-score`, `--bends refuse|clamp`, `--bend-tolerance`, `--leg-scale min,max`, `--plant-feet`, `--stance`, `--raised-by-head`, `--carry-idle`, and the person when the sequence holds more than one.
445
680
 
446
- **Download chest-regions.json** saves two files in the browser: `chest-regions.json`, formatted exactly as the public example files, and `chest-regions.binding.json`, which names the package source hash, the rig and motion SHA-256 values, the export time, the viewer and runtime versions and the mapping used. Export refuses `CHEST_REGION_NAME_DUPLICATE`, `CHEST_REGION_BONE_UNKNOWN <bone>`, `CHEST_REGION_OUT_OF_CROP <name>`, `CHEST_REGION_OFF_GRID` and `CHEST_REGIONS_OVERLAP <a> <b>`. The overlap test is geometric: two regions are refused when the distance between their polygons is less than the sum of their bands. The generator refuses per mesh vertex, so this check can refuse a pair it would accept, never the reverse.
681
+ **Apply · Retarget** sends `POST /api/rebuild {retarget: {sequence, map, flags, configHash}}` — a reference id, the map by content and typed flags; the server maps the flags to argv. Into a scratch directory under the review state directory it writes the sequence (`pose.json`) and the map (`map.json`), runs `rig-pose clean --pose pose.json --out pose.clean.json …` and `rig-pose retarget --rig <handoff rig> --map map.json --sequence pose.clean.json --out motion.json …`, copies the handoff rig's files beside the new `motion.json` as `rig/`, and finishes with the shared tail: the per-file diff against the inspected rig and `rigc build` with Apply's default pack flags, using the `rigc` installed beside `@rig-c/pose`. The package's `motion.json` is never written; review writes no keys. Every `FAIL` line of a refusal is shown verbatim — `POSE_NOT_CLEANED`, `POSE_BEND_ONE_WAY`, `RAISED_HEAD`, `MOTION_NAME_FREE`, `MAP_RESOLVES` / `MAP_BONE_EXISTS` — and the stage keeps its model. A built job loads in place, the new animation is chosen, the sequence is shown as the reference beside the rig, and the run's FEET, STANCE, FLIP, SHORT, END-ON, LEG-SCALE and REACH lines are listed; each line seeks to the first frame it names, at the motion's own key time for that frame (the run's report `frameTimes`). `context.applied` becomes `rig-review-applied/4`, with the `rig-review-retarget/1` record (sequence by reference, path and hash; map by content and hash; flags and their argv), the producer's line, what the scratch motion holds against the inspected rig's, the per-file diff and the listed lines. **Reset** returns to the inspected build. `rig-pose compare --sheet` is not run: it needs a detector pass over the render, which review does not run.
447
682
 
448
- Field values stay in the inspection's local draft. They never enter the review session's feedback, nothing is written into the package, and an export is review evidence, not an approval. Regenerating a mesh from these regions is the producer's job (see `docs/DESIGN.md`).
683
+ To reproduce an Apply outside review, run the same producer with the record's flags in an empty directory holding the sequence as `pose.json` and the record's map as `map.json`: `rig-pose clean --pose pose.json --out pose.clean.json <op.clean…>` then `rig-pose retarget --rig <handoff rig> --map map.json --sequence pose.clean.json --out motion.json <op.args…>`. Its `motion.json` is byte-equal to the scratch build's `rig/motion.json`.
449
684
 
450
685
  ## License
451
686
 
package/docs/DESIGN.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  The delivery unit is an existing output folder or an optional curated review package handed from an agent to a person. This reusable app owns inspection, reference comparison, annotations and feedback export. Upstream compiler, splitter and runtime remain their respective tools. The existing producing agent needs no change to open its output here.
6
6
 
7
- The person uses one inspection workspace with **Setup** and **Animate** sections and a review dialog for feedback (the compare page keeps **View**, **Rig**, **Playback** and **Feedback** sections). Setup covers setup geometry, source art, pivot/joint placement, meshes, weights and draw order; Animate covers angle coverage, frame intervals, reference video/poses, contact/deformation and audio inspection. Motion is strictly **view, annotate and export**: time/angle controls navigate inspection. This app does not author animation keys, retime motion or write `motion.json`. Future modeling touch-up remains in scope as a design goal; it is not implemented by this starter.
7
+ The person uses one inspection workspace with **Setup** and **Animate** sections and a review dialog for feedback (the compare page keeps **View**, **Rig**, **Playback** and **Feedback** sections). Setup covers setup geometry, source art, pivot/joint placement, meshes, weights and draw order; Animate covers angle coverage, frame intervals, reference video/poses, contact/deformation, audio inspection and the inspection environment (wind, gravity and impulses through the published player's `environment`/`moved`; its state is feedback context, never a model or motion edit) and the physics tuning panel (#103: constraint values edited as config patch ops and applied through the producer, never written into the model by review), and the readings block (#108: the compiler's own `rigc render` readings of the drawn animation, run on the inspected build and on the latest scratch build, shown as timeline ticks and worst-frame lines), and the requirements block (#113: the scene's declared requirements written as a `spine-parts-requirements/1` file and measured by the installed producer's `rig-parts check --requirements`, one worst-frame line each). Motion is strictly **view, annotate and export**: time/angle controls navigate inspection. This app does not author animation keys, retime motion or write `motion.json`. Future modeling touch-up remains in scope as a design goal; it is not implemented by this starter.
8
8
 
9
9
  ## Implemented foundation
10
10
 
@@ -16,8 +16,26 @@ Feedback has an inclusive integer frame interval, target `{kind: bone|slot|rig,
16
16
 
17
17
  Identity, revision and content hash bind every decision. A submission also records separate exact rig/motion hashes and paths, packageRevision, viewerVersion and installed runtimeVersion; absent source stays null. The producing agent can retrieve the saved JSON with the read-only feedback CLI after the UI closes. Opening never means reviewed. The person explicitly approves, requests changes or returns to pending, independently of checkpoint outcomes. Changed inputs do not inherit prior approval. Feedback writes remain outside source, are bounded and atomic, and require the app's exact Origin.
18
18
 
19
+ ## Retarget tuning (#114)
20
+
21
+ Animate's Retarget block edits what a pose retarget is made from — the `rig-pose-map/1` (per chain, the bone that carries each segment), the clean and retarget flags and the declared sequence — and hands them to the installed `@rig-c/pose`, which writes the motion in scratch; review authors no keys and never writes the package's `motion.json`. The block is the "point → rebuild → watch → repeat" loop of the modeling instruments applied to motion inputs: the producer's refusals come back by name, the built animation plays beside its reference, and its FEET / STANCE / FLIP / SHORT / END-ON / LEG-SCALE / REACH lines seek to their frames. Key editing stays outside the app.
22
+
19
23
  ## Modeling touch-up direction
20
24
 
25
+ The chest-region editor and Edit Mesh exist for detail that cannot be said in words. Issue #99 states the loop: **point → rebuild → watch → repeat → submit**. Review builds no mesh itself; **Apply · Rebuild runs the installed producer as a child process** on the user's own designation, into a scratch build beside the inspected one, and the viewer reloads that build in place. Every applied state goes into the draft and the submitted feedback (`context.applied`), so the AI receiving the review gets the exact last applied state, not a description of it. Edit Mesh's delta has no producer operation yet and refuses by name.
26
+
27
+ Every Apply needs a binding to what the inspected rig was built from (issue #101): the **producer handoff**, declared (`open --config`, or the review.json `producer` block) or found at fixed places (`config.json`, `rig/config.json`, and last `../config.json` beside an opened `out/`, which the bar names as outside the folder). Its files are hashed into the reviewed identity beside the package, and once per session a **baseline** rebuilds the whole rig from them and says whether it reproduces the inspected one. An op that rebuilds the whole rig runs only on a reproduced baseline (`EDIT_BASELINE_MISMATCH` names the first differing path); `remesh` checks its own part (`REMESH_BASELINE_MISMATCH`). A patch that touches only what `rig-parts retune` splices — constraints, the idle's tracks and duration, the blink's numbers — runs retune instead (parts 1.44.0+, #136), which holds every other byte of the inspected rig itself, so a repaired build can still take a physics or idle edit; bones, stops, regions, turn and the blink's groups and still keep the whole-rig path and its guard.
28
+
29
+ Edits reach the producer as data (issue #102): every edit card states its edit as one **config patch** (`rig-review-config-patch/1`: the config hash it was made against, JSON Pointer ops, the producer and the op), and one generic runner applies it to a scratch copy of the handoff config, runs the op (`rig` for the whole rig, `remesh` for one part's entry), compiles and runs `check` on the result. A stale patch is refused (`EDIT_PATCH_STALE`) rather than rebased. The page's client exposes `applyPatch(patch)` with #99's stream, in-place reload and Reset; the physics tuning panel (#103) is the first editing UI that produces them. What was applied — the patch, its hash, the producer's line, check's summary and the rig.json diff — rides in `context.applied`, so the AI receiving the review can rebuild exactly that state.
30
+
31
+ Physics tuning (#103) edits only what the handoff config declares: a constraint row is resolved by `name` (and kind) against the config, never by index alone, and every op records both the index (its pointer) and the name, which the server checks (`EDIT_PATCH_NAME_MISMATCH`). Rows the rig stage adds (the `turn` twins) have no config field and are shown not editable with the reason. An Apply reloads under the world it was pressed in so before and after are seen under one wind, gust and jolt state; review judges no value (a mass of 0 is rigc's `A23` to refuse). A build whose check FAILed may be loaded and looked at, but a review written on it is not submitted (`EDIT_SUBMIT_UNCHECKED`). Live preview before Apply waits on play#147 and, when it lands, is labelled preview and never recorded as applied state. The readout's BEFORE for the inspected build is that build's own check: once the baseline reproduces, `rig-parts check` runs on the baseline output (byte-identical to the inspected rig) in the background, and its summary is stored beside the baseline record.
32
+
33
+ The producer writes motion in scratch; review never authors keys. Idle tracks (#104) edit the idle the producer keys, never the package's motion.json: every edit is a config patch op at `/motion/tracks/<i>/…` or `/motion/blink/…`, and the producer (`retune` on parts 1.44.0+, else `rig --config`; then `check`) writes the keys. The panel offers only what the producer accepts where its rule is closed — a period is `motion.duration / n` (rig-parts `periodFits`: the duration over the period within 1e-9 of a whole number) — and pre-checks the pending idle under the producer's own names and words (`CONFIG_PERIOD_DIVIDES_DURATION`, `CONFIG_BONE_PROPERTY_KEYED_ONCE`, `CONFIG_NAME_RESOLVES`, `RIG_BLINK_INSIDE_IDLE`), so a refusal before Apply is the producer's line, and the producer judges again on Apply. The preview (decision on #104: allowed) drives the edited bones through play's `adjust` with the producer's sine `v(t) = base + amp·sin(2π(t/period − phase))` on the bone the inspected idle keys (`<bone>_ctl` under ctl keys, read off the handoff rig, never guessed: a property the idle does not key yet is refused by name, `IDLE_PREVIEW_TARGET_UNKNOWN`); it is labelled "preview — Apply decides", is cleared on Apply and Reset, and is never recorded or submitted. Blink edits are eased keys and are not previewed.
34
+
35
+ The Mesh panel (#106) edits one part's `meshes.<part>` entry as the producer's config loader reads it and rebuilds that part with `remesh --mesh`; it is a mesh-density and region editor for the producer's own parameters, not a mesher — review triangulates nothing, and the only geometry it writes is region points (whole rig px) and circle centres. The submitted patch is exactly one op, `replace /meshes/<part>`, so nothing else in the config can move with it. It offers the modes whose fields it draws (grid, contour); `influence` waits until the producer documents it in `remesh`. Its pre-check is the config loader's rules in the loader's names; what the rig stage measures (the keep-radius floor, bridges, heat sources, the remesh rules) is the producer's to say, and comes back verbatim. After a build, three counts must agree — `remesh.json`'s after-count, the revision model's triangles and what the overlay draws — and the panel names each, so an overlay still drawing the old mesh is visible as a disagreement, not hidden.
36
+
37
+ Readings (#108) are watch instruments, not edits and not gates: the chosen pairs go to the installed compiler as names, `rigc render` reads them on the inspected build and on the session's latest scratch build, and review only draws what rigc wrote — the frames each reading's JSON lists as ticks (set-equal, two colours) and its worst frame as one line. Review decides nothing about the rig's names: an unknown slot or bone is rigc's refusal, shown verbatim. `--angle-range` writes no file, so its ticks come from its printed line (lowest and highest frame). The Environment block's wind and gravity are passed as a one-step `--environment` schedule only while its controls are applied and the installed rigc lists the flag; a time schedule (#112) is outside this card.
38
+
21
39
  Future modeling interactions should select actual parts, bones, pivots and mesh vertices in the posed view, inspect weights and ordering, and adjust bounded setup geometry with before/after comparison. Angle inspection needs declared axes and measured coverage; one scalar animation dial does not establish compound view synthesis.
22
40
 
23
41
  The modeling requirements to preserve are local mesh-density adjustment, edge/curvature refinement, direct source-image comparison, a chest-region center/radius control, an integrated parts viewer with preset switching, and a face/body angle deformation sweep. These need actual geometry/source evidence and explicit acceptance criteria; they are not implemented by illustrative controls or a successful parser load.
@@ -38,13 +56,19 @@ Any future modeling edit needs provenance, undo/redo, explicit units and an expo
38
56
 
39
57
  The single review page is one inspection workspace: a left column, the stage, the Inspector under the stage and one status footer. There is no right panel.
40
58
 
41
- - **Left column.** A fixed header row (Parts title and slot search) and one scrolling body. Nothing sits above the header and it never scrolls. The body holds, in order, the checkpoints/reference strip (current checkpoint title, checkpoints and the reference select, folded into a compact strip that opens when the package supplies checkpoints or references), the part list and browser Settings; it is the column's only scroll container.
59
+ - **Left column.** A fixed header row (Parts title and slot search on one line; the search's name, *Find slot*, is its `aria-label`) and one scrolling body. Nothing sits above the header and it never scrolls. The body holds, in order, the checkpoints/reference strip (current checkpoint title, checkpoints and the reference select, folded into a compact strip that opens when the package supplies checkpoints or references), the part list and browser Settings; it is the column's only scroll container. Settings opens and closes from its own header (`#settings-toggle`, `aria-expanded`, controlling `#settings-body`); it starts collapsed and the state is a browser preference.
42
60
  - **Header.** The **Setup** / **Animate** mode switch sits next to the package title and is the Inspector's only section control: its buttons carry `data-inspector` and `aria-pressed`, and every section change (click, `showInspector` from a part selection or a tool, a restored bookmark) goes through them. On the right, **Write review** (primary) opens the review dialog. The top bar has no feedback export buttons; `rig-review feedback --format json|issue` is the export path.
43
61
  - **View bar.** One row: the persistent image / mesh edges / bone points layer toggles (keys `I`, `M`, `B`), camera sync and zoom, then the **View** menu (a `<details>` holding Side by side, Reference and Rig, labelled with the effective choice) at the right end. The toggles stay native checkboxes, so their checked state is the pressed state, the draft input and the `layers` control value. A disabled toggle names the footer's layer reason as its tooltip. Turning images off leaves the mesh/bone overlay alone. Appearance lives only in Settings (its Theme select shows the inspection's current appearance and drives it), the per-inspection Repeat idle toggle lives in the Animate transport row, and there is no Copy view link button: the persisted `#review=` fragment stays in the address bar and the numeric region stays behind **View region**.
44
62
  - **Panel headers and scrolling.** The left column's header is a fixed row outside its scrolling body. The Inspector heading, the Settings title and each drawer's header (title and close) carry `.sticky-header` and are their panel's first child, so the body scrolls beneath them. Every scrolling region shares one thin scrollbar rule on `*` whose colours are the `--rv-scrollbar-*` tokens. Resize separators are drawn 4px with a 16px transparent hit area (22px on coarse pointers) and use `--rv-separator` / `--rv-separator-hover`, never a scrollbar colour.
45
- - **Inspector.** Two sections and no tab row: **Setup** is the mesh-review composition (selected part, part presets, the modeling-review proposals for mesh density, contour curvature and chest position) and holds no control that seeks or plays; **Animate** has the transport with the Repeat idle toggle, frame navigation, the motion-review proposals and angle inspection with its axis panel, sound cues and report events. Their element ids and saved section names (`rig`, `playback`) are unchanged. A saved `view` or `feedback` section from older links and drafts restores without selecting a section or opening the dialog.
63
+ - **Chest-region editor.** In Setup only, the view bar's **Chest regions** toggle (`#chest-edit`, `aria-pressed`) opens `#chest-editor-bar` under the view bar: R / L groups (X, Y, R, Band), Mirror R → L, Download JSON, **Apply · Rebuild** and **Reset**. Apply sends the rows to the installed producer (`rig-parts remesh` on that part, then `rigc build`) through `POST /api/rebuild` (`src/rebuild.ts`, `src/producer.ts`) and loads the compiled scratch build in place; the producer's lines stream into `#chest-editor-status` and `#chest-editor-log`, a refusal shows its first named line verbatim, and Reset returns to the inspected build (README, *Chest regions*). The circles, their centre / core / band handles and labels are drawn on `#rig-overlay` through the crop → world mapping; handle drags (move, r, band) are clamped live with the example rebuild script's non-overlap rule (`CHEST_REGIONS_CLAMPED`) and never move the camera, while other drags and Space-drags pan. The values are the Setup **Chest regions · detail** block's fields, so its draft, export and binding apply unchanged; the block keeps names, bones, semi-axes, clip and the contour. Review builds no mesh: the rebuild is the producer's, into scratch under the state directory, never into the package. A **Circles** / **Landmarks** switch (`#bust-mode`, a hidden input the draft keeps) selects what Apply sends: Landmarks (#105) places the right apex, left apex and cleavage with draggable markers (`#lm-*`), alpha and clearance sliders and optional bone names, draws the producer's derived `bust_r` / `bust_l` live (`src/chest-landmarks.ts`, a transcription of parts' `deriveBustRegions`) and sends `{part, bust, configHash}`; review pre-checks only `CONFIG_BUST_CLEARANCE` and leaves bones and names to the producer.
64
+ - **Mesh panel.** In Setup, after Chest regions · detail: `#mesh-panel` for the selected mesh part — mode-specific sliders with number fields (`#mesh-params`), regions (`#mesh-regions`; **Add circle**, **Draw polygon** and **Drag on stage** with `aria-pressed`), segments (`#mesh-segments`, bones from the loaded model, ribs stations per chain), the pending diff (`#mesh-diff`), the pre-check (`#mesh-precheck`), **Apply** / **Reset** / **Folds reading**, the status (`#mesh-status`), the counts (`#mesh-counts`) and the `TEXTURE_STRETCH` readout (`#mesh-readout`). Region colours are theme tokens (`--rv-mesh-region`, `--rv-mesh-region-fill`, `--rv-mesh-handle`).
65
+ - **Weights.** In Setup, after the Mesh panel: `#weights` (#107), a watch layer. **Show weights** (`#weights-show`, `aria-pressed`) draws the selected slot's setup-pose mesh on `#rig-overlay` with each vertex coloured by the picked bone's stored weight (`#weights-bone`, influencing bones first) on the ramp tokens `--rv-weight-0` / `--rv-weight-half` / `--rv-weight-1`, or by its dominant bone (`#weights-mode` Max influence, `--rv-weight-bone-1…6`); `#weights-legend` and the hover readout `#weights-hover` sit under it. Weights are read from the compiled model document (`src/weights.ts` over the shared reader `src/setup-mesh.ts`); positions come from a never-played `RigPlayer` over the same assets (`web/setup-pose.ts`, shared with Edit Mesh), since the published `posedFrame()` is empty before `play`. A package without the document refuses `WEIGHTS_MODEL_UNAVAILABLE`. No edit, Apply or record; the Mesh edit preview wins when both are on.
66
+ - **Bones.** In Setup, after Weights: `#bones-panel` (#109). **Edit bones on stage** (`#bones-edit`) draws the handoff config's bone anchors — `at`, chain `points`, `tip` — on `#rig-overlay` through the crop → world mapping (tokens `--rv-bone-line`, `-origin`, `-point`, `-tip`, `-pending`, `-selected`, `-ink`, `-label-bg`; the stage stays dark in both schemes, so both blocks carry the same values). A drag moves an anchor by whole rig px; each move is a `replace` op at its JSON Pointer naming the bone (`src/bone-anchors.ts`, `web/bones-panel.ts`). Check (`POST /api/bones-check`, `src/bones-check.ts`) runs `propose --from-config` and `compare` on a scratch copy of the patched config and shows their lines as printed; a LINT line blocks nothing. Apply is the generic patch runner's whole-rig `rig` op behind the reproduced baseline. Adding, removing or re-parenting bones is out of scope.
67
+ - **Rig fields.** In Setup, after Bones: `#rig-fields` (#110). The producer config's small fields as patch ops at their own pointers (`src/rig-fields.ts`, `web/rig-fields-panel.ts`): a rigid part's bone (`/regions/<part>`, a select), a lash's blink still row (`/motion/blink/still/<part>`, a dashed line across the part dragged in whole rig px), a stop's inward / outward bounds (`/stops/<i>`, two filled arcs at the pivot around a dashed rest aim, their ends dragged in whole degrees), an inherit mode per bone (`/bones/<i>/inherit`, a chain's `link_inherit`) and `turn`'s pivot and per-part depth (`/turn/pivot`, `/turn/depths/<part>`, sliders). Stage handles live in `#rig-fields-stage` over the rig viewport (tokens `--rv-field-still`, `-inward`, `-inward-line`, `-outward`, `-outward-line`, `-rest`, `-handle-bg`, `-ink`; the stage stays dark, so both blocks carry the same values). The turn dial holds the drawn build's `turn` bone at x through play's `adjust`, composed with the idle preview, and shows `#rig-fields-dial-label` (*preview — Apply decides*); it is never recorded. Apply is the generic runner's whole-rig `rig` op behind the reproduced baseline; the readout quotes `BLINK_NO_HOLE` and `FEET_STAY_LEVEL` before and after, and **Angle range** runs one #108 reading. Adding a stop and `turn.deform` are out of scope.
68
+ - **Mesh edit.** In Setup only, the view bar's **Edit mesh** toggle (`#mesh-edit`, `aria-pressed`) opens `#mesh-editor-bar`: sub-modes Modify / Create / Delete / Edges (keys `V` / `C` / `X` / `E`, one pressed), Undo, Redo, Reset, Download JSON and **Apply · Rebuild** (#135: `rig-parts remesh --delta` through the shared runner, the revision loaded in place, the producer's refusal verbatim; while a delta revision is on the stage Reset returns to the inspected build). A package without the compiled model document refuses `MESH_EDIT_MODEL_UNAVAILABLE` and does not open; whole rig refuses `MESH_EDIT_SELECT_SLOT`; a non-mesh or linked-mesh slot `MESH_EDIT_NO_MESH <slot>`. It edits in setup space (#100): the attachment's entry in the model document, placed through the setup-pose bones of a never-played player (`src/setup-mesh.ts` + `web/setup-pose.ts`, shared with Weights) and drawn over the paused stage with the stage camera. It records a typed delta (`src/mesh-edit.ts`, `basis: "setup"`, the shape `rig-parts remesh --delta` reads) and previews it over the original triangulation on the overlay; Delete removes only constraint edges the delta added (the triangulation is the producer's); it is not a mesher and never retriangulates, writes the package or runs a producer. The chest editor and mesh edit are exclusive: turning one on turns the other off.
69
+ - **Inspector.** Two sections and no tab row: **Setup** is the mesh-review composition (selected part, part presets, the modeling-review proposals for mesh density, contour curvature and chest position) and holds no control that seeks or plays; **Animate** has the transport with the Repeat idle toggle, frame navigation, the motion-review proposals and angle inspection with its axis panel, sound cues and report events. Their element ids and saved section names (`rig`, `playback`) are unchanged. Each section opens with a sticky block row (`.block-row`, `web/inspector-blocks.ts`): Setup — Part · Modeling · Chest · Mesh · Weights · Bones · Fields; Animate — Transport (with frame navigation, motion tools and the angle panel) · Sound · Environment · Physics · Idle tracks · Readings · Report, in document order. A button scrolls its block to just under the row inside the section and marks it `aria-current`; the active block then follows scrolling (IntersectionObserver; at the end of the section the last visible block). The row is one line that scrolls sideways when narrow. The active block per section is the only thing the row adds to the inspection draft (`ui.inspectorBlocks`); the scroll offset stays with the Inspector scroll memory, and the control protocol and saved-view link are unchanged. A saved `view` or `feedback` section from older links and drafts restores without selecting a section or opening the dialog.
46
70
  - **Review dialog.** A native modal `<dialog id="review-dialog">` holds the former Feedback section unchanged (review status, reviewed source with the read-only source-file list, selected checkpoint, structured feedback, frame-strip export; every element id kept) and the **Submit review** button (`#save`). **Write review**, a part's or a proposal tool's Add feedback opens it with `showModal()` and pauses playback; Escape or Close dismisses it and focus returns to **Write review**. Its inputs stay in the document while it is closed, so the draft mechanism (autosave, recovery, conflict handling) is unchanged and closing loses nothing. The modal makes the rest of the page inert, so the dialog repeats the footer's message and draft state in its own status line; the footer keeps reporting both.
47
- - **Footer.** Message (truncated, full text as tooltip) on the left; player, effective layout, renderer, layers, idle, remote control and draft state in the middle; the numeric view region (behind **View region**) and the source hash on the right.
71
+ - **Footer.** Message (truncated, full text as tooltip) on the left; player, effective layout, renderer, layers, idle, remote control and draft state in the middle; the numeric view region (behind **View region**) and the source hash on the right. The View region body is a `position:fixed` popover above the 40px footer, right-aligned, its width and height clamped to the viewport and its content scrolling inside, so the footer's clipping never hides it and opening it never scrolls the page.
48
72
 
49
73
  The compare page keeps its own markup and stylesheet: sources left, Parts explorer right, its four Inspector sections, its export buttons and its separator colour. It shares the workspace coordinator, so its separators are drawn at the same 4px.
50
74
 
@@ -63,6 +87,10 @@ The Parts explorer (the review page's left column; the compare page's right one)
63
87
 
64
88
  Published Play1.12.0 supplies a frozen `slotIdentity()` snapshot coherent with `posedFrame().generation`. Review retains copied frame/identity together only after equal-generation and source-slot/order checks, uses the player's default public filter, and never seeks/reposes/changes physics merely to read identity. Exact owning skin/key, active set/skin and null reasons come from that snapshot, not a unique-declaration or texture match. A resolved identity can remain known without visible geometry or fit bounds. Clip declarations do not invent keys when the API reports no drawable attachment.
65
89
 
90
+ Selecting the selected row again (click, Space or Enter) clears the selection, the same as **Whole rig**; the control value is then `null`.
91
+
92
+ Each row shows a 24×24 CSS-pixel thumbnail (2× backing store) of the attachment the held identity snapshot says is active: the region the compiled model's `pages[].regions[]` declares under that attachment's path (or name), cut from the loaded page image by its page UVs. It is drawn when the row scrolls into view and cached per skin/attachment key. A row whose attachment has no such region (no active attachment, a non-image kind, no or several regions of that name, a rotated packing, a page that did not load) shows the type icon, and its tooltip names the reason. A whole atlas page is never a part's thumbnail. Row text and the slot count are unchanged; the compare page registers no thumbnail source and keeps its icons.
93
+
66
94
  Live selection follows a slot. A source-bound bookmark captures its exact identity tuple at that pose; restore validates it after applying time/animation/set and drawing, with transactional rollback on a named mismatch. Old slot-only bookmarks resolve through current public identity. Generation belongs to current readback/feedback, not a stable source target. Named weights and original part alpha remain unavailable without an exact original-source mapping. A conservative bounds box and counts are not source-faithfulness or physics validation.
67
95
 
68
96
  A future optimization operation sends explicit source-bound selection/bounds to the owning producer. That producer returns a new package revision/hash and provenance including producer version/request identity; Review never regenerates rig, motion or mesh bytes. Candidate Apply/undo/export requires its own implemented contract and acceptance, rather than being inferred from the explorer.
@@ -73,7 +101,7 @@ Side and bottom panels have pointer and keyboard separators with separator seman
73
101
 
74
102
  The allocator measures the layout rectangle minus stage chrome and keeps a 240×180 CSS-pixel stage minimum. It shrinks side panels proportionally toward their own minima; when the width still cannot hold them it turns the left, then the right column, and when the height cannot hold it the Inspector, into a native modal drawer opened from a trigger outside it. Effective sizes are computed from desired sizes and never written back; a separator inverts the allocator so a clamped pixel value is not saved as the desktop desire.
75
103
 
76
- Browser defaults live in one closed, versioned record: theme, default camera sync, default idle repeat and the three desired panel sizes. The saved `rightWidth` is the Parts explorer's width wherever the page shows it, so the review page's left parts column keeps the width chosen before the move; `leftWidth` is the compare page's sources column. Each field carries a logical stamp, so stale whole-record writes from other tabs converge per field without wall-clock ordering; an equal-stamp disagreement quarantines only that field until an explicit edit repairs it. Denied, unavailable or corrupt storage falls back to in-page defaults. The legacy same-origin appearance key migrates once, only when no new record exists. Reset layout changes geometry only.
104
+ Browser defaults live in one closed, versioned record: theme, default camera sync, default idle repeat, whether Settings is collapsed (`settingsCollapsed`, default `true`) and the three desired panel sizes. A record saved before `settingsCollapsed` existed carries neither the value nor its stamp and loads as collapsed at stamp revision 0; the schema name is unchanged. The saved `rightWidth` is the Parts explorer's width wherever the page shows it, so the review page's left parts column keeps the width chosen before the move; `leftWidth` is the compare page's sources column. Each field carries a logical stamp, so stale whole-record writes from other tabs converge per field without wall-clock ordering; an equal-stamp disagreement quarantines only that field until an explicit edit repairs it. Denied, unavailable or corrupt storage falls back to in-page defaults. The legacy same-origin appearance key migrates once, only when no new record exists. Reset layout changes geometry only.
77
105
 
78
106
  Content state stays source-bound. Each Inspector section's x/y scroll offset is captured before its panel hides and restored after it shows, after two frames of layout, and again when its content extent changes. Restoring clamps to the current extent without replacing the remembered offset, and intentional user scrolling supersedes a pending restore. Offsets are kept in the source draft's UI state, never in the global record or a copied view link.
79
107