@rig-c/review 0.10.5 → 0.11.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.
package/README.md CHANGED
@@ -453,7 +453,28 @@ refusal is the producer's line verbatim (`CONFIG_CONSTRAINT_*`,
453
453
  `CONFIG_NAME_RESOLVES`, rigc's `A23` for a mass of 0) and the stage is
454
454
  unchanged. **Reset** returns to the inspected build and clears the pending ops.
455
455
  Submit refuses a review written on an applied build whose check failed
456
- (`EDIT_SUBMIT_UNCHECKED`). Live preview before Apply waits on play#147.
456
+ (`EDIT_SUBMIT_UNCHECKED`).
457
+
458
+ With pinned published Play **1.14.0**, **Preview pending physics** previews
459
+ `strength`, `damping`, `inertia`, `wind`, `gravity` and `mix` through the live
460
+ player's public `setPhysicsParameters` / `readPhysicsParameters` /
461
+ `clearPhysicsParameters`. Slider input updates the standing patch while dragging;
462
+ the label separates the pending override from the last evaluated effective values.
463
+ The next evaluation uses it after authored keys and sliders, before physics. While
464
+ paused, the page explicitly re-poses the held time and draws; it never seeks or
465
+ resets history to preview. `mass` and `x` / `y` / `rotate` stay Apply-only.
466
+ **Clear preview** disables preview and removes its overrides, keeping the pending
467
+ ops and their Apply bytes. Authored values resume on the next evaluation; the
468
+ integrated history is retained, so clearing does not undo motion caused earlier.
469
+ Apply and a model replacement clear the preview; Reset clears both preview and
470
+ pending ops. Preview state is never recorded as applied or submitted.
471
+
472
+ Play retains the current standing patch across play and seek, with no history of
473
+ patch changes. A backward seek after an edit recomputes the past using the patch
474
+ standing now, rather than reproducing the old trajectory. A newly opened track
475
+ first evaluates its ordinary authored pose, then receives the patch as pending;
476
+ at seek time 0 its effective values can therefore still be authored until the next
477
+ evaluation. No live track means a named refusal; preview never creates one.
457
478
 
458
479
  Below it, the **Idle tracks** panel (`#idle-tracks`) edits the idle through
459
480
  the producer. It lists the handoff config's `motion.tracks`: a single track
@@ -473,7 +494,11 @@ pending ops (`/motion/tracks/<i>/…`, `/motion/blink/…`) are listed with a
473
494
  per-op **Revert** and kept in the local draft. While edits are pending the
474
495
  **preview** drives the edited bones through the player's `adjust` with the
475
496
  producer's sine on the bone the idle keys (`<bone>_ctl` under ctl keys, read
476
- off the handoff rig), labelled "preview — Apply decides"; it is never
497
+ off the handoff rig). It evaluates from Play's `BoneAdjust` context's
498
+ `trackTime`, including seek replay, and the turn dial receives the same context.
499
+ It reads no displayed player clock and adds no compensating re-pose after seek.
500
+ With unchanged patch, animation, environment, reset and step partition, seek and
501
+ continuous evaluation agree bit for bit. It is labelled "preview — Apply decides"; it is never
477
502
  recorded or submitted, and a property the inspected idle does not key yet is
478
503
  not previewed (`IDLE_PREVIEW_TARGET_UNKNOWN`). **Apply** clears the preview and
