@rig-c/review 0.10.4 → 0.10.5

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
@@ -587,7 +587,7 @@ The Parts explorer (left column; right on the compare page) selects an actual mo
587
587
 
588
588
  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
589
 
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.
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.
591
591
 
592
592
  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
593
 
@@ -640,7 +640,7 @@ In **Setup**, after the Mesh panel, the **Weights** block (`#weights`, #107; **W
640
640
  - **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
641
  - **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
642
  - **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.
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.
644
644
  - **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
645
 
646
646
  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 +680,7 @@ Review paints no image: a new cut names no `under`, and a patch may only keep, m
680
680
 
681
681
  ## Mesh edit (typed delta, preview only)
682
682
 
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.
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.
684
684
 
685
685
  The bar has four sub-modes, one pressed at a time, with single keys that avoid the layer keys `I` / `M` / `B`:
686
686
 
package/docs/DESIGN.md CHANGED
@@ -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.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.
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.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.
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
 
@@ -248,8 +248,8 @@ Edit Mesh's **Download JSON** (#80, setup space since #100) saves two files; nei
248
248
  "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
249
  ```
250
250
 
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).
251
+ - **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.
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). 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
253
  - **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
254
  - `units` and `preview` are text for a reader; nothing reads them.
255
255
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rig-c/review",
3
- "version": "0.10.4",
3
+ "version": "0.10.5",
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.13.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/main.ts CHANGED
@@ -14,7 +14,7 @@ import {chestOverlayCircles,chestOverlayPolygons,chestRegionRows,chestRowId,eval
14
14
  import {bindChestEditor,canvasToCrop,bustLabel,bustSides,LANDMARKS,landmarkInputId,rowCircle,type ChestEditor,type LandmarkKey} from './chest-editor.ts';
15
15
  import {bindMeshEditor,meshEditSource,sha256Hex,type MeshEditor} from './mesh-editor.ts';
16
16
  import {bindWeightsView,setupPoseMesh,type WeightsView} from './weights-view.ts';
17
- import {modelBoneNames,readSetupPose} from './setup-pose.ts';
17
+ import {readSetupPose} from './setup-pose.ts';
18
18
  import {bindBonesPanel,type BonesPanel,type BonesCheckView} from './bones-panel.ts';
19
19
  import {bindCutsPanel,type CutsPanel} from './cuts-panel.ts';
20
20
  import {bindClothPanel,type ClothPanel} from './cloth-panel.ts';
@@ -1367,7 +1367,7 @@ function reposePreview():void {if(player?.adjust)player.repose();}
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(){
1369
1369
  if(data.manifest.model&&!partModel)return {ok:false as const,code:'MESH_EDIT_RIG_UNAVAILABLE: the compiled model document is still loading'};
1370
- return meshEditSource({slot:parts?.value()?.slot??null,model:data.manifest.model?partModel:null,pose:slot=>assets?readSetupPose(assets,slot,modelBoneNames(partModel),document.createElement('div')):null,mapping:chestMapping});
1370
+ return meshEditSource({slot:parts?.value()?.slot??null,model:data.manifest.model?partModel:null,pose:slot=>assets?readSetupPose(assets,slot,document.createElement('div')):null,mapping:chestMapping});
1371
1371
  }
1372
1372
  async function exportMesh(doc:MeshEditDocument):Promise<void> {
1373
1373
  const text=meshEditText(doc),digest=await sha256Hex(text);
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Mesh edit (Setup): a view-bar toggle, an editor bar of sub-modes (Modify / Create / Delete / Edges) and direct
3
3
  * manipulation of the selected slot's actual mesh on the rig pane. The mesh is the slot's attachment in the compiled
4
- * model document at the **setup pose** (#100): the stored vertices placed through the setup-pose bones of a never-played
5
- * player (`web/setup-pose.ts`, shared with Setup · Weights) and drawn over the stage with the stage's own camera — the
4
+ * model document at the **setup pose** (#100): Play's public readSetupMesh coordinates, topology and hull
5
+ * (`web/setup-pose.ts`, shared with Setup · Weights), drawn over the stage with the stage's own camera — the
6
6
  * stage itself stays paused at the pose it held, under the veil. Everything is a typed delta (`src/mesh-edit.ts`,
7
7
  * `basis: "setup"`) previewed over the original triangulation; nothing is retriangulated or written into the package
8
8
  * from here. Apply · Rebuild (#135) hands the exported document to the host, which sends it through the shared runner as
@@ -11,7 +11,7 @@
11
11
  * inspected build, and the local draft keeps the applied delta (edits over the revision are not written to it).
12
12
  */
13
13
  import type {CropWorldMapping} from '../src/chest-regions.ts';
14
- import {placeSetupMesh,readSetupMesh,setupModel} from '../src/setup-mesh.ts';
14
+ import {readStoredSetupMesh,setupModel} from '../src/setup-mesh.ts';
15
15
  import {
16
16
  MESH_EDIT_CODES,MESH_EDIT_NO_MESH,MESH_EDIT_PREVIEW_NOTE,MESH_EDIT_RIG_UNAVAILABLE,MESH_EDIT_SELECT_SLOT,MeshEditSession,affineOf,applyAffine,applyMeshOps,attachmentHashText,draftOps,hitMesh,
17
17
  invertAffine,meshEditDocument,parseMeshDraft,screenDeltaToCrop,verticesInBox,writeMeshDraft,type MeshEditDocument,type MeshEditResult,type MeshOp,type MeshSource,
@@ -56,7 +56,7 @@ export function cropFromWorld(mapping:Extract<CropWorldMapping,{available:true}>
56
56
  type SourceResult={ok:true;source:Omit<MeshSource,'hash'>;entry:unknown;world:number[]}|{ok:false;code:string};
57
57
  /**
58
58
  * The selected slot's mesh in setup space: its attachment's entry in the hash-verified compiled model document (the
59
- * attachment the setup pose resolves), its stored vertices placed through the setup-pose bones (`world`, flat x, y)
59
+ * attachment the public setup identity resolves), its public readSetupMesh vertices (`world`, flat x, y)
60
60
  * and mapped to crop px. Refuses, in order: no model document (`MESH_EDIT_MODEL_UNAVAILABLE`), whole rig, no setup pose,
61
61
  * a non-mesh or linked attachment, no crop mapping.
62
62
  */
@@ -66,17 +66,18 @@ export function meshEditSource(input:{slot:string|null;model:unknown;pose:(slot:
66
66
  let pose:SetupPose|null;
67
67
  try{pose=input.pose(slot);}catch(error){return {ok:false,code:`${MESH_EDIT_RIG_UNAVAILABLE}: ${error instanceof Error?error.message:String(error)}`};}
68
68
  if(!pose)return {ok:false,code:`${MESH_EDIT_RIG_UNAVAILABLE}: no rig loaded`};
69
- const read=readSetupMesh(input.model,slot,pose.identity,MESH_EDIT_CODES);if(!read.ok)return read;
69
+ const read=readStoredSetupMesh(input.model,slot,pose.identity,MESH_EDIT_CODES);if(!read.ok)return !pose.identity&&pose.reason?{ok:false,code:`${read.code} (${pose.reason})`}:read;
70
70
  const mesh=read.mesh,noMesh=(why:string)=>({ok:false as const,code:`${MESH_EDIT_NO_MESH} ${slot}: ${why}`});
71
71
  if(mesh.linked)return noMesh(`${mesh.skin} / ${mesh.attachment} is a linked mesh of ${mesh.source.slot} / ${mesh.source.attachment}; edit its source mesh`);
72
- if(mesh.hull===null)return noMesh('the model entry stores no hull');
73
72
  if(!input.mapping.available)return {ok:false,code:`${input.mapping.reason}: mesh edit needs the crop mapping`};
74
- let world:number[]|null;
75
- try{world=placeSetupMesh(mesh,pose.world);}catch(error){return {ok:false,code:`${MESH_EDIT_RIG_UNAVAILABLE}: ${error instanceof Error?error.message:String(error)}`};}
76
- if(!world)return {ok:false,code:`MESH_EDIT_SOURCE_MISMATCH: ${slot} has a non-finite setup-pose vertex`};
73
+ let geometry:ReturnType<SetupPose['readMesh']>;
74
+ try{geometry=pose.readMesh();}catch(error){return {ok:false,code:`${MESH_EDIT_RIG_UNAVAILABLE}: ${error instanceof Error?error.message:String(error)}`};}
75
+ if(!geometry||geometry.slot!==slot||geometry.skin!==mesh.skin||geometry.attachment!==mesh.attachment||geometry.vertices.length!==mesh.vertices.length*2)return {ok:false,code:`MESH_EDIT_SOURCE_MISMATCH: ${slot} setup geometry does not match its stored identity or vertex count`};
76
+ if(geometry.hull===null)return noMesh('the model entry stores no hull');
77
+ const world=geometry.vertices;
77
78
  const toCrop=cropFromWorld(input.mapping);if(!toCrop)return {ok:false,code:'MESH_EDIT_MAPPING_SINGULAR'};
78
79
  const vertices:Vec[]=[];for(let i=0;i<world.length;i+=2)vertices.push(toCrop(world[i]!,world[i+1]!));
79
- return {ok:true,source:{slot,skin:mesh.skin,attachment:mesh.attachment,vertices,triangles:mesh.triangles,hull:mesh.hull},entry:mesh.entry,world};
80
+ return {ok:true,source:{slot,skin:mesh.skin,attachment:mesh.attachment,vertices,triangles:geometry.triangles,hull:geometry.hull},entry:mesh.entry,world};
80
81
  }
81
82
  /** Apply · Rebuild with an empty delta: nothing is sent. */
82
83
  export const MESH_EDIT_NOTHING_PENDING='MESH_EDIT_NOTHING_PENDING: the delta holds no op; nothing is sent';
package/web/setup-pose.ts CHANGED
@@ -1,26 +1,27 @@
1
1
  /**
2
- * The setup pose, read from the published player: a throwaway `RigPlayer` over the loaded assets that never plays, so
3
- * its `bone()` and `slotIdentity()` answer the setup pose (the published `posedFrame()` is empty before `play`). Shared
4
- * by Setup · Weights (#107) and Edit Mesh (#100). The stage player is never touched; the throwaway one is disposed
5
- * before this returns, so every bone the caller may ask for is read here.
2
+ * Setup identity through the published player, followed by named geometry through Play's public readSetupMesh.
3
+ * The short-lived player reads identity only: setup sliders can select an attachment different from slot.setup.
4
+ * It never plays or reads bone matrices, and never touches the stage player.
6
5
  */
7
- import {RigPlayer,type ModelAssets,type RenderMode} from '@rig-c/play';
8
- import type {WorldTransform} from '../src/setup-mesh.ts';
6
+ import {RigPlayer,readSetupMesh,type ModelAssets,type RenderMode,type SetupMesh,type SlotIdentityReason} from '@rig-c/play';
9
7
 
10
8
  export interface SetupPose {
11
- /** The slot's attachment at the setup pose, or null when it shows none there. */
9
+ /** Exact supplying table/key, or the public null identity (never guessed from declarations). */
12
10
  identity:{skin:string;attachment:string}|null;
13
- /** A named bone's setup-pose world transform; throws for a bone that was not read. */
14
- world:(bone:string)=>WorldTransform;
11
+ /** Active skin selection, distinct from the supplying table in identity.skin. Null means a fresh no-skin player. */
12
+ skin:string|null;
13
+ reason:SlotIdentityReason|null;
14
+ /** Detached geometry from the public API; lazy so callers can name non-mesh/linked refusals first. */
15
+ readMesh:()=>SetupMesh|null;
15
16
  }
16
- /** Reads `slot`'s setup-pose identity and every bone in `bones` (unknown names are skipped). Throws what the player throws. */
17
- export function readSetupPose(assets:ModelAssets,slot:string,bones:readonly string[],root:HTMLElement,mode:RenderMode='dom'):SetupPose {
18
- const player=new RigPlayer(root,assets,{mode,slots:[slot]});
17
+ /** Reads setup identity without advancing time. Throws public setup/identity/geometry refusals unchanged. */
18
+ export function readSetupPose(assets:ModelAssets,slot:string,root:HTMLElement,mode:RenderMode='dom',skin?:string):SetupPose {
19
+ const player=new RigPlayer(root,assets,{mode,slots:[slot],...(skin===undefined?{}:{skin})});
19
20
  try{
20
- const row=player.slotIdentity().slots.find(r=>r.slot===slot),identity=row&&row.attachment&&row.skin?{skin:row.skin,attachment:row.attachment}:null;
21
- const map=new Map<string,WorldTransform>();
22
- for(const name of bones){try{const b=player.bone(name);map.set(name,{x:b.x,y:b.y,a:b.a,b:b.b,c:b.c,d:b.d});}catch{/* not a bone of this rig */}}
23
- return {identity,world:name=>{const t=map.get(name);if(!t)throw new Error(`no setup-pose transform for bone ${name}`);return t;}};
21
+ const snapshot=player.slotIdentity(),row=snapshot.slots.find(r=>r.slot===slot);
22
+ const identity=row&&row.attachment!==null&&row.skin!==null?{skin:row.skin,attachment:row.attachment}:null;
23
+ return {identity,skin:snapshot.skin,reason:row?.reason??null,
24
+ readMesh:()=>identity?readSetupMesh(assets,{slot,attachment:identity.attachment,skin:snapshot.skin}):null};
24
25
  }finally{player.dispose();}
25
26
  }
26
27
  /** The bone names a compiled model document declares, in stored order. */
@@ -3,11 +3,10 @@
3
3
  * coloured by that bone's stored weight on the theme's ramp (0 → 1), or switch to max influence and every vertex takes
4
4
  * its dominant bone's colour; hovering a vertex reads its stored value. Weights come from the compiled model document
5
5
  * (`src/weights.ts` over the shared reader `src/setup-mesh.ts`); vertex positions come from the published player's
6
- * setup pose (`web/setup-pose.ts`, a second `RigPlayer` over the same assets that never plays — the same placement
7
- * Edit Mesh uses). Nothing is edited, applied, recorded or sent.
6
+ * readSetupMesh API (`web/setup-pose.ts`, shared with Edit Mesh). A never-played player reads setup identity only.
7
+ * Nothing is edited, applied, recorded or sent.
8
8
  */
9
- import type {ModelAssets,RenderMode} from '@rig-c/play';
10
- import {placeSetupMesh} from '../src/setup-mesh.ts';
9
+ import type {ModelAssets,RenderMode,SetupMesh as PlaySetupMesh} from '@rig-c/play';
11
10
  import {WEIGHTS_SELECT_SLOT,dominantInfluence,meanColor,meshWeights,rampColor,weightOf,weightText,weightsModel,type MeshWeights} from '../src/weights.ts';
12
11
  import {modelBoneNames,readSetupPose} from './setup-pose.ts';
13
12
  import type {OverlayPoint,OverlayProject,OverlayWeights} from './rig-overlays.ts';
@@ -20,19 +19,20 @@ export const WEIGHTS_SOURCE_MISMATCH='WEIGHTS_SOURCE_MISMATCH';
20
19
  export const WEIGHTS_RIG_UNAVAILABLE='WEIGHTS_RIG_UNAVAILABLE';
21
20
 
22
21
  /** The selected slot's mesh at the setup pose: world vertices (one per stored vertex), triangles and identity. */
23
- export interface SetupMesh {slot:string;vertices:number[];triangles:number[];identity:{skin:string;attachment:string}|null}
22
+ export interface SetupMesh extends PlaySetupMesh {identity:{skin:string;attachment:string}|null}
24
23
  export type SetupMeshResult={ok:true;mesh:SetupMesh;weights:MeshWeights}|{ok:false;code:string};
25
24
  /**
26
- * The slot's attachment is the one the setup pose resolves (`readSetupPose`); its stored vertices are placed through
27
- * the setup-pose bones (`placeSetupMesh`). The stage player is not touched.
25
+ * The public setup identity names the target. Play owns its setup coordinates, topology, hull and deform.
26
+ * The active skin selection is passed independently of the identity's supplying table; the stage is not touched.
28
27
  */
29
- export function setupPoseMesh(assets:ModelAssets,model:unknown,slot:string,root:HTMLElement,mode:RenderMode='dom'):SetupMeshResult {
28
+ export function setupPoseMesh(assets:ModelAssets,model:unknown,slot:string,root:HTMLElement,mode:RenderMode='dom',skin?:string):SetupMeshResult {
30
29
  const available=weightsModel(model);if(!available.ok)return available;
31
30
  try{
32
- const pose=readSetupPose(assets,slot,modelBoneNames(model),root,mode);
33
- const result=meshWeights(model,slot,pose.identity);if(!result.ok)return result;
34
- const vertices=placeSetupMesh(result.weights,pose.world);if(!vertices)return {ok:false,code:`${WEIGHTS_SOURCE_MISMATCH}: ${slot} has a non-finite setup-pose vertex`};
35
- return {ok:true,weights:result.weights,mesh:{slot,vertices,triangles:result.weights.triangles,identity:pose.identity}};
31
+ const pose=readSetupPose(assets,slot,root,mode,skin);
32
+ const result=meshWeights(model,slot,pose.identity);if(!result.ok)return !pose.identity&&pose.reason?{ok:false,code:`${result.code} (${pose.reason})`}:result;
33
+ const mesh=pose.readMesh();
34
+ if(!mesh||mesh.slot!==slot||mesh.skin!==result.weights.skin||mesh.attachment!==result.weights.attachment||mesh.vertices.length!==result.weights.vertices.length*2)return {ok:false,code:`${WEIGHTS_SOURCE_MISMATCH}: ${slot} setup geometry does not match its stored identity or vertex count`};
35
+ return {ok:true,weights:result.weights,mesh:{...mesh,identity:pose.identity}};
36
36
  }catch(error){return {ok:false,code:`${WEIGHTS_RIG_UNAVAILABLE}: ${error instanceof Error?error.message:String(error)}`};}
37
37
  }
38
38