@rig-c/review 0.10.4 → 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.12.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. The attachment is the one a never-played player resolves for the slot, and the positions are the stored bone-space coordinates through that player's setup-pose bones with the published player's skinning formula — the stage player is not touched, so the drawing can sit apart from an animated pose on the stage.
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 stored setup-pose vertices placed through the setup-pose bones of a never-played player — the same reader and placement the Weights block uses. A package without the model document (a Spine export alone, or the bundled demo) refuses `MESH_EDIT_MODEL_UNAVAILABLE` and the editor does not open. With **Whole rig** selected it refuses `MESH_EDIT_SELECT_SLOT`; a slot whose setup attachment is not a mesh refuses `MESH_EDIT_NO_MESH <slot>`, and so does a linked mesh (edit its source mesh). Entering pauses playback (the stage keeps the pose it held, under the veil; the mesh is drawn at the setup pose with the stage's own camera), turns the chest-region editor off and opens the mesh editor bar under the view bar. The rig pane dims except the slot's own triangles; the mesh is drawn at full opacity with every vertex as a handle, hull edges, interior triangulation edges and added constraint edges each in their own theme token.
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,11 +62,11 @@ 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 come from a never-played `RigPlayer` over the same assets (`web/setup-pose.ts`, shared with Edit Mesh), since the published `posedFrame()` is empty before `play`. A package without the document refuses `WEIGHTS_MODEL_UNAVAILABLE`. No edit, Apply or record; the Mesh edit preview wins when both are on.
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.
69
- - **Mesh edit.** In Setup only, the view bar's **Edit mesh** toggle (`#mesh-edit`, `aria-pressed`) opens `#mesh-editor-bar`: sub-modes Modify / Create / Delete / Edges (keys `V` / `C` / `X` / `E`, one pressed), Undo, Redo, Reset, Download JSON and **Apply · Rebuild** (#135: `rig-parts remesh --delta` through the shared runner, the revision loaded in place, the producer's refusal verbatim; while a delta revision is on the stage Reset returns to the inspected build). A package without the compiled model document refuses `MESH_EDIT_MODEL_UNAVAILABLE` and does not open; whole rig refuses `MESH_EDIT_SELECT_SLOT`; a non-mesh or linked-mesh slot `MESH_EDIT_NO_MESH <slot>`. It edits in setup space (#100): the attachment's entry in the model document, placed through the setup-pose bones of a never-played player (`src/setup-mesh.ts` + `web/setup-pose.ts`, shared with Weights) and drawn over the paused stage with the stage camera. It records a typed delta (`src/mesh-edit.ts`, `basis: "setup"`, the shape `rig-parts remesh --delta` reads) and previews it over the original triangulation on the overlay; Delete removes only constraint edges the delta added (the triangulation is the producer's); it is not a mesher and never retriangulates, writes the package or runs a producer. The chest editor and mesh edit are exclusive: turning one on turns the other off.
69
+ - **Mesh edit.** In Setup only, the view bar's **Edit mesh** toggle (`#mesh-edit`, `aria-pressed`) opens `#mesh-editor-bar`: sub-modes Modify / Create / Delete / Edges (keys `V` / `C` / `X` / `E`, one pressed), Undo, Redo, Reset, Download JSON and **Apply · Rebuild** (#135: `rig-parts remesh --delta` through the shared runner, the revision loaded in place, the producer's refusal verbatim; while a delta revision is on the stage Reset returns to the inspected build). A package without the compiled model document refuses `MESH_EDIT_MODEL_UNAVAILABLE` and does not open; whole rig refuses `MESH_EDIT_SELECT_SLOT`; a non-mesh or linked-mesh slot `MESH_EDIT_NO_MESH <slot>`. It edits in setup space (#100): the attachment's stored provenance in the model document (`src/setup-mesh.ts`) and geometry from Play's public `readSetupMesh` (`web/setup-pose.ts`, shared with Weights) and drawn over the paused stage with the stage camera. It records a typed delta (`src/mesh-edit.ts`, `basis: "setup"`, the shape `rig-parts remesh --delta` reads) and previews it over the original triangulation on the overlay; Delete removes only constraint edges the delta added (the triangulation is the producer's); it is not a mesher and never retriangulates, writes the package or runs a producer. The chest editor and mesh edit are exclusive: turning one on turns the other off.
70
70
  - **Inspector.** Two sections and no tab row: **Setup** is the mesh-review composition (selected part, part presets, the modeling-review proposals for mesh density, contour curvature and chest position) and holds no control that seeks or plays; **Animate** has the transport with the Repeat idle toggle, frame navigation, the motion-review proposals and angle inspection with its axis panel, sound cues and report events. Their element ids and saved section names (`rig`, `playback`) are unchanged. Each section opens with a sticky block row (`.block-row`, `web/inspector-blocks.ts`): Setup — Part · Modeling · Chest · Mesh · Weights · Bones · Fields; Animate — Transport (with frame navigation, motion tools and the angle panel) · Sound · Environment · Physics · Idle tracks · Readings · Report, in document order. A button scrolls its block to just under the row inside the section and marks it `aria-current`; the active block then follows scrolling (IntersectionObserver; at the end of the section the last visible block). The row is one line that scrolls sideways when narrow. The active block per section is the only thing the row adds to the inspection draft (`ui.inspectorBlocks`); the scroll offset stays with the Inspector scroll memory, and the control protocol and saved-view link are unchanged. A saved `view` or `feedback` section from older links and drafts restores without selecting a section or opening the dialog.
71
71
  - **Review dialog.** A native modal `<dialog id="review-dialog">` holds the former Feedback section unchanged (review status, reviewed source with the read-only source-file list, selected checkpoint, structured feedback, frame-strip export; every element id kept) and the **Submit review** button (`#save`). **Write review**, a part's or a proposal tool's Add feedback opens it with `showModal()` and pauses playback; Escape or Close dismisses it and focus returns to **Write review**. Its inputs stay in the document while it is closed, so the draft mechanism (autosave, recovery, conflict handling) is unchanged and closing loses nothing. The modal makes the rest of the page inert, so the dialog repeats the footer's message and draft state in its own status line; the footer keeps reporting both.
72
72
  - **Footer.** Message (truncated, full text as tooltip) on the left; player, effective layout, renderer, layers, idle, remote control and draft state in the middle; the numeric view region (behind **View region**) and the source hash on the right. The View region body is a `position:fixed` popover above the 40px footer, right-aligned, its width and height clamped to the viewport and its content scrolling inside, so the footer's clipping never hides it and opening it never scrolls the page.
@@ -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.12.0 supplies a frozen `slotIdentity()` snapshot coherent with `posedFrame().generation`. Review retains copied frame/identity together only after equal-generation and source-slot/order checks, uses the player's default public filter, and never seeks/reposes/changes physics merely to read identity. Exact owning skin/key, active set/skin and null reasons come from that snapshot, not a unique-declaration or texture match. A resolved identity can remain known without visible geometry or fit bounds. Clip declarations do not invent keys when the API reports no drawable attachment.
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
 
@@ -248,8 +252,8 @@ Edit Mesh's **Download JSON** (#80, setup space since #100) saves two files; nei
248
252
  "ops":[{"op":"move","v":4,"from":[5,5],"to":[6.5,4]},{"op":"edge","a":0,"b":2},{"op":"add","at":[5,0],"onEdge":[0,1]},{"op":"remove","v":3}]}