479
504
  sends one patch (`rig --idle-keys <the inspected rig's>`, then `check`, about
@@ -587,7 +612,7 @@ The Parts explorer (left column; right on the compare page) selects an actual mo
587
612
 
588
613
  Solo preserves the full held pose's resolved clips and filters only drawing. In context, the whole rig remains visible with an amber dashed conservative selection box; this box is geometry/clip bounds, not exact source-alpha coverage. Selection changes no track, skin, set, motion, source asset or review decision. The selected slot's side/mode survives source-bound links and local drafts. The Setup section shows actual posed mesh counts or region corners, page/region, current skin/set context and declared attachment candidates. **Use slot for feedback** explicitly prefills the target; selecting a row alone submits nothing.
589
614
 
590
- The pinned published `@rig-c/play`1.13.0 supplies `slotIdentity()` and frame generation. The explorer atomically retains the public identity snapshot with the same-generation held frame: exact attachment key, owning skin, active set/skin, full z order and explicit hidden/no-attachment/timeline-null/set-hide reasons. Hidden/null slots keep null keys with their reported reason; declarations and texture/UV names never replace that public result. An identity-known attachment need not have a visible primitive or positive fit bounds. The current UI follows its selected slot as animation changes, updating the displayed identity to the actual held frame.
615
+ The pinned published `@rig-c/play`1.14.0 supplies `slotIdentity()` and frame generation. The explorer atomically retains the public identity snapshot with the same-generation held frame: exact attachment key, owning skin, active set/skin, full z order and explicit hidden/no-attachment/timeline-null/set-hide reasons. Hidden/null slots keep null keys with their reported reason; declarations and texture/UV names never replace that public result. An identity-known attachment need not have a visible primitive or positive fit bounds. The current UI follows its selected slot as animation changes, updating the displayed identity to the actual held frame.
591
616
 
592
617
  Source-bound links/drafts and explicit feedback preserve that side/slot plus exact attachment/owning skin/set/active skin/null reason. A restored explicit target is checked after restoring its animation/time/set and drawing; `PART_IDENTITY_MISMATCH` rolls back rather than silently substituting another key. Older slot-only links remain readable and resolve from the current authoritative snapshot. Runtime generation is current observation/feedback evidence, never a persistent identity. Named vertex weights and original source alpha remain unavailable in this inspection; packed pages are not original part images.
593
618
 
@@ -640,7 +665,7 @@ In **Setup**, after the Mesh panel, the **Weights** block (`#weights`, #107; **W
640
665
  - **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.
641
666
  - **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.
642
667
  - **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.
643
- - **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. A never-played player reads only the public setup identity, including any setup-slider attachment switch. Positions, triangles and hull come directly from published Play 1.13.0's `readSetupMesh`, which includes selected setup-slider deform; Review supplies no skinning formula. Its active skin selection (null for a fresh player) stays distinct from the supplying table used for stored metadata. Hidden/null identities keep their public reason and are never guessed from declarations. The stage player is not touched, so the drawing can sit apart from an animated pose on the stage.
668
+ - **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. A never-played player reads only the public setup identity, including any setup-slider attachment switch. Positions, triangles and hull come directly from published Play 1.14.0's `readSetupMesh`, which includes selected setup-slider deform; Review supplies no skinning formula. Its active skin selection (null for a fresh player) stays distinct from the supplying table used for stored metadata. Hidden/null identities keep their public reason and are never guessed from declarations. The stage player is not touched, so the drawing can sit apart from an animated pose on the stage.
644
669
  - **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`.
645
670
 
646
671
  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.
@@ -680,7 +705,7 @@ Review paints no image: a new cut names no `under`, and a patch may only keep, m
680
705
 
681
706
  ## Mesh edit (typed delta, preview only)
682
707
 
683
- 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 full-double setup world coordinates, triangles and hull from published Play 1.13.0's `readSetupMesh(assets, {slot, attachment, skin})`, shared with Weights. A never-played player reads only the public setup identity, including any setup-slider attachment switch; the named geometry read owns setup-slider deform. Its active skin selection (null for a fresh player) is distinct from the supplying table returned as `skin`. Hidden/null identities remain unavailable with the public reason; Review never guesses a mesh from declarations. 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.
708
+ 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 full-double setup world coordinates, triangles and hull from published Play 1.14.0's `readSetupMesh(assets, {slot, attachment, skin})`, shared with Weights. A never-played player reads only the public setup identity, including any setup-slider attachment switch; the named geometry read owns setup-slider deform. Its active skin selection (null for a fresh player) is distinct from the supplying table returned as `skin`. Hidden/null identities remain unavailable with the public reason; Review never guesses a mesh from declarations. 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.
684
709
 
685
710
  The bar has four sub-modes, one pressed at a time, with single keys that avoid the layer keys `I` / `M` / `B`:
686
711
 
package/docs/DESIGN.md CHANGED
@@ -28,9 +28,9 @@ Every Apply needs a binding to what the inspected rig was built from (issue #101
28
28
 
29
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
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.
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`). Published Play 1.14.0 supplies the six-field live preview (#206): strength, damping, inertia, wind, gravity and mix use its public set/read/clear methods; mass and component toggles still require Apply. Slider input updates the pending override; the label distinguishes it from the last evaluated effective values. The next evaluation uses the patch; a paused page explicitly re-poses the held time without seeking. Clear removes overrides while preserving pending ops and integrated history, and authored values resume at the next evaluation; it does not undo the old trajectory. Apply and model replacement clear the preview. It is labelled "preview — Apply decides" and is never recorded as applied state. Play replays the current standing patch, not historical edits: backward seek recomputes with today's patch, and a newly opened track's first authored pose precedes its pending patch. 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
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.
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 uses Play 1.14.0's `BoneAdjust` context `trackTime` through both composed adjustments, with no displayed player clock or seek-compensation re-pose. Bit-exact seek/continuous equivalence requires the unchanged patch, animation, environment, reset and step partition. 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
34
 
35
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
36
 
@@ -62,7 +62,7 @@ The single review page is one inspection workspace: a left column, the stage, th
62
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.
63
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
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, triangles and hull come directly from published Play 1.13.0's `readSetupMesh` (`web/setup-pose.ts`, shared with Edit Mesh), including selected setup-slider deform. A never-played `RigPlayer` reads only public setup identity; its active skin selection stays distinct from the supplying table, and null identities are never guessed. A package without the document refuses `WEIGHTS_MODEL_UNAVAILABLE`. No edit, Apply or record; the Mesh edit preview wins when both are on.
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, triangles and hull come directly from published Play 1.14.0's `readSetupMesh` (`web/setup-pose.ts`, shared with Edit Mesh), including selected setup-slider deform. A never-played `RigPlayer` reads only public setup identity; its active skin selection stays distinct from the supplying table, and null identities are never guessed. A package without the document refuses `WEIGHTS_MODEL_UNAVAILABLE`. No edit, Apply or record; the Mesh edit preview wins when both are on.
66
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
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
68
  - **Cloth.** In Setup, after Cuts: `#cloth-panel` (#111). One form per cloth tag the installed reader accepts (`bottomwear`, `back hair`, `front hair` — its own `CLOTH_TARGETS`), holding the recipe's fields as the config states them; each change is a patch op at `/cloth/<tag>/…` (`src/cloth-recipe.ts`, `web/cloth-panel.ts`). **Show links** draws, in rig px of the setup pose, every chain the tag's parts are meshed on from the config's `points` and `tip`, and after an Apply the chains of the config the producer wrote (one label per set). Apply runs `rig-parts propose --cloth --write-config` on the declared parts, painting and keypoints, then builds the written config (`build` when the config declares `limbs`, else `rig --config`) and reloads it under the stage's Environment. Review never merges the producer's draft: it reads only the written config, lists where it differs from its input and refuses one that differs outside the drafted sections. Hand-editing the drafted constraints is the physics block's, after the draft.
@@ -86,7 +86,7 @@ The export uses dedicated real canvas players and leaves the live playhead/sessi
86
86
 
87
87
  The Parts explorer (the review page's left column; the compare page's right one) provides source-hash-bound slot selection, held-pose clipped Solo, Whole/context, conservative bounds fit, a diagnostic selection box, actual mesh/region counts and reproducible selection context. This is inspection rather than geometry editing. All rows use the public held identity snapshot's exact z order, including hidden/null slots. Current source-bound links/drafts and control observations retain side/slot/mode.
88
88
 
89
- Published Play1.13.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.
89
+ Published Play1.14.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.
90
90
 
91
91
  Selecting the selected row again (click, Space or Enter) clears the selection, the same as **Whole rig**; the control value is then `null`.
92
92
 
@@ -216,7 +216,11 @@ The page refuses before sending a patch that is not that one op (`MESH_PATCH_SCO
216
216
 
217
217
  The producer derives `bust_r` / `bust_l` (`R = round(d − clearance)`, `r = round(alpha × R)`, `band = R − r`, centre on the apex) and refuses `CONFIG_BUST_CLEARANCE`, `CONFIG_NAME_RESOLVES` and `CONFIG_REGION_NAME_UNIQUE` by name; the job's `request` carries `bust` beside `regions` (review's derivation, recorded, never sent). Clearance and bones are not judged by the server: the page refuses `CONFIG_BUST_CLEARANCE` before Apply on the producer's own test, and anything else is the producer's line verbatim.
218
218
 
219
- **Idle tracks (#104).** `GET /api/rebuild` also returns `idle` (or `null` without a handoff): `{source, keying}`. `source` is `{available:true, source:{duration, tracks[], blink|null, bones[], chains:{<chain>: <links>}}}` — `config.motion` read for its shapes only (a single track `{bone, prop, amp, period, phase, base?}`, `prop` one of rig-parts' `rotate translatex translatey scalex scaley shearx sheary`; a chain track `{chain, amps[], period, phase, lag}`; the blink as written), the bones `config.bones` declares with chain links expanded (`<chain>0…`) — or `{available:false, reason:"IDLE_TRACKS_UNAVAILABLE: …"}`. `keying` says where the inspected idle keys each track, read off the handoff rig's `rig.json` and `motion.json`: `{keys:"ctl"|"direct", source, keyed:{"<config bone>.<prop>": "<keyed bone>"}, controls[]}` (`direct` when `rig.json` declares `invariants.idleDrivesMeshes`, which `rig --idle-keys direct` writes; otherwise `ctl`, the producer's default, under which a keyed bone a mesh is weighted to is keyed on `<bone>_ctl`), or `{error:"IDLE_KEYS_UNKNOWN: …"}`. The panel's edits are ordinary config patch ops: field ops at `/motion/tracks/<i>/<field>` (and `/motion/tracks/<i>/amps/<k>` for one chain link) on the config's own indices, then removed tracks as `remove /motion/tracks/<i>` from the highest index down, then added tracks as `add /motion/tracks/-`, then `/motion/blink/<field>`; Apply sends them as one `rig` patch with `--idle-keys <keying.keys>` (no flag when the keying could not be read); track and blink-number edits run `retune` on parts 1.44.0+ (*Routing*). The local draft's `#idle-pending` record is `{schema:"rig-review-idle-draft/1", ops}` and is restored only when the ops are exactly the ones the panel writes for the idle they describe. The preview is page state only: it is in neither the draft, the patch nor `context.applied`.
219
+ **Physics live preview (#206).** The page uses exactly published Play 1.14.0. Only pending ops for source-matched editable constraint rows feed `setPhysicsParameters(name, patch)`, restricted to `strength`, `damping`, `inertia`, `wind`, `gravity` and `mix`. `mass` and component toggles remain producer Apply inputs only. `readPhysicsParameters(name)` supplies both the pending `override` and last evaluated `effective` snapshots for the preview label. Set/clear themselves neither evaluate nor move time or reset integrated history. The next evaluation applies overrides after authored/keyed values and sliders, before integration; a paused page explicitly calls `repose()` at the held time and draws. Clear preview removes the overrides and disables preview while keeping every pending patch op. Authored values resume at the next evaluation, preserving history rather than undoing motion caused by old values. Apply and model replacement clear preview; Reset also clears pending ops. No preview preference, parameter snapshot or override is added to the draft, patch, `context.applied` or feedback. The draft still contains only the existing pending edits; Apply's request and producer route are unchanged.
220
+
221
+ Play retains the standing patch across new play, seek, skin replay and composed-track promotion, without a historical journal. A backward seek after an edit recomputes the interval using the patch standing now. A new track evaluates its ordinary authored first pose before receiving that patch as pending: at seek time 0, `override` can report the patch while `effective` still reports authored values until the next evaluation. Unknown/non-physics names, unsupported fields, non-finite values and calls without a live track keep Play's named refusals. Preview never opens a track or writes an input asset.
222
+
223
+ **Idle tracks (#104).** `GET /api/rebuild` also returns `idle` (or `null` without a handoff): `{source, keying}`. `source` is `{available:true, source:{duration, tracks[], blink|null, bones[], chains:{<chain>: <links>}}}` — `config.motion` read for its shapes only (a single track `{bone, prop, amp, period, phase, base?}`, `prop` one of rig-parts' `rotate translatex translatey scalex scaley shearx sheary`; a chain track `{chain, amps[], period, phase, lag}`; the blink as written), the bones `config.bones` declares with chain links expanded (`<chain>0…`) — or `{available:false, reason:"IDLE_TRACKS_UNAVAILABLE: …"}`. `keying` says where the inspected idle keys each track, read off the handoff rig's `rig.json` and `motion.json`: `{keys:"ctl"|"direct", source, keyed:{"<config bone>.<prop>": "<keyed bone>"}, controls[]}` (`direct` when `rig.json` declares `invariants.idleDrivesMeshes`, which `rig --idle-keys direct` writes; otherwise `ctl`, the producer's default, under which a keyed bone a mesh is weighted to is keyed on `<bone>_ctl`), or `{error:"IDLE_KEYS_UNKNOWN: …"}`. The panel's edits are ordinary config patch ops: field ops at `/motion/tracks/<i>/<field>` (and `/motion/tracks/<i>/amps/<k>` for one chain link) on the config's own indices, then removed tracks as `remove /motion/tracks/<i>` from the highest index down, then added tracks as `add /motion/tracks/-`, then `/motion/blink/<field>`; Apply sends them as one `rig` patch with `--idle-keys <keying.keys>` (no flag when the keying could not be read); track and blink-number edits run `retune` on parts 1.44.0+ (*Routing*). The local draft's `#idle-pending` record is `{schema:"rig-review-idle-draft/1", ops}` and is restored only when the ops are exactly the ones the panel writes for the idle they describe. The preview is page state only: it is in neither the draft, the patch nor `context.applied`. Its `BoneAdjust` uses Play's `ctx.trackTime` (track-0 unwrapped evaluation time), including seek replay, and the composed turn adjustment receives that same context. Review does not read displayed `player.time` in the adjustment and never adds a compensating re-pose after seek. Bit-exact seek/continuous equivalence is for an unchanged patch, animation, environment, reset and step partition, as Play documents.
220
224
 
221
225
  The issue export adds an `Applied:` line for a valid record (`patch <hash>… (rig; replace /constraints/0/damping) with @rig-c/parts 1.42.0 · check PASS · rig.json differs at 1 path(s) · motion.json, sets.json, mesh_report.json unchanged · 31 images unchanged · model <hash>…`; a record without `files` says `rig.json differs at N path(s)` alone); the raw context follows it. The review's `reviewedSource` still names the inspected build.
222
226
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rig-c/review",
3
- "version": "0.10.5",
3
+ "version": "0.11.0",
4
4
  "type": "module",
5
5
  "description": "Local human rig inspection and structured feedback export",
6
6
  "license": "MIT",
@@ -17,7 +17,7 @@
17
17
  },
18
18
  "dependencies": {
19
19
  "@napi-rs/canvas": "1.0.11",
20
- "@rig-c/play": "1.13.0"
20
+ "@rig-c/play": "1.14.0"
21
21
  },
22
22
  "devDependencies": {
23
23
  "@types/bun": "^1.3.0",
package/web/idle-panel.ts CHANGED
@@ -7,19 +7,19 @@
7
7
  * "preview — Apply decides"; it is cleared on Apply and Reset and is never recorded: the source draft keeps only the
8
8
  * pending ops (`#idle-pending`) and the panel's own inputs carry no ids.
9
9
  */
10
- import {AMP_RANGE,addTrack,entryChanged,idleDraftState,idleDraftText,idleOps,initialState,isChain,orderedEntries,periodFits,periodOffers,precheck,previewLocal,previewPlan,removeTrack,setBlink,stateFromOps,setField,setLinkAmp,SETUP_DEFAULT,SINGLE_PROPS,type BlinkField,type ChainField,type IdleKeying,type IdleSource,type IdleState,type IdleStatus,type LocalChannel,type PreviewPlan,type SingleField} from '../src/idle-tracks.ts';
10
+ import {AMP_RANGE,addTrack,entryChanged,idleDraftState,idleDraftText,idleOps,initialState,isChain,orderedEntries,periodFits,periodOffers,precheck,previewPlan,removeTrack,setBlink,stateFromOps,setField,setLinkAmp,SINGLE_PROPS,type BlinkField,type ChainField,type IdleKeying,type IdleSource,type IdleState,type IdleStatus,type PreviewPlan,type SingleField} from '../src/idle-tracks.ts';
11
11
  import type {PatchOp} from '../src/config-patch.ts';
12
+ import type {BoneAdjust} from '@rig-c/play';
13
+ import {idlePreviewAdjust,type BoneSetup} from './preview.ts';
14
+ export type {BoneSetup} from './preview.ts';
12
15
 
13
- export interface BoneSetup {names:readonly string[];get(bone:string):Partial<Record<LocalChannel,number>>|undefined}
14
16
  /** play's `BoneAdjust`, as the panel writes it. */
15
- export type IdleAdjust=(bones:{bone(name:string):Record<LocalChannel,number>})=>void;
17
+ export type IdleAdjust=BoneAdjust;
16
18
  export interface IdlePanelDeps {
17
19
  apply:(ops:PatchOp[],keys:'ctl'|'direct'|null)=>Promise<void>;
18
20
  reset:()=>Promise<void>;
19
21
  /** Hand the preview to the player (`player.adjust`), or null to clear it; the caller re-poses and draws. */
20
22
  preview:(fn:IdleAdjust|null)=>void;
21
- /** The player's time, read inside `adjust` (the pose being posed). */
22
- clock:()=>number;
23
23
  /** Bones as pane pixels on the drawn frame, for the chain handles; null when nothing is drawn. */
24
24
  bonePoints:(bones:readonly string[])=>Map<string,{x:number;y:number}>|null;
25
25
  pane:HTMLElement;
@@ -75,13 +75,12 @@ export function bindIdlePanel(deps:IdlePanelDeps):IdlePanel {
75
75
  const tryChange=(make:()=>IdleState)=>{try{change(make());}catch(error){panelStatus(error instanceof Error?error.message:String(error),true);}};
76
76
 
77
77
  // ---- preview ----
78
- /** The adjust for the current plan: each write holds its bone's channel at the sine's value at the player's time. */
78
+ /** Each write uses Play's current evaluation track time, including during seek replay. */
79
79
  function drivePreview(){
80
80
  plan=null;
81
81
  if(!previewOn||!source||!state||!keying||!setup){deps.preview(null);renderLabel();return;}
82
82
  const current=previewPlan(source,state,keying,new Set(setup.names));plan=current;
83
- const duration=source.duration,writes=current.writes.map(w=>({w,setup:setup!.get(w.bone)?.[w.channel]??SETUP_DEFAULT[w.channel]}));
84
- deps.preview(writes.length?bones=>{const t=deps.clock()%duration;for(const {w,setup:s} of writes)bones.bone(w.bone)[w.channel]=previewLocal(w,s,t);}:null);
83
+ deps.preview(idlePreviewAdjust(current,setup));
85
84
  renderLabel();
86
85
  }
87
86
  function stopPreview(){previewOn=false;plan=null;deps.preview(null);renderLabel();}
package/web/index.html CHANGED
@@ -6,6 +6,6 @@
6
6
  <header class="topbar"><div class="brand"><span class="brand-mark">r</span>rig review</div><div><strong id="package-title">Review package</strong><span id="revision" class="subtle"></span></div><div class="mode-switch" role="group" aria-label="Mode"><button type="button" data-mode="rig" data-inspector="rig" aria-controls="inspector-rig" aria-pressed="true">Setup</button><button type="button" data-mode="playback" data-inspector="playback" aria-controls="inspector-playback" aria-pressed="false">Animate</button></div><div class="top-actions"><span id="dirty" class="subtle">Loading…</span><button id="write-review" type="button" class="primary" aria-haspopup="dialog" aria-controls="review-dialog">Write review</button></div></header>
7
7
  <div class="layout">
8
8
  <aside class="left-panel"><div class="left-header parts-header"><h2>PARTS</h2><input id="part-search" data-part-search type="search" autocomplete="off" aria-label="Find slot" placeholder="Find slot"></div><details id="package-context" class="package-context"><summary><span class="eyebrow">CHECKPOINTS</span><span id="checkpoint-title">Select a checkpoint</span></summary><nav id="checkpoints" aria-label="Checkpoints"></nav><section><label for="reference-select" class="label">Reference</label><select id="reference-select"></select><div id="reference-info" class="hint"></div><div id="person-control" hidden><label for="person-select" class="label space-top">Person in input pose</label><select id="person-select"></select></div></section></details><aside id="parts-explorer" class="parts-explorer" aria-label="Parts explorer"><button type="button" data-part-whole aria-pressed="true">Whole rig</button><label class="part-solo"><input id="part-solo" data-part-solo type="checkbox" disabled>Solo selected slot</label><p data-part-status class="hint" role="status">Loading actual slots…</p><ul data-part-list aria-label="Actual rig slots"></ul><p class="hint">Select in context or enable Solo. Thumbnails are each slot's declared page region; a type icon stands in where the model declares none. Atlas pages are not part thumbnails.</p></aside></aside>
9
- <main><div class="viewbar"><fieldset id="layer-controls" class="layer-toggles"><legend class="sr-only">Rig layers</legend><label class="layer-toggle" title="No rig supplied"><input aria-keyshortcuts="I" id="layer-images" type="checkbox" checked disabled><span>Images</span><kbd aria-hidden="true">I</kbd></label><label class="layer-toggle" title="No rig supplied"><input aria-keyshortcuts="M" id="layer-mesh" type="checkbox" disabled><span>Mesh edges</span><kbd aria-hidden="true">M</kbd></label><label class="layer-toggle" title="No rig supplied"><input aria-keyshortcuts="B" id="layer-bones" type="checkbox" disabled><span>Bone points</span><kbd aria-hidden="true">B</kbd></label></fieldset><div class="view-tools"><button id="chest-edit" type="button" class="view-tool" aria-pressed="false" aria-controls="chest-editor-bar" title="Place the two chest regions on the rig: drag a centre, the core edge or the band" hidden>Chest regions</button><button id="mesh-edit" type="button" class="view-tool" aria-pressed="false" aria-controls="mesh-editor-bar" title="Edit the selected slot's actual mesh as a typed delta: preview only, the producer retriangulates" hidden>Edit mesh</button></div><div class="zoom-controls"><label class="camera-sync"><input id="camera-sync" type="checkbox" checked>Sync cameras</label><button id="zoom-out" aria-label="Zoom out">−</button><output id="zoom-value">100%</output><button id="zoom-in" aria-label="Zoom in">+</button><button id="fit">Fit</button></div><details id="view-menu" class="view-menu"><summary aria-label="View layout">View: <span id="view-menu-label">Side by side</span></summary><div class="view-menu-list" role="group" aria-label="View"><button type="button" data-view="compare" aria-pressed="true">Side by side</button><button type="button" data-view="reference" aria-pressed="false">Reference</button><button type="button" data-view="rig" aria-pressed="false">Rig</button></div></details></div><div id="chest-editor-bar" class="chest-editor-bar" role="region" aria-label="Chest region editor" hidden><div class="bust-group" role="group" aria-label="Chest editor mode"><input id="bust-mode" type="hidden" value="circles"><button id="bust-mode-circles" type="button" data-bust-mode="circles" aria-pressed="true" title="Two circles, each centre, core and band set by hand (#99)">Circles</button><button id="bust-mode-landmarks" type="button" data-bust-mode="landmarks" aria-pressed="false" title="Three clicks (right apex, left apex, cleavage); the producer derives both circles (contour.bust)">Landmarks</button></div><div class="bust-group" data-chest-mode="circles"><span class="bust-tag bust-tag-r">● R</span><label>X<input id="bust-r-x" type="number" step="1" inputmode="decimal" aria-label="R region centre x"></label><label>Y<input id="bust-r-y" type="number" step="1" inputmode="decimal" aria-label="R region centre y"></label><label>R<input id="bust-r-r" type="number" min="6" max="120" step="1" inputmode="decimal" aria-label="R region core radius"></label><label>Band<input id="bust-r-band" type="number" min="4" max="120" step="1" inputmode="decimal" aria-label="R region band"></label></div><div class="bust-group" data-chest-mode="circles"><span class="bust-tag bust-tag-l">● L</span><label>X<input id="bust-l-x" type="number" step="1" inputmode="decimal" aria-label="L region centre x"></label><label>Y<input id="bust-l-y" type="number" step="1" inputmode="decimal" aria-label="L region centre y"></label><label>R<input id="bust-l-r" type="number" min="6" max="120" step="1" inputmode="decimal" aria-label="L region core radius"></label><label>Band<input id="bust-l-band" type="number" min="4" max="120" step="1" inputmode="decimal" aria-label="L region band"></label></div><div id="bust-landmarks" class="bust-group" data-chest-mode="landmarks" role="group" aria-label="Chest landmarks (rig px)" hidden><button id="lm-pick-apex_r" type="button" data-landmark="apex_r" aria-pressed="false" title="Click the part to place the apex r; drag its marker to move it">Apex R</button><label>X<input id="lm-apex_r-x" type="number" step="1" inputmode="decimal" aria-label="Apex R x (rig px)"></label><label>Y<input id="lm-apex_r-y" type="number" step="1" inputmode="decimal" aria-label="Apex R y (rig px)"></label><button id="lm-pick-apex_l" type="button" data-landmark="apex_l" aria-pressed="false" title="Click the part to place the apex l; drag its marker to move it">Apex L</button><label>X<input id="lm-apex_l-x" type="number" step="1" inputmode="decimal" aria-label="Apex L x (rig px)"></label><label>Y<input id="lm-apex_l-y" type="number" step="1" inputmode="decimal" aria-label="Apex L y (rig px)"></label><button id="lm-pick-cleavage" type="button" data-landmark="cleavage" aria-pressed="false" title="Click the part to place the cleavage; drag its marker to move it">Cleavage</button><label>X<input id="lm-cleavage-x" type="number" step="1" inputmode="decimal" aria-label="Cleavage x (rig px)"></label><label>Y<input id="lm-cleavage-y" type="number" step="1" inputmode="decimal" aria-label="Cleavage y (rig px)"></label></div><div id="bust-landmark-params" class="bust-group" data-chest-mode="landmarks" role="group" aria-label="Landmark constants" hidden><label title="Core ratio r / (r + band); the producer's default is 0.22">α<input id="lm-alpha" type="range" min="0.05" max="0.95" step="0.01" value="0.22" aria-label="Alpha, core ratio"><output id="lm-alpha-value" for="lm-alpha">0.22</output></label><label title="Cleavage clearance in rig px; the producer's default is 3.8">Clearance<input id="lm-clearance" type="range" min="0" max="40" step="0.1" value="3.8" aria-label="Clearance, rig px"><output id="lm-clearance-value" for="lm-clearance">3.8</output></label><label title="Control bone for bust_r; empty is the producer's default boob_r">Bone R<input id="lm-bone_r" type="text" list="lm-bones" placeholder="boob_r" spellcheck="false" autocomplete="off"></label><label title="Control bone for bust_l; empty is the producer's default boob_l">Bone L<input id="lm-bone_l" type="text" list="lm-bones" placeholder="boob_l" spellcheck="false" autocomplete="off"></label><datalist id="lm-bones"></datalist></div><span id="lm-derived" class="hint" data-chest-mode="landmarks" aria-live="polite" hidden></span><div class="bust-group" data-chest-mode="circles"><button id="bust-mirror" type="button" title="Copy R's y, core radius and band to L">Mirror R → L</button><button id="bust-download" type="button" title="Download chest-regions.json and its binding; evidence, not an approval">Download JSON</button></div><div class="bust-group"><button id="bust-apply" type="button" title="Run the installed producer (rig-parts remesh) on these regions or landmarks into a scratch build and load it here; the package is unchanged">Apply · Rebuild</button><button id="bust-reset" type="button" title="Return the stage to the inspected build" disabled>Reset</button></div><span id="handoff-line" class="hint handoff-line" aria-live="polite" title="Producer handoff: the config, parts and inspected rig an Apply rebuilds from"></span><span id="chest-editor-status" class="hint" aria-live="polite"></span><pre id="chest-editor-log" class="rebuild-log" aria-label="Producer output" hidden></pre><textarea id="rebuild-applied" class="sr-only" hidden aria-hidden="true" tabindex="-1"></textarea></div><div id="mesh-editor-bar" class="chest-editor-bar mesh-editor-bar" role="region" aria-label="Mesh edit" hidden><div class="bust-group" role="group" aria-label="Mesh edit sub-mode"><button id="mesh-mode-modify" type="button" data-mesh-mode="modify" aria-pressed="false" aria-keyshortcuts="V" title="Modify · key V">Modify<kbd aria-hidden="true">V</kbd></button><button id="mesh-mode-create" type="button" data-mesh-mode="create" aria-pressed="false" aria-keyshortcuts="C" title="Create · key C">Create<kbd aria-hidden="true">C</kbd></button><button id="mesh-mode-delete" type="button" data-mesh-mode="delete" aria-pressed="false" aria-keyshortcuts="X" title="Delete · key X">Delete<kbd aria-hidden="true">X</kbd></button><button id="mesh-mode-edges" type="button" data-mesh-mode="edges" aria-pressed="false" aria-keyshortcuts="E" title="Edges · key E">Edges<kbd aria-hidden="true">E</kbd></button></div><div class="bust-group"><button id="mesh-undo" type="button" aria-keyshortcuts="Control+Z Meta+Z" title="Undo the last action · Cmd/Ctrl-Z" disabled>Undo</button><button id="mesh-redo" type="button" aria-keyshortcuts="Control+Shift+Z Meta+Shift+Z" title="Redo · Shift-Cmd/Ctrl-Z" disabled>Redo</button><button id="mesh-reset" type="button" title="Drop the whole delta for this attachment (asks first)" disabled>Reset</button></div><div class="bust-group"><button id="mesh-download" type="button" title="Download mesh-edit.json and its binding; evidence, not an approval">Download JSON</button><button id="mesh-apply" type="button" title="Send the delta to the producer (rig-parts remesh --delta) and load the scratch revision in place; nothing is written into the package">Apply · Rebuild</button></div><textarea id="mesh-edit-draft" class="sr-only" hidden aria-hidden="true" tabindex="-1"></textarea><span id="mesh-editor-status" class="hint" aria-live="polite"></span></div><div id="stage-layout" class="stages"><section id="reference-pane" class="stage-pane" aria-label="Reference"><span id="reference-label" class="pane-label">Reference</span><div id="reference-viewport" class="viewport"><div id="reference-content" class="reference-content"></div><div id="reference-empty" class="empty">No reference supplied.</div></div></section><section id="rig-pane" class="stage-pane" aria-label="Rig"><span id="rig-label" class="pane-label">Generated rig · read only</span><div id="rig-viewport" class="viewport"><div id="rig-root"></div><canvas id="rig-overlay" class="rig-overlay" aria-label="Actual rig mesh and bone overlay"></canvas><div id="idle-handles" class="idle-handles" hidden aria-label="Chain amplitude handles"></div><div id="rig-fields-stage" class="rig-fields-stage" hidden aria-label="Rig field handles"></div><div id="rig-empty" class="empty">No compiled model supplied.</div></div></section></div><div class="stage-footer"><span id="coordinates">Drag to pan · scroll to zoom · cameras synchronized</span><span>Source files remain unchanged.</span></div><section class="inspector" aria-label="Inspection controls"><div class="inspector-heading sticky-header"><strong>INSPECTOR</strong><span class="hint">Local view controls · source files stay unchanged</span></div><div class="inspector-body"><section id="applied-records" class="applied-records" aria-labelledby="applied-records-heading" hidden><div class="row"><span id="applied-records-heading" class="eyebrow">APPLIED RECORDS</span><button id="applied-undo" type="button" disabled>Undo last Apply (0 deep)</button></div><ol id="applied-list" class="applied-list"></ol><p id="applied-status" class="hint" aria-live="polite"></p></section><section id="inspector-rig" aria-label="Setup" data-inspector-panel="rig"><nav class="block-row" aria-label="Setup blocks"><button type="button" data-block-target="selected-part">Part</button><button type="button" data-block-target="modeling-tools">Modeling</button><button type="button" data-block-target="chest-regions">Chest</button><button type="button" data-block-target="mesh-panel">Mesh</button><button type="button" data-block-target="weights">Weights</button><button type="button" data-block-target="bones-panel">Bones</button><button type="button" data-block-target="rig-fields">Fields</button><button type="button" data-block-target="cuts-panel">Cuts</button><button type="button" data-block-target="cloth-panel">Cloth</button></nav><section id="selected-part" aria-label="Selected part" aria-live="polite"></section><section id="modeling-tools"><div class="eyebrow">MODELING REVIEW</div><button data-proposal="mesh" class="tool">Mesh density <small>Add feedback</small></button><button data-proposal="contour" class="tool">Contour curvature <small>Add feedback</small></button><button data-proposal="chest" class="tool">Chest position <small>Add feedback</small></button><label for="part-set" class="label space-top">Part presets</label><select id="part-set" disabled><option>No sets supplied</option></select><p class="hint">Preview only · does not create motion keys.</p></section><section id="chest-regions" class="chest-regions" aria-labelledby="chest-regions-heading"><div class="eyebrow" id="chest-regions-heading">CHEST REGIONS · DETAIL</div><p class="hint">Crop pixels, x screen-right and y down (<code>family-chest-regions/1</code>). Place the circles with <strong>Chest regions</strong> in the view bar; this block holds names, bones, semi-axes, clip and the contour. Equal semi-axes export as a circle, unequal ones (or a clip) as the generator's 32-sample polygon. Exporting writes nothing into the package and is not an approval.</p><p id="chest-mapping" class="hint"></p><div class="chest-grid"><label>Crop width<input id="chest-crop-w" type="number" step="any" inputmode="decimal"></label><label>Crop height<input id="chest-crop-h" type="number" step="any" inputmode="decimal"></label></div><fieldset class="chest-row"><legend>Region 1</legend><label>Name<input id="chest-0-name" type="text" autocomplete="off" spellcheck="false"></label><label>Bone<select id="chest-0-bone"><option value="">Select bone</option></select></label><label>Centre x<input id="chest-0-cx" type="number" step="any" inputmode="decimal"></label><label>Centre y<input id="chest-0-cy" type="number" step="any" inputmode="decimal"></label><label>Semi-axis x<input id="chest-0-ax" type="number" step="any" inputmode="decimal"></label><label>Semi-axis y<input id="chest-0-ay" type="number" step="any" inputmode="decimal"></label><label>Spacing<input id="chest-0-spacing" type="number" step="any" inputmode="decimal"></label><label>Band<input id="chest-0-band" type="number" step="any" inputmode="decimal"></label><label>Medial clip x<input id="chest-0-clip" type="number" step="any" inputmode="decimal" placeholder="none"></label></fieldset><fieldset class="chest-row"><legend>Region 2</legend><label>Name<input id="chest-1-name" type="text" autocomplete="off" spellcheck="false"></label><label>Bone<select id="chest-1-bone"><option value="">Select bone</option></select></label><label>Centre x<input id="chest-1-cx" type="number" step="any" inputmode="decimal"></label><label>Centre y<input id="chest-1-cy" type="number" step="any" inputmode="decimal"></label><label>Semi-axis x<input id="chest-1-ax" type="number" step="any" inputmode="decimal"></label><label>Semi-axis y<input id="chest-1-ay" type="number" step="any" inputmode="decimal"></label><label>Spacing<input id="chest-1-spacing" type="number" step="any" inputmode="decimal"></label><label>Band<input id="chest-1-band" type="number" step="any" inputmode="decimal"></label><label>Medial clip x<input id="chest-1-clip" type="number" step="any" inputmode="decimal" placeholder="none"></label></fieldset><fieldset class="chest-row"><legend>Contour</legend><label>Tolerance<input id="chest-tolerance" type="number" step="any" inputmode="decimal"></label><label>Margin<input id="chest-margin" type="number" step="any" inputmode="decimal"></label><label>Spacing<input id="chest-spacing" type="number" step="any" inputmode="decimal"></label><label>Max edge<input id="chest-max-edge" type="number" step="any" inputmode="decimal"></label><label>Stray<input id="chest-stray" type="number" step="any" inputmode="decimal"></label><label class="chest-check"><input id="chest-bridges" type="checkbox">Bridges</label></fieldset><details class="chest-more"><summary>Generator and mesh fields</summary><label for="chest-generator" class="label space-top">Public generator</label><input id="chest-generator" type="text" autocomplete="off" spellcheck="false" placeholder="Generator that will read this file"><label for="chest-r" class="label space-top">Mesh r (optional)</label><input id="chest-r" type="number" step="any" inputmode="decimal"><label for="chest-segments" class="label space-top">Mesh segments JSON (optional)</label><textarea id="chest-segments" rows="3" spellcheck="false"></textarea><label for="chest-source-rule" class="label space-top">Source rule (optional)</label><input id="chest-source-rule" type="text" autocomplete="off"></details><p id="chest-status" class="hint" aria-live="polite"></p><button id="chest-export" type="button" class="outline block">Download chest-regions.json</button></section><section id="mesh-panel" class="physics-panel mesh-panel" aria-labelledby="mesh-panel-heading"><div class="row"><span id="mesh-panel-heading" class="eyebrow">MESH · APPLY REMESHES</span></div><p id="mesh-panel-state" class="hint">Select a mesh part in the parts list.</p><p class="hint">Edits the selected part's <code>meshes.&lt;part&gt;</code> entry from the handoff config (the grid and contour modes). Apply sends the whole pending entry to the installed producer's <code>remesh --mesh</code> (one patch op at <code>/meshes/&lt;part&gt;</code>), compiles and checks the revision, and reloads it with the mesh edges on; Reset returns to the inspected build and the config's entry. Slider ranges are review's; the number field takes any value, and the producer judges it.</p><div id="mesh-params" class="physics-controls mesh-params"></div><div class="row mesh-tools"><span class="eyebrow">REGIONS</span><button id="mesh-add-circle" type="button" disabled>Add circle</button><button id="mesh-add-polygon" type="button" aria-pressed="false" disabled>Draw polygon</button><button id="mesh-regions-edit" type="button" aria-pressed="false" disabled>Drag on stage</button></div><div id="mesh-regions" class="mesh-regions"></div><div class="row mesh-tools"><span class="eyebrow">SEGMENTS</span><select id="mesh-segment-bone" aria-label="Bone to add as a segment" disabled></select><button id="mesh-segment-add" type="button" disabled>Add</button></div><ol id="mesh-segments" class="physics-pending-list mesh-segments"></ol><div class="row"><span class="eyebrow">PENDING ENTRY · DIFF AGAINST THE CONFIG</span></div><ol id="mesh-diff" class="physics-pending-list"></ol><ul id="mesh-precheck" class="mesh-precheck" aria-label="Review pre-check in the config loader's names"></ul><div class="row"><button id="mesh-panel-apply" type="button" class="primary" disabled>Apply</button><button id="mesh-panel-reset" type="button" disabled>Reset</button><button id="mesh-folds" type="button" title="rigc render --folds of the drawn animation on the inspected build and on this revision (#108 readings)">Folds reading</button></div><output id="mesh-status" class="environment-readout"></output><div id="mesh-counts" class="physics-readout" aria-label="Vertex and triangle counts before and after"></div><div id="mesh-readout" class="physics-readout" aria-label="TEXTURE_STRETCH before and after Apply"></div><div id="density-strip" class="density-strip" aria-labelledby="density-strip-heading"><div class="row"><span id="density-strip-heading" class="eyebrow">DENSITY STRIP · NOTHING IS APPLIED</span></div><p id="density-strip-field" class="hint">Select a mesh part (grid or contour entry) to build a density strip.</p><p class="hint">Builds N values of the part's density field side by side, each as its own <code>replace /meshes/&lt;part&gt;</code> patch through remesh (into its own scratch folder). Rows are labels only: Load puts a row on the stage as pending, and the Mesh panel's Apply applies it. Closing or replacing the strip removes its scratch.</p><div class="row density-strip-form"><label>N<input id="density-strip-n" type="number" min="2" max="5" step="1" value="5" aria-label="Number of values"></label><label>from<input id="density-strip-from" type="number" step="any" aria-label="From value"></label><label>to<input id="density-strip-to" type="number" step="any" aria-label="To value"></label><button id="density-strip-build" type="button" disabled>Build strip</button><button id="density-strip-back" type="button" disabled title="Return the stage to the applied build it showed before a row was loaded">Back</button><button id="density-strip-close" type="button" disabled>Close strip</button></div><output id="density-strip-status" class="environment-readout"></output><div id="density-strip-table" class="density-strip-table"></div></div><textarea id="mesh-pending" class="sr-only" hidden aria-hidden="true" tabindex="-1"></textarea></section><section id="weights" class="physics-panel weights-block" aria-labelledby="weights-heading"><div class="row"><span id="weights-heading" class="eyebrow">WEIGHTS · SETUP POSE · WATCH ONLY</span></div><p class="hint">The selected slot's mesh at the setup pose, each vertex coloured by the weight the compiled model document stores for the picked bone (0 → 1), or by its dominant bone under <strong>Max influence</strong>. An unweighted mesh is its slot's bone at 1.0. Hover a vertex for its stored values. Nothing here edits, applies or records a weight.</p><div class="row weights-controls"><button id="weights-show" type="button" aria-pressed="false">Show weights</button><label>Colour by<select id="weights-mode"><option value="bone">Bone weight</option><option value="max">Max influence</option></select></label><label>Bone<select id="weights-bone" disabled></select></label></div><p id="weights-state" class="hint" aria-live="polite"></p><div id="weights-legend" class="weights-legend" aria-label="Weights legend"></div><output id="weights-hover" class="environment-readout"></output></section><section id="bones-panel" class="physics-panel bones-panel" aria-labelledby="bones-panel-heading"><div class="row"><span id="bones-panel-heading" class="eyebrow">BONES · CONFIG ANCHORS · APPLY REBUILDS</span></div><p id="bones-panel-state" class="hint">Reading the producer handoff…</p><p class="hint">The producer config's bones at their <code>at</code>, chain <code>points</code> and <code>tip</code> (rig px, the setup pose; the stage may be posed by an animation). Drag a handle on the stage or edit the numbers: each move is a config patch op at <code>/bones/&lt;i&gt;/at</code>, <code>/bones/&lt;i&gt;/points/&lt;k&gt;</code> or <code>/bones/&lt;i&gt;/tip</code> naming its bone, moved in whole rig px. <strong>Check</strong> runs <code>propose --from-config</code> on a scratch copy of the patched config (LINT and coverage lines; LINT blocks nothing) and <code>compare</code> against the handoff config. <strong>Apply</strong> rebuilds the whole rig (<code>rig --config</code>, then <code>check</code>) and needs the reproduced baseline; Reset returns to the inspected build and clears the pending ops. Adding, removing or re-parenting bones is out of scope.</p><div class="row bones-controls"><button id="bones-edit" type="button" aria-pressed="false" disabled>Edit bones on stage</button><label for="bones-select">Bone</label><select id="bones-select" disabled></select></div><div id="bones-anchor-editor" class="bones-anchor-editor"></div><div class="row"><span class="eyebrow">PENDING OPS</span></div><ol id="bones-pending-list" class="physics-pending-list"></ol><div class="row"><button id="bones-check" type="button" disabled>Check</button><button id="bones-apply" type="button" class="primary" disabled>Apply</button><button id="bones-reset-build" type="button" disabled>Reset</button></div><output id="bones-status" class="environment-readout"></output><div id="bones-compare" class="bones-lines" aria-label="compare: origin and tip distance per moved bone"></div><div id="bones-lint" class="bones-lines" aria-label="propose --from-config: LINT and coverage lines"></div><textarea id="bones-pending" class="sr-only" hidden aria-hidden="true" tabindex="-1"></textarea></section><section id="rig-fields" class="physics-panel rig-fields" aria-labelledby="rig-fields-heading"><div class="row"><span id="rig-fields-heading" class="eyebrow">RIG FIELDS · CONFIG · APPLY REBUILDS</span><span id="rig-fields-dial-label" class="idle-preview-label" hidden>preview — Apply decides</span></div><p id="rig-fields-state" class="hint">Reading the producer handoff…</p><p class="hint">Small producer config fields, each a config patch op: the bone a rigid part rides (<code>/regions/&lt;part&gt;</code>), a lash's blink still row (<code>/motion/blink/still/&lt;part&gt;</code>, dragged on the stage), a stop's inward / outward bounds (<code>/stops/&lt;i&gt;</code>, arcs dragged at the pivot), a bone's inherit mode (<code>/bones/&lt;i&gt;/inherit</code>; a chain's <code>link_inherit</code>) and <code>turn</code>'s pivot and per-part depth (<code>/turn/pivot</code>, <code>/turn/depths/&lt;part&gt;</code>). <strong>Apply</strong> rebuilds the whole rig (<code>rig --config</code>, then <code>check</code>) and needs the reproduced baseline; the producer's refusal is shown as printed. The dial only previews the drawn build's <code>turn</code> bone and is never recorded. Reset returns to the inspected build and clears the pending ops.</p><div id="rig-fields-region" class="rig-fields-group"></div><div id="rig-fields-still" class="rig-fields-group"></div><div id="rig-fields-stops" class="rig-fields-group"></div><div id="rig-fields-inherit" class="rig-fields-group"></div><div id="rig-fields-turn" class="rig-fields-group"></div><div class="row"><span class="eyebrow">PENDING OPS</span></div><ol id="rig-fields-pending-list" class="physics-pending-list"></ol><div class="row"><button id="rig-fields-apply" type="button" class="primary" disabled>Apply</button><button id="rig-fields-reset" type="button" disabled>Reset</button><button id="rig-fields-reading" type="button" disabled>Angle range</button></div><output id="rig-fields-status" class="environment-readout"></output><div id="rig-fields-readout" class="physics-readout" aria-label="BLINK_NO_HOLE and FEET_STAY_LEVEL before and after"></div><textarea id="rig-fields-pending" class="sr-only" hidden aria-hidden="true" tabindex="-1"></textarea></section><section id="cuts-panel" class="physics-panel bones-panel cuts-panel" aria-labelledby="cuts-panel-heading"><div class="row"><span id="cuts-panel-heading" class="eyebrow">CUTS · PATCHES · LIMBS · APPLY RUNS RIG-PARTS BUILD</span></div><p id="cuts-panel-state" class="hint">Reading the producer handoff…</p><p class="hint">The producer config's <code>assemble.cuts</code> polygons, <code>assemble.patches</code> boxes and each limb joint with its <code>range</code> arc, on the stage in rig px of the setup pose. Drag a vertex, a box corner or a joint, or edit the numbers: each change is a config patch op (<code>/assemble/cuts/&lt;i&gt;/polygon/&lt;k&gt;</code>, <code>/assemble/patches/&lt;i&gt;/box</code>, <code>/limbs/&lt;limb&gt;/segments/&lt;k&gt;/range</code>; a joint is its bone's origin, <code>/bones/…</code>). A new cut or patch adds its entry and a <code>regions</code> bone. <strong>Apply</strong> runs <code>rig-parts build</code> (assemble → rig → check) from the declared painting and See-through runs into scratch and needs the reproduced baseline; the producer's refusals (<code>ASSEMBLE_CUT_*</code>, <code>ASSEMBLE_UNDER_*</code>, <code>CONFIG_LIMB_*</code>, <code>CONFIG_PART_UNIQUE</code>) are shown as printed, and the recomposite error map, the <code>seam pair</code> lines and the build's wall clock come back with the build. Review paints no <code>under</code> or <code>cap</code> image. Reset returns to the inspected build and clears the pending ops.</p><div class="row bones-controls"><button id="cuts-edit" type="button" aria-pressed="false" disabled>Edit cuts on stage</button><label for="cuts-select">Entry</label><select id="cuts-select" disabled></select></div><div id="cuts-editor" class="bones-anchor-editor"></div><div id="cuts-add" class="row cuts-add"><span class="eyebrow">ADD</span><select data-cuts-add="from" aria-label="Plan part the new cut takes pixels from"></select><input data-cuts-add="name" type="text" autocomplete="off" placeholder="new part" aria-label="New part name"><input data-cuts-add="bone" type="text" list="lm-bones" autocomplete="off" placeholder="bone" aria-label="Bone the new part's region rides"><select data-cuts-add="draw" aria-label="Where the new part is drawn"></select><input data-cuts-add="overlap" type="number" min="0" step="1" value="0" inputmode="numeric" aria-label="Cut overlap band in rig px"><select data-cuts-add="alpha" aria-label="New patch alpha"><option value="silhouette">silhouette</option><option value="box">box</option></select><button type="button" data-cuts-add="place-cut">Place cut</button><button type="button" data-cuts-add="place-patch">Place patch</button><button type="button" data-cuts-add="finish">Finish</button><button type="button" data-cuts-add="cancel">Cancel</button></div><div class="row"><span class="eyebrow">PENDING OPS</span></div><ol id="cuts-pending-list" class="physics-pending-list"></ol><div class="row"><button id="cuts-apply" type="button" class="primary" disabled>Apply</button><button id="cuts-reset" type="button" disabled>Reset</button></div><output id="cuts-status" class="environment-readout"></output><div id="cuts-readout" class="physics-readout" aria-label="The build's refusals, cut lines, recomposite, seam pairs and wall clock"></div><textarea id="cuts-pending" class="sr-only" hidden aria-hidden="true" tabindex="-1"></textarea></section><section id="cloth-panel" class="physics-panel bones-panel cloth-panel" aria-labelledby="cloth-panel-heading"><div class="row"><span id="cloth-panel-heading" class="eyebrow">CLOTH · RECIPE PER TAG · APPLY RUNS RIG-PARTS PROPOSE --CLOTH</span></div><p id="cloth-panel-state" class="hint">Reading the producer handoff…</p><p class="hint">The producer config's <code>cloth</code> block: one recipe per tag the installed reader accepts, with its fields as written. Each change is a config patch op at <code>/cloth/&lt;tag&gt;/…</code>. <strong>Show links</strong> draws the chains the tag's parts are meshed on, from the config's <code>points</code> and <code>tip</code> in rig px of the setup pose, and after an Apply the chains of the config the producer wrote. <strong>Apply</strong> runs <code>rig-parts propose --cloth --write-config</code> on the declared parts, painting and keypoints into scratch, then builds the written config (<code>rig-parts build</code> when the config declares <code>limbs</code>, else <code>rig --config</code>) and reloads it under the Environment's wind and gravity; the producer's refusals (<code>CONFIG_CLOTH_*</code>, <code>PROPOSE_CLOTH_DRAFTED</code>, <code>PROPOSE_CLOTH_PART</code>, <code>PROPOSE_WRITE_CONFIG_*</code>) are shown as printed. Review edits the recipe, never the drafted constraints. Reset returns to the inspected build and clears the pending ops.</p><div class="row bones-controls"><button id="cloth-links" type="button" aria-pressed="false" disabled>Show links on stage</button><label for="cloth-tag">Tag</label><select id="cloth-tag" disabled></select><label class="cloth-declared"><input id="cloth-declared" type="checkbox" disabled> recipe declared</label></div><div id="cloth-fields" class="cloth-fields"></div><div class="row"><span class="eyebrow">PENDING OPS</span></div><ol id="cloth-pending-list" class="physics-pending-list"></ol><div class="row"><button id="cloth-apply" type="button" class="primary" disabled>Apply</button><button id="cloth-reset" type="button" disabled>Reset</button></div><output id="cloth-status" class="environment-readout"></output><div id="cloth-readout" class="physics-readout" aria-label="Propose's refusals, the written config's diff, the route and check"></div><textarea id="cloth-pending" class="sr-only" hidden aria-hidden="true" tabindex="-1"></textarea></section></section><section id="inspector-playback" aria-label="Animate" data-inspector-panel="playback" hidden><nav class="block-row" aria-label="Animate blocks"><button type="button" data-block-target="transport" data-block-includes="frame-controls motion-tools angle-panel">Transport</button><button type="button" data-block-target="sound-panel">Sound</button><button type="button" data-block-target="environment">Environment</button><button type="button" data-block-target="physics-panel">Physics</button><button type="button" data-block-target="idle-tracks">Idle tracks</button><button type="button" data-block-target="readings">Readings</button><button type="button" data-block-target="requirements">Requirements</button><button type="button" data-block-target="retarget">Retarget</button><button type="button" data-block-target="report-panel">Report</button></nav><section id="transport" class="transport"><button id="play" aria-label="Play" disabled>▶</button><label for="animation-select" class="sr-only">Animation</label><select id="animation-select" disabled><option>No animations supplied</option></select><input id="scrub" type="range" min="0" max="1" step="0.001" value="0" aria-label="Playback time"><output id="time-label">0.0000 / 0.0000 s</output><output id="frame-label">rate unknown</output><label class="idle-repeat"><input id="idle-repeat" type="checkbox" checked>Repeat idle</label><div id="reading-ticks" class="reading-ticks" hidden aria-label="Reading ticks"></div><div id="idle-blink-strip" class="idle-blink-strip" hidden><button id="idle-blink-marker" type="button" class="idle-blink-marker" aria-label="Blink time">◆</button></div></section><section id="frame-controls" class="frame-controls" hidden aria-label="Frame navigation using the explicit clock"><label for="frame-number">Frame</label><input id="frame-number" type="number" min="0" step="1" inputmode="numeric" aria-label="Frame number to inspect"><button id="frame-jump" type="button">Go</button><input id="frame-scrub" type="range" min="0" max="1" step="1" value="0" aria-label="Scrub by frame"><span id="frame-clock-label" class="hint"></span><p id="frame-outside" class="hint" hidden>The current time is outside the explicit frame range. Enter a frame number to go to its exact time.</p></section><section id="motion-tools"><div class="eyebrow">MOTION · EDITS BUILD THROUGH THE PRODUCER</div><button data-proposal="pose" class="tool">Inspect pose <small>Add feedback</small></button><button data-proposal="timing" class="tool">Inspect interval <small>Add feedback</small></button><button data-proposal="audio" class="tool">Audio links <small>Add feedback</small></button><button data-proposal="angle" class="tool">Angle inspection <small id="axis-state">No inspection axes supplied</small></button></section><section id="angle-panel" class="angle-panel" hidden><label for="axis-select">Rig inspection axis</label><select id="axis-select"></select><label id="axis-label" for="axis-value"></label><input id="axis-value" type="range" disabled><output id="axis-value-label"></output><p class="hint">Maps one inspection axis to its animation time. This does not rotate the camera.</p></section><section id="sound-panel" class="sound-panel"><div class="row"><span class="eyebrow">SOUND CUES</span><label><input id="sync-audio" type="checkbox">Play cue audio in sync</label></div><div id="cues"></div><div class="row"><label for="audio-volume">Audio volume</label><input id="audio-volume" type="range" min="0" max="1" step="0.05" value="0.8"><output id="audio-volume-label">80%</output></div><div id="event-log" class="hint" aria-live="polite">Runtime events: none</div></section><section id="environment" class="environment-panel" aria-labelledby="environment-heading"><div class="row"><span id="environment-heading" class="eyebrow">ENVIRONMENT · INSPECTION ONLY</span></div><p class="hint">Wind, gravity and impulses go to the player while you inspect. Nothing is written to the model or motion; feedback records the state as context.</p><input id="environment-applied" type="checkbox" hidden aria-hidden="true" tabindex="-1"><div class="row"><label for="wind-base">Wind</label><input id="wind-base" type="range" min="-100" max="100" step="1" value="0" disabled><output id="wind-base-label">0</output></div><div class="row"><label><input id="wind-breeze" type="checkbox" disabled>Breeze</label><button id="wind-gust" type="button" disabled>Gust</button><label><input id="wind-repeat" type="checkbox" disabled>Repeat</label></div><div class="row"><label for="wind-gust-peak">Gust peak</label><input id="wind-gust-peak" type="number" min="0" max="200" step="1" value="48" inputmode="decimal" disabled><label for="wind-gust-duration">Duration (s)</label><input id="wind-gust-duration" type="number" min="0.1" max="20" step="0.1" value="2.4" inputmode="decimal" disabled></div><output id="wind-readout" class="environment-readout">No compiled model drawn · environment unavailable</output><div class="row"><label for="gravity">Gravity</label><input id="gravity" type="range" min="0" max="4" step="0.05" value="1" disabled><output id="gravity-label">1.00</output></div><div class="row"><button id="jolt" type="button" disabled>Jolt</button><label title="Dragging the rig pane moves the rig's physics instead of the camera. Hold Space to pan."><input id="drag-trail" type="checkbox" disabled>Drag to trail</label><button id="physics-reset" type="button" disabled>Reset physics</button></div><p class="hint">Drag to trail: dragging the rig pane tells the physics the rig moved; the camera stays. Hold Space to pan. Reset returns the runtime start values and re-poses from the current time, clearing the physics history.</p><div class="row"><label title="Readings and Apply's check read this wind and gravity as a rigc-environment/1 schedule; the wind clock restarts with the clip so you watch the same world"><input id="environment-use" type="checkbox" disabled>Use for readings</label><button id="environment-export" type="button" disabled>Export schedule</button><button id="environment-check" type="button" disabled>Check under this world</button></div><output id="environment-schedule" class="environment-readout">No schedule · the controls are not applied (the player stands at the runtime start values)</output><div id="environment-check-lines" class="environment-check-lines" aria-live="polite"></div><p class="hint">The schedule is the controls as a <code>rigc-environment/1</code> file from the clip's first frame: one step when the wind is steady, else one step per frame at 12 fps (between frames rigc holds the frame's value). A one-off Gust is not in it; Repeat puts gusts in it. Check under this world runs <code>rig-parts check --environment</code> on the build on the stage; the default-world line is that build's own check.</p></section><section id="physics-panel" class="physics-panel" aria-labelledby="physics-panel-heading"><div class="row"><span id="physics-panel-heading" class="eyebrow">PHYSICS TUNING · APPLY REBUILDS</span></div><p id="physics-panel-state" class="hint">No compiled model drawn.</p><p class="hint">Each edit is a config patch op at <code>/constraints/&lt;i&gt;/&lt;field&gt;</code> naming its constraint. Apply runs the installed producer (<code>rig --config</code>, then <code>check</code>) into a scratch build and reloads it under the same wind, gust and jolt state; Reset returns to the inspected build and clears the pending ops. Live preview before Apply waits on play#147.</p><div class="row physics-chain-row"><label for="physics-chain">Chain</label><select id="physics-chain" disabled></select><button id="physics-chain-pick" type="button" aria-pressed="false" disabled>Pick on stage</button></div><div class="row physics-ramp-row"><label for="physics-ramp-field">Ramp</label><select id="physics-ramp-field" disabled></select><label for="physics-ramp-root">root</label><input id="physics-ramp-root" type="number" step="any" inputmode="decimal" disabled><label for="physics-ramp-tip">tip</label><input id="physics-ramp-tip" type="number" step="any" inputmode="decimal" disabled><button id="physics-ramp" type="button" disabled>Ramp</button></div><div class="physics-work"><div id="physics-rows" class="physics-table-wrap" role="region" aria-label="Physics constraints the config declares" tabindex="0"></div><div id="physics-row-editor" class="physics-row-editor"></div></div><div id="physics-locked" class="physics-table-wrap physics-locked" role="region" aria-label="Constraints that are not editable" tabindex="0" hidden></div><div class="row"><span class="eyebrow">PENDING OPS</span></div><ol id="physics-pending-list" class="physics-pending-list"></ol><div class="row"><button id="physics-apply" type="button" class="primary" disabled>Apply</button><button id="physics-reset-build" type="button" disabled>Reset</button></div><output id="physics-apply-status" class="environment-readout"></output><div id="physics-readout" class="physics-readout" aria-label="Check readout before and after Apply"></div><textarea id="physics-pending" class="sr-only" hidden aria-hidden="true" tabindex="-1"></textarea></section><section id="idle-tracks" class="physics-panel idle-panel" aria-labelledby="idle-tracks-heading"><div class="row"><span id="idle-tracks-heading" class="eyebrow">IDLE TRACKS · APPLY REBUILDS</span><span id="idle-preview-label" class="idle-preview-label" aria-live="polite" hidden>preview — Apply decides</span></div><p id="idle-tracks-state" class="hint">No compiled model drawn.</p><p class="hint">Each edit is a config patch op at <code>/motion/tracks/&lt;i&gt;/…</code> or <code>/motion/blink/…</code>. Period is offered only among the divisors of <code>motion.duration</code> (the producer refuses any other, <code>CONFIG_PERIOD_DIVIDES_DURATION</code>). While edits are pending the preview drives the edited bones through the player's <code>adjust</code> with the producer's sine, <code>v(t) = base + amp·sin(2π(t/period − phase))</code>, on the bone the idle keys (<code>&lt;bone&gt;_ctl</code> under ctl keys); it is never recorded or submitted. Apply runs the installed producer (<code>rig --config</code>, then <code>check</code>) into a scratch build and replaces the preview with the built idle; Reset returns to the inspected build and clears the pending ops.</p><p id="idle-keys-line" class="hint"></p><div class="physics-work"><div id="idle-track-list" class="physics-table-wrap" role="region" aria-label="Idle tracks the config states" tabindex="0"></div><div id="idle-track-editor" class="physics-row-editor"></div></div><datalist id="idle-bones"></datalist><div class="row idle-add-row"><span class="eyebrow">ADD</span><input data-idle-add-bone type="text" list="idle-bones" autocomplete="off" placeholder="bone" aria-label="Bone for a new track" disabled><select data-idle-add-prop aria-label="Property for a new track" disabled></select><button data-idle-add-single type="button" disabled>Add track</button><select data-idle-add-chain aria-label="Chain for a new chain track" disabled></select><button data-idle-add-chain-button type="button" disabled>Add chain track</button></div><div id="idle-blink" class="idle-blink"></div><div class="row"><label class="idle-preview-toggle"><input data-idle-preview type="checkbox" checked>Preview edits (not recorded)</label></div><div id="idle-precheck" class="idle-precheck" aria-label="Review pre-check"></div><div class="row"><span class="eyebrow">PENDING OPS</span></div><ol id="idle-pending-list" class="physics-pending-list"></ol><div class="row"><button id="idle-apply" type="button" class="primary" disabled>Apply</button><button id="idle-reset" type="button" disabled>Reset</button></div><output id="idle-apply-status" class="environment-readout"></output><div id="idle-readout" class="physics-readout" aria-label="Check readout before and after Apply"></div><textarea id="idle-pending" class="sr-only" hidden aria-hidden="true" tabindex="-1"></textarea></section><section id="readings" class="readings-panel" aria-labelledby="readings-heading"><div class="row"><span id="readings-heading" class="eyebrow">READINGS · RIGC RENDER</span></div><p class="hint">Each reading is one <code>rigc render</code> of the drawn animation, on the inspected build and on the latest scratch build. Its frames become ticks under the timeline (<span class="tick-swatch inspected" aria-hidden="true"></span> inspected, <span class="tick-swatch scratch" aria-hidden="true"></span> scratch; click one to seek) and each pair gets its worst-frame line. A reading, not a gate: an unknown slot or bone is refused with rigc's own line.</p><datalist id="readings-slots"></datalist><datalist id="readings-bones"></datalist><div class="row"><label><input id="readings-folds" type="checkbox">Folds</label><label><input id="readings-overlaps" type="checkbox">Overlaps</label></div><div class="row readings-pair"><label><input id="readings-pass-on" type="checkbox">Pass-through</label><input id="readings-pass-cloth" type="text" list="readings-slots" autocomplete="off" placeholder="cloth slot" aria-label="Pass-through cloth slot"><span aria-hidden="true">:</span><input id="readings-pass-leg" type="text" list="readings-slots" autocomplete="off" placeholder="leg slot" aria-label="Pass-through leg slot"></div><div class="row readings-pair"><label><input id="readings-seam-on" type="checkbox">Seam</label><input id="readings-seam-a" type="text" list="readings-slots" autocomplete="off" placeholder="slot" aria-label="Seam first slot"><span aria-hidden="true">:</span><input id="readings-seam-b" type="text" list="readings-slots" autocomplete="off" placeholder="slot" aria-label="Seam second slot"></div><div class="row readings-pair"><label><input id="readings-angle-on" type="checkbox">Angle range</label><input id="readings-angle-bone" type="text" list="readings-bones" autocomplete="off" placeholder="bone" aria-label="Angle range bone"><span aria-hidden="true">vs</span><input id="readings-angle-relative" type="text" list="readings-bones" autocomplete="off" placeholder="relative bone" aria-label="Angle range relative-to bone"></div><div class="row"><button id="readings-run" type="button" disabled>Run readings</button></div><p id="readings-status" class="hint" aria-live="polite">No compiled model drawn.</p><ul id="readings-worst" class="readings-worst" aria-label="Worst frame per reading"></ul></section><section id="requirements" class="readings-panel requirements-panel" aria-labelledby="requirements-heading"><div class="row"><span id="requirements-heading" class="eyebrow">REQUIREMENTS · RIG-PARTS CHECK</span></div><p class="hint">Declare what the scene asks of the motion: a slot (picked on the stage), its mesh or a bone, an animation and a kind, with your own bar. Check writes a <code>spine-parts-requirements/1</code> file into scratch and runs the installed producer's <code>rig-parts check --requirements</code> on the inspected rig or the latest scratch build; each requirement gets one line with its worst frame (click to seek). A refusal is the producer's, by name. Waivers (<code>may_fold</code>), scene targets and the part exception lists are not written here. Submitted feedback carries the file by hash and content.</p><div id="requirements-body"></div></section><section id="retarget" class="readings-panel requirements-panel retarget-panel" aria-labelledby="retarget-heading"><div class="row"><span id="retarget-heading" class="eyebrow">RETARGET · RIG-POSE</span></div><p class="hint">Put a pose sequence the package declares onto this rig: edit the <code>rig-pose-map/1</code> (per chain, the bone that carries each segment), set the clean and retarget flags, and Apply. The installed <code>@rig-c/pose</code> runs <code>rig-pose clean</code> and <code>rig-pose retarget</code> onto the handoff rig, and its <code>rigc</code> compiles the result, all into scratch; the new animation plays here beside the reference (pick the sequence under Reference). Review writes no keys and never writes the package's <code>motion.json</code>. Refusals (<code>POSE_NOT_CLEANED</code>, <code>POSE_BEND_ONE_WAY</code>, <code>RAISED_HEAD</code>, <code>MOTION_NAME_FREE</code>, <code>MAP_*</code>) are the producer's, verbatim. FEET, STANCE, FLIP, SHORT, END-ON, LEG-SCALE and REACH lines seek to their frame.</p><div id="retarget-body"></div></section><section id="report-panel" class="report-panel" aria-labelledby="report-heading"><div class="report-header"><h2 id="report-heading">Report events</h2><span id="report-summary" class="subtle"></span><a id="report-source-link" target="_blank" rel="noopener" hidden>Source report</a></div><p id="report-unavailable" class="hint" hidden>No rig-pose-report/1 events supplied.</p><div id="report-browser"><div class="report-controls"><label for="report-category" class="sr-only">Report event category</label><select id="report-category"><option value="all">All events</option></select><button id="report-previous" type="button" aria-label="Previous event page">Previous</button><output id="report-page"></output><button id="report-next" type="button" aria-label="Next event page">Next</button></div><div id="report-empty-categories" class="report-empty-categories"></div><p id="report-missing" class="hint" hidden></p><ol id="report-events" class="report-event-list" aria-label="Read-only report events"></ol><section id="report-selection" class="report-selection" hidden aria-labelledby="report-selection-heading"><h3 id="report-selection-heading"></h3><p id="report-timing" class="hint"></p><div class="report-controls"><label id="report-time-label" for="report-time" hidden>Select explicit time</label><select id="report-time" hidden></select><button id="report-seek" type="button" disabled>Go to explicit time</button></div><div id="report-target-control" hidden><label for="report-target" class="label space-top">Select a target named in the report</label><select id="report-target"></select></div><p id="report-prefill-note" class="hint"></p><p id="report-provenance" class="hint"></p><details><summary>Source event details</summary><pre id="report-details"></pre></details></section></div></section></section></div></section></main></div><footer class="status-bar"><span id="message" class="status-message" role="status" aria-live="polite">Loading local review package…</span><div class="status-readouts" aria-label="Status"><span id="player-state" class="subtle">No rig supplied</span><span id="layout-state" class="subtle" role="status" aria-label="Effective layout">Loading inputs…</span><span id="renderer-state" class="subtle" aria-label="Last draw backend" title="DOM images for rigid parts; WebGL meshes when available, Canvas2D fallback.">Not rendered</span><span id="layer-state" class="hint">No rig supplied</span><span id="idle-state" class="hint" role="status">Idle preview unavailable</span><span id="control-state" class="hint" role="status"></span><div class="draft-notice"><span id="draft-state" role="status" aria-live="polite"></span><div id="draft-recovery"></div></div></div><div class="status-source"><details class="view-region"><summary>View region</summary><div class="view-region-body"><span class="hint">Numeric source region · opening this link restores a paused inspection</span><pre id="view-coordinates" class="numeric-view" aria-label="Current numeric inspection region"></pre><p class="hint">Sync cameras links fit-relative zoom and viewport-relative pan across visible panes. Turn it off to inspect each pane independently. Toolbar zoom and Fit use the last active pane. Hold Space while dragging a video to pan without taking over its native controls.</p></div></details><span id="source-hash" class="subtle"></span></div></footer><dialog id="review-dialog" class="review-dialog" aria-labelledby="review-dialog-title"><div class="review-dialog-header"><h2 id="review-dialog-title">Write review</h2><span class="hint">Drafts save locally as you type · nothing is submitted until you press Submit</span><button id="review-dialog-close" type="button">Close</button></div><section id="inspector-feedback" class="review-dialog-body" aria-label="Feedback"><div class="review-summary"><div class="eyebrow">REVIEW</div><label for="review-status" class="label">Review status</label><select id="review-status"><option value="pending">Pending</option><option value="approved">Approved</option><option value="changes-requested">Changes requested</option></select><section class="provenance" aria-label="Reviewed source"><div class="eyebrow">REVIEWED SOURCE</div><div class="provenance-row"><span>Package revision</span><strong id="provenance-version">—</strong></div><div class="provenance-row"><span>rig SHA-256</span><code id="provenance-rig">—</code></div><div class="provenance-row"><span>motion SHA-256</span><code id="provenance-motion">—</code></div><details><summary>Full provenance</summary><pre id="provenance-full"></pre></details><div class="source-files"><div class="eyebrow">SOURCE FILES · READ ONLY</div><div id="source-files"></div></div><p class="hint">Submits server-verified file hashes and tool versions. Your selected review status is preserved.</p></section><div class="divider"></div></div><div id="checkpoint-detail"><div class="eyebrow">SELECTED CHECKPOINT</div><h1 id="detail-title">Checkpoint</h1><p id="detail-note" class="hint"></p><fieldset class="outcome"><legend>Checkpoint outcome</legend><label><input type="radio" name="outcome" value="pending" checked>Pending</label><label><input type="radio" name="outcome" value="pass">Pass</label><label><input type="radio" name="outcome" value="fail">Needs changes</label></fieldset><label for="checkpoint-note" class="label space-top">Review notes</label><textarea id="checkpoint-note" rows="5" placeholder="Describe the part, conditions and what looks wrong."></textarea></div><div class="divider"></div><section id="proposal-panel"><div class="eyebrow">STRUCTURED FEEDBACK</div><div class="label">Frame range · inclusive</div><div class="frame-range"><label>Start<input id="feedback-start" type="number" min="0" step="1" inputmode="numeric" placeholder="e.g. 18"></label><label>End<input id="feedback-end" type="number" min="0" step="1" inputmode="numeric" placeholder="e.g. 32"></label></div><p id="feedback-range-hint" class="hint">Checking frame bounds…</p><label for="feedback-target-kind" class="label space-top">Target type</label><select id="feedback-target-kind"><option value="">Select target</option><option value="rig">Entire rig</option><option value="bone">Bone</option><option value="slot">Slot</option></select><label for="feedback-target-name" class="label space-top">Target name</label><input id="feedback-target-name" type="text" list="feedback-target-names" autocomplete="off" placeholder="Actual bone or slot name"><datalist id="feedback-target-names"></datalist><p id="feedback-target-hint" class="hint"></p><label for="feedback-category" class="label space-top">Category</label><select id="feedback-category"><option value="">Select category</option><option value="defect">Defect</option><option value="quality">Quality</option></select><label for="feedback-summary" class="label space-top">One-line summary</label><input id="feedback-summary" type="text" maxlength="1000" placeholder="e.g. The left wrist moves outside the sleeve"><button id="add-proposal" class="outline block">Add feedback</button><p id="proposal-boundary" class="hint">Records the current checkpoint, time, reference, camera and inspection axis. Motion is read only: inspect, mark ranges and export feedback. No keys or source files are edited.</p><p id="feedback-warnings" class="hint" role="status" hidden></p><p id="feedback-applied-check" class="hint error" role="alert" hidden></p><div id="proposal-list"></div></section><details id="strip-panel" class="report-panel"><summary>Export frame strip</summary><p class="hint">Uses the feedback form start and end frames, inclusive. Inspection time and source files remain unchanged.</p><label for="strip-scope" class="label">Export range</label><select id="strip-scope"><option value="interval">Entered start–end frames</option><option value="feedback">All feedback ranges</option></select><label for="strip-reference" class="label space-top">Reference video</label><select id="strip-reference"><option value="">Select reference video</option></select><label for="strip-format" class="label space-top">Format</label><select id="strip-format"><option value="png">PNG strip</option><option value="json">JSON frames and provenance</option></select><p class="hint">PNG requires frameTimes in the selected report. Without it, JSON preserves requested frames with time: null. Batch export requires every feedback item to match the selected animation. Unsubmitted feedback is included; exporting does not submit the review.</p><button id="strip-export" type="button" disabled>Download strip</button><p id="strip-status" class="hint" role="status" aria-live="polite">Loading package…</p></details></section><div class="review-dialog-actions"><span id="review-message" class="review-message" role="status" aria-live="polite"></span><span id="review-draft-state" class="review-draft-state" role="status" aria-live="polite"></span><button id="save" class="primary" disabled>Submit review</button></div></dialog></div>
9
+ <main><div class="viewbar"><fieldset id="layer-controls" class="layer-toggles"><legend class="sr-only">Rig layers</legend><label class="layer-toggle" title="No rig supplied"><input aria-keyshortcuts="I" id="layer-images" type="checkbox" checked disabled><span>Images</span><kbd aria-hidden="true">I</kbd></label><label class="layer-toggle" title="No rig supplied"><input aria-keyshortcuts="M" id="layer-mesh" type="checkbox" disabled><span>Mesh edges</span><kbd aria-hidden="true">M</kbd></label><label class="layer-toggle" title="No rig supplied"><input aria-keyshortcuts="B" id="layer-bones" type="checkbox" disabled><span>Bone points</span><kbd aria-hidden="true">B</kbd></label></fieldset><div class="view-tools"><button id="chest-edit" type="button" class="view-tool" aria-pressed="false" aria-controls="chest-editor-bar" title="Place the two chest regions on the rig: drag a centre, the core edge or the band" hidden>Chest regions</button><button id="mesh-edit" type="button" class="view-tool" aria-pressed="false" aria-controls="mesh-editor-bar" title="Edit the selected slot's actual mesh as a typed delta: preview only, the producer retriangulates" hidden>Edit mesh</button></div><div class="zoom-controls"><label class="camera-sync"><input id="camera-sync" type="checkbox" checked>Sync cameras</label><button id="zoom-out" aria-label="Zoom out">−</button><output id="zoom-value">100%</output><button id="zoom-in" aria-label="Zoom in">+</button><button id="fit">Fit</button></div><details id="view-menu" class="view-menu"><summary aria-label="View layout">View: <span id="view-menu-label">Side by side</span></summary><div class="view-menu-list" role="group" aria-label="View"><button type="button" data-view="compare" aria-pressed="true">Side by side</button><button type="button" data-view="reference" aria-pressed="false">Reference</button><button type="button" data-view="rig" aria-pressed="false">Rig</button></div></details></div><div id="chest-editor-bar" class="chest-editor-bar" role="region" aria-label="Chest region editor" hidden><div class="bust-group" role="group" aria-label="Chest editor mode"><input id="bust-mode" type="hidden" value="circles"><button id="bust-mode-circles" type="button" data-bust-mode="circles" aria-pressed="true" title="Two circles, each centre, core and band set by hand (#99)">Circles</button><button id="bust-mode-landmarks" type="button" data-bust-mode="landmarks" aria-pressed="false" title="Three clicks (right apex, left apex, cleavage); the producer derives both circles (contour.bust)">Landmarks</button></div><div class="bust-group" data-chest-mode="circles"><span class="bust-tag bust-tag-r">● R</span><label>X<input id="bust-r-x" type="number" step="1" inputmode="decimal" aria-label="R region centre x"></label><label>Y<input id="bust-r-y" type="number" step="1" inputmode="decimal" aria-label="R region centre y"></label><label>R<input id="bust-r-r" type="number" min="6" max="120" step="1" inputmode="decimal" aria-label="R region core radius"></label><label>Band<input id="bust-r-band" type="number" min="4" max="120" step="1" inputmode="decimal" aria-label="R region band"></label></div><div class="bust-group" data-chest-mode="circles"><span class="bust-tag bust-tag-l">● L</span><label>X<input id="bust-l-x" type="number" step="1" inputmode="decimal" aria-label="L region centre x"></label><label>Y<input id="bust-l-y" type="number" step="1" inputmode="decimal" aria-label="L region centre y"></label><label>R<input id="bust-l-r" type="number" min="6" max="120" step="1" inputmode="decimal" aria-label="L region core radius"></label><label>Band<input id="bust-l-band" type="number" min="4" max="120" step="1" inputmode="decimal" aria-label="L region band"></label></div><div id="bust-landmarks" class="bust-group" data-chest-mode="landmarks" role="group" aria-label="Chest landmarks (rig px)" hidden><button id="lm-pick-apex_r" type="button" data-landmark="apex_r" aria-pressed="false" title="Click the part to place the apex r; drag its marker to move it">Apex R</button><label>X<input id="lm-apex_r-x" type="number" step="1" inputmode="decimal" aria-label="Apex R x (rig px)"></label><label>Y<input id="lm-apex_r-y" type="number" step="1" inputmode="decimal" aria-label="Apex R y (rig px)"></label><button id="lm-pick-apex_l" type="button" data-landmark="apex_l" aria-pressed="false" title="Click the part to place the apex l; drag its marker to move it">Apex L</button><label>X<input id="lm-apex_l-x" type="number" step="1" inputmode="decimal" aria-label="Apex L x (rig px)"></label><label>Y<input id="lm-apex_l-y" type="number" step="1" inputmode="decimal" aria-label="Apex L y (rig px)"></label><button id="lm-pick-cleavage" type="button" data-landmark="cleavage" aria-pressed="false" title="Click the part to place the cleavage; drag its marker to move it">Cleavage</button><label>X<input id="lm-cleavage-x" type="number" step="1" inputmode="decimal" aria-label="Cleavage x (rig px)"></label><label>Y<input id="lm-cleavage-y" type="number" step="1" inputmode="decimal" aria-label="Cleavage y (rig px)"></label></div><div id="bust-landmark-params" class="bust-group" data-chest-mode="landmarks" role="group" aria-label="Landmark constants" hidden><label title="Core ratio r / (r + band); the producer's default is 0.22">α<input id="lm-alpha" type="range" min="0.05" max="0.95" step="0.01" value="0.22" aria-label="Alpha, core ratio"><output id="lm-alpha-value" for="lm-alpha">0.22</output></label><label title="Cleavage clearance in rig px; the producer's default is 3.8">Clearance<input id="lm-clearance" type="range" min="0" max="40" step="0.1" value="3.8" aria-label="Clearance, rig px"><output id="lm-clearance-value" for="lm-clearance">3.8</output></label><label title="Control bone for bust_r; empty is the producer's default boob_r">Bone R<input id="lm-bone_r" type="text" list="lm-bones" placeholder="boob_r" spellcheck="false" autocomplete="off"></label><label title="Control bone for bust_l; empty is the producer's default boob_l">Bone L<input id="lm-bone_l" type="text" list="lm-bones" placeholder="boob_l" spellcheck="false" autocomplete="off"></label><datalist id="lm-bones"></datalist></div><span id="lm-derived" class="hint" data-chest-mode="landmarks" aria-live="polite" hidden></span><div class="bust-group" data-chest-mode="circles"><button id="bust-mirror" type="button" title="Copy R's y, core radius and band to L">Mirror R → L</button><button id="bust-download" type="button" title="Download chest-regions.json and its binding; evidence, not an approval">Download JSON</button></div><div class="bust-group"><button id="bust-apply" type="button" title="Run the installed producer (rig-parts remesh) on these regions or landmarks into a scratch build and load it here; the package is unchanged">Apply · Rebuild</button><button id="bust-reset" type="button" title="Return the stage to the inspected build" disabled>Reset</button></div><span id="handoff-line" class="hint handoff-line" aria-live="polite" title="Producer handoff: the config, parts and inspected rig an Apply rebuilds from"></span><span id="chest-editor-status" class="hint" aria-live="polite"></span><pre id="chest-editor-log" class="rebuild-log" aria-label="Producer output" hidden></pre><textarea id="rebuild-applied" class="sr-only" hidden aria-hidden="true" tabindex="-1"></textarea></div><div id="mesh-editor-bar" class="chest-editor-bar mesh-editor-bar" role="region" aria-label="Mesh edit" hidden><div class="bust-group" role="group" aria-label="Mesh edit sub-mode"><button id="mesh-mode-modify" type="button" data-mesh-mode="modify" aria-pressed="false" aria-keyshortcuts="V" title="Modify · key V">Modify<kbd aria-hidden="true">V</kbd></button><button id="mesh-mode-create" type="button" data-mesh-mode="create" aria-pressed="false" aria-keyshortcuts="C" title="Create · key C">Create<kbd aria-hidden="true">C</kbd></button><button id="mesh-mode-delete" type="button" data-mesh-mode="delete" aria-pressed="false" aria-keyshortcuts="X" title="Delete · key X">Delete<kbd aria-hidden="true">X</kbd></button><button id="mesh-mode-edges" type="button" data-mesh-mode="edges" aria-pressed="false" aria-keyshortcuts="E" title="Edges · key E">Edges<kbd aria-hidden="true">E</kbd></button></div><div class="bust-group"><button id="mesh-undo" type="button" aria-keyshortcuts="Control+Z Meta+Z" title="Undo the last action · Cmd/Ctrl-Z" disabled>Undo</button><button id="mesh-redo" type="button" aria-keyshortcuts="Control+Shift+Z Meta+Shift+Z" title="Redo · Shift-Cmd/Ctrl-Z" disabled>Redo</button><button id="mesh-reset" type="button" title="Drop the whole delta for this attachment (asks first)" disabled>Reset</button></div><div class="bust-group"><button id="mesh-download" type="button" title="Download mesh-edit.json and its binding; evidence, not an approval">Download JSON</button><button id="mesh-apply" type="button" title="Send the delta to the producer (rig-parts remesh --delta) and load the scratch revision in place; nothing is written into the package">Apply · Rebuild</button></div><textarea id="mesh-edit-draft" class="sr-only" hidden aria-hidden="true" tabindex="-1"></textarea><span id="mesh-editor-status" class="hint" aria-live="polite"></span></div><div id="stage-layout" class="stages"><section id="reference-pane" class="stage-pane" aria-label="Reference"><span id="reference-label" class="pane-label">Reference</span><div id="reference-viewport" class="viewport"><div id="reference-content" class="reference-content"></div><div id="reference-empty" class="empty">No reference supplied.</div></div></section><section id="rig-pane" class="stage-pane" aria-label="Rig"><span id="rig-label" class="pane-label">Generated rig · read only</span><div id="rig-viewport" class="viewport"><div id="rig-root"></div><canvas id="rig-overlay" class="rig-overlay" aria-label="Actual rig mesh and bone overlay"></canvas><div id="idle-handles" class="idle-handles" hidden aria-label="Chain amplitude handles"></div><div id="rig-fields-stage" class="rig-fields-stage" hidden aria-label="Rig field handles"></div><div id="rig-empty" class="empty">No compiled model supplied.</div></div></section></div><div class="stage-footer"><span id="coordinates">Drag to pan · scroll to zoom · cameras synchronized</span><span>Source files remain unchanged.</span></div><section class="inspector" aria-label="Inspection controls"><div class="inspector-heading sticky-header"><strong>INSPECTOR</strong><span class="hint">Local view controls · source files stay unchanged</span></div><div class="inspector-body"><section id="applied-records" class="applied-records" aria-labelledby="applied-records-heading" hidden><div class="row"><span id="applied-records-heading" class="eyebrow">APPLIED RECORDS</span><button id="applied-undo" type="button" disabled>Undo last Apply (0 deep)</button></div><ol id="applied-list" class="applied-list"></ol><p id="applied-status" class="hint" aria-live="polite"></p></section><section id="inspector-rig" aria-label="Setup" data-inspector-panel="rig"><nav class="block-row" aria-label="Setup blocks"><button type="button" data-block-target="selected-part">Part</button><button type="button" data-block-target="modeling-tools">Modeling</button><button type="button" data-block-target="chest-regions">Chest</button><button type="button" data-block-target="mesh-panel">Mesh</button><button type="button" data-block-target="weights">Weights</button><button type="button" data-block-target="bones-panel">Bones</button><button type="button" data-block-target="rig-fields">Fields</button><button type="button" data-block-target="cuts-panel">Cuts</button><button type="button" data-block-target="cloth-panel">Cloth</button></nav><section id="selected-part" aria-label="Selected part" aria-live="polite"></section><section id="modeling-tools"><div class="eyebrow">MODELING REVIEW</div><button data-proposal="mesh" class="tool">Mesh density <small>Add feedback</small></button><button data-proposal="contour" class="tool">Contour curvature <small>Add feedback</small></button><button data-proposal="chest" class="tool">Chest position <small>Add feedback</small></button><label for="part-set" class="label space-top">Part presets</label><select id="part-set" disabled><option>No sets supplied</option></select><p class="hint">Preview only · does not create motion keys.</p></section><section id="chest-regions" class="chest-regions" aria-labelledby="chest-regions-heading"><div class="eyebrow" id="chest-regions-heading">CHEST REGIONS · DETAIL</div><p class="hint">Crop pixels, x screen-right and y down (<code>family-chest-regions/1</code>). Place the circles with <strong>Chest regions</strong> in the view bar; this block holds names, bones, semi-axes, clip and the contour. Equal semi-axes export as a circle, unequal ones (or a clip) as the generator's 32-sample polygon. Exporting writes nothing into the package and is not an approval.</p><p id="chest-mapping" class="hint"></p><div class="chest-grid"><label>Crop width<input id="chest-crop-w" type="number" step="any" inputmode="decimal"></label><label>Crop height<input id="chest-crop-h" type="number" step="any" inputmode="decimal"></label></div><fieldset class="chest-row"><legend>Region 1</legend><label>Name<input id="chest-0-name" type="text" autocomplete="off" spellcheck="false"></label><label>Bone<select id="chest-0-bone"><option value="">Select bone</option></select></label><label>Centre x<input id="chest-0-cx" type="number" step="any" inputmode="decimal"></label><label>Centre y<input id="chest-0-cy" type="number" step="any" inputmode="decimal"></label><label>Semi-axis x<input id="chest-0-ax" type="number" step="any" inputmode="decimal"></label><label>Semi-axis y<input id="chest-0-ay" type="number" step="any" inputmode="decimal"></label><label>Spacing<input id="chest-0-spacing" type="number" step="any" inputmode="decimal"></label><label>Band<input id="chest-0-band" type="number" step="any" inputmode="decimal"></label><label>Medial clip x<input id="chest-0-clip" type="number" step="any" inputmode="decimal" placeholder="none"></label></fieldset><fieldset class="chest-row"><legend>Region 2</legend><label>Name<input id="chest-1-name" type="text" autocomplete="off" spellcheck="false"></label><label>Bone<select id="chest-1-bone"><option value="">Select bone</option></select></label><label>Centre x<input id="chest-1-cx" type="number" step="any" inputmode="decimal"></label><label>Centre y<input id="chest-1-cy" type="number" step="any" inputmode="decimal"></label><label>Semi-axis x<input id="chest-1-ax" type="number" step="any" inputmode="decimal"></label><label>Semi-axis y<input id="chest-1-ay" type="number" step="any" inputmode="decimal"></label><label>Spacing<input id="chest-1-spacing" type="number" step="any" inputmode="decimal"></label><label>Band<input id="chest-1-band" type="number" step="any" inputmode="decimal"></label><label>Medial clip x<input id="chest-1-clip" type="number" step="any" inputmode="decimal" placeholder="none"></label></fieldset><fieldset class="chest-row"><legend>Contour</legend><label>Tolerance<input id="chest-tolerance" type="number" step="any" inputmode="decimal"></label><label>Margin<input id="chest-margin" type="number" step="any" inputmode="decimal"></label><label>Spacing<input id="chest-spacing" type="number" step="any" inputmode="decimal"></label><label>Max edge<input id="chest-max-edge" type="number" step="any" inputmode="decimal"></label><label>Stray<input id="chest-stray" type="number" step="any" inputmode="decimal"></label><label class="chest-check"><input id="chest-bridges" type="checkbox">Bridges</label></fieldset><details class="chest-more"><summary>Generator and mesh fields</summary><label for="chest-generator" class="label space-top">Public generator</label><input id="chest-generator" type="text" autocomplete="off" spellcheck="false" placeholder="Generator that will read this file"><label for="chest-r" class="label space-top">Mesh r (optional)</label><input id="chest-r" type="number" step="any" inputmode="decimal"><label for="chest-segments" class="label space-top">Mesh segments JSON (optional)</label><textarea id="chest-segments" rows="3" spellcheck="false"></textarea><label for="chest-source-rule" class="label space-top">Source rule (optional)</label><input id="chest-source-rule" type="text" autocomplete="off"></details><p id="chest-status" class="hint" aria-live="polite"></p><button id="chest-export" type="button" class="outline block">Download chest-regions.json</button></section><section id="mesh-panel" class="physics-panel mesh-panel" aria-labelledby="mesh-panel-heading"><div class="row"><span id="mesh-panel-heading" class="eyebrow">MESH · APPLY REMESHES</span></div><p id="mesh-panel-state" class="hint">Select a mesh part in the parts list.</p><p class="hint">Edits the selected part's <code>meshes.&lt;part&gt;</code> entry from the handoff config (the grid and contour modes). Apply sends the whole pending entry to the installed producer's <code>remesh --mesh</code> (one patch op at <code>/meshes/&lt;part&gt;</code>), compiles and checks the revision, and reloads it with the mesh edges on; Reset returns to the inspected build and the config's entry. Slider ranges are review's; the number field takes any value, and the producer judges it.</p><div id="mesh-params" class="physics-controls mesh-params"></div><div class="row mesh-tools"><span class="eyebrow">REGIONS</span><button id="mesh-add-circle" type="button" disabled>Add circle</button><button id="mesh-add-polygon" type="button" aria-pressed="false" disabled>Draw polygon</button><button id="mesh-regions-edit" type="button" aria-pressed="false" disabled>Drag on stage</button></div><div id="mesh-regions" class="mesh-regions"></div><div class="row mesh-tools"><span class="eyebrow">SEGMENTS</span><select id="mesh-segment-bone" aria-label="Bone to add as a segment" disabled></select><button id="mesh-segment-add" type="button" disabled>Add</button></div><ol id="mesh-segments" class="physics-pending-list mesh-segments"></ol><div class="row"><span class="eyebrow">PENDING ENTRY · DIFF AGAINST THE CONFIG</span></div><ol id="mesh-diff" class="physics-pending-list"></ol><ul id="mesh-precheck" class="mesh-precheck" aria-label="Review pre-check in the config loader's names"></ul><div class="row"><button id="mesh-panel-apply" type="button" class="primary" disabled>Apply</button><button id="mesh-panel-reset" type="button" disabled>Reset</button><button id="mesh-folds" type="button" title="rigc render --folds of the drawn animation on the inspected build and on this revision (#108 readings)">Folds reading</button></div><output id="mesh-status" class="environment-readout"></output><div id="mesh-counts" class="physics-readout" aria-label="Vertex and triangle counts before and after"></div><div id="mesh-readout" class="physics-readout" aria-label="TEXTURE_STRETCH before and after Apply"></div><div id="density-strip" class="density-strip" aria-labelledby="density-strip-heading"><div class="row"><span id="density-strip-heading" class="eyebrow">DENSITY STRIP · NOTHING IS APPLIED</span></div><p id="density-strip-field" class="hint">Select a mesh part (grid or contour entry) to build a density strip.</p><p class="hint">Builds N values of the part's density field side by side, each as its own <code>replace /meshes/&lt;part&gt;</code> patch through remesh (into its own scratch folder). Rows are labels only: Load puts a row on the stage as pending, and the Mesh panel's Apply applies it. Closing or replacing the strip removes its scratch.</p><div class="row density-strip-form"><label>N<input id="density-strip-n" type="number" min="2" max="5" step="1" value="5" aria-label="Number of values"></label><label>from<input id="density-strip-from" type="number" step="any" aria-label="From value"></label><label>to<input id="density-strip-to" type="number" step="any" aria-label="To value"></label><button id="density-strip-build" type="button" disabled>Build strip</button><button id="density-strip-back" type="button" disabled title="Return the stage to the applied build it showed before a row was loaded">Back</button><button id="density-strip-close" type="button" disabled>Close strip</button></div><output id="density-strip-status" class="environment-readout"></output><div id="density-strip-table" class="density-strip-table"></div></div><textarea id="mesh-pending" class="sr-only" hidden aria-hidden="true" tabindex="-1"></textarea></section><section id="weights" class="physics-panel weights-block" aria-labelledby="weights-heading"><div class="row"><span id="weights-heading" class="eyebrow">WEIGHTS · SETUP POSE · WATCH ONLY</span></div><p class="hint">The selected slot's mesh at the setup pose, each vertex coloured by the weight the compiled model document stores for the picked bone (0 → 1), or by its dominant bone under <strong>Max influence</strong>. An unweighted mesh is its slot's bone at 1.0. Hover a vertex for its stored values. Nothing here edits, applies or records a weight.</p><div class="row weights-controls"><button id="weights-show" type="button" aria-pressed="false">Show weights</button><label>Colour by<select id="weights-mode"><option value="bone">Bone weight</option><option value="max">Max influence</option></select></label><label>Bone<select id="weights-bone" disabled></select></label></div><p id="weights-state" class="hint" aria-live="polite"></p><div id="weights-legend" class="weights-legend" aria-label="Weights legend"></div><output id="weights-hover" class="environment-readout"></output></section><section id="bones-panel" class="physics-panel bones-panel" aria-labelledby="bones-panel-heading"><div class="row"><span id="bones-panel-heading" class="eyebrow">BONES · CONFIG ANCHORS · APPLY REBUILDS</span></div><p id="bones-panel-state" class="hint">Reading the producer handoff…</p><p class="hint">The producer config's bones at their <code>at</code>, chain <code>points</code> and <code>tip</code> (rig px, the setup pose; the stage may be posed by an animation). Drag a handle on the stage or edit the numbers: each move is a config patch op at <code>/bones/&lt;i&gt;/at</code>, <code>/bones/&lt;i&gt;/points/&lt;k&gt;</code> or <code>/bones/&lt;i&gt;/tip</code> naming its bone, moved in whole rig px. <strong>Check</strong> runs <code>propose --from-config</code> on a scratch copy of the patched config (LINT and coverage lines; LINT blocks nothing) and <code>compare</code> against the handoff config. <strong>Apply</strong> rebuilds the whole rig (<code>rig --config</code>, then <code>check</code>) and needs the reproduced baseline; Reset returns to the inspected build and clears the pending ops. Adding, removing or re-parenting bones is out of scope.</p><div class="row bones-controls"><button id="bones-edit" type="button" aria-pressed="false" disabled>Edit bones on stage</button><label for="bones-select">Bone</label><select id="bones-select" disabled></select></div><div id="bones-anchor-editor" class="bones-anchor-editor"></div><div class="row"><span class="eyebrow">PENDING OPS</span></div><ol id="bones-pending-list" class="physics-pending-list"></ol><div class="row"><button id="bones-check" type="button" disabled>Check</button><button id="bones-apply" type="button" class="primary" disabled>Apply</button><button id="bones-reset-build" type="button" disabled>Reset</button></div><output id="bones-status" class="environment-readout"></output><div id="bones-compare" class="bones-lines" aria-label="compare: origin and tip distance per moved bone"></div><div id="bones-lint" class="bones-lines" aria-label="propose --from-config: LINT and coverage lines"></div><textarea id="bones-pending" class="sr-only" hidden aria-hidden="true" tabindex="-1"></textarea></section><section id="rig-fields" class="physics-panel rig-fields" aria-labelledby="rig-fields-heading"><div class="row"><span id="rig-fields-heading" class="eyebrow">RIG FIELDS · CONFIG · APPLY REBUILDS</span><span id="rig-fields-dial-label" class="idle-preview-label" hidden>preview — Apply decides</span></div><p id="rig-fields-state" class="hint">Reading the producer handoff…</p><p class="hint">Small producer config fields, each a config patch op: the bone a rigid part rides (<code>/regions/&lt;part&gt;</code>), a lash's blink still row (<code>/motion/blink/still/&lt;part&gt;</code>, dragged on the stage), a stop's inward / outward bounds (<code>/stops/&lt;i&gt;</code>, arcs dragged at the pivot), a bone's inherit mode (<code>/bones/&lt;i&gt;/inherit</code>; a chain's <code>link_inherit</code>) and <code>turn</code>'s pivot and per-part depth (<code>/turn/pivot</code>, <code>/turn/depths/&lt;part&gt;</code>). <strong>Apply</strong> rebuilds the whole rig (<code>rig --config</code>, then <code>check</code>) and needs the reproduced baseline; the producer's refusal is shown as printed. The dial only previews the drawn build's <code>turn</code> bone and is never recorded. Reset returns to the inspected build and clears the pending ops.</p><div id="rig-fields-region" class="rig-fields-group"></div><div id="rig-fields-still" class="rig-fields-group"></div><div id="rig-fields-stops" class="rig-fields-group"></div><div id="rig-fields-inherit" class="rig-fields-group"></div><div id="rig-fields-turn" class="rig-fields-group"></div><div class="row"><span class="eyebrow">PENDING OPS</span></div><ol id="rig-fields-pending-list" class="physics-pending-list"></ol><div class="row"><button id="rig-fields-apply" type="button" class="primary" disabled>Apply</button><button id="rig-fields-reset" type="button" disabled>Reset</button><button id="rig-fields-reading" type="button" disabled>Angle range</button></div><output id="rig-fields-status" class="environment-readout"></output><div id="rig-fields-readout" class="physics-readout" aria-label="BLINK_NO_HOLE and FEET_STAY_LEVEL before and after"></div><textarea id="rig-fields-pending" class="sr-only" hidden aria-hidden="true" tabindex="-1"></textarea></section><section id="cuts-panel" class="physics-panel bones-panel cuts-panel" aria-labelledby="cuts-panel-heading"><div class="row"><span id="cuts-panel-heading" class="eyebrow">CUTS · PATCHES · LIMBS · APPLY RUNS RIG-PARTS BUILD</span></div><p id="cuts-panel-state" class="hint">Reading the producer handoff…</p><p class="hint">The producer config's <code>assemble.cuts</code> polygons, <code>assemble.patches</code> boxes and each limb joint with its <code>range</code> arc, on the stage in rig px of the setup pose. Drag a vertex, a box corner or a joint, or edit the numbers: each change is a config patch op (<code>/assemble/cuts/&lt;i&gt;/polygon/&lt;k&gt;</code>, <code>/assemble/patches/&lt;i&gt;/box</code>, <code>/limbs/&lt;limb&gt;/segments/&lt;k&gt;/range</code>; a joint is its bone's origin, <code>/bones/…</code>). A new cut or patch adds its entry and a <code>regions</code> bone. <strong>Apply</strong> runs <code>rig-parts build</code> (assemble → rig → check) from the declared painting and See-through runs into scratch and needs the reproduced baseline; the producer's refusals (<code>ASSEMBLE_CUT_*</code>, <code>ASSEMBLE_UNDER_*</code>, <code>CONFIG_LIMB_*</code>, <code>CONFIG_PART_UNIQUE</code>) are shown as printed, and the recomposite error map, the <code>seam pair</code> lines and the build's wall clock come back with the build. Review paints no <code>under</code> or <code>cap</code> image. Reset returns to the inspected build and clears the pending ops.</p><div class="row bones-controls"><button id="cuts-edit" type="button" aria-pressed="false" disabled>Edit cuts on stage</button><label for="cuts-select">Entry</label><select id="cuts-select" disabled></select></div><div id="cuts-editor" class="bones-anchor-editor"></div><div id="cuts-add" class="row cuts-add"><span class="eyebrow">ADD</span><select data-cuts-add="from" aria-label="Plan part the new cut takes pixels from"></select><input data-cuts-add="name" type="text" autocomplete="off" placeholder="new part" aria-label="New part name"><input data-cuts-add="bone" type="text" list="lm-bones" autocomplete="off" placeholder="bone" aria-label="Bone the new part's region rides"><select data-cuts-add="draw" aria-label="Where the new part is drawn"></select><input data-cuts-add="overlap" type="number" min="0" step="1" value="0" inputmode="numeric" aria-label="Cut overlap band in rig px"><select data-cuts-add="alpha" aria-label="New patch alpha"><option value="silhouette">silhouette</option><option value="box">box</option></select><button type="button" data-cuts-add="place-cut">Place cut</button><button type="button" data-cuts-add="place-patch">Place patch</button><button type="button" data-cuts-add="finish">Finish</button><button type="button" data-cuts-add="cancel">Cancel</button></div><div class="row"><span class="eyebrow">PENDING OPS</span></div><ol id="cuts-pending-list" class="physics-pending-list"></ol><div class="row"><button id="cuts-apply" type="button" class="primary" disabled>Apply</button><button id="cuts-reset" type="button" disabled>Reset</button></div><output id="cuts-status" class="environment-readout"></output><div id="cuts-readout" class="physics-readout" aria-label="The build's refusals, cut lines, recomposite, seam pairs and wall clock"></div><textarea id="cuts-pending" class="sr-only" hidden aria-hidden="true" tabindex="-1"></textarea></section><section id="cloth-panel" class="physics-panel bones-panel cloth-panel" aria-labelledby="cloth-panel-heading"><div class="row"><span id="cloth-panel-heading" class="eyebrow">CLOTH · RECIPE PER TAG · APPLY RUNS RIG-PARTS PROPOSE --CLOTH</span></div><p id="cloth-panel-state" class="hint">Reading the producer handoff…</p><p class="hint">The producer config's <code>cloth</code> block: one recipe per tag the installed reader accepts, with its fields as written. Each change is a config patch op at <code>/cloth/&lt;tag&gt;/…</code>. <strong>Show links</strong> draws the chains the tag's parts are meshed on, from the config's <code>points</code> and <code>tip</code> in rig px of the setup pose, and after an Apply the chains of the config the producer wrote. <strong>Apply</strong> runs <code>rig-parts propose --cloth --write-config</code> on the declared parts, painting and keypoints into scratch, then builds the written config (<code>rig-parts build</code> when the config declares <code>limbs</code>, else <code>rig --config</code>) and reloads it under the Environment's wind and gravity; the producer's refusals (<code>CONFIG_CLOTH_*</code>, <code>PROPOSE_CLOTH_DRAFTED</code>, <code>PROPOSE_CLOTH_PART</code>, <code>PROPOSE_WRITE_CONFIG_*</code>) are shown as printed. Review edits the recipe, never the drafted constraints. Reset returns to the inspected build and clears the pending ops.</p><div class="row bones-controls"><button id="cloth-links" type="button" aria-pressed="false" disabled>Show links on stage</button><label for="cloth-tag">Tag</label><select id="cloth-tag" disabled></select><label class="cloth-declared"><input id="cloth-declared" type="checkbox" disabled> recipe declared</label></div><div id="cloth-fields" class="cloth-fields"></div><div class="row"><span class="eyebrow">PENDING OPS</span></div><ol id="cloth-pending-list" class="physics-pending-list"></ol><div class="row"><button id="cloth-apply" type="button" class="primary" disabled>Apply</button><button id="cloth-reset" type="button" disabled>Reset</button></div><output id="cloth-status" class="environment-readout"></output><div id="cloth-readout" class="physics-readout" aria-label="Propose's refusals, the written config's diff, the route and check"></div><textarea id="cloth-pending" class="sr-only" hidden aria-hidden="true" tabindex="-1"></textarea></section></section><section id="inspector-playback" aria-label="Animate" data-inspector-panel="playback" hidden><nav class="block-row" aria-label="Animate blocks"><button type="button" data-block-target="transport" data-block-includes="frame-controls motion-tools angle-panel">Transport</button><button type="button" data-block-target="sound-panel">Sound</button><button type="button" data-block-target="environment">Environment</button><button type="button" data-block-target="physics-panel">Physics</button><button type="button" data-block-target="idle-tracks">Idle tracks</button><button type="button" data-block-target="readings">Readings</button><button type="button" data-block-target="requirements">Requirements</button><button type="button" data-block-target="retarget">Retarget</button><button type="button" data-block-target="report-panel">Report</button></nav><section id="transport" class="transport"><button id="play" aria-label="Play" disabled>▶</button><label for="animation-select" class="sr-only">Animation</label><select id="animation-select" disabled><option>No animations supplied</option></select><input id="scrub" type="range" min="0" max="1" step="0.001" value="0" aria-label="Playback time"><output id="time-label">0.0000 / 0.0000 s</output><output id="frame-label">rate unknown</output><label class="idle-repeat"><input id="idle-repeat" type="checkbox" checked>Repeat idle</label><div id="reading-ticks" class="reading-ticks" hidden aria-label="Reading ticks"></div><div id="idle-blink-strip" class="idle-blink-strip" hidden><button id="idle-blink-marker" type="button" class="idle-blink-marker" aria-label="Blink time">◆</button></div></section><section id="frame-controls" class="frame-controls" hidden aria-label="Frame navigation using the explicit clock"><label for="frame-number">Frame</label><input id="frame-number" type="number" min="0" step="1" inputmode="numeric" aria-label="Frame number to inspect"><button id="frame-jump" type="button">Go</button><input id="frame-scrub" type="range" min="0" max="1" step="1" value="0" aria-label="Scrub by frame"><span id="frame-clock-label" class="hint"></span><p id="frame-outside" class="hint" hidden>The current time is outside the explicit frame range. Enter a frame number to go to its exact time.</p></section><section id="motion-tools"><div class="eyebrow">MOTION · EDITS BUILD THROUGH THE PRODUCER</div><button data-proposal="pose" class="tool">Inspect pose <small>Add feedback</small></button><button data-proposal="timing" class="tool">Inspect interval <small>Add feedback</small></button><button data-proposal="audio" class="tool">Audio links <small>Add feedback</small></button><button data-proposal="angle" class="tool">Angle inspection <small id="axis-state">No inspection axes supplied</small></button></section><section id="angle-panel" class="angle-panel" hidden><label for="axis-select">Rig inspection axis</label><select id="axis-select"></select><label id="axis-label" for="axis-value"></label><input id="axis-value" type="range" disabled><output id="axis-value-label"></output><p class="hint">Maps one inspection axis to its animation time. This does not rotate the camera.</p></section><section id="sound-panel" class="sound-panel"><div class="row"><span class="eyebrow">SOUND CUES</span><label><input id="sync-audio" type="checkbox">Play cue audio in sync</label></div><div id="cues"></div><div class="row"><label for="audio-volume">Audio volume</label><input id="audio-volume" type="range" min="0" max="1" step="0.05" value="0.8"><output id="audio-volume-label">80%</output></div><div id="event-log" class="hint" aria-live="polite">Runtime events: none</div></section><section id="environment" class="environment-panel" aria-labelledby="environment-heading"><div class="row"><span id="environment-heading" class="eyebrow">ENVIRONMENT · INSPECTION ONLY</span></div><p class="hint">Wind, gravity and impulses go to the player while you inspect. Nothing is written to the model or motion; feedback records the state as context.</p><input id="environment-applied" type="checkbox" hidden aria-hidden="true" tabindex="-1"><div class="row"><label for="wind-base">Wind</label><input id="wind-base" type="range" min="-100" max="100" step="1" value="0" disabled><output id="wind-base-label">0</output></div><div class="row"><label><input id="wind-breeze" type="checkbox" disabled>Breeze</label><button id="wind-gust" type="button" disabled>Gust</button><label><input id="wind-repeat" type="checkbox" disabled>Repeat</label></div><div class="row"><label for="wind-gust-peak">Gust peak</label><input id="wind-gust-peak" type="number" min="0" max="200" step="1" value="48" inputmode="decimal" disabled><label for="wind-gust-duration">Duration (s)</label><input id="wind-gust-duration" type="number" min="0.1" max="20" step="0.1" value="2.4" inputmode="decimal" disabled></div><output id="wind-readout" class="environment-readout">No compiled model drawn · environment unavailable</output><div class="row"><label for="gravity">Gravity</label><input id="gravity" type="range" min="0" max="4" step="0.05" value="1" disabled><output id="gravity-label">1.00</output></div><div class="row"><button id="jolt" type="button" disabled>Jolt</button><label title="Dragging the rig pane moves the rig's physics instead of the camera. Hold Space to pan."><input id="drag-trail" type="checkbox" disabled>Drag to trail</label><button id="physics-reset" type="button" disabled>Reset physics</button></div><p class="hint">Drag to trail: dragging the rig pane tells the physics the rig moved; the camera stays. Hold Space to pan. Reset returns the runtime start values and re-poses from the current time, clearing the physics history.</p><div class="row"><label title="Readings and Apply's check read this wind and gravity as a rigc-environment/1 schedule; the wind clock restarts with the clip so you watch the same world"><input id="environment-use" type="checkbox" disabled>Use for readings</label><button id="environment-export" type="button" disabled>Export schedule</button><button id="environment-check" type="button" disabled>Check under this world</button></div><output id="environment-schedule" class="environment-readout">No schedule · the controls are not applied (the player stands at the runtime start values)</output><div id="environment-check-lines" class="environment-check-lines" aria-live="polite"></div><p class="hint">The schedule is the controls as a <code>rigc-environment/1</code> file from the clip's first frame: one step when the wind is steady, else one step per frame at 12 fps (between frames rigc holds the frame's value). A one-off Gust is not in it; Repeat puts gusts in it. Check under this world runs <code>rig-parts check --environment</code> on the build on the stage; the default-world line is that build's own check.</p></section><section id="physics-panel" class="physics-panel" aria-labelledby="physics-panel-heading"><div class="row"><span id="physics-panel-heading" class="eyebrow">PHYSICS TUNING · APPLY REBUILDS</span></div><p id="physics-panel-state" class="hint">No compiled model drawn.</p><p class="hint">Each edit is a config patch op at <code>/constraints/&lt;i&gt;/&lt;field&gt;</code> naming its constraint. Apply runs the installed producer (<code>rig --config</code>, then <code>check</code>) into a scratch build and reloads it under the same wind, gust and jolt state; Reset returns to the inspected build and clears the pending ops. Live preview changes strength, damping, inertia, wind, gravity and mix on the next evaluation; mass and component toggles require Apply. Clear resumes authored values without resetting integrated history.</p><div class="row"><label><input data-physics-preview type="checkbox" checked>Preview pending physics</label><button id="physics-preview-clear" type="button">Clear preview</button></div><output id="physics-preview-label" class="environment-readout">Preview off · Apply decides.</output><div class="row physics-chain-row"><label for="physics-chain">Chain</label><select id="physics-chain" disabled></select><button id="physics-chain-pick" type="button" aria-pressed="false" disabled>Pick on stage</button></div><div class="row physics-ramp-row"><label for="physics-ramp-field">Ramp</label><select id="physics-ramp-field" disabled></select><label for="physics-ramp-root">root</label><input id="physics-ramp-root" type="number" step="any" inputmode="decimal" disabled><label for="physics-ramp-tip">tip</label><input id="physics-ramp-tip" type="number" step="any" inputmode="decimal" disabled><button id="physics-ramp" type="button" disabled>Ramp</button></div><div class="physics-work"><div id="physics-rows" class="physics-table-wrap" role="region" aria-label="Physics constraints the config declares" tabindex="0"></div><div id="physics-row-editor" class="physics-row-editor"></div></div><div id="physics-locked" class="physics-table-wrap physics-locked" role="region" aria-label="Constraints that are not editable" tabindex="0" hidden></div><div class="row"><span class="eyebrow">PENDING OPS</span></div><ol id="physics-pending-list" class="physics-pending-list"></ol><div class="row"><button id="physics-apply" type="button" class="primary" disabled>Apply</button><button id="physics-reset-build" type="button" disabled>Reset</button></div><output id="physics-apply-status" class="environment-readout"></output><div id="physics-readout" class="physics-readout" aria-label="Check readout before and after Apply"></div><textarea id="physics-pending" class="sr-only" hidden aria-hidden="true" tabindex="-1"></textarea></section><section id="idle-tracks" class="physics-panel idle-panel" aria-labelledby="idle-tracks-heading"><div class="row"><span id="idle-tracks-heading" class="eyebrow">IDLE TRACKS · APPLY REBUILDS</span><span id="idle-preview-label" class="idle-preview-label" aria-live="polite" hidden>preview — Apply decides</span></div><p id="idle-tracks-state" class="hint">No compiled model drawn.</p><p class="hint">Each edit is a config patch op at <code>/motion/tracks/&lt;i&gt;/…</code> or <code>/motion/blink/…</code>. Period is offered only among the divisors of <code>motion.duration</code> (the producer refuses any other, <code>CONFIG_PERIOD_DIVIDES_DURATION</code>). While edits are pending the preview drives the edited bones through the player's <code>adjust</code> with the producer's sine, <code>v(t) = base + amp·sin(2π(t/period − phase))</code>, on the bone the idle keys (<code>&lt;bone&gt;_ctl</code> under ctl keys); it is never recorded or submitted. Apply runs the installed producer (<code>rig --config</code>, then <code>check</code>) into a scratch build and replaces the preview with the built idle; Reset returns to the inspected build and clears the pending ops.</p><p id="idle-keys-line" class="hint"></p><div class="physics-work"><div id="idle-track-list" class="physics-table-wrap" role="region" aria-label="Idle tracks the config states" tabindex="0"></div><div id="idle-track-editor" class="physics-row-editor"></div></div><datalist id="idle-bones"></datalist><div class="row idle-add-row"><span class="eyebrow">ADD</span><input data-idle-add-bone type="text" list="idle-bones" autocomplete="off" placeholder="bone" aria-label="Bone for a new track" disabled><select data-idle-add-prop aria-label="Property for a new track" disabled></select><button data-idle-add-single type="button" disabled>Add track</button><select data-idle-add-chain aria-label="Chain for a new chain track" disabled></select><button data-idle-add-chain-button type="button" disabled>Add chain track</button></div><div id="idle-blink" class="idle-blink"></div><div class="row"><label class="idle-preview-toggle"><input data-idle-preview type="checkbox" checked>Preview edits (not recorded)</label></div><div id="idle-precheck" class="idle-precheck" aria-label="Review pre-check"></div><div class="row"><span class="eyebrow">PENDING OPS</span></div><ol id="idle-pending-list" class="physics-pending-list"></ol><div class="row"><button id="idle-apply" type="button" class="primary" disabled>Apply</button><button id="idle-reset" type="button" disabled>Reset</button></div><output id="idle-apply-status" class="environment-readout"></output><div id="idle-readout" class="physics-readout" aria-label="Check readout before and after Apply"></div><textarea id="idle-pending" class="sr-only" hidden aria-hidden="true" tabindex="-1"></textarea></section><section id="readings" class="readings-panel" aria-labelledby="readings-heading"><div class="row"><span id="readings-heading" class="eyebrow">READINGS · RIGC RENDER</span></div><p class="hint">Each reading is one <code>rigc render</code> of the drawn animation, on the inspected build and on the latest scratch build. Its frames become ticks under the timeline (<span class="tick-swatch inspected" aria-hidden="true"></span> inspected, <span class="tick-swatch scratch" aria-hidden="true"></span> scratch; click one to seek) and each pair gets its worst-frame line. A reading, not a gate: an unknown slot or bone is refused with rigc's own line.</p><datalist id="readings-slots"></datalist><datalist id="readings-bones"></datalist><div class="row"><label><input id="readings-folds" type="checkbox">Folds</label><label><input id="readings-overlaps" type="checkbox">Overlaps</label></div><div class="row readings-pair"><label><input id="readings-pass-on" type="checkbox">Pass-through</label><input id="readings-pass-cloth" type="text" list="readings-slots" autocomplete="off" placeholder="cloth slot" aria-label="Pass-through cloth slot"><span aria-hidden="true">:</span><input id="readings-pass-leg" type="text" list="readings-slots" autocomplete="off" placeholder="leg slot" aria-label="Pass-through leg slot"></div><div class="row readings-pair"><label><input id="readings-seam-on" type="checkbox">Seam</label><input id="readings-seam-a" type="text" list="readings-slots" autocomplete="off" placeholder="slot" aria-label="Seam first slot"><span aria-hidden="true">:</span><input id="readings-seam-b" type="text" list="readings-slots" autocomplete="off" placeholder="slot" aria-label="Seam second slot"></div><div class="row readings-pair"><label><input id="readings-angle-on" type="checkbox">Angle range</label><input id="readings-angle-bone" type="text" list="readings-bones" autocomplete="off" placeholder="bone" aria-label="Angle range bone"><span aria-hidden="true">vs</span><input id="readings-angle-relative" type="text" list="readings-bones" autocomplete="off" placeholder="relative bone" aria-label="Angle range relative-to bone"></div><div class="row"><button id="readings-run" type="button" disabled>Run readings</button></div><p id="readings-status" class="hint" aria-live="polite">No compiled model drawn.</p><ul id="readings-worst" class="readings-worst" aria-label="Worst frame per reading"></ul></section><section id="requirements" class="readings-panel requirements-panel" aria-labelledby="requirements-heading"><div class="row"><span id="requirements-heading" class="eyebrow">REQUIREMENTS · RIG-PARTS CHECK</span></div><p class="hint">Declare what the scene asks of the motion: a slot (picked on the stage), its mesh or a bone, an animation and a kind, with your own bar. Check writes a <code>spine-parts-requirements/1</code> file into scratch and runs the installed producer's <code>rig-parts check --requirements</code> on the inspected rig or the latest scratch build; each requirement gets one line with its worst frame (click to seek). A refusal is the producer's, by name. Waivers (<code>may_fold</code>), scene targets and the part exception lists are not written here. Submitted feedback carries the file by hash and content.</p><div id="requirements-body"></div></section><section id="retarget" class="readings-panel requirements-panel retarget-panel" aria-labelledby="retarget-heading"><div class="row"><span id="retarget-heading" class="eyebrow">RETARGET · RIG-POSE</span></div><p class="hint">Put a pose sequence the package declares onto this rig: edit the <code>rig-pose-map/1</code> (per chain, the bone that carries each segment), set the clean and retarget flags, and Apply. The installed <code>@rig-c/pose</code> runs <code>rig-pose clean</code> and <code>rig-pose retarget</code> onto the handoff rig, and its <code>rigc</code> compiles the result, all into scratch; the new animation plays here beside the reference (pick the sequence under Reference). Review writes no keys and never writes the package's <code>motion.json</code>. Refusals (<code>POSE_NOT_CLEANED</code>, <code>POSE_BEND_ONE_WAY</code>, <code>RAISED_HEAD</code>, <code>MOTION_NAME_FREE</code>, <code>MAP_*</code>) are the producer's, verbatim. FEET, STANCE, FLIP, SHORT, END-ON, LEG-SCALE and REACH lines seek to their frame.</p><div id="retarget-body"></div></section><section id="report-panel" class="report-panel" aria-labelledby="report-heading"><div class="report-header"><h2 id="report-heading">Report events</h2><span id="report-summary" class="subtle"></span><a id="report-source-link" target="_blank" rel="noopener" hidden>Source report</a></div><p id="report-unavailable" class="hint" hidden>No rig-pose-report/1 events supplied.</p><div id="report-browser"><div class="report-controls"><label for="report-category" class="sr-only">Report event category</label><select id="report-category"><option value="all">All events</option></select><button id="report-previous" type="button" aria-label="Previous event page">Previous</button><output id="report-page"></output><button id="report-next" type="button" aria-label="Next event page">Next</button></div><div id="report-empty-categories" class="report-empty-categories"></div><p id="report-missing" class="hint" hidden></p><ol id="report-events" class="report-event-list" aria-label="Read-only report events"></ol><section id="report-selection" class="report-selection" hidden aria-labelledby="report-selection-heading"><h3 id="report-selection-heading"></h3><p id="report-timing" class="hint"></p><div class="report-controls"><label id="report-time-label" for="report-time" hidden>Select explicit time</label><select id="report-time" hidden></select><button id="report-seek" type="button" disabled>Go to explicit time</button></div><div id="report-target-control" hidden><label for="report-target" class="label space-top">Select a target named in the report</label><select id="report-target"></select></div><p id="report-prefill-note" class="hint"></p><p id="report-provenance" class="hint"></p><details><summary>Source event details</summary><pre id="report-details"></pre></details></section></div></section></section></div></section></main></div><footer class="status-bar"><span id="message" class="status-message" role="status" aria-live="polite">Loading local review package…</span><div class="status-readouts" aria-label="Status"><span id="player-state" class="subtle">No rig supplied</span><span id="layout-state" class="subtle" role="status" aria-label="Effective layout">Loading inputs…</span><span id="renderer-state" class="subtle" aria-label="Last draw backend" title="DOM images for rigid parts; WebGL meshes when available, Canvas2D fallback.">Not rendered</span><span id="layer-state" class="hint">No rig supplied</span><span id="idle-state" class="hint" role="status">Idle preview unavailable</span><span id="control-state" class="hint" role="status"></span><div class="draft-notice"><span id="draft-state" role="status" aria-live="polite"></span><div id="draft-recovery"></div></div></div><div class="status-source"><details class="view-region"><summary>View region</summary><div class="view-region-body"><span class="hint">Numeric source region · opening this link restores a paused inspection</span><pre id="view-coordinates" class="numeric-view" aria-label="Current numeric inspection region"></pre><p class="hint">Sync cameras links fit-relative zoom and viewport-relative pan across visible panes. Turn it off to inspect each pane independently. Toolbar zoom and Fit use the last active pane. Hold Space while dragging a video to pan without taking over its native controls.</p></div></details><span id="source-hash" class="subtle"></span></div></footer><dialog id="review-dialog" class="review-dialog" aria-labelledby="review-dialog-title"><div class="review-dialog-header"><h2 id="review-dialog-title">Write review</h2><span class="hint">Drafts save locally as you type · nothing is submitted until you press Submit</span><button id="review-dialog-close" type="button">Close</button></div><section id="inspector-feedback" class="review-dialog-body" aria-label="Feedback"><div class="review-summary"><div class="eyebrow">REVIEW</div><label for="review-status" class="label">Review status</label><select id="review-status"><option value="pending">Pending</option><option value="approved">Approved</option><option value="changes-requested">Changes requested</option></select><section class="provenance" aria-label="Reviewed source"><div class="eyebrow">REVIEWED SOURCE</div><div class="provenance-row"><span>Package revision</span><strong id="provenance-version">—</strong></div><div class="provenance-row"><span>rig SHA-256</span><code id="provenance-rig">—</code></div><div class="provenance-row"><span>motion SHA-256</span><code id="provenance-motion">—</code></div><details><summary>Full provenance</summary><pre id="provenance-full"></pre></details><div class="source-files"><div class="eyebrow">SOURCE FILES · READ ONLY</div><div id="source-files"></div></div><p class="hint">Submits server-verified file hashes and tool versions. Your selected review status is preserved.</p></section><div class="divider"></div></div><div id="checkpoint-detail"><div class="eyebrow">SELECTED CHECKPOINT</div><h1 id="detail-title">Checkpoint</h1><p id="detail-note" class="hint"></p><fieldset class="outcome"><legend>Checkpoint outcome</legend><label><input type="radio" name="outcome" value="pending" checked>Pending</label><label><input type="radio" name="outcome" value="pass">Pass</label><label><input type="radio" name="outcome" value="fail">Needs changes</label></fieldset><label for="checkpoint-note" class="label space-top">Review notes</label><textarea id="checkpoint-note" rows="5" placeholder="Describe the part, conditions and what looks wrong."></textarea></div><div class="divider"></div><section id="proposal-panel"><div class="eyebrow">STRUCTURED FEEDBACK</div><div class="label">Frame range · inclusive</div><div class="frame-range"><label>Start<input id="feedback-start" type="number" min="0" step="1" inputmode="numeric" placeholder="e.g. 18"></label><label>End<input id="feedback-end" type="number" min="0" step="1" inputmode="numeric" placeholder="e.g. 32"></label></div><p id="feedback-range-hint" class="hint">Checking frame bounds…</p><label for="feedback-target-kind" class="label space-top">Target type</label><select id="feedback-target-kind"><option value="">Select target</option><option value="rig">Entire rig</option><option value="bone">Bone</option><option value="slot">Slot</option></select><label for="feedback-target-name" class="label space-top">Target name</label><input id="feedback-target-name" type="text" list="feedback-target-names" autocomplete="off" placeholder="Actual bone or slot name"><datalist id="feedback-target-names"></datalist><p id="feedback-target-hint" class="hint"></p><label for="feedback-category" class="label space-top">Category</label><select id="feedback-category"><option value="">Select category</option><option value="defect">Defect</option><option value="quality">Quality</option></select><label for="feedback-summary" class="label space-top">One-line summary</label><input id="feedback-summary" type="text" maxlength="1000" placeholder="e.g. The left wrist moves outside the sleeve"><button id="add-proposal" class="outline block">Add feedback</button><p id="proposal-boundary" class="hint">Records the current checkpoint, time, reference, camera and inspection axis. Motion is read only: inspect, mark ranges and export feedback. No keys or source files are edited.</p><p id="feedback-warnings" class="hint" role="status" hidden></p><p id="feedback-applied-check" class="hint error" role="alert" hidden></p><div id="proposal-list"></div></section><details id="strip-panel" class="report-panel"><summary>Export frame strip</summary><p class="hint">Uses the feedback form start and end frames, inclusive. Inspection time and source files remain unchanged.</p><label for="strip-scope" class="label">Export range</label><select id="strip-scope"><option value="interval">Entered start–end frames</option><option value="feedback">All feedback ranges</option></select><label for="strip-reference" class="label space-top">Reference video</label><select id="strip-reference"><option value="">Select reference video</option></select><label for="strip-format" class="label space-top">Format</label><select id="strip-format"><option value="png">PNG strip</option><option value="json">JSON frames and provenance</option></select><p class="hint">PNG requires frameTimes in the selected report. Without it, JSON preserves requested frames with time: null. Batch export requires every feedback item to match the selected animation. Unsubmitted feedback is included; exporting does not submit the review.</p><button id="strip-export" type="button" disabled>Download strip</button><p id="strip-status" class="hint" role="status" aria-live="polite">Loading package…</p></details></section><div class="review-dialog-actions"><span id="review-message" class="review-message" role="status" aria-live="polite"></span><span id="review-draft-state" class="review-draft-state" role="status" aria-live="polite"></span><button id="save" class="primary" disabled>Submit review</button></div></dialog></div>
10
10
  <script type="module" src="main.js"></script>
11
11
  </body></html>
package/web/main.ts CHANGED
@@ -53,6 +53,7 @@ import {appliedCheckRefusal,type CheckSummary} from '../src/applied-context.ts';
53
53
  import {appliedBlockOps,recordBlock,staleReadouts} from '../src/applied-blocks.ts';
54
54
  import {bindPhysicsPanel,type PhysicsPanel} from './physics-panel.ts';
55
55
  import {bindIdlePanel,type BoneSetup,type IdlePanel} from './idle-panel.ts';
56
+ import {composeAdjust,PhysicsPreview} from './preview.ts';
56
57
  import {bindRigFieldsPanel,type RigFieldsPanel} from './rig-fields-panel.ts';
57
58
  import {assertRigFieldOps,RIG_FIELDS_READOUT_BARS,TURN_BONE} from '../src/rig-fields.ts';
58
59
  import {IDLE_READOUT_BARS,type LocalChannel} from '../src/idle-tracks.ts';
@@ -150,9 +151,10 @@ let joltQueue:JoltStep[]=[],environmentNote='';
150
151
  let idlePanel:IdlePanel|null=null;
151
152
  /** Setup · Rig fields (#110): the panel; its turn dial and the idle preview share `player.adjust` (both run, idle first). */
152
153
  let rigFieldsPanel:RigFieldsPanel|null=null,idleAdjust:BoneAdjust|null=null,dialAdjust:BoneAdjust|null=null;
153
- function setAdjust():void {if(!player)return;const a=idleAdjust,b=dialAdjust;player.adjust=a&&b?bones=>{a(bones);b(bones);}:a??b;if(!playing)drawRig(()=>player!.draw());}
154
+ function setAdjust():void {if(!player)return;player.adjust=composeAdjust(idleAdjust,dialAdjust);if(!playing)drawRig(()=>player!.draw());}
154
155
  /** #106: the Setup Mesh panel, and the overlay's last snapshot (bar 1 reads its per-slot triangle count). */
155
156
  let meshPanel:MeshPanel|null=null,lastOverlay:OverlaySnapshot|null=null,densityStrip:DensityStripPanel|null=null;
157
+ const physicsPreview=new PhysicsPreview();
156
158
  let physicsPanel:PhysicsPanel|null=null,inspectedDocument:unknown=null,inspectedDocumentReason:string|null=null,stageCheck:{lines:string[];check:CheckSummary|null}|null=null;
157
159
  /** Animate · Readings (#108): the block, the last finished job (its ticks and worst lines), and the run in flight. */
158
160
  let requirementsPanel:RequirementsPanel|null=null,retargetPanel:RetargetPanel|null=null;
@@ -406,7 +408,7 @@ function renderTransport(): void {
406
408
  renderFrameTransport();
407
409
  renderFeedbackRange();
408
410
  renderReadingTicks();
409
- idlePanel?.place(duration);
411
+ idlePanel?.place(duration);physicsPanel?.refreshPreview();
410
412
  }
411
413
 
412
414
  /** Re-lay the reading ticks only when what places them changes (renderTransport runs every played frame). */
@@ -490,7 +492,7 @@ function chooseAnimation(name: string, move: ClipMove = { kind: 'choose' }): voi
490
492
  function seek(value: number, move?: ClipMove): void {
491
493
  time = duration > 0 ? Math.min(Math.max(0, value), duration) : Math.max(0, value);
492
494
  clipMoved(move ?? { kind: 'seek', to: time });
493
- if (player && player.current) { try { player.seek(time); reposePreview(); drawRig(()=>player!.draw()); } catch (error) {text('renderer-state',rendererStatus(null,'failed').label); message(errorText(error), true); } }
495
+ if (player && player.current) { try { player.seek(time); drawRig(()=>player!.draw()); } catch (error) {text('renderer-state',rendererStatus(null,'failed').label); message(errorText(error), true); } }
494
496
  if (video && video.readyState >= 1) holdVideoReference(video, referenceGeneration);
495
497
  renderSequenceFrame(); renderTransport();
496
498
  }
@@ -898,7 +900,7 @@ function bind(): void {
898
900
  clothPanel=bindClothPanel({project:()=>player?rigProject():null,mapping:()=>chestMapping,inspector:()=>selectedInspector(),pause,changed:()=>redrawOverlay(false),saved:()=>persistence?.save(),
899
901
  apply:applyCloth,reset:resetClothBuild,token:name=>getComputedStyle(document.documentElement).getPropertyValue(name).trim()});
900
902
  viewDisposers.push(()=>clothPanel?.dispose());
901
- physicsPanel=bindPhysicsPanel({apply:async ops=>{await applyPhysics(ops);},reset:resetPhysicsBuild,pane:el('rig-pane'),saved:()=>persistence?.save(),
903
+ physicsPanel=bindPhysicsPanel({preview:edits=>{const before=JSON.stringify(physicsPreview.read()),next=physicsPreview.sync(player,edits);if(player?.current&&!playing&&before!==JSON.stringify(next)){player.repose();drawRig(()=>player!.draw());}return physicsPreview.read();},previewRead:()=>physicsPreview.read(),apply:async ops=>{await applyPhysics(ops);},reset:resetPhysicsBuild,pane:el('rig-pane'),saved:()=>persistence?.save(),
902
904
  bonePoints:bones=>{if(!player)return null;const project=rigProject(),pane=el('rig-pane').getBoundingClientRect(),view=el('rig-root').parentElement!.getBoundingClientRect(),out=new Map<string,{x:number;y:number}>();for(const name of bones){try{const b=player.bone(name),p=project(b.x,b.y);out.set(name,{x:p.x+view.left-pane.left,y:p.y+view.top-pane.top});}catch{/* a bone this model lacks is not pickable */}}return out;}});
903
905
  viewDisposers.push(()=>physicsPanel?.dispose());
904
906
  meshPanel=bindMeshPanel({pane:el('rig-pane'),project:()=>player?rigProject():null,mapping:()=>chestMapping,bones:()=>data.feedbackCatalog.bones??[],
@@ -907,8 +909,8 @@ function bind(): void {
907
909
  viewDisposers.push(()=>meshPanel?.dispose());
908
910
  // #173: the density strip beside the Mesh panel's density field.
909
911
  densityStrip=bindDensityStrip({part:()=>meshPanel?.part()??null,entry:()=>meshPanel?.entry()??null,build:async values=>{await buildDensityStrip(values);},load:async i=>{await loadStripRow(i);},back:async()=>{await stripBack();},close:async()=>{await closeDensityStrip();}});
910
- idlePanel=bindIdlePanel({apply:async(ops,keys)=>{await applyIdle(ops,keys);},reset:resetIdleBuild,pane:el('rig-pane'),saved:()=>persistence?.save(),clock:()=>player?.time??0,
911
- preview:fn=>{idleAdjust=fn as BoneAdjust|null;setAdjust();},
912
+ idlePanel=bindIdlePanel({apply:async(ops,keys)=>{await applyIdle(ops,keys);},reset:resetIdleBuild,pane:el('rig-pane'),saved:()=>persistence?.save(),
913
+ preview:fn=>{idleAdjust=fn;setAdjust();},
912
914
  bonePoints:bones=>{if(!player)return null;const project=rigProject(),out=new Map<string,{x:number;y:number}>();for(const name of bones){try{const b=player.bone(name);out.set(name,project(b.x,b.y));}catch{/* a bone this model lacks has no handle */}}return out;}});
913
915
  viewDisposers.push(()=>idlePanel?.dispose());
914
916
  rigFieldsPanel=bindRigFieldsPanel({apply:async ops=>{await applyRigFields(ops);},reset:resetRigFieldsBuild,reading:async(bone,relative)=>{await runReadings({readings:[{kind:'angle-range',bone,relative}]});},
@@ -1029,13 +1031,13 @@ async function inspectControlTime(target:number,signal:AbortSignal):Promise<void
1029
1031
  // #191: a control seek is a clip move like the transport's: a seek to 0 restarts the wind clock while the exported world is in use.
1030
1032
  clipMoved({kind:'seek',to:target});
1031
1033
  const media=video;
1032
- if(!media){if(player){player.seek(target);reposePreview();drawRig(()=>player!.draw());}time=target;renderSequenceFrame();renderTransport();return;}
1034
+ if(!media){if(player){player.seek(target);drawRig(()=>player!.draw());}time=target;renderSequenceFrame();renderTransport();return;}
1033
1035
  if(media.readyState<1||target>media.duration)throw new Error('CONTROL_REFERENCE_UNAVAILABLE');
1034
1036
  const previousPosition=media.currentTime;
1035
1037
  const commit=(actual:number)=>{
1036
1038
  if(signal.aborted||media!==video)throw new Error('CONTROL_SUPERSEDED');
1037
1039
  if(!Number.isFinite(actual)||actual<0||actual>duration)throw new Error('CONTROL_TIME_OUTSIDE_DOMAIN');
1038
- if(player){player.seek(actual);reposePreview();drawRig(()=>player!.draw());}time=actual;
1040
+ if(player){player.seek(actual);drawRig(()=>player!.draw());}time=actual;
1039
1041
  controlledVideoSample={media,position:media.currentTime,time:actual};if(axisInspection&&duration>0){const axis=data.manifest.axes.find(item=>item.id===select('axis-select').value&&item.animation===animation);if(axis){const value=axis.min+actual/duration*(axis.max-axis.min);input('axis-value').value=String(value);text('axis-value-label',value.toFixed(3));}}renderSequenceFrame();renderTransport();
1040
1042
  };
1041
1043
  try{const actual=await decodedControlTime(media,target,signal);if(signal.aborted||media!==video)throw new Error('CONTROL_SUPERSEDED');commit(actual);if(Math.abs(actual-target)>1e-6)message(`Reference decoded ${actual.toFixed(4)} s · requested ${target.toFixed(4)} s. The rig and saved view use the decoded timestamp.`);}
@@ -1361,8 +1363,6 @@ function boneSetup(document:unknown):BoneSetup {
1361
1363
  for(const b of Array.isArray(list)?list:[]){if(!b||typeof (b as {name?:unknown}).name!=='string')continue;const o=b as Record<string,unknown>,v:Partial<Record<LocalChannel,number>>={};for(const c of ['rotation','x','y','scaleX','scaleY','shearX','shearY'] as const)if(typeof o[c]==='number')v[c]=o[c] as number;map.set(o.name as string,v);}
1362
1364
  return {names:[...map.keys()],get:bone=>map.get(bone)};
1363
1365
  }
1364
- /** After a seek the held pose is re-posed through the preview's adjust at the restored track time (a seek's replay steps a track the player's time does not read). */
1365
- function reposePreview():void {if(player?.adjust)player.repose();}
1366
1366
  /** Setup's mesh edit: the selected slot's actual mesh as a typed delta. A two-file client download; nothing reaches the session or the package. */
1367
1367
  /** Edit Mesh's source (#100): the selected slot's mesh in the model document at the setup pose (a never-played player's bones). */
1368
1368
  function meshSource(){
@@ -1590,7 +1590,7 @@ async function applyPatch(patch:ConfigPatch,world:StageWorld|null=null):Promise<
1590
1590
  /** #166: undo the last Apply, and the applied records as the server holds them. */
1591
1591
  undo:()=>undoApply(),history:()=>appliedHistory?JSON.parse(JSON.stringify(appliedHistory)):null,
1592
1592
  status:async()=>rebuildStatus??await loadRebuildStatus(),
1593
- physics:{rows:()=>physicsPanel?.rows()??[],chains:()=>physicsPanel?.chains()??[],edits:()=>physicsPanel?.edits()??[],set:(name:string,field:Parameters<PhysicsPanel['set']>[1],value:number)=>physicsPanel?.set(name,field,value),ramp:(id:string,field:Parameters<PhysicsPanel['set']>[1],root:number,tip:number)=>physicsPanel?.ramp(id,field,root,tip),select:(name:string)=>physicsPanel?.select(name),apply:()=>applyPhysics(physicsPanel?.edits().map(e=>({op:e.op,path:e.path,value:e.value,name:e.name}))??[]),reset:()=>resetPhysicsBuild()},
1593
+ physics:{rows:()=>physicsPanel?.rows()??[],chains:()=>physicsPanel?.chains()??[],edits:()=>physicsPanel?.edits()??[],preview:()=>physicsPreview.read(),clearPreview:()=>physicsPanel?.clearPreview(),set:(name:string,field:Parameters<PhysicsPanel['set']>[1],value:number)=>physicsPanel?.set(name,field,value),ramp:(id:string,field:Parameters<PhysicsPanel['set']>[1],root:number,tip:number)=>physicsPanel?.ramp(id,field,root,tip),select:(name:string)=>physicsPanel?.select(name),apply:()=>applyPhysics(physicsPanel?.edits().map(e=>({op:e.op,path:e.path,value:e.value,name:e.name}))??[]),reset:()=>resetPhysicsBuild()},
1594
1594
  /** Inspection only: one slot's posed world geometry on the drawn frame (what the stage drew), for comparing against `rigc render --geometry`. */
1595
1595
  posed:(slot:string)=>{const frame=player?.posedFrame();const found=frame?.order.find(item=>item.slot===slot);return found?JSON.parse(JSON.stringify(found,(key,value)=>key==='bitmap'||key==='image'||key==='page'?undefined:value)):null;},
1596
1596
  bone:(name:string)=>player?JSON.parse(JSON.stringify(player.bone(name))):null,
@@ -1612,7 +1612,7 @@ async function applyPatch(patch:ConfigPatch,world:StageWorld|null=null):Promise<
1612
1612
  replay:(t:number,dt:number,steps:readonly {t:number;wind?:[number,number];gravity?:[number,number]}[]=[])=>{
1613
1613
  if(!player?.current||!(dt>0)||!(t>=0))return null;
1614
1614
  const at=(k:number)=>{for(const step of steps)if(Math.max(0,Math.ceil(step.t/dt-1e-9))===k)player!.environment({...(step.wind?{wind:step.wind}:{}),...(step.gravity?{gravity:step.gravity}:{})});};
1615
- pause();if(steps.length)player.environment(null);at(0);player.seek(0);reposePreview();player.resume();
1615
+ pause();if(steps.length)player.environment(null);at(0);player.seek(0);player.resume();
1616
1616
  try{for(let k=1,n=Math.round(t/dt);k<=n;k++){at(k);player.frame(dt);}}finally{player.pause();}
1617
1617
  drawRig(()=>player!.draw());time=player.time;renderTransport();
1618
1618
  const posedTime=player.time;if(steps.length){if(environmentApplied())player.environment(environmentFor(wind.read(),gravityValue()));else player.environment(null);}
@@ -1668,6 +1668,7 @@ async function applyPhysics(ops:PatchOp[]):Promise<PatchJobView|RebuildRefusal|n
1668
1668
  const status=rebuildStatus??await loadRebuildStatus();
1669
1669
  const patch=status&&ops.length?makePatch(status,ops,{cmd:'rig'}):null;
1670
1670
  if(!patch){const reason=!ops.length?'PHYSICS_NOTHING_PENDING: no pending ops':status?.reason??'REBUILD_STATUS_UNAVAILABLE';physicsPanel?.status(reason,true);return null;}
1671
+ physicsPanel?.clearPreview();
1671
1672
  const world=stageWorld(),before=stageCheck;
1672
1673
  physicsPanel?.busy(true);physicsPanel?.status(`Applying ${ops.length} op(s) with ${patch.producer.name} ${patch.producer.version}: ${patchRoute(patch.ops,patch.producer.version)} --config, compile, check…`,false);
1673
1674
  try{
@@ -1902,7 +1903,7 @@ function sampleLocals(bone:string,dt:number,n:number):{t:number;rotation:number;
1902
1903
  if(!player?.current||!(dt>0)||!Number.isInteger(n)||n<0)return null;
1903
1904
  const p=player,inner=p.adjust,rows=new Map<number,{t:number;rotation:number;x:number;y:number;scaleX:number;scaleY:number;shearX:number;shearY:number}>();
1904
1905
  let recording=false;
1905
- const read:BoneAdjust=bones=>{inner?.(bones);if(!recording)return;const l=bones.bone(bone);rows.set(p.time,{t:p.time,rotation:l.rotation,x:l.x,y:l.y,scaleX:l.scaleX,scaleY:l.scaleY,shearX:l.shearX,shearY:l.shearY});};
1906
+ const read:BoneAdjust=(bones,ctx)=>{inner?.(bones,ctx);if(!recording)return;const l=bones.bone(bone);rows.set(ctx.trackTime??0,{t:ctx.trackTime??0,rotation:l.rotation,x:l.x,y:l.y,scaleX:l.scaleX,scaleY:l.scaleY,shearX:l.shearX,shearY:l.shearY});};
1906
1907
  pause();p.adjust=read;
1907
1908
  try{p.seek(0);recording=true;p.repose();p.resume();for(let k=1;k<=n;k++)p.frame(dt);}
1908
1909
  finally{recording=false;p.pause();p.adjust=inner;drawRig(()=>p.draw());time=p.time;renderTransport();}
@@ -1923,6 +1924,7 @@ let stageSource:{url:string;prefix:string;hash:string}|null=null;
1923
1924
  async function swapRig(modelUrl:string,prefix:string,sha256:string,world:StageWorld|null=null):Promise<void> {
1924
1925
  const keepAnimation=animation,keepTime=player?.time??time,wasPlaying=playing,keepSet=select('part-set').disabled?'':select('part-set').value;
1925
1926
  const next=await loadModel({modelUrl,setsUrl:null,resolvePage:(name,url)=>{const target=new URL(name,url);if(target.origin!==location.origin||!target.pathname.startsWith(prefix))throw new Error(`Model page escapes its build: ${name}`);return target.href;}});
1927
+ physicsPanel?.clearPreview();
1926
1928
  pause();player?.dispose();assets?.dispose();assets=next;player=new RigPlayer(el('rig-root'),assets);stageModelHash=sha256;stageSource={url:modelUrl,prefix,hash:sha256};
1927
1929
  // A scratch build may hold another set of animations (a retarget adds one, #114): the picker lists what this build has.
1928
1930
  {const animations=select('animation-select');animations.replaceChildren(...assets.animations.map(item=>option(item.name,data.manifest.animations.find(a=>a.id===item.name)?.label??item.name)));animations.disabled=assets.animations.length===0;}
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import {applyRamp,componentValue,draftEdits,draftText,nearestChain,patchOps,physicsChains,rowControls,setEdit,TUNING_FIELDS,COMPONENT_FIELDS,type PendingEdit,type PhysicsChain,type PhysicsField,type TuningRow,type TuningTable} from '../src/physics-tuning.ts';
8
8
  import type {PatchOp} from '../src/config-patch.ts';
9
+ import type {PhysicsPreviewRead} from './preview.ts';
9
10
 
10
11
  export interface PhysicsPanelDeps {
11
12
  apply:(ops:PatchOp[])=>Promise<void>;
@@ -15,6 +16,9 @@ export interface PhysicsPanelDeps {
15
16
  pane:HTMLElement;
16
17
  /** The draft record changed (the panel already wrote `#physics-pending`). */
17
18
  saved:()=>void;
19
+ /** Pending six-field overrides only; the caller uses Play's public live API. */
20
+ preview:(edits:readonly PendingEdit[])=>PhysicsPreviewRead[];
21
+ previewRead:()=>PhysicsPreviewRead[];
18
22
  }
19
23
  export interface PhysicsPanel {
20
24
  load(table:TuningTable,bones:readonly {name:string;parent?:string|null}[]):void;
@@ -23,6 +27,7 @@ export interface PhysicsPanel {
23
27
  ramp(chainId:string,field:PhysicsField,root:number,tip:number):void;
24
28
  select(name:string):void;
25
29
  restore():void;clear():void;
30
+ clearPreview():void;refreshPreview():void;
26
31
  applied(ops:readonly PatchOp[]|null):void;
27
32
  status(text:string,error:boolean):void;
28
33
  readout(lines:{name:string;before:string;after:string}[]):void;
@@ -38,15 +43,31 @@ export function bindPhysicsPanel(deps:PhysicsPanelDeps):PhysicsPanel {
38
43
  const state=node('physics-panel-state'),rowsArea=node('physics-rows'),editor=node('physics-row-editor'),locked=node('physics-locked'),pending=node('physics-pending-list');
39
44
  const chainSelect=node<HTMLSelectElement>('physics-chain'),pick=node<HTMLButtonElement>('physics-chain-pick'),rampField=node<HTMLSelectElement>('physics-ramp-field'),rampRoot=node<HTMLInputElement>('physics-ramp-root'),rampTip=node<HTMLInputElement>('physics-ramp-tip'),rampButton=node<HTMLButtonElement>('physics-ramp');
40
45
  const applyButton=node<HTMLButtonElement>('physics-apply'),resetButton=node<HTMLButtonElement>('physics-reset-build'),status=node('physics-apply-status'),readoutArea=node('physics-readout'),draft=node<HTMLTextAreaElement>('physics-pending');
46
+ const previewBox=document.querySelector<HTMLInputElement>('#physics-panel [data-physics-preview]')!,previewClear=node<HTMLButtonElement>('physics-preview-clear'),previewLabel=node('physics-preview-label');
41
47
  let table:TuningRow[]=[],chains:PhysicsChain[]=[],edits=new Map<string,PendingEdit>(),selected:string|null=null,appliedOps:string[]=[],busy=false,picking=false;
42
48
  rampField.replaceChildren(...[...TUNING_FIELDS,...COMPONENT_FIELDS].map(f=>make('option',{value:f,textContent:f})));
43
49
  rampField.value='damping';
44
50
 
45
51
  const save=()=>{draft.value=draftText(edits);draft.dispatchEvent(new Event('input',{bubbles:true}));deps.saved();};
46
- const change=(next:Map<string,PendingEdit>)=>{edits=next;save();render();};
52
+ let previewActive=false;
53
+ const change=(next:Map<string,PendingEdit>,live=false)=>{edits=next;save();drivePreview();if(live){renderRows();renderPending();}else render();};
47
54
  const tryChange=(make:()=>Map<string,PendingEdit>)=>{try{change(make());}catch(error){panelStatus(error instanceof Error?error.message:String(error),true);}};
48
55
  function panelStatus(text:string,error:boolean){status.textContent=text;status.title=text;status.classList.toggle('error',error);}
49
56
  const row=(name:string)=>table.find(r=>r.name===name&&r.configIndex!==null)??table.find(r=>r.name===name);
57
+ function showPreview(reads:PhysicsPreviewRead[]){
58
+ const message=reads.length?`preview — Apply decides · ${reads.map(({name,read})=>`${name}: pending ${JSON.stringify(read.override)} · last evaluated ${JSON.stringify(read.effective)}`).join(' · ')}`:'Preview off · mass and x/y/rotate require Apply. Clear resumes authored values on the next evaluation; integrated history is kept.';
59
+ if(message!==previewLabel.textContent){previewLabel.textContent=message;previewLabel.title=message;}
60
+ previewLabel.classList.remove('error');
61
+ }
62
+ function drivePreview(){
63
+ try{
64
+ const reads=deps.preview(previewBox.checked&&!busy?[...edits.values()].filter(e=>!appliedOps.includes(JSON.stringify({op:e.op,path:e.path,value:e.value,name:e.name}))):[]);
65
+ previewActive=reads.length>0;showPreview(reads);
66
+ }catch(error){previewActive=false;previewLabel.textContent=error instanceof Error?error.message:String(error);previewLabel.title=previewLabel.textContent;previewLabel.classList.add('error');}
67
+ }
68
+ function clearPreview(){deps.preview([]);previewActive=false;showPreview([]);}
69
+ previewBox.onchange=()=>drivePreview();
70
+ previewClear.onclick=()=>{previewBox.checked=false;clearPreview();};
50
71
 
51
72
  function renderRows(){
52
73
  const editable=table.filter(r=>r.editable);
@@ -77,8 +98,8 @@ export function bindPhysicsPanel(deps:PhysicsPanelDeps):PhysicsPanel {
77
98
  const label=`${r.name} ${control.field}`;
78
99
  const range=make('input',{type:'range',min:String(control.min),max:String(control.max),step:String(control.step),value:String(control.value),disabled:busy});range.setAttribute('aria-label',label);range.dataset.field=control.field;
79
100
  const number=make('input',{type:'number',step:'any',value:String(control.value),disabled:busy,inputMode:'decimal'});number.setAttribute('aria-label',`${label} value`);number.dataset.field=control.field;
80
- const commit=(raw:string)=>{const value=Number(raw);if(raw.trim()===''||!Number.isFinite(value)){panelStatus(`PHYSICS_VALUE_INVALID: ${r.name}.${control.field} must be a finite number`,true);return;}tryChange(()=>setEdit(edits,r,control.field,value));};
81
- range.onchange=()=>commit(range.value);range.oninput=()=>{number.value=range.value;};number.onchange=()=>commit(number.value);
101
+ const commit=(raw:string,live=false)=>{const value=Number(raw);if(raw.trim()===''||!Number.isFinite(value)){panelStatus(`PHYSICS_VALUE_INVALID: ${r.name}.${control.field} must be a finite number`,true);return;}try{change(setEdit(edits,r,control.field,value),live);}catch(error){panelStatus(error instanceof Error?error.message:String(error),true);}};
102
+ range.onchange=()=>commit(range.value);range.oninput=()=>{number.value=range.value;commit(range.value,true);};number.onchange=()=>commit(number.value);
82
103
  list.append(make('label',{className:control.pending?'physics-control pending':'physics-control'},make('span',{textContent:control.field}),range,number,make('output',{textContent:control.pending?`was ${shown(control.base)}`:''})));
83
104
  }else{
84
105
  const box=make('input',{type:'checkbox',checked:control.on,disabled:busy});box.setAttribute('aria-label',`${r.name} ${control.field} component`);box.dataset.field=control.field;
@@ -105,7 +126,7 @@ export function bindPhysicsPanel(deps:PhysicsPanelDeps):PhysicsPanel {
105
126
  if(chains.some(c=>c.id===keep))chainSelect.value=keep;
106
127
  for(const control of [chainSelect,rampField,rampRoot,rampTip,rampButton,pick])control.disabled=busy||!chains.length;
107
128
  }
108
- function render(){renderRows();renderEditor();renderPending();renderChains();}
129
+ function render(){renderRows();renderEditor();renderPending();renderChains();previewBox.disabled=busy;previewClear.disabled=busy;}
109
130
 
110
131
  chainSelect.onchange=()=>{const chain=chains.find(c=>c.id===chainSelect.value);if(chain){selected=chain.links[0]!.name;seedRamp(chain);render();}};
111
132
  rampField.onchange=()=>{const chain=chains.find(c=>c.id===chainSelect.value);if(chain)seedRamp(chain);};
@@ -123,29 +144,31 @@ export function bindPhysicsPanel(deps:PhysicsPanelDeps):PhysicsPanel {
123
144
  chainSelect.value=chain.id;chainSelect.dispatchEvent(new Event('change'));panelStatus(`Picked ${chain.label}.`,false);
124
145
  };
125
146
  deps.pane.addEventListener('pointerdown',onPick,{capture:true});
126
- applyButton.onclick=()=>{if(!edits.size||busy)return;void deps.apply(patchOps(edits));};
147
+ applyButton.onclick=()=>{if(!edits.size||busy)return;clearPreview();void deps.apply(patchOps(edits));};
127
148
  resetButton.onclick=()=>{if(busy)return;void deps.reset();};
128
149
 
129
150
  const panel:PhysicsPanel={
130
151
  load(value,bones){
131
- if(!value.available){table=[];chains=[];state.textContent=value.reason;state.classList.add('error');render();return;}
152
+ if(!value.available){clearPreview();table=[];chains=[];edits=new Map();state.textContent=value.reason;state.classList.add('error');render();return;}
132
153
  table=value.rows;chains=physicsChains(table,bones);
133
154
  const editable=table.filter(r=>r.editable).length;
134
155
  state.classList.remove('error');state.textContent=`${editable} physics constraints editable from the handoff config · ${table.length-editable} not editable (reasons below) · ${chains.length} chains from bone parentage`;
135
- edits=draftEdits(draft.value,table);render();
156
+ edits=draftEdits(draft.value,table);if(previewActive)drivePreview();render();
136
157
  },
137
158
  rows:()=>table.map(r=>({...r,values:{...r.values}})),chains:()=>chains.map(c=>({...c,links:[...c.links]})),edits:()=>[...edits.values()].map(e=>({...e})),
138
159
  set(name,field,value){const r=row(name);if(!r)throw new Error(`PHYSICS_ROW_UNKNOWN: ${name}`);change(setEdit(edits,r,field,value));},
139
160
  ramp(chainId,field,root,tip){const chain=chains.find(c=>c.id===chainId);if(!chain)throw new Error(`PHYSICS_CHAIN_UNKNOWN: ${chainId}`);change(applyRamp(edits,chain,field,root,tip));},
140
161
  select(name){selected=name;chainSelect.value='';render();},
141
- restore(){edits=draftEdits(draft.value,table);render();},
162
+ restore(){clearPreview();edits=draftEdits(draft.value,table);render();},
142
163
  clear(){change(new Map());},
164
+ clearPreview,
165
+ refreshPreview(){if(previewActive)try{showPreview(deps.previewRead());}catch(error){previewLabel.textContent=error instanceof Error?error.message:String(error);previewLabel.classList.add('error');}},
143
166
  applied(ops){appliedOps=(ops??[]).map(o=>JSON.stringify({op:o.op,path:o.path,value:o.value,name:o.name}));renderPending();},
144
167
  status:panelStatus,
145
168
  readout(lines){readoutArea.replaceChildren(...lines.flatMap(l=>[make('div',{className:'physics-readout-line'},make('span',{className:'eyebrow',textContent:'BEFORE'}),make('code',{textContent:l.before,title:l.before})),make('div',{className:'physics-readout-line'},make('span',{className:'eyebrow',textContent:'AFTER'}),make('code',{textContent:l.after,title:l.after}))]));},
146
169
  busy(value){busy=value;render();},
147
- dispose(){deps.pane.removeEventListener('pointerdown',onPick,{capture:true});},
170
+ dispose(){clearPreview();deps.pane.removeEventListener('pointerdown',onPick,{capture:true});},
148
171
  };
149
- render();
172
+ showPreview([]);render();
150
173
  return panel;
151
174
  }
package/web/preview.ts ADDED
@@ -0,0 +1,52 @@
1
+ /** Review's preview adapters. All posing, parameter validation and history belong to published Play. */
2
+ import type {BoneAdjust,PhysicsParameterPatch,PhysicsParameterRead,RigPlayer} from '@rig-c/play';
3
+ import {previewLocal,SETUP_DEFAULT,type LocalChannel,type PreviewPlan} from '../src/idle-tracks.ts';
4
+ import type {PendingEdit} from '../src/physics-tuning.ts';
5
+
6
+ export interface BoneSetup {names:readonly string[];get(bone:string):Partial<Record<LocalChannel,number>>|undefined}
7
+
8
+ /** Preserve the core's evaluation context through the idle + turn composition. */
9
+ export function composeAdjust(a:BoneAdjust|null,b:BoneAdjust|null):BoneAdjust|null {
10
+ return a&&b?(bones,ctx)=>{a(bones,ctx);b(bones,ctx);}:a??b;
11
+ }
12
+
13
+ /** Uses the existing idle preview conversion; no player clock or seek compensation. */
14
+ export function idlePreviewAdjust(plan:PreviewPlan,setup:BoneSetup):BoneAdjust|null {
15
+ const writes=plan.writes.map(w=>({w,setup:setup.get(w.bone)?.[w.channel]??SETUP_DEFAULT[w.channel]}));
16
+ return writes.length?(bones,ctx)=>{
17
+ if(ctx.trackTime===null)return;
18
+ for(const {w,setup:s} of writes)bones.bone(w.bone)[w.channel]=previewLocal(w,s,ctx.trackTime);
19
+ }:null;
20
+ }
21
+
22
+ export const LIVE_PHYSICS_FIELDS=['strength','damping','inertia','wind','gravity','mix'] as const;
23
+ type PhysicsPlayer=Pick<RigPlayer,'setPhysicsParameters'|'readPhysicsParameters'|'clearPhysicsParameters'>;
24
+ export interface PhysicsPreviewRead {name:string;read:PhysicsParameterRead}
25
+
26
+ /** Owns only this panel's pending overrides. Set/read/clear never evaluate, seek or reset physics. */
27
+ export class PhysicsPreview {
28
+ private player:PhysicsPlayer|null=null;
29
+ private patches=new Map<string,PhysicsParameterPatch>();
30
+ sync(player:PhysicsPlayer|null,edits:readonly PendingEdit[]):PhysicsPreviewRead[] {
31
+ const next=new Map<string,PhysicsParameterPatch>();
32
+ for(const e of edits)if((LIVE_PHYSICS_FIELDS as readonly string[]).includes(e.field)){
33
+ next.set(e.name,{...next.get(e.name),[e.field]:e.value});
34
+ }
35
+ if(this.player!==player){this.clear();this.player=player;}
36
+ if(!player){if(next.size)throw new Error('PHYSICS_PREVIEW_UNAVAILABLE: no live player');return [];}
37
+ for(const [name,patch] of this.patches){
38
+ const wanted=next.get(name);
39
+ // Play merges patches: removing a field must clear the old patch before setting the remaining fields.
40
+ if(!wanted||Object.keys(patch).some(k=>!(k in wanted))){player.clearPhysicsParameters(name);this.patches.delete(name);}
41
+ }
42
+ for(const [name,patch] of next)if(JSON.stringify(this.patches.get(name))!==JSON.stringify(patch)){
43
+ player.setPhysicsParameters(name,patch);this.patches.set(name,patch);
44
+ }
45
+ return this.read();
46
+ }
47
+ read():PhysicsPreviewRead[] {return this.player?[...this.patches.keys()].map(name=>({name,read:this.player!.readPhysicsParameters(name)})):[];}
48
+ clear():void {
49
+ for(const name of this.patches.keys())this.player?.clearPhysicsParameters(name);
50
+ this.patches.clear();this.player=null;
51
+ }
52
+ }