249
253
  ```
250
254
 
251
- - **Space.** Every position is a setup-pose crop pixel of the rig canvas, x right, y down: the attachment's stored vertices in the compiled model document (`skeleton.model.json`), placed through the setup-pose bones and mapped by the model stage (`world = (x − width/2, height − y)`, root at the world origin). `basis` is always `"setup"`; the producer refuses any other basis (`REMESH_DELTA_BASIS`). A package without the model document has no setup mesh: Edit Mesh refuses `MESH_EDIT_MODEL_UNAVAILABLE` and exports nothing.
252
- - **Source.** `vertexCount`, `triangleCount` and `hullLength` are the stored entry's (`uvs`/`vertices`, `triangles`, `hull`; the first `hull` vertices, in order, are the outline). `hash` is the SHA-256 of `JSON.stringify` of the attachment's entry (`skins[name].attachments.<slot>.<attachment>`) in the hash-verified model document. The producer holds both to the input part's own (`REMESH_DELTA_SOURCE`) and requires `skin` `"default"` with `slot` and `attachment` the part's name. A linked mesh is not editable (edit its source).
255
+ - **Space.** Every position is a setup-pose crop pixel of the rig canvas, x right, y down: the attachment's full-double world coordinates from published Play 1.13.0's `readSetupMesh`, including selected setup-slider deform, mapped by the model stage (`world = (x − width/2, height − y)`, root at the world origin). `basis` is always `"setup"`; the producer refuses any other basis (`REMESH_DELTA_BASIS`). A package without the model document has no setup mesh: Edit Mesh refuses `MESH_EDIT_MODEL_UNAVAILABLE` and exports nothing.
256
+ - **Source.** `vertexCount`, `triangleCount` and `hullLength` are the stored entry's (`uvs`/`vertices`, `triangles`, `hull`; the first `hull` vertices, in order, are the outline). `hash` is the SHA-256 of `JSON.stringify` of the attachment's entry (`skins[name].attachments.<slot>.<attachment>`) in the hash-verified model document. The producer holds both to the input part's own (`REMESH_DELTA_SOURCE`) and requires `skin` `"default"` with `slot` and `attachment` the part's name. A linked mesh is not editable (edit its source). Geometry, triangles and hull come directly from that API. A never-played player reads only the public setup identity (setup sliders may change it); its active skin selection is passed independently of the supplying table used to read stored provenance. A hidden/null setup identity stays unavailable with its public reason, without attachment inference.
253
257
  - **Ops**, in order, at most 300: `move {v, from, to}` — `from` is the vertex's exact current position, unrounded (the producer refuses a `from` more than 1e-3 px off), `to` rounded to 0.01 px; `add {at, onEdge?}` — the next id, splitting edge `onEdge` when given; `remove {v}` — the vertex and its triangulation edges, never a vertex that ends a constraint edge; `edge {a, b}` — a constraint edge; `unedge {a, b}` — only a constraint edge this delta added (hull and triangulation edges are the producer's; review refuses `MESH_EDIT_HULL_EDGE`, `MESH_EDIT_TRIANGULATION_EDGE` and `MESH_EDIT_CONSTRAINT_END` before recording). Vertex ids are the attachment's indices; an added vertex takes the next id; a removed id is never reused. Each op carries exactly these keys.
254
258
  - `units` and `preview` are text for a reader; nothing reads them.
255
259
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rig-c/review",
3
- "version": "0.10.4",
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.12.0"
20
+ "@rig-c/play": "1.14.0"
21
21
  },
22
22
  "devDependencies": {
23
23
  "@types/bun": "^1.3.0",
package/src/setup-mesh.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * The setup-space mesh reader shared by Setup · Weights (#107) and Edit Mesh (#100): one slot's mesh attachment read
3
- * from the compiled model document as stored, plus the published player's skinning formula that places it. Pure — no
3
+ * from the compiled model document as stored. Geometry is read separately through Play's public readSetupMesh. Pure — no
4
4
  * DOM, no I/O; nothing here edits, normalises or infers a vertex or a weight.
5
5
  *
6
6
  * `rigc-compiled/3` stores a mesh attachment's setup-pose vertices at `skins[].attachments.<slot>.<name>.vertices`:
@@ -66,28 +66,15 @@ function attachmentOf(model:Obj,skin:string,slot:string,name:string):Obj|undefin
66
66
  const entry=isObject(slots)?slots[name]:undefined;
67
67
  return isObject(entry)?entry:undefined;
68
68
  }
69
- /** Without an identity: the slot's mesh attachment named like the slot, else its only mesh (first skin that has it). */
70
- function guessFree(model:Obj,slot:string):{skin:string;attachment:string}|null {
71
- const found:{skin:string;attachment:string}[]=[];
72
- for(const s of Array.isArray(model.skins)?model.skins:[]){
73
- if(!isObject(s)||typeof s.name!=='string'||!isObject(s.attachments)||!isObject(s.attachments[slot]))continue;
74
- for(const [name,a]of Object.entries(s.attachments[slot] as Obj))if(isObject(a)&&(a.kind==='mesh'||a.kind==='linkedmesh'))found.push({skin:s.name,attachment:name});
75
- }
76
- return found.find(f=>f.attachment===slot)??(found.length===1?found[0]!:null);
77
- }
78
-
79
- /**
80
- * The setup-pose mesh of `slot`'s attachment `active` (the identity the setup pose resolved; null takes the slot's mesh
81
- * named like the slot, else its only one), exactly as stored.
82
- */
83
- export function readSetupMesh(model:unknown,slot:string|null,active:{skin:string;attachment:string}|null,codes:SetupMeshCodes):SetupMeshReadResult {
69
+ /** Stored metadata for the exact supplying table/key reported by the public setup identity. Null is never guessed. */
70
+ export function readStoredSetupMesh(model:unknown,slot:string|null,active:{skin:string;attachment:string}|null,codes:SetupMeshCodes):SetupMeshReadResult {
84
71
  const doc=setupModel(model,codes);if(!doc.ok)return doc;
85
72
  if(!slot)return {ok:false,code:`${codes.select}: ${codes.selectText}`};
86
73
  const slots=Array.isArray(doc.model.slots)?doc.model.slots:[];
87
74
  const slotEntry=slots.find(s=>isObject(s)&&s.name===slot) as Obj|undefined;
88
75
  if(!slotEntry||typeof slotEntry.bone!=='string')return {ok:false,code:`${codes.noMesh} ${slot}: the model has no such slot`};
89
- const key=active??guessFree(doc.model,slot);
90
- if(!key)return {ok:false,code:`${codes.noMesh} ${slot}: no mesh attachment`};
76
+ const key=active;
77
+ if(!key)return {ok:false,code:`${codes.noMesh} ${slot}: no mesh attachment at the setup pose`};
91
78
  const own=attachmentOf(doc.model,key.skin,slot,key.attachment);let entry=own,source={skin:key.skin,slot,attachment:key.attachment};
92
79
  if(entry?.kind==='linkedmesh'){
93
80
  if(typeof entry.skin!=='string'||typeof entry.slot!=='string'||typeof entry.source!=='string')return {ok:false,code:`${codes.invalid}: ${slot} / ${key.attachment} is a linked mesh without skin, slot and source`};
@@ -120,25 +107,3 @@ export function readSetupMesh(model:unknown,slot:string|null,active:{skin:string
120
107
  const order:string[]=[];for(const row of vertices)for(const i of row)if(!order.includes(i.bone))order.push(i.bone);
121
108
  return {ok:true,mesh:{slot,skin:key.skin,attachment:key.attachment,source,linked:own.kind==='linkedmesh',weighted:v.weighted,vertices,bones:order,triangles:[...triangles] as number[],hull,entry:own}};
122
109
  }
123
-
124
- /** A bone's world transform as the published player reports it (`BoneWorld`'s x, y, a, b, c, d). */
125
- export interface WorldTransform {x:number;y:number;a:number;b:number;c:number;d:number}
126
- /**
127
- * One vertex's world position from its stored influences and the bones' world transforms: the published player's own
128
- * skinning formula (`worldVertices` in `@rig-c/play`: coordinates and weights through `Math.fround`, each influence
129
- * `(x·a + y·b + worldX, x·c + y·d + worldY) · weight`, summed; an unweighted vertex is its slot bone's transform at 1).
130
- */
131
- export function skinVertex(influences:readonly WeightInfluence[],world:(bone:string)=>WorldTransform,weighted:boolean):{x:number;y:number} {
132
- let wx=0,wy=0;
133
- for(const i of influences){
134
- const t=world(i.bone),x=Math.fround(i.x),y=Math.fround(i.y),w=weighted?Math.fround(i.weight):1;
135
- wx+=(x*t.a+y*t.b+t.x)*w;wy+=(x*t.c+y*t.d+t.y)*w;
136
- }
137
- return {x:wx,y:wy};
138
- }
139
- /** Every vertex of a read mesh placed in world space through `world` (flat x, y pairs), or null when one is not finite. */
140
- export function placeSetupMesh(mesh:Pick<SetupMeshRead,'vertices'|'weighted'>,world:(bone:string)=>WorldTransform):number[]|null {
141
- const out:number[]=[];
142
- for(const influences of mesh.vertices){const p=skinVertex(influences,world,mesh.weighted);if(!Number.isFinite(p.x)||!Number.isFinite(p.y))return null;out.push(p.x,p.y);}
143
- return out;
144
- }
package/src/weights.ts CHANGED
@@ -2,14 +2,13 @@
2
2
  * Setup-space mesh weights (#107), read from the compiled model document as stored. Watch-only: nothing here edits,
3
3
  * normalises or infers a weight. The reader itself is shared with Edit Mesh (#100): `src/setup-mesh.ts`.
4
4
  */
5
- import {readSetupMesh,setupModel,type SetupMeshCodes,type WeightInfluence} from './setup-mesh.ts';
5
+ import {readStoredSetupMesh,setupModel,type SetupMeshCodes,type WeightInfluence} from './setup-mesh.ts';
6
6
  export const WEIGHTS_MODEL_UNAVAILABLE='WEIGHTS_MODEL_UNAVAILABLE';
7
7
  export const WEIGHTS_SELECT_SLOT='WEIGHTS_SELECT_SLOT';
8
8
  export const WEIGHTS_NO_MESH='WEIGHTS_NO_MESH';
9
9
  export const WEIGHTS_MODEL_INVALID='WEIGHTS_MODEL_INVALID';
10
10
 
11
- export type {WeightInfluence,WorldTransform} from './setup-mesh.ts';
12
- export {skinVertex} from './setup-mesh.ts';
11
+ export type {WeightInfluence} from './setup-mesh.ts';
13
12
  export interface MeshWeights {
14
13
  slot:string;skin:string;attachment:string;
15
14
  /** Where the vertices were read: the attachment itself, or a linked mesh's source. */
@@ -31,11 +30,11 @@ export const WEIGHTS_CODES:SetupMeshCodes=Object.freeze({unavailable:WEIGHTS_MOD
31
30
  export const weightsModel=(model:unknown)=>setupModel(model,WEIGHTS_CODES);
32
31
 
33
32
  /**
34
- * The setup-pose weights of `slot`'s mesh attachment `active` (the identity the held pose resolved; null takes the
35
- * slot's mesh named like the slot, else its only one). Weights are returned exactly as stored.
33
+ * The setup-pose weights of `slot`'s mesh attachment `active` (the identity the setup pose resolved; null preserves its
34
+ * absence as reported by the public setup identity). Weights are returned exactly as stored.
36
35
  */
37
36
  export function meshWeights(model:unknown,slot:string|null,active:{skin:string;attachment:string}|null):WeightsResult {
38
- const read=readSetupMesh(model,slot,active,WEIGHTS_CODES);if(!read.ok)return read;
37
+ const read=readStoredSetupMesh(model,slot,active,WEIGHTS_CODES);if(!read.ok)return read;
39
38
  const {slot:s,skin,attachment,source,weighted,vertices,bones,triangles}=read.mesh;
40
39
  return {ok:true,weights:{slot:s,skin,attachment,source,weighted,vertices,bones,triangles}};
41
40
  }
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();}