@rig-c/review 0.8.1 → 0.9.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 +15 -8
- package/docs/DESIGN.md +1 -1
- package/docs/REVIEW_PACKAGE.md +12 -6
- package/package.json +1 -1
- package/src/baseline-reach.ts +177 -0
- package/src/baseline.ts +14 -9
- package/src/cli.ts +2 -2
- package/src/front.ts +3 -0
- package/src/handoff.ts +59 -7
- package/src/manifest.ts +13 -3
- package/src/open.ts +12 -3
- package/src/rebuild.ts +185 -19
- package/src/server.ts +7 -0
- package/web/index.html +1 -1
- package/web/main.ts +58 -2
- package/web/rebuild-client.ts +36 -6
- package/web/styles.css +1 -0
package/web/index.html
CHANGED
|
@@ -6,6 +6,6 @@
|
|
|
6
6
|
<header class="topbar"><div class="brand"><span class="brand-mark">r</span>rig review</div><div><strong id="package-title">Review package</strong><span id="revision" class="subtle"></span></div><div class="mode-switch" role="group" aria-label="Mode"><button type="button" data-mode="rig" data-inspector="rig" aria-controls="inspector-rig" aria-pressed="true">Setup</button><button type="button" data-mode="playback" data-inspector="playback" aria-controls="inspector-playback" aria-pressed="false">Animate</button></div><div class="top-actions"><span id="dirty" class="subtle">Loading…</span><button id="write-review" type="button" class="primary" aria-haspopup="dialog" aria-controls="review-dialog">Write review</button></div></header>
|
|
7
7
|
<div class="layout">
|
|
8
8
|
<aside class="left-panel"><div class="left-header parts-header"><h2>PARTS</h2><input id="part-search" data-part-search type="search" autocomplete="off" aria-label="Find slot" placeholder="Find slot"></div><details id="package-context" class="package-context"><summary><span class="eyebrow">CHECKPOINTS</span><span id="checkpoint-title">Select a checkpoint</span></summary><nav id="checkpoints" aria-label="Checkpoints"></nav><section><label for="reference-select" class="label">Reference</label><select id="reference-select"></select><div id="reference-info" class="hint"></div><div id="person-control" hidden><label for="person-select" class="label space-top">Person in input pose</label><select id="person-select"></select></div></section></details><aside id="parts-explorer" class="parts-explorer" aria-label="Parts explorer"><button type="button" data-part-whole aria-pressed="true">Whole rig</button><label class="part-solo"><input id="part-solo" data-part-solo type="checkbox" disabled>Solo selected slot</label><p data-part-status class="hint" role="status">Loading actual slots…</p><ul data-part-list aria-label="Actual rig slots"></ul><p class="hint">Select in context or enable Solo. Thumbnails are each slot's declared page region; a type icon stands in where the model declares none. Atlas pages are not part thumbnails.</p></aside></aside>
|
|
9
|
-
<main><div class="viewbar"><fieldset id="layer-controls" class="layer-toggles"><legend class="sr-only">Rig layers</legend><label class="layer-toggle" title="No rig supplied"><input aria-keyshortcuts="I" id="layer-images" type="checkbox" checked disabled><span>Images</span><kbd aria-hidden="true">I</kbd></label><label class="layer-toggle" title="No rig supplied"><input aria-keyshortcuts="M" id="layer-mesh" type="checkbox" disabled><span>Mesh edges</span><kbd aria-hidden="true">M</kbd></label><label class="layer-toggle" title="No rig supplied"><input aria-keyshortcuts="B" id="layer-bones" type="checkbox" disabled><span>Bone points</span><kbd aria-hidden="true">B</kbd></label></fieldset><div class="view-tools"><button id="chest-edit" type="button" class="view-tool" aria-pressed="false" aria-controls="chest-editor-bar" title="Place the two chest regions on the rig: drag a centre, the core edge or the band" hidden>Chest regions</button><button id="mesh-edit" type="button" class="view-tool" aria-pressed="false" aria-controls="mesh-editor-bar" title="Edit the selected slot's actual mesh as a typed delta: preview only, the producer retriangulates" hidden>Edit mesh</button></div><div class="zoom-controls"><label class="camera-sync"><input id="camera-sync" type="checkbox" checked>Sync cameras</label><button id="zoom-out" aria-label="Zoom out">−</button><output id="zoom-value">100%</output><button id="zoom-in" aria-label="Zoom in">+</button><button id="fit">Fit</button></div><details id="view-menu" class="view-menu"><summary aria-label="View layout">View: <span id="view-menu-label">Side by side</span></summary><div class="view-menu-list" role="group" aria-label="View"><button type="button" data-view="compare" aria-pressed="true">Side by side</button><button type="button" data-view="reference" aria-pressed="false">Reference</button><button type="button" data-view="rig" aria-pressed="false">Rig</button></div></details></div><div id="chest-editor-bar" class="chest-editor-bar" role="region" aria-label="Chest region editor" hidden><div class="bust-group" role="group" aria-label="Chest editor mode"><input id="bust-mode" type="hidden" value="circles"><button id="bust-mode-circles" type="button" data-bust-mode="circles" aria-pressed="true" title="Two circles, each centre, core and band set by hand (#99)">Circles</button><button id="bust-mode-landmarks" type="button" data-bust-mode="landmarks" aria-pressed="false" title="Three clicks (right apex, left apex, cleavage); the producer derives both circles (contour.bust)">Landmarks</button></div><div class="bust-group" data-chest-mode="circles"><span class="bust-tag bust-tag-r">● R</span><label>X<input id="bust-r-x" type="number" step="1" inputmode="decimal" aria-label="R region centre x"></label><label>Y<input id="bust-r-y" type="number" step="1" inputmode="decimal" aria-label="R region centre y"></label><label>R<input id="bust-r-r" type="number" min="6" max="120" step="1" inputmode="decimal" aria-label="R region core radius"></label><label>Band<input id="bust-r-band" type="number" min="4" max="120" step="1" inputmode="decimal" aria-label="R region band"></label></div><div class="bust-group" data-chest-mode="circles"><span class="bust-tag bust-tag-l">● L</span><label>X<input id="bust-l-x" type="number" step="1" inputmode="decimal" aria-label="L region centre x"></label><label>Y<input id="bust-l-y" type="number" step="1" inputmode="decimal" aria-label="L region centre y"></label><label>R<input id="bust-l-r" type="number" min="6" max="120" step="1" inputmode="decimal" aria-label="L region core radius"></label><label>Band<input id="bust-l-band" type="number" min="4" max="120" step="1" inputmode="decimal" aria-label="L region band"></label></div><div id="bust-landmarks" class="bust-group" data-chest-mode="landmarks" role="group" aria-label="Chest landmarks (rig px)" hidden><button id="lm-pick-apex_r" type="button" data-landmark="apex_r" aria-pressed="false" title="Click the part to place the apex r; drag its marker to move it">Apex R</button><label>X<input id="lm-apex_r-x" type="number" step="1" inputmode="decimal" aria-label="Apex R x (rig px)"></label><label>Y<input id="lm-apex_r-y" type="number" step="1" inputmode="decimal" aria-label="Apex R y (rig px)"></label><button id="lm-pick-apex_l" type="button" data-landmark="apex_l" aria-pressed="false" title="Click the part to place the apex l; drag its marker to move it">Apex L</button><label>X<input id="lm-apex_l-x" type="number" step="1" inputmode="decimal" aria-label="Apex L x (rig px)"></label><label>Y<input id="lm-apex_l-y" type="number" step="1" inputmode="decimal" aria-label="Apex L y (rig px)"></label><button id="lm-pick-cleavage" type="button" data-landmark="cleavage" aria-pressed="false" title="Click the part to place the cleavage; drag its marker to move it">Cleavage</button><label>X<input id="lm-cleavage-x" type="number" step="1" inputmode="decimal" aria-label="Cleavage x (rig px)"></label><label>Y<input id="lm-cleavage-y" type="number" step="1" inputmode="decimal" aria-label="Cleavage y (rig px)"></label></div><div id="bust-landmark-params" class="bust-group" data-chest-mode="landmarks" role="group" aria-label="Landmark constants" hidden><label title="Core ratio r / (r + band); the producer's default is 0.22">α<input id="lm-alpha" type="range" min="0.05" max="0.95" step="0.01" value="0.22" aria-label="Alpha, core ratio"><output id="lm-alpha-value" for="lm-alpha">0.22</output></label><label title="Cleavage clearance in rig px; the producer's default is 3.8">Clearance<input id="lm-clearance" type="range" min="0" max="40" step="0.1" value="3.8" aria-label="Clearance, rig px"><output id="lm-clearance-value" for="lm-clearance">3.8</output></label><label title="Control bone for bust_r; empty is the producer's default boob_r">Bone R<input id="lm-bone_r" type="text" list="lm-bones" placeholder="boob_r" spellcheck="false" autocomplete="off"></label><label title="Control bone for bust_l; empty is the producer's default boob_l">Bone L<input id="lm-bone_l" type="text" list="lm-bones" placeholder="boob_l" spellcheck="false" autocomplete="off"></label><datalist id="lm-bones"></datalist></div><span id="lm-derived" class="hint" data-chest-mode="landmarks" aria-live="polite" hidden></span><div class="bust-group" data-chest-mode="circles"><button id="bust-mirror" type="button" title="Copy R's y, core radius and band to L">Mirror R → L</button><button id="bust-download" type="button" title="Download chest-regions.json and its binding; evidence, not an approval">Download JSON</button></div><div class="bust-group"><button id="bust-apply" type="button" title="Run the installed producer (rig-parts remesh) on these regions or landmarks into a scratch build and load it here; the package is unchanged">Apply · Rebuild</button><button id="bust-reset" type="button" title="Return the stage to the inspected build" disabled>Reset</button></div><span id="handoff-line" class="hint handoff-line" aria-live="polite" title="Producer handoff: the config, parts and inspected rig an Apply rebuilds from"></span><span id="chest-editor-status" class="hint" aria-live="polite"></span><pre id="chest-editor-log" class="rebuild-log" aria-label="Producer output" hidden></pre><textarea id="rebuild-applied" class="sr-only" hidden aria-hidden="true" tabindex="-1"></textarea></div><div id="mesh-editor-bar" class="chest-editor-bar mesh-editor-bar" role="region" aria-label="Mesh edit" hidden><div class="bust-group" role="group" aria-label="Mesh edit sub-mode"><button id="mesh-mode-modify" type="button" data-mesh-mode="modify" aria-pressed="false" aria-keyshortcuts="V" title="Modify · key V">Modify<kbd aria-hidden="true">V</kbd></button><button id="mesh-mode-create" type="button" data-mesh-mode="create" aria-pressed="false" aria-keyshortcuts="C" title="Create · key C">Create<kbd aria-hidden="true">C</kbd></button><button id="mesh-mode-delete" type="button" data-mesh-mode="delete" aria-pressed="false" aria-keyshortcuts="X" title="Delete · key X">Delete<kbd aria-hidden="true">X</kbd></button><button id="mesh-mode-edges" type="button" data-mesh-mode="edges" aria-pressed="false" aria-keyshortcuts="E" title="Edges · key E">Edges<kbd aria-hidden="true">E</kbd></button></div><div class="bust-group"><button id="mesh-undo" type="button" aria-keyshortcuts="Control+Z Meta+Z" title="Undo the last action · Cmd/Ctrl-Z" disabled>Undo</button><button id="mesh-redo" type="button" aria-keyshortcuts="Control+Shift+Z Meta+Shift+Z" title="Redo · Shift-Cmd/Ctrl-Z" disabled>Redo</button><button id="mesh-reset" type="button" title="Drop the whole delta for this attachment (asks first)" disabled>Reset</button></div><div class="bust-group"><button id="mesh-download" type="button" title="Download mesh-edit.json and its binding; evidence, not an approval">Download JSON</button><button id="mesh-apply" type="button" title="Send the delta to the producer (rig-parts remesh --delta) and load the scratch revision in place; nothing is written into the package">Apply · Rebuild</button></div><textarea id="mesh-edit-draft" class="sr-only" hidden aria-hidden="true" tabindex="-1"></textarea><span id="mesh-editor-status" class="hint" aria-live="polite"></span></div><div id="stage-layout" class="stages"><section id="reference-pane" class="stage-pane" aria-label="Reference"><span id="reference-label" class="pane-label">Reference</span><div id="reference-viewport" class="viewport"><div id="reference-content" class="reference-content"></div><div id="reference-empty" class="empty">No reference supplied.</div></div></section><section id="rig-pane" class="stage-pane" aria-label="Rig"><span id="rig-label" class="pane-label">Generated rig · read only</span><div id="rig-viewport" class="viewport"><div id="rig-root"></div><canvas id="rig-overlay" class="rig-overlay" aria-label="Actual rig mesh and bone overlay"></canvas><div id="idle-handles" class="idle-handles" hidden aria-label="Chain amplitude handles"></div><div id="rig-fields-stage" class="rig-fields-stage" hidden aria-label="Rig field handles"></div><div id="rig-empty" class="empty">No compiled model supplied.</div></div></section></div><div class="stage-footer"><span id="coordinates">Drag to pan · scroll to zoom · cameras synchronized</span><span>Source files remain unchanged.</span></div><section class="inspector" aria-label="Inspection controls"><div class="inspector-heading sticky-header"><strong>INSPECTOR</strong><span class="hint">Local view controls · source files stay unchanged</span></div><div class="inspector-body"><section id="inspector-rig" aria-label="Setup" data-inspector-panel="rig"><nav class="block-row" aria-label="Setup blocks"><button type="button" data-block-target="selected-part">Part</button><button type="button" data-block-target="modeling-tools">Modeling</button><button type="button" data-block-target="chest-regions">Chest</button><button type="button" data-block-target="mesh-panel">Mesh</button><button type="button" data-block-target="weights">Weights</button><button type="button" data-block-target="bones-panel">Bones</button><button type="button" data-block-target="rig-fields">Fields</button><button type="button" data-block-target="cuts-panel">Cuts</button><button type="button" data-block-target="cloth-panel">Cloth</button></nav><section id="selected-part" aria-label="Selected part" aria-live="polite"></section><section id="modeling-tools"><div class="eyebrow">MODELING REVIEW</div><button data-proposal="mesh" class="tool">Mesh density <small>Add feedback</small></button><button data-proposal="contour" class="tool">Contour curvature <small>Add feedback</small></button><button data-proposal="chest" class="tool">Chest position <small>Add feedback</small></button><label for="part-set" class="label space-top">Part presets</label><select id="part-set" disabled><option>No sets supplied</option></select><p class="hint">Preview only · does not create motion keys.</p></section><section id="chest-regions" class="chest-regions" aria-labelledby="chest-regions-heading"><div class="eyebrow" id="chest-regions-heading">CHEST REGIONS · DETAIL</div><p class="hint">Crop pixels, x screen-right and y down (<code>family-chest-regions/1</code>). Place the circles with <strong>Chest regions</strong> in the view bar; this block holds names, bones, semi-axes, clip and the contour. Equal semi-axes export as a circle, unequal ones (or a clip) as the generator's 32-sample polygon. Exporting writes nothing into the package and is not an approval.</p><p id="chest-mapping" class="hint"></p><div class="chest-grid"><label>Crop width<input id="chest-crop-w" type="number" step="any" inputmode="decimal"></label><label>Crop height<input id="chest-crop-h" type="number" step="any" inputmode="decimal"></label></div><fieldset class="chest-row"><legend>Region 1</legend><label>Name<input id="chest-0-name" type="text" autocomplete="off" spellcheck="false"></label><label>Bone<select id="chest-0-bone"><option value="">Select bone</option></select></label><label>Centre x<input id="chest-0-cx" type="number" step="any" inputmode="decimal"></label><label>Centre y<input id="chest-0-cy" type="number" step="any" inputmode="decimal"></label><label>Semi-axis x<input id="chest-0-ax" type="number" step="any" inputmode="decimal"></label><label>Semi-axis y<input id="chest-0-ay" type="number" step="any" inputmode="decimal"></label><label>Spacing<input id="chest-0-spacing" type="number" step="any" inputmode="decimal"></label><label>Band<input id="chest-0-band" type="number" step="any" inputmode="decimal"></label><label>Medial clip x<input id="chest-0-clip" type="number" step="any" inputmode="decimal" placeholder="none"></label></fieldset><fieldset class="chest-row"><legend>Region 2</legend><label>Name<input id="chest-1-name" type="text" autocomplete="off" spellcheck="false"></label><label>Bone<select id="chest-1-bone"><option value="">Select bone</option></select></label><label>Centre x<input id="chest-1-cx" type="number" step="any" inputmode="decimal"></label><label>Centre y<input id="chest-1-cy" type="number" step="any" inputmode="decimal"></label><label>Semi-axis x<input id="chest-1-ax" type="number" step="any" inputmode="decimal"></label><label>Semi-axis y<input id="chest-1-ay" type="number" step="any" inputmode="decimal"></label><label>Spacing<input id="chest-1-spacing" type="number" step="any" inputmode="decimal"></label><label>Band<input id="chest-1-band" type="number" step="any" inputmode="decimal"></label><label>Medial clip x<input id="chest-1-clip" type="number" step="any" inputmode="decimal" placeholder="none"></label></fieldset><fieldset class="chest-row"><legend>Contour</legend><label>Tolerance<input id="chest-tolerance" type="number" step="any" inputmode="decimal"></label><label>Margin<input id="chest-margin" type="number" step="any" inputmode="decimal"></label><label>Spacing<input id="chest-spacing" type="number" step="any" inputmode="decimal"></label><label>Max edge<input id="chest-max-edge" type="number" step="any" inputmode="decimal"></label><label>Stray<input id="chest-stray" type="number" step="any" inputmode="decimal"></label><label class="chest-check"><input id="chest-bridges" type="checkbox">Bridges</label></fieldset><details class="chest-more"><summary>Generator and mesh fields</summary><label for="chest-generator" class="label space-top">Public generator</label><input id="chest-generator" type="text" autocomplete="off" spellcheck="false" placeholder="Generator that will read this file"><label for="chest-r" class="label space-top">Mesh r (optional)</label><input id="chest-r" type="number" step="any" inputmode="decimal"><label for="chest-segments" class="label space-top">Mesh segments JSON (optional)</label><textarea id="chest-segments" rows="3" spellcheck="false"></textarea><label for="chest-source-rule" class="label space-top">Source rule (optional)</label><input id="chest-source-rule" type="text" autocomplete="off"></details><p id="chest-status" class="hint" aria-live="polite"></p><button id="chest-export" type="button" class="outline block">Download chest-regions.json</button></section><section id="mesh-panel" class="physics-panel mesh-panel" aria-labelledby="mesh-panel-heading"><div class="row"><span id="mesh-panel-heading" class="eyebrow">MESH · APPLY REMESHES</span></div><p id="mesh-panel-state" class="hint">Select a mesh part in the parts list.</p><p class="hint">Edits the selected part's <code>meshes.<part></code> entry from the handoff config (the grid and contour modes). Apply sends the whole pending entry to the installed producer's <code>remesh --mesh</code> (one patch op at <code>/meshes/<part></code>), compiles and checks the revision, and reloads it with the mesh edges on; Reset returns to the inspected build and the config's entry. Slider ranges are review's; the number field takes any value, and the producer judges it.</p><div id="mesh-params" class="physics-controls mesh-params"></div><div class="row mesh-tools"><span class="eyebrow">REGIONS</span><button id="mesh-add-circle" type="button" disabled>Add circle</button><button id="mesh-add-polygon" type="button" aria-pressed="false" disabled>Draw polygon</button><button id="mesh-regions-edit" type="button" aria-pressed="false" disabled>Drag on stage</button></div><div id="mesh-regions" class="mesh-regions"></div><div class="row mesh-tools"><span class="eyebrow">SEGMENTS</span><select id="mesh-segment-bone" aria-label="Bone to add as a segment" disabled></select><button id="mesh-segment-add" type="button" disabled>Add</button></div><ol id="mesh-segments" class="physics-pending-list mesh-segments"></ol><div class="row"><span class="eyebrow">PENDING ENTRY · DIFF AGAINST THE CONFIG</span></div><ol id="mesh-diff" class="physics-pending-list"></ol><ul id="mesh-precheck" class="mesh-precheck" aria-label="Review pre-check in the config loader's names"></ul><div class="row"><button id="mesh-panel-apply" type="button" class="primary" disabled>Apply</button><button id="mesh-panel-reset" type="button" disabled>Reset</button><button id="mesh-folds" type="button" title="rigc render --folds of the drawn animation on the inspected build and on this revision (#108 readings)">Folds reading</button></div><output id="mesh-status" class="environment-readout"></output><div id="mesh-counts" class="physics-readout" aria-label="Vertex and triangle counts before and after"></div><div id="mesh-readout" class="physics-readout" aria-label="TEXTURE_STRETCH before and after Apply"></div><textarea id="mesh-pending" class="sr-only" hidden aria-hidden="true" tabindex="-1"></textarea></section><section id="weights" class="physics-panel weights-block" aria-labelledby="weights-heading"><div class="row"><span id="weights-heading" class="eyebrow">WEIGHTS · SETUP POSE · WATCH ONLY</span></div><p class="hint">The selected slot's mesh at the setup pose, each vertex coloured by the weight the compiled model document stores for the picked bone (0 → 1), or by its dominant bone under <strong>Max influence</strong>. An unweighted mesh is its slot's bone at 1.0. Hover a vertex for its stored values. Nothing here edits, applies or records a weight.</p><div class="row weights-controls"><button id="weights-show" type="button" aria-pressed="false">Show weights</button><label>Colour by<select id="weights-mode"><option value="bone">Bone weight</option><option value="max">Max influence</option></select></label><label>Bone<select id="weights-bone" disabled></select></label></div><p id="weights-state" class="hint" aria-live="polite"></p><div id="weights-legend" class="weights-legend" aria-label="Weights legend"></div><output id="weights-hover" class="environment-readout"></output></section><section id="bones-panel" class="physics-panel bones-panel" aria-labelledby="bones-panel-heading"><div class="row"><span id="bones-panel-heading" class="eyebrow">BONES · CONFIG ANCHORS · APPLY REBUILDS</span></div><p id="bones-panel-state" class="hint">Reading the producer handoff…</p><p class="hint">The producer config's bones at their <code>at</code>, chain <code>points</code> and <code>tip</code> (rig px, the setup pose; the stage may be posed by an animation). Drag a handle on the stage or edit the numbers: each move is a config patch op at <code>/bones/<i>/at</code>, <code>/bones/<i>/points/<k></code> or <code>/bones/<i>/tip</code> naming its bone, moved in whole rig px. <strong>Check</strong> runs <code>propose --from-config</code> on a scratch copy of the patched config (LINT and coverage lines; LINT blocks nothing) and <code>compare</code> against the handoff config. <strong>Apply</strong> rebuilds the whole rig (<code>rig --config</code>, then <code>check</code>) and needs the reproduced baseline; Reset returns to the inspected build and clears the pending ops. Adding, removing or re-parenting bones is out of scope.</p><div class="row bones-controls"><button id="bones-edit" type="button" aria-pressed="false" disabled>Edit bones on stage</button><label for="bones-select">Bone</label><select id="bones-select" disabled></select></div><div id="bones-anchor-editor" class="bones-anchor-editor"></div><div class="row"><span class="eyebrow">PENDING OPS</span></div><ol id="bones-pending-list" class="physics-pending-list"></ol><div class="row"><button id="bones-check" type="button" disabled>Check</button><button id="bones-apply" type="button" class="primary" disabled>Apply</button><button id="bones-reset-build" type="button" disabled>Reset</button></div><output id="bones-status" class="environment-readout"></output><div id="bones-compare" class="bones-lines" aria-label="compare: origin and tip distance per moved bone"></div><div id="bones-lint" class="bones-lines" aria-label="propose --from-config: LINT and coverage lines"></div><textarea id="bones-pending" class="sr-only" hidden aria-hidden="true" tabindex="-1"></textarea></section><section id="rig-fields" class="physics-panel rig-fields" aria-labelledby="rig-fields-heading"><div class="row"><span id="rig-fields-heading" class="eyebrow">RIG FIELDS · CONFIG · APPLY REBUILDS</span><span id="rig-fields-dial-label" class="idle-preview-label" hidden>preview — Apply decides</span></div><p id="rig-fields-state" class="hint">Reading the producer handoff…</p><p class="hint">Small producer config fields, each a config patch op: the bone a rigid part rides (<code>/regions/<part></code>), a lash's blink still row (<code>/motion/blink/still/<part></code>, dragged on the stage), a stop's inward / outward bounds (<code>/stops/<i></code>, arcs dragged at the pivot), a bone's inherit mode (<code>/bones/<i>/inherit</code>; a chain's <code>link_inherit</code>) and <code>turn</code>'s pivot and per-part depth (<code>/turn/pivot</code>, <code>/turn/depths/<part></code>). <strong>Apply</strong> rebuilds the whole rig (<code>rig --config</code>, then <code>check</code>) and needs the reproduced baseline; the producer's refusal is shown as printed. The dial only previews the drawn build's <code>turn</code> bone and is never recorded. Reset returns to the inspected build and clears the pending ops.</p><div id="rig-fields-region" class="rig-fields-group"></div><div id="rig-fields-still" class="rig-fields-group"></div><div id="rig-fields-stops" class="rig-fields-group"></div><div id="rig-fields-inherit" class="rig-fields-group"></div><div id="rig-fields-turn" class="rig-fields-group"></div><div class="row"><span class="eyebrow">PENDING OPS</span></div><ol id="rig-fields-pending-list" class="physics-pending-list"></ol><div class="row"><button id="rig-fields-apply" type="button" class="primary" disabled>Apply</button><button id="rig-fields-reset" type="button" disabled>Reset</button><button id="rig-fields-reading" type="button" disabled>Angle range</button></div><output id="rig-fields-status" class="environment-readout"></output><div id="rig-fields-readout" class="physics-readout" aria-label="BLINK_NO_HOLE and FEET_STAY_LEVEL before and after"></div><textarea id="rig-fields-pending" class="sr-only" hidden aria-hidden="true" tabindex="-1"></textarea></section><section id="cuts-panel" class="physics-panel bones-panel cuts-panel" aria-labelledby="cuts-panel-heading"><div class="row"><span id="cuts-panel-heading" class="eyebrow">CUTS · PATCHES · LIMBS · APPLY RUNS RIG-PARTS BUILD</span></div><p id="cuts-panel-state" class="hint">Reading the producer handoff…</p><p class="hint">The producer config's <code>assemble.cuts</code> polygons, <code>assemble.patches</code> boxes and each limb joint with its <code>range</code> arc, on the stage in rig px of the setup pose. Drag a vertex, a box corner or a joint, or edit the numbers: each change is a config patch op (<code>/assemble/cuts/<i>/polygon/<k></code>, <code>/assemble/patches/<i>/box</code>, <code>/limbs/<limb>/segments/<k>/range</code>; a joint is its bone's origin, <code>/bones/…</code>). A new cut or patch adds its entry and a <code>regions</code> bone. <strong>Apply</strong> runs <code>rig-parts build</code> (assemble → rig → check) from the declared painting and See-through runs into scratch and needs the reproduced baseline; the producer's refusals (<code>ASSEMBLE_CUT_*</code>, <code>ASSEMBLE_UNDER_*</code>, <code>CONFIG_LIMB_*</code>, <code>CONFIG_PART_UNIQUE</code>) are shown as printed, and the recomposite error map, the <code>seam pair</code> lines and the build's wall clock come back with the build. Review paints no <code>under</code> or <code>cap</code> image. Reset returns to the inspected build and clears the pending ops.</p><div class="row bones-controls"><button id="cuts-edit" type="button" aria-pressed="false" disabled>Edit cuts on stage</button><label for="cuts-select">Entry</label><select id="cuts-select" disabled></select></div><div id="cuts-editor" class="bones-anchor-editor"></div><div id="cuts-add" class="row cuts-add"><span class="eyebrow">ADD</span><select data-cuts-add="from" aria-label="Plan part the new cut takes pixels from"></select><input data-cuts-add="name" type="text" autocomplete="off" placeholder="new part" aria-label="New part name"><input data-cuts-add="bone" type="text" list="lm-bones" autocomplete="off" placeholder="bone" aria-label="Bone the new part's region rides"><select data-cuts-add="draw" aria-label="Where the new part is drawn"></select><input data-cuts-add="overlap" type="number" min="0" step="1" value="0" inputmode="numeric" aria-label="Cut overlap band in rig px"><select data-cuts-add="alpha" aria-label="New patch alpha"><option value="silhouette">silhouette</option><option value="box">box</option></select><button type="button" data-cuts-add="place-cut">Place cut</button><button type="button" data-cuts-add="place-patch">Place patch</button><button type="button" data-cuts-add="finish">Finish</button><button type="button" data-cuts-add="cancel">Cancel</button></div><div class="row"><span class="eyebrow">PENDING OPS</span></div><ol id="cuts-pending-list" class="physics-pending-list"></ol><div class="row"><button id="cuts-apply" type="button" class="primary" disabled>Apply</button><button id="cuts-reset" type="button" disabled>Reset</button></div><output id="cuts-status" class="environment-readout"></output><div id="cuts-readout" class="physics-readout" aria-label="The build's refusals, cut lines, recomposite, seam pairs and wall clock"></div><textarea id="cuts-pending" class="sr-only" hidden aria-hidden="true" tabindex="-1"></textarea></section><section id="cloth-panel" class="physics-panel bones-panel cloth-panel" aria-labelledby="cloth-panel-heading"><div class="row"><span id="cloth-panel-heading" class="eyebrow">CLOTH · RECIPE PER TAG · APPLY RUNS RIG-PARTS PROPOSE --CLOTH</span></div><p id="cloth-panel-state" class="hint">Reading the producer handoff…</p><p class="hint">The producer config's <code>cloth</code> block: one recipe per tag the installed reader accepts, with its fields as written. Each change is a config patch op at <code>/cloth/<tag>/…</code>. <strong>Show links</strong> draws the chains the tag's parts are meshed on, from the config's <code>points</code> and <code>tip</code> in rig px of the setup pose, and after an Apply the chains of the config the producer wrote. <strong>Apply</strong> runs <code>rig-parts propose --cloth --write-config</code> on the declared parts, painting and keypoints into scratch, then builds the written config (<code>rig-parts build</code> when the config declares <code>limbs</code>, else <code>rig --config</code>) and reloads it under the Environment's wind and gravity; the producer's refusals (<code>CONFIG_CLOTH_*</code>, <code>PROPOSE_CLOTH_DRAFTED</code>, <code>PROPOSE_CLOTH_PART</code>, <code>PROPOSE_WRITE_CONFIG_*</code>) are shown as printed. Review edits the recipe, never the drafted constraints. Reset returns to the inspected build and clears the pending ops.</p><div class="row bones-controls"><button id="cloth-links" type="button" aria-pressed="false" disabled>Show links on stage</button><label for="cloth-tag">Tag</label><select id="cloth-tag" disabled></select><label class="cloth-declared"><input id="cloth-declared" type="checkbox" disabled> recipe declared</label></div><div id="cloth-fields" class="cloth-fields"></div><div class="row"><span class="eyebrow">PENDING OPS</span></div><ol id="cloth-pending-list" class="physics-pending-list"></ol><div class="row"><button id="cloth-apply" type="button" class="primary" disabled>Apply</button><button id="cloth-reset" type="button" disabled>Reset</button></div><output id="cloth-status" class="environment-readout"></output><div id="cloth-readout" class="physics-readout" aria-label="Propose's refusals, the written config's diff, the route and check"></div><textarea id="cloth-pending" class="sr-only" hidden aria-hidden="true" tabindex="-1"></textarea></section></section><section id="inspector-playback" aria-label="Animate" data-inspector-panel="playback" hidden><nav class="block-row" aria-label="Animate blocks"><button type="button" data-block-target="transport" data-block-includes="frame-controls motion-tools angle-panel">Transport</button><button type="button" data-block-target="sound-panel">Sound</button><button type="button" data-block-target="environment">Environment</button><button type="button" data-block-target="physics-panel">Physics</button><button type="button" data-block-target="idle-tracks">Idle tracks</button><button type="button" data-block-target="readings">Readings</button><button type="button" data-block-target="requirements">Requirements</button><button type="button" data-block-target="retarget">Retarget</button><button type="button" data-block-target="report-panel">Report</button></nav><section id="transport" class="transport"><button id="play" aria-label="Play" disabled>▶</button><label for="animation-select" class="sr-only">Animation</label><select id="animation-select" disabled><option>No animations supplied</option></select><input id="scrub" type="range" min="0" max="1" step="0.001" value="0" aria-label="Playback time"><output id="time-label">0.0000 / 0.0000 s</output><output id="frame-label">rate unknown</output><label class="idle-repeat"><input id="idle-repeat" type="checkbox" checked>Repeat idle</label><div id="reading-ticks" class="reading-ticks" hidden aria-label="Reading ticks"></div><div id="idle-blink-strip" class="idle-blink-strip" hidden><button id="idle-blink-marker" type="button" class="idle-blink-marker" aria-label="Blink time">◆</button></div></section><section id="frame-controls" class="frame-controls" hidden aria-label="Frame navigation using the explicit clock"><label for="frame-number">Frame</label><input id="frame-number" type="number" min="0" step="1" inputmode="numeric" aria-label="Frame number to inspect"><button id="frame-jump" type="button">Go</button><input id="frame-scrub" type="range" min="0" max="1" step="1" value="0" aria-label="Scrub by frame"><span id="frame-clock-label" class="hint"></span><p id="frame-outside" class="hint" hidden>The current time is outside the explicit frame range. Enter a frame number to go to its exact time.</p></section><section id="motion-tools"><div class="eyebrow">MOTION · EDITS BUILD THROUGH THE PRODUCER</div><button data-proposal="pose" class="tool">Inspect pose <small>Add feedback</small></button><button data-proposal="timing" class="tool">Inspect interval <small>Add feedback</small></button><button data-proposal="audio" class="tool">Audio links <small>Add feedback</small></button><button data-proposal="angle" class="tool">Angle inspection <small id="axis-state">No inspection axes supplied</small></button></section><section id="angle-panel" class="angle-panel" hidden><label for="axis-select">Rig inspection axis</label><select id="axis-select"></select><label id="axis-label" for="axis-value"></label><input id="axis-value" type="range" disabled><output id="axis-value-label"></output><p class="hint">Maps one inspection axis to its animation time. This does not rotate the camera.</p></section><section id="sound-panel" class="sound-panel"><div class="row"><span class="eyebrow">SOUND CUES</span><label><input id="sync-audio" type="checkbox">Play cue audio in sync</label></div><div id="cues"></div><div class="row"><label for="audio-volume">Audio volume</label><input id="audio-volume" type="range" min="0" max="1" step="0.05" value="0.8"><output id="audio-volume-label">80%</output></div><div id="event-log" class="hint" aria-live="polite">Runtime events: none</div></section><section id="environment" class="environment-panel" aria-labelledby="environment-heading"><div class="row"><span id="environment-heading" class="eyebrow">ENVIRONMENT · INSPECTION ONLY</span></div><p class="hint">Wind, gravity and impulses go to the player while you inspect. Nothing is written to the model or motion; feedback records the state as context.</p><input id="environment-applied" type="checkbox" hidden aria-hidden="true" tabindex="-1"><div class="row"><label for="wind-base">Wind</label><input id="wind-base" type="range" min="-100" max="100" step="1" value="0" disabled><output id="wind-base-label">0</output></div><div class="row"><label><input id="wind-breeze" type="checkbox" disabled>Breeze</label><button id="wind-gust" type="button" disabled>Gust</button><label><input id="wind-repeat" type="checkbox" disabled>Repeat</label></div><div class="row"><label for="wind-gust-peak">Gust peak</label><input id="wind-gust-peak" type="number" min="0" max="200" step="1" value="48" inputmode="decimal" disabled><label for="wind-gust-duration">Duration (s)</label><input id="wind-gust-duration" type="number" min="0.1" max="20" step="0.1" value="2.4" inputmode="decimal" disabled></div><output id="wind-readout" class="environment-readout">No compiled model drawn · environment unavailable</output><div class="row"><label for="gravity">Gravity</label><input id="gravity" type="range" min="0" max="4" step="0.05" value="1" disabled><output id="gravity-label">1.00</output></div><div class="row"><button id="jolt" type="button" disabled>Jolt</button><label title="Dragging the rig pane moves the rig's physics instead of the camera. Hold Space to pan."><input id="drag-trail" type="checkbox" disabled>Drag to trail</label><button id="physics-reset" type="button" disabled>Reset physics</button></div><p class="hint">Drag to trail: dragging the rig pane tells the physics the rig moved; the camera stays. Hold Space to pan. Reset returns the runtime start values and re-poses from the current time, clearing the physics history.</p><div class="row"><label title="Readings and Apply's check read this wind and gravity as a rigc-environment/1 schedule; the wind clock restarts with the clip so you watch the same world"><input id="environment-use" type="checkbox" disabled>Use for readings</label><button id="environment-export" type="button" disabled>Export schedule</button><button id="environment-check" type="button" disabled>Check under this world</button></div><output id="environment-schedule" class="environment-readout">No schedule · the controls are not applied (the player stands at the runtime start values)</output><div id="environment-check-lines" class="environment-check-lines" aria-live="polite"></div><p class="hint">The schedule is the controls as a <code>rigc-environment/1</code> file from the clip's first frame: one step when the wind is steady, else one step per frame at 12 fps (between frames rigc holds the frame's value). A one-off Gust is not in it; Repeat puts gusts in it. Check under this world runs <code>rig-parts check --environment</code> on the build on the stage; the default-world line is that build's own check.</p></section><section id="physics-panel" class="physics-panel" aria-labelledby="physics-panel-heading"><div class="row"><span id="physics-panel-heading" class="eyebrow">PHYSICS TUNING · APPLY REBUILDS</span></div><p id="physics-panel-state" class="hint">No compiled model drawn.</p><p class="hint">Each edit is a config patch op at <code>/constraints/<i>/<field></code> naming its constraint. Apply runs the installed producer (<code>rig --config</code>, then <code>check</code>) into a scratch build and reloads it under the same wind, gust and jolt state; Reset returns to the inspected build and clears the pending ops. Live preview before Apply waits on play#147.</p><div class="row physics-chain-row"><label for="physics-chain">Chain</label><select id="physics-chain" disabled></select><button id="physics-chain-pick" type="button" aria-pressed="false" disabled>Pick on stage</button></div><div class="row physics-ramp-row"><label for="physics-ramp-field">Ramp</label><select id="physics-ramp-field" disabled></select><label for="physics-ramp-root">root</label><input id="physics-ramp-root" type="number" step="any" inputmode="decimal" disabled><label for="physics-ramp-tip">tip</label><input id="physics-ramp-tip" type="number" step="any" inputmode="decimal" disabled><button id="physics-ramp" type="button" disabled>Ramp</button></div><div class="physics-work"><div id="physics-rows" class="physics-table-wrap" role="region" aria-label="Physics constraints the config declares" tabindex="0"></div><div id="physics-row-editor" class="physics-row-editor"></div></div><div id="physics-locked" class="physics-table-wrap physics-locked" role="region" aria-label="Constraints that are not editable" tabindex="0" hidden></div><div class="row"><span class="eyebrow">PENDING OPS</span></div><ol id="physics-pending-list" class="physics-pending-list"></ol><div class="row"><button id="physics-apply" type="button" class="primary" disabled>Apply</button><button id="physics-reset-build" type="button" disabled>Reset</button></div><output id="physics-apply-status" class="environment-readout"></output><div id="physics-readout" class="physics-readout" aria-label="Check readout before and after Apply"></div><textarea id="physics-pending" class="sr-only" hidden aria-hidden="true" tabindex="-1"></textarea></section><section id="idle-tracks" class="physics-panel idle-panel" aria-labelledby="idle-tracks-heading"><div class="row"><span id="idle-tracks-heading" class="eyebrow">IDLE TRACKS · APPLY REBUILDS</span><span id="idle-preview-label" class="idle-preview-label" aria-live="polite" hidden>preview — Apply decides</span></div><p id="idle-tracks-state" class="hint">No compiled model drawn.</p><p class="hint">Each edit is a config patch op at <code>/motion/tracks/<i>/…</code> or <code>/motion/blink/…</code>. Period is offered only among the divisors of <code>motion.duration</code> (the producer refuses any other, <code>CONFIG_PERIOD_DIVIDES_DURATION</code>). While edits are pending the preview drives the edited bones through the player's <code>adjust</code> with the producer's sine, <code>v(t) = base + amp·sin(2π(t/period − phase))</code>, on the bone the idle keys (<code><bone>_ctl</code> under ctl keys); it is never recorded or submitted. Apply runs the installed producer (<code>rig --config</code>, then <code>check</code>) into a scratch build and replaces the preview with the built idle; Reset returns to the inspected build and clears the pending ops.</p><p id="idle-keys-line" class="hint"></p><div class="physics-work"><div id="idle-track-list" class="physics-table-wrap" role="region" aria-label="Idle tracks the config states" tabindex="0"></div><div id="idle-track-editor" class="physics-row-editor"></div></div><datalist id="idle-bones"></datalist><div class="row idle-add-row"><span class="eyebrow">ADD</span><input data-idle-add-bone type="text" list="idle-bones" autocomplete="off" placeholder="bone" aria-label="Bone for a new track" disabled><select data-idle-add-prop aria-label="Property for a new track" disabled></select><button data-idle-add-single type="button" disabled>Add track</button><select data-idle-add-chain aria-label="Chain for a new chain track" disabled></select><button data-idle-add-chain-button type="button" disabled>Add chain track</button></div><div id="idle-blink" class="idle-blink"></div><div class="row"><label class="idle-preview-toggle"><input data-idle-preview type="checkbox" checked>Preview edits (not recorded)</label></div><div id="idle-precheck" class="idle-precheck" aria-label="Review pre-check"></div><div class="row"><span class="eyebrow">PENDING OPS</span></div><ol id="idle-pending-list" class="physics-pending-list"></ol><div class="row"><button id="idle-apply" type="button" class="primary" disabled>Apply</button><button id="idle-reset" type="button" disabled>Reset</button></div><output id="idle-apply-status" class="environment-readout"></output><div id="idle-readout" class="physics-readout" aria-label="Check readout before and after Apply"></div><textarea id="idle-pending" class="sr-only" hidden aria-hidden="true" tabindex="-1"></textarea></section><section id="readings" class="readings-panel" aria-labelledby="readings-heading"><div class="row"><span id="readings-heading" class="eyebrow">READINGS · RIGC RENDER</span></div><p class="hint">Each reading is one <code>rigc render</code> of the drawn animation, on the inspected build and on the latest scratch build. Its frames become ticks under the timeline (<span class="tick-swatch inspected" aria-hidden="true"></span> inspected, <span class="tick-swatch scratch" aria-hidden="true"></span> scratch; click one to seek) and each pair gets its worst-frame line. A reading, not a gate: an unknown slot or bone is refused with rigc's own line.</p><datalist id="readings-slots"></datalist><datalist id="readings-bones"></datalist><div class="row"><label><input id="readings-folds" type="checkbox">Folds</label><label><input id="readings-overlaps" type="checkbox">Overlaps</label></div><div class="row readings-pair"><label><input id="readings-pass-on" type="checkbox">Pass-through</label><input id="readings-pass-cloth" type="text" list="readings-slots" autocomplete="off" placeholder="cloth slot" aria-label="Pass-through cloth slot"><span aria-hidden="true">:</span><input id="readings-pass-leg" type="text" list="readings-slots" autocomplete="off" placeholder="leg slot" aria-label="Pass-through leg slot"></div><div class="row readings-pair"><label><input id="readings-seam-on" type="checkbox">Seam</label><input id="readings-seam-a" type="text" list="readings-slots" autocomplete="off" placeholder="slot" aria-label="Seam first slot"><span aria-hidden="true">:</span><input id="readings-seam-b" type="text" list="readings-slots" autocomplete="off" placeholder="slot" aria-label="Seam second slot"></div><div class="row readings-pair"><label><input id="readings-angle-on" type="checkbox">Angle range</label><input id="readings-angle-bone" type="text" list="readings-bones" autocomplete="off" placeholder="bone" aria-label="Angle range bone"><span aria-hidden="true">vs</span><input id="readings-angle-relative" type="text" list="readings-bones" autocomplete="off" placeholder="relative bone" aria-label="Angle range relative-to bone"></div><div class="row"><button id="readings-run" type="button" disabled>Run readings</button></div><p id="readings-status" class="hint" aria-live="polite">No compiled model drawn.</p><ul id="readings-worst" class="readings-worst" aria-label="Worst frame per reading"></ul></section><section id="requirements" class="readings-panel requirements-panel" aria-labelledby="requirements-heading"><div class="row"><span id="requirements-heading" class="eyebrow">REQUIREMENTS · RIG-PARTS CHECK</span></div><p class="hint">Declare what the scene asks of the motion: a slot (picked on the stage), its mesh or a bone, an animation and a kind, with your own bar. Check writes a <code>spine-parts-requirements/1</code> file into scratch and runs the installed producer's <code>rig-parts check --requirements</code> on the inspected rig or the latest scratch build; each requirement gets one line with its worst frame (click to seek). A refusal is the producer's, by name. Waivers (<code>may_fold</code>), scene targets and the part exception lists are not written here. Submitted feedback carries the file by hash and content.</p><div id="requirements-body"></div></section><section id="retarget" class="readings-panel requirements-panel retarget-panel" aria-labelledby="retarget-heading"><div class="row"><span id="retarget-heading" class="eyebrow">RETARGET · RIG-POSE</span></div><p class="hint">Put a pose sequence the package declares onto this rig: edit the <code>rig-pose-map/1</code> (per chain, the bone that carries each segment), set the clean and retarget flags, and Apply. The installed <code>@rig-c/pose</code> runs <code>rig-pose clean</code> and <code>rig-pose retarget</code> onto the handoff rig, and its <code>rigc</code> compiles the result, all into scratch; the new animation plays here beside the reference (pick the sequence under Reference). Review writes no keys and never writes the package's <code>motion.json</code>. Refusals (<code>POSE_NOT_CLEANED</code>, <code>POSE_BEND_ONE_WAY</code>, <code>RAISED_HEAD</code>, <code>MOTION_NAME_FREE</code>, <code>MAP_*</code>) are the producer's, verbatim. FEET, STANCE, FLIP, SHORT, END-ON, LEG-SCALE and REACH lines seek to their frame.</p><div id="retarget-body"></div></section><section id="report-panel" class="report-panel" aria-labelledby="report-heading"><div class="report-header"><h2 id="report-heading">Report events</h2><span id="report-summary" class="subtle"></span><a id="report-source-link" target="_blank" rel="noopener" hidden>Source report</a></div><p id="report-unavailable" class="hint" hidden>No rig-pose-report/1 events supplied.</p><div id="report-browser"><div class="report-controls"><label for="report-category" class="sr-only">Report event category</label><select id="report-category"><option value="all">All events</option></select><button id="report-previous" type="button" aria-label="Previous event page">Previous</button><output id="report-page"></output><button id="report-next" type="button" aria-label="Next event page">Next</button></div><div id="report-empty-categories" class="report-empty-categories"></div><p id="report-missing" class="hint" hidden></p><ol id="report-events" class="report-event-list" aria-label="Read-only report events"></ol><section id="report-selection" class="report-selection" hidden aria-labelledby="report-selection-heading"><h3 id="report-selection-heading"></h3><p id="report-timing" class="hint"></p><div class="report-controls"><label id="report-time-label" for="report-time" hidden>Select explicit time</label><select id="report-time" hidden></select><button id="report-seek" type="button" disabled>Go to explicit time</button></div><div id="report-target-control" hidden><label for="report-target" class="label space-top">Select a target named in the report</label><select id="report-target"></select></div><p id="report-prefill-note" class="hint"></p><p id="report-provenance" class="hint"></p><details><summary>Source event details</summary><pre id="report-details"></pre></details></section></div></section></section></div></section></main></div><footer class="status-bar"><span id="message" class="status-message" role="status" aria-live="polite">Loading local review package…</span><div class="status-readouts" aria-label="Status"><span id="player-state" class="subtle">No rig supplied</span><span id="layout-state" class="subtle" role="status" aria-label="Effective layout">Loading inputs…</span><span id="renderer-state" class="subtle" aria-label="Last draw backend" title="DOM images for rigid parts; WebGL meshes when available, Canvas2D fallback.">Not rendered</span><span id="layer-state" class="hint">No rig supplied</span><span id="idle-state" class="hint" role="status">Idle preview unavailable</span><span id="control-state" class="hint" role="status"></span><div class="draft-notice"><span id="draft-state" role="status" aria-live="polite"></span><div id="draft-recovery"></div></div></div><div class="status-source"><details class="view-region"><summary>View region</summary><div class="view-region-body"><span class="hint">Numeric source region · opening this link restores a paused inspection</span><pre id="view-coordinates" class="numeric-view" aria-label="Current numeric inspection region"></pre><p class="hint">Sync cameras links fit-relative zoom and viewport-relative pan across visible panes. Turn it off to inspect each pane independently. Toolbar zoom and Fit use the last active pane. Hold Space while dragging a video to pan without taking over its native controls.</p></div></details><span id="source-hash" class="subtle"></span></div></footer><dialog id="review-dialog" class="review-dialog" aria-labelledby="review-dialog-title"><div class="review-dialog-header"><h2 id="review-dialog-title">Write review</h2><span class="hint">Drafts save locally as you type · nothing is submitted until you press Submit</span><button id="review-dialog-close" type="button">Close</button></div><section id="inspector-feedback" class="review-dialog-body" aria-label="Feedback"><div class="review-summary"><div class="eyebrow">REVIEW</div><label for="review-status" class="label">Review status</label><select id="review-status"><option value="pending">Pending</option><option value="approved">Approved</option><option value="changes-requested">Changes requested</option></select><section class="provenance" aria-label="Reviewed source"><div class="eyebrow">REVIEWED SOURCE</div><div class="provenance-row"><span>Package revision</span><strong id="provenance-version">—</strong></div><div class="provenance-row"><span>rig SHA-256</span><code id="provenance-rig">—</code></div><div class="provenance-row"><span>motion SHA-256</span><code id="provenance-motion">—</code></div><details><summary>Full provenance</summary><pre id="provenance-full"></pre></details><div class="source-files"><div class="eyebrow">SOURCE FILES · READ ONLY</div><div id="source-files"></div></div><p class="hint">Submits server-verified file hashes and tool versions. Your selected review status is preserved.</p></section><div class="divider"></div></div><div id="checkpoint-detail"><div class="eyebrow">SELECTED CHECKPOINT</div><h1 id="detail-title">Checkpoint</h1><p id="detail-note" class="hint"></p><fieldset class="outcome"><legend>Checkpoint outcome</legend><label><input type="radio" name="outcome" value="pending" checked>Pending</label><label><input type="radio" name="outcome" value="pass">Pass</label><label><input type="radio" name="outcome" value="fail">Needs changes</label></fieldset><label for="checkpoint-note" class="label space-top">Review notes</label><textarea id="checkpoint-note" rows="5" placeholder="Describe the part, conditions and what looks wrong."></textarea></div><div class="divider"></div><section id="proposal-panel"><div class="eyebrow">STRUCTURED FEEDBACK</div><div class="label">Frame range · inclusive</div><div class="frame-range"><label>Start<input id="feedback-start" type="number" min="0" step="1" inputmode="numeric" placeholder="e.g. 18"></label><label>End<input id="feedback-end" type="number" min="0" step="1" inputmode="numeric" placeholder="e.g. 32"></label></div><p id="feedback-range-hint" class="hint">Checking frame bounds…</p><label for="feedback-target-kind" class="label space-top">Target type</label><select id="feedback-target-kind"><option value="">Select target</option><option value="rig">Entire rig</option><option value="bone">Bone</option><option value="slot">Slot</option></select><label for="feedback-target-name" class="label space-top">Target name</label><input id="feedback-target-name" type="text" list="feedback-target-names" autocomplete="off" placeholder="Actual bone or slot name"><datalist id="feedback-target-names"></datalist><p id="feedback-target-hint" class="hint"></p><label for="feedback-category" class="label space-top">Category</label><select id="feedback-category"><option value="">Select category</option><option value="defect">Defect</option><option value="quality">Quality</option></select><label for="feedback-summary" class="label space-top">One-line summary</label><input id="feedback-summary" type="text" maxlength="1000" placeholder="e.g. The left wrist moves outside the sleeve"><button id="add-proposal" class="outline block">Add feedback</button><p id="proposal-boundary" class="hint">Records the current checkpoint, time, reference, camera and inspection axis. Motion is read only: inspect, mark ranges and export feedback. No keys or source files are edited.</p><p id="feedback-warnings" class="hint" role="status" hidden></p><p id="feedback-applied-check" class="hint error" role="alert" hidden></p><div id="proposal-list"></div></section><details id="strip-panel" class="report-panel"><summary>Export frame strip</summary><p class="hint">Uses the feedback form start and end frames, inclusive. Inspection time and source files remain unchanged.</p><label for="strip-scope" class="label">Export range</label><select id="strip-scope"><option value="interval">Entered start–end frames</option><option value="feedback">All feedback ranges</option></select><label for="strip-reference" class="label space-top">Reference video</label><select id="strip-reference"><option value="">Select reference video</option></select><label for="strip-format" class="label space-top">Format</label><select id="strip-format"><option value="png">PNG strip</option><option value="json">JSON frames and provenance</option></select><p class="hint">PNG requires frameTimes in the selected report. Without it, JSON preserves requested frames with time: null. Batch export requires every feedback item to match the selected animation. Unsubmitted feedback is included; exporting does not submit the review.</p><button id="strip-export" type="button" disabled>Download strip</button><p id="strip-status" class="hint" role="status" aria-live="polite">Loading package…</p></details></section><div class="review-dialog-actions"><span id="review-message" class="review-message" role="status" aria-live="polite"></span><span id="review-draft-state" class="review-draft-state" role="status" aria-live="polite"></span><button id="save" class="primary" disabled>Submit review</button></div></dialog></div>
|
|
9
|
+
<main><div class="viewbar"><fieldset id="layer-controls" class="layer-toggles"><legend class="sr-only">Rig layers</legend><label class="layer-toggle" title="No rig supplied"><input aria-keyshortcuts="I" id="layer-images" type="checkbox" checked disabled><span>Images</span><kbd aria-hidden="true">I</kbd></label><label class="layer-toggle" title="No rig supplied"><input aria-keyshortcuts="M" id="layer-mesh" type="checkbox" disabled><span>Mesh edges</span><kbd aria-hidden="true">M</kbd></label><label class="layer-toggle" title="No rig supplied"><input aria-keyshortcuts="B" id="layer-bones" type="checkbox" disabled><span>Bone points</span><kbd aria-hidden="true">B</kbd></label></fieldset><div class="view-tools"><button id="chest-edit" type="button" class="view-tool" aria-pressed="false" aria-controls="chest-editor-bar" title="Place the two chest regions on the rig: drag a centre, the core edge or the band" hidden>Chest regions</button><button id="mesh-edit" type="button" class="view-tool" aria-pressed="false" aria-controls="mesh-editor-bar" title="Edit the selected slot's actual mesh as a typed delta: preview only, the producer retriangulates" hidden>Edit mesh</button></div><div class="zoom-controls"><label class="camera-sync"><input id="camera-sync" type="checkbox" checked>Sync cameras</label><button id="zoom-out" aria-label="Zoom out">−</button><output id="zoom-value">100%</output><button id="zoom-in" aria-label="Zoom in">+</button><button id="fit">Fit</button></div><details id="view-menu" class="view-menu"><summary aria-label="View layout">View: <span id="view-menu-label">Side by side</span></summary><div class="view-menu-list" role="group" aria-label="View"><button type="button" data-view="compare" aria-pressed="true">Side by side</button><button type="button" data-view="reference" aria-pressed="false">Reference</button><button type="button" data-view="rig" aria-pressed="false">Rig</button></div></details></div><div id="chest-editor-bar" class="chest-editor-bar" role="region" aria-label="Chest region editor" hidden><div class="bust-group" role="group" aria-label="Chest editor mode"><input id="bust-mode" type="hidden" value="circles"><button id="bust-mode-circles" type="button" data-bust-mode="circles" aria-pressed="true" title="Two circles, each centre, core and band set by hand (#99)">Circles</button><button id="bust-mode-landmarks" type="button" data-bust-mode="landmarks" aria-pressed="false" title="Three clicks (right apex, left apex, cleavage); the producer derives both circles (contour.bust)">Landmarks</button></div><div class="bust-group" data-chest-mode="circles"><span class="bust-tag bust-tag-r">● R</span><label>X<input id="bust-r-x" type="number" step="1" inputmode="decimal" aria-label="R region centre x"></label><label>Y<input id="bust-r-y" type="number" step="1" inputmode="decimal" aria-label="R region centre y"></label><label>R<input id="bust-r-r" type="number" min="6" max="120" step="1" inputmode="decimal" aria-label="R region core radius"></label><label>Band<input id="bust-r-band" type="number" min="4" max="120" step="1" inputmode="decimal" aria-label="R region band"></label></div><div class="bust-group" data-chest-mode="circles"><span class="bust-tag bust-tag-l">● L</span><label>X<input id="bust-l-x" type="number" step="1" inputmode="decimal" aria-label="L region centre x"></label><label>Y<input id="bust-l-y" type="number" step="1" inputmode="decimal" aria-label="L region centre y"></label><label>R<input id="bust-l-r" type="number" min="6" max="120" step="1" inputmode="decimal" aria-label="L region core radius"></label><label>Band<input id="bust-l-band" type="number" min="4" max="120" step="1" inputmode="decimal" aria-label="L region band"></label></div><div id="bust-landmarks" class="bust-group" data-chest-mode="landmarks" role="group" aria-label="Chest landmarks (rig px)" hidden><button id="lm-pick-apex_r" type="button" data-landmark="apex_r" aria-pressed="false" title="Click the part to place the apex r; drag its marker to move it">Apex R</button><label>X<input id="lm-apex_r-x" type="number" step="1" inputmode="decimal" aria-label="Apex R x (rig px)"></label><label>Y<input id="lm-apex_r-y" type="number" step="1" inputmode="decimal" aria-label="Apex R y (rig px)"></label><button id="lm-pick-apex_l" type="button" data-landmark="apex_l" aria-pressed="false" title="Click the part to place the apex l; drag its marker to move it">Apex L</button><label>X<input id="lm-apex_l-x" type="number" step="1" inputmode="decimal" aria-label="Apex L x (rig px)"></label><label>Y<input id="lm-apex_l-y" type="number" step="1" inputmode="decimal" aria-label="Apex L y (rig px)"></label><button id="lm-pick-cleavage" type="button" data-landmark="cleavage" aria-pressed="false" title="Click the part to place the cleavage; drag its marker to move it">Cleavage</button><label>X<input id="lm-cleavage-x" type="number" step="1" inputmode="decimal" aria-label="Cleavage x (rig px)"></label><label>Y<input id="lm-cleavage-y" type="number" step="1" inputmode="decimal" aria-label="Cleavage y (rig px)"></label></div><div id="bust-landmark-params" class="bust-group" data-chest-mode="landmarks" role="group" aria-label="Landmark constants" hidden><label title="Core ratio r / (r + band); the producer's default is 0.22">α<input id="lm-alpha" type="range" min="0.05" max="0.95" step="0.01" value="0.22" aria-label="Alpha, core ratio"><output id="lm-alpha-value" for="lm-alpha">0.22</output></label><label title="Cleavage clearance in rig px; the producer's default is 3.8">Clearance<input id="lm-clearance" type="range" min="0" max="40" step="0.1" value="3.8" aria-label="Clearance, rig px"><output id="lm-clearance-value" for="lm-clearance">3.8</output></label><label title="Control bone for bust_r; empty is the producer's default boob_r">Bone R<input id="lm-bone_r" type="text" list="lm-bones" placeholder="boob_r" spellcheck="false" autocomplete="off"></label><label title="Control bone for bust_l; empty is the producer's default boob_l">Bone L<input id="lm-bone_l" type="text" list="lm-bones" placeholder="boob_l" spellcheck="false" autocomplete="off"></label><datalist id="lm-bones"></datalist></div><span id="lm-derived" class="hint" data-chest-mode="landmarks" aria-live="polite" hidden></span><div class="bust-group" data-chest-mode="circles"><button id="bust-mirror" type="button" title="Copy R's y, core radius and band to L">Mirror R → L</button><button id="bust-download" type="button" title="Download chest-regions.json and its binding; evidence, not an approval">Download JSON</button></div><div class="bust-group"><button id="bust-apply" type="button" title="Run the installed producer (rig-parts remesh) on these regions or landmarks into a scratch build and load it here; the package is unchanged">Apply · Rebuild</button><button id="bust-reset" type="button" title="Return the stage to the inspected build" disabled>Reset</button></div><span id="handoff-line" class="hint handoff-line" aria-live="polite" title="Producer handoff: the config, parts and inspected rig an Apply rebuilds from"></span><span id="chest-editor-status" class="hint" aria-live="polite"></span><pre id="chest-editor-log" class="rebuild-log" aria-label="Producer output" hidden></pre><textarea id="rebuild-applied" class="sr-only" hidden aria-hidden="true" tabindex="-1"></textarea></div><div id="mesh-editor-bar" class="chest-editor-bar mesh-editor-bar" role="region" aria-label="Mesh edit" hidden><div class="bust-group" role="group" aria-label="Mesh edit sub-mode"><button id="mesh-mode-modify" type="button" data-mesh-mode="modify" aria-pressed="false" aria-keyshortcuts="V" title="Modify · key V">Modify<kbd aria-hidden="true">V</kbd></button><button id="mesh-mode-create" type="button" data-mesh-mode="create" aria-pressed="false" aria-keyshortcuts="C" title="Create · key C">Create<kbd aria-hidden="true">C</kbd></button><button id="mesh-mode-delete" type="button" data-mesh-mode="delete" aria-pressed="false" aria-keyshortcuts="X" title="Delete · key X">Delete<kbd aria-hidden="true">X</kbd></button><button id="mesh-mode-edges" type="button" data-mesh-mode="edges" aria-pressed="false" aria-keyshortcuts="E" title="Edges · key E">Edges<kbd aria-hidden="true">E</kbd></button></div><div class="bust-group"><button id="mesh-undo" type="button" aria-keyshortcuts="Control+Z Meta+Z" title="Undo the last action · Cmd/Ctrl-Z" disabled>Undo</button><button id="mesh-redo" type="button" aria-keyshortcuts="Control+Shift+Z Meta+Shift+Z" title="Redo · Shift-Cmd/Ctrl-Z" disabled>Redo</button><button id="mesh-reset" type="button" title="Drop the whole delta for this attachment (asks first)" disabled>Reset</button></div><div class="bust-group"><button id="mesh-download" type="button" title="Download mesh-edit.json and its binding; evidence, not an approval">Download JSON</button><button id="mesh-apply" type="button" title="Send the delta to the producer (rig-parts remesh --delta) and load the scratch revision in place; nothing is written into the package">Apply · Rebuild</button></div><textarea id="mesh-edit-draft" class="sr-only" hidden aria-hidden="true" tabindex="-1"></textarea><span id="mesh-editor-status" class="hint" aria-live="polite"></span></div><div id="stage-layout" class="stages"><section id="reference-pane" class="stage-pane" aria-label="Reference"><span id="reference-label" class="pane-label">Reference</span><div id="reference-viewport" class="viewport"><div id="reference-content" class="reference-content"></div><div id="reference-empty" class="empty">No reference supplied.</div></div></section><section id="rig-pane" class="stage-pane" aria-label="Rig"><span id="rig-label" class="pane-label">Generated rig · read only</span><div id="rig-viewport" class="viewport"><div id="rig-root"></div><canvas id="rig-overlay" class="rig-overlay" aria-label="Actual rig mesh and bone overlay"></canvas><div id="idle-handles" class="idle-handles" hidden aria-label="Chain amplitude handles"></div><div id="rig-fields-stage" class="rig-fields-stage" hidden aria-label="Rig field handles"></div><div id="rig-empty" class="empty">No compiled model supplied.</div></div></section></div><div class="stage-footer"><span id="coordinates">Drag to pan · scroll to zoom · cameras synchronized</span><span>Source files remain unchanged.</span></div><section class="inspector" aria-label="Inspection controls"><div class="inspector-heading sticky-header"><strong>INSPECTOR</strong><span class="hint">Local view controls · source files stay unchanged</span></div><div class="inspector-body"><section id="applied-records" class="applied-records" aria-labelledby="applied-records-heading" hidden><div class="row"><span id="applied-records-heading" class="eyebrow">APPLIED RECORDS</span><button id="applied-undo" type="button" disabled>Undo last Apply (0 deep)</button></div><ol id="applied-list" class="applied-list"></ol><p id="applied-status" class="hint" aria-live="polite"></p></section><section id="inspector-rig" aria-label="Setup" data-inspector-panel="rig"><nav class="block-row" aria-label="Setup blocks"><button type="button" data-block-target="selected-part">Part</button><button type="button" data-block-target="modeling-tools">Modeling</button><button type="button" data-block-target="chest-regions">Chest</button><button type="button" data-block-target="mesh-panel">Mesh</button><button type="button" data-block-target="weights">Weights</button><button type="button" data-block-target="bones-panel">Bones</button><button type="button" data-block-target="rig-fields">Fields</button><button type="button" data-block-target="cuts-panel">Cuts</button><button type="button" data-block-target="cloth-panel">Cloth</button></nav><section id="selected-part" aria-label="Selected part" aria-live="polite"></section><section id="modeling-tools"><div class="eyebrow">MODELING REVIEW</div><button data-proposal="mesh" class="tool">Mesh density <small>Add feedback</small></button><button data-proposal="contour" class="tool">Contour curvature <small>Add feedback</small></button><button data-proposal="chest" class="tool">Chest position <small>Add feedback</small></button><label for="part-set" class="label space-top">Part presets</label><select id="part-set" disabled><option>No sets supplied</option></select><p class="hint">Preview only · does not create motion keys.</p></section><section id="chest-regions" class="chest-regions" aria-labelledby="chest-regions-heading"><div class="eyebrow" id="chest-regions-heading">CHEST REGIONS · DETAIL</div><p class="hint">Crop pixels, x screen-right and y down (<code>family-chest-regions/1</code>). Place the circles with <strong>Chest regions</strong> in the view bar; this block holds names, bones, semi-axes, clip and the contour. Equal semi-axes export as a circle, unequal ones (or a clip) as the generator's 32-sample polygon. Exporting writes nothing into the package and is not an approval.</p><p id="chest-mapping" class="hint"></p><div class="chest-grid"><label>Crop width<input id="chest-crop-w" type="number" step="any" inputmode="decimal"></label><label>Crop height<input id="chest-crop-h" type="number" step="any" inputmode="decimal"></label></div><fieldset class="chest-row"><legend>Region 1</legend><label>Name<input id="chest-0-name" type="text" autocomplete="off" spellcheck="false"></label><label>Bone<select id="chest-0-bone"><option value="">Select bone</option></select></label><label>Centre x<input id="chest-0-cx" type="number" step="any" inputmode="decimal"></label><label>Centre y<input id="chest-0-cy" type="number" step="any" inputmode="decimal"></label><label>Semi-axis x<input id="chest-0-ax" type="number" step="any" inputmode="decimal"></label><label>Semi-axis y<input id="chest-0-ay" type="number" step="any" inputmode="decimal"></label><label>Spacing<input id="chest-0-spacing" type="number" step="any" inputmode="decimal"></label><label>Band<input id="chest-0-band" type="number" step="any" inputmode="decimal"></label><label>Medial clip x<input id="chest-0-clip" type="number" step="any" inputmode="decimal" placeholder="none"></label></fieldset><fieldset class="chest-row"><legend>Region 2</legend><label>Name<input id="chest-1-name" type="text" autocomplete="off" spellcheck="false"></label><label>Bone<select id="chest-1-bone"><option value="">Select bone</option></select></label><label>Centre x<input id="chest-1-cx" type="number" step="any" inputmode="decimal"></label><label>Centre y<input id="chest-1-cy" type="number" step="any" inputmode="decimal"></label><label>Semi-axis x<input id="chest-1-ax" type="number" step="any" inputmode="decimal"></label><label>Semi-axis y<input id="chest-1-ay" type="number" step="any" inputmode="decimal"></label><label>Spacing<input id="chest-1-spacing" type="number" step="any" inputmode="decimal"></label><label>Band<input id="chest-1-band" type="number" step="any" inputmode="decimal"></label><label>Medial clip x<input id="chest-1-clip" type="number" step="any" inputmode="decimal" placeholder="none"></label></fieldset><fieldset class="chest-row"><legend>Contour</legend><label>Tolerance<input id="chest-tolerance" type="number" step="any" inputmode="decimal"></label><label>Margin<input id="chest-margin" type="number" step="any" inputmode="decimal"></label><label>Spacing<input id="chest-spacing" type="number" step="any" inputmode="decimal"></label><label>Max edge<input id="chest-max-edge" type="number" step="any" inputmode="decimal"></label><label>Stray<input id="chest-stray" type="number" step="any" inputmode="decimal"></label><label class="chest-check"><input id="chest-bridges" type="checkbox">Bridges</label></fieldset><details class="chest-more"><summary>Generator and mesh fields</summary><label for="chest-generator" class="label space-top">Public generator</label><input id="chest-generator" type="text" autocomplete="off" spellcheck="false" placeholder="Generator that will read this file"><label for="chest-r" class="label space-top">Mesh r (optional)</label><input id="chest-r" type="number" step="any" inputmode="decimal"><label for="chest-segments" class="label space-top">Mesh segments JSON (optional)</label><textarea id="chest-segments" rows="3" spellcheck="false"></textarea><label for="chest-source-rule" class="label space-top">Source rule (optional)</label><input id="chest-source-rule" type="text" autocomplete="off"></details><p id="chest-status" class="hint" aria-live="polite"></p><button id="chest-export" type="button" class="outline block">Download chest-regions.json</button></section><section id="mesh-panel" class="physics-panel mesh-panel" aria-labelledby="mesh-panel-heading"><div class="row"><span id="mesh-panel-heading" class="eyebrow">MESH · APPLY REMESHES</span></div><p id="mesh-panel-state" class="hint">Select a mesh part in the parts list.</p><p class="hint">Edits the selected part's <code>meshes.<part></code> entry from the handoff config (the grid and contour modes). Apply sends the whole pending entry to the installed producer's <code>remesh --mesh</code> (one patch op at <code>/meshes/<part></code>), compiles and checks the revision, and reloads it with the mesh edges on; Reset returns to the inspected build and the config's entry. Slider ranges are review's; the number field takes any value, and the producer judges it.</p><div id="mesh-params" class="physics-controls mesh-params"></div><div class="row mesh-tools"><span class="eyebrow">REGIONS</span><button id="mesh-add-circle" type="button" disabled>Add circle</button><button id="mesh-add-polygon" type="button" aria-pressed="false" disabled>Draw polygon</button><button id="mesh-regions-edit" type="button" aria-pressed="false" disabled>Drag on stage</button></div><div id="mesh-regions" class="mesh-regions"></div><div class="row mesh-tools"><span class="eyebrow">SEGMENTS</span><select id="mesh-segment-bone" aria-label="Bone to add as a segment" disabled></select><button id="mesh-segment-add" type="button" disabled>Add</button></div><ol id="mesh-segments" class="physics-pending-list mesh-segments"></ol><div class="row"><span class="eyebrow">PENDING ENTRY · DIFF AGAINST THE CONFIG</span></div><ol id="mesh-diff" class="physics-pending-list"></ol><ul id="mesh-precheck" class="mesh-precheck" aria-label="Review pre-check in the config loader's names"></ul><div class="row"><button id="mesh-panel-apply" type="button" class="primary" disabled>Apply</button><button id="mesh-panel-reset" type="button" disabled>Reset</button><button id="mesh-folds" type="button" title="rigc render --folds of the drawn animation on the inspected build and on this revision (#108 readings)">Folds reading</button></div><output id="mesh-status" class="environment-readout"></output><div id="mesh-counts" class="physics-readout" aria-label="Vertex and triangle counts before and after"></div><div id="mesh-readout" class="physics-readout" aria-label="TEXTURE_STRETCH before and after Apply"></div><textarea id="mesh-pending" class="sr-only" hidden aria-hidden="true" tabindex="-1"></textarea></section><section id="weights" class="physics-panel weights-block" aria-labelledby="weights-heading"><div class="row"><span id="weights-heading" class="eyebrow">WEIGHTS · SETUP POSE · WATCH ONLY</span></div><p class="hint">The selected slot's mesh at the setup pose, each vertex coloured by the weight the compiled model document stores for the picked bone (0 → 1), or by its dominant bone under <strong>Max influence</strong>. An unweighted mesh is its slot's bone at 1.0. Hover a vertex for its stored values. Nothing here edits, applies or records a weight.</p><div class="row weights-controls"><button id="weights-show" type="button" aria-pressed="false">Show weights</button><label>Colour by<select id="weights-mode"><option value="bone">Bone weight</option><option value="max">Max influence</option></select></label><label>Bone<select id="weights-bone" disabled></select></label></div><p id="weights-state" class="hint" aria-live="polite"></p><div id="weights-legend" class="weights-legend" aria-label="Weights legend"></div><output id="weights-hover" class="environment-readout"></output></section><section id="bones-panel" class="physics-panel bones-panel" aria-labelledby="bones-panel-heading"><div class="row"><span id="bones-panel-heading" class="eyebrow">BONES · CONFIG ANCHORS · APPLY REBUILDS</span></div><p id="bones-panel-state" class="hint">Reading the producer handoff…</p><p class="hint">The producer config's bones at their <code>at</code>, chain <code>points</code> and <code>tip</code> (rig px, the setup pose; the stage may be posed by an animation). Drag a handle on the stage or edit the numbers: each move is a config patch op at <code>/bones/<i>/at</code>, <code>/bones/<i>/points/<k></code> or <code>/bones/<i>/tip</code> naming its bone, moved in whole rig px. <strong>Check</strong> runs <code>propose --from-config</code> on a scratch copy of the patched config (LINT and coverage lines; LINT blocks nothing) and <code>compare</code> against the handoff config. <strong>Apply</strong> rebuilds the whole rig (<code>rig --config</code>, then <code>check</code>) and needs the reproduced baseline; Reset returns to the inspected build and clears the pending ops. Adding, removing or re-parenting bones is out of scope.</p><div class="row bones-controls"><button id="bones-edit" type="button" aria-pressed="false" disabled>Edit bones on stage</button><label for="bones-select">Bone</label><select id="bones-select" disabled></select></div><div id="bones-anchor-editor" class="bones-anchor-editor"></div><div class="row"><span class="eyebrow">PENDING OPS</span></div><ol id="bones-pending-list" class="physics-pending-list"></ol><div class="row"><button id="bones-check" type="button" disabled>Check</button><button id="bones-apply" type="button" class="primary" disabled>Apply</button><button id="bones-reset-build" type="button" disabled>Reset</button></div><output id="bones-status" class="environment-readout"></output><div id="bones-compare" class="bones-lines" aria-label="compare: origin and tip distance per moved bone"></div><div id="bones-lint" class="bones-lines" aria-label="propose --from-config: LINT and coverage lines"></div><textarea id="bones-pending" class="sr-only" hidden aria-hidden="true" tabindex="-1"></textarea></section><section id="rig-fields" class="physics-panel rig-fields" aria-labelledby="rig-fields-heading"><div class="row"><span id="rig-fields-heading" class="eyebrow">RIG FIELDS · CONFIG · APPLY REBUILDS</span><span id="rig-fields-dial-label" class="idle-preview-label" hidden>preview — Apply decides</span></div><p id="rig-fields-state" class="hint">Reading the producer handoff…</p><p class="hint">Small producer config fields, each a config patch op: the bone a rigid part rides (<code>/regions/<part></code>), a lash's blink still row (<code>/motion/blink/still/<part></code>, dragged on the stage), a stop's inward / outward bounds (<code>/stops/<i></code>, arcs dragged at the pivot), a bone's inherit mode (<code>/bones/<i>/inherit</code>; a chain's <code>link_inherit</code>) and <code>turn</code>'s pivot and per-part depth (<code>/turn/pivot</code>, <code>/turn/depths/<part></code>). <strong>Apply</strong> rebuilds the whole rig (<code>rig --config</code>, then <code>check</code>) and needs the reproduced baseline; the producer's refusal is shown as printed. The dial only previews the drawn build's <code>turn</code> bone and is never recorded. Reset returns to the inspected build and clears the pending ops.</p><div id="rig-fields-region" class="rig-fields-group"></div><div id="rig-fields-still" class="rig-fields-group"></div><div id="rig-fields-stops" class="rig-fields-group"></div><div id="rig-fields-inherit" class="rig-fields-group"></div><div id="rig-fields-turn" class="rig-fields-group"></div><div class="row"><span class="eyebrow">PENDING OPS</span></div><ol id="rig-fields-pending-list" class="physics-pending-list"></ol><div class="row"><button id="rig-fields-apply" type="button" class="primary" disabled>Apply</button><button id="rig-fields-reset" type="button" disabled>Reset</button><button id="rig-fields-reading" type="button" disabled>Angle range</button></div><output id="rig-fields-status" class="environment-readout"></output><div id="rig-fields-readout" class="physics-readout" aria-label="BLINK_NO_HOLE and FEET_STAY_LEVEL before and after"></div><textarea id="rig-fields-pending" class="sr-only" hidden aria-hidden="true" tabindex="-1"></textarea></section><section id="cuts-panel" class="physics-panel bones-panel cuts-panel" aria-labelledby="cuts-panel-heading"><div class="row"><span id="cuts-panel-heading" class="eyebrow">CUTS · PATCHES · LIMBS · APPLY RUNS RIG-PARTS BUILD</span></div><p id="cuts-panel-state" class="hint">Reading the producer handoff…</p><p class="hint">The producer config's <code>assemble.cuts</code> polygons, <code>assemble.patches</code> boxes and each limb joint with its <code>range</code> arc, on the stage in rig px of the setup pose. Drag a vertex, a box corner or a joint, or edit the numbers: each change is a config patch op (<code>/assemble/cuts/<i>/polygon/<k></code>, <code>/assemble/patches/<i>/box</code>, <code>/limbs/<limb>/segments/<k>/range</code>; a joint is its bone's origin, <code>/bones/…</code>). A new cut or patch adds its entry and a <code>regions</code> bone. <strong>Apply</strong> runs <code>rig-parts build</code> (assemble → rig → check) from the declared painting and See-through runs into scratch and needs the reproduced baseline; the producer's refusals (<code>ASSEMBLE_CUT_*</code>, <code>ASSEMBLE_UNDER_*</code>, <code>CONFIG_LIMB_*</code>, <code>CONFIG_PART_UNIQUE</code>) are shown as printed, and the recomposite error map, the <code>seam pair</code> lines and the build's wall clock come back with the build. Review paints no <code>under</code> or <code>cap</code> image. Reset returns to the inspected build and clears the pending ops.</p><div class="row bones-controls"><button id="cuts-edit" type="button" aria-pressed="false" disabled>Edit cuts on stage</button><label for="cuts-select">Entry</label><select id="cuts-select" disabled></select></div><div id="cuts-editor" class="bones-anchor-editor"></div><div id="cuts-add" class="row cuts-add"><span class="eyebrow">ADD</span><select data-cuts-add="from" aria-label="Plan part the new cut takes pixels from"></select><input data-cuts-add="name" type="text" autocomplete="off" placeholder="new part" aria-label="New part name"><input data-cuts-add="bone" type="text" list="lm-bones" autocomplete="off" placeholder="bone" aria-label="Bone the new part's region rides"><select data-cuts-add="draw" aria-label="Where the new part is drawn"></select><input data-cuts-add="overlap" type="number" min="0" step="1" value="0" inputmode="numeric" aria-label="Cut overlap band in rig px"><select data-cuts-add="alpha" aria-label="New patch alpha"><option value="silhouette">silhouette</option><option value="box">box</option></select><button type="button" data-cuts-add="place-cut">Place cut</button><button type="button" data-cuts-add="place-patch">Place patch</button><button type="button" data-cuts-add="finish">Finish</button><button type="button" data-cuts-add="cancel">Cancel</button></div><div class="row"><span class="eyebrow">PENDING OPS</span></div><ol id="cuts-pending-list" class="physics-pending-list"></ol><div class="row"><button id="cuts-apply" type="button" class="primary" disabled>Apply</button><button id="cuts-reset" type="button" disabled>Reset</button></div><output id="cuts-status" class="environment-readout"></output><div id="cuts-readout" class="physics-readout" aria-label="The build's refusals, cut lines, recomposite, seam pairs and wall clock"></div><textarea id="cuts-pending" class="sr-only" hidden aria-hidden="true" tabindex="-1"></textarea></section><section id="cloth-panel" class="physics-panel bones-panel cloth-panel" aria-labelledby="cloth-panel-heading"><div class="row"><span id="cloth-panel-heading" class="eyebrow">CLOTH · RECIPE PER TAG · APPLY RUNS RIG-PARTS PROPOSE --CLOTH</span></div><p id="cloth-panel-state" class="hint">Reading the producer handoff…</p><p class="hint">The producer config's <code>cloth</code> block: one recipe per tag the installed reader accepts, with its fields as written. Each change is a config patch op at <code>/cloth/<tag>/…</code>. <strong>Show links</strong> draws the chains the tag's parts are meshed on, from the config's <code>points</code> and <code>tip</code> in rig px of the setup pose, and after an Apply the chains of the config the producer wrote. <strong>Apply</strong> runs <code>rig-parts propose --cloth --write-config</code> on the declared parts, painting and keypoints into scratch, then builds the written config (<code>rig-parts build</code> when the config declares <code>limbs</code>, else <code>rig --config</code>) and reloads it under the Environment's wind and gravity; the producer's refusals (<code>CONFIG_CLOTH_*</code>, <code>PROPOSE_CLOTH_DRAFTED</code>, <code>PROPOSE_CLOTH_PART</code>, <code>PROPOSE_WRITE_CONFIG_*</code>) are shown as printed. Review edits the recipe, never the drafted constraints. Reset returns to the inspected build and clears the pending ops.</p><div class="row bones-controls"><button id="cloth-links" type="button" aria-pressed="false" disabled>Show links on stage</button><label for="cloth-tag">Tag</label><select id="cloth-tag" disabled></select><label class="cloth-declared"><input id="cloth-declared" type="checkbox" disabled> recipe declared</label></div><div id="cloth-fields" class="cloth-fields"></div><div class="row"><span class="eyebrow">PENDING OPS</span></div><ol id="cloth-pending-list" class="physics-pending-list"></ol><div class="row"><button id="cloth-apply" type="button" class="primary" disabled>Apply</button><button id="cloth-reset" type="button" disabled>Reset</button></div><output id="cloth-status" class="environment-readout"></output><div id="cloth-readout" class="physics-readout" aria-label="Propose's refusals, the written config's diff, the route and check"></div><textarea id="cloth-pending" class="sr-only" hidden aria-hidden="true" tabindex="-1"></textarea></section></section><section id="inspector-playback" aria-label="Animate" data-inspector-panel="playback" hidden><nav class="block-row" aria-label="Animate blocks"><button type="button" data-block-target="transport" data-block-includes="frame-controls motion-tools angle-panel">Transport</button><button type="button" data-block-target="sound-panel">Sound</button><button type="button" data-block-target="environment">Environment</button><button type="button" data-block-target="physics-panel">Physics</button><button type="button" data-block-target="idle-tracks">Idle tracks</button><button type="button" data-block-target="readings">Readings</button><button type="button" data-block-target="requirements">Requirements</button><button type="button" data-block-target="retarget">Retarget</button><button type="button" data-block-target="report-panel">Report</button></nav><section id="transport" class="transport"><button id="play" aria-label="Play" disabled>▶</button><label for="animation-select" class="sr-only">Animation</label><select id="animation-select" disabled><option>No animations supplied</option></select><input id="scrub" type="range" min="0" max="1" step="0.001" value="0" aria-label="Playback time"><output id="time-label">0.0000 / 0.0000 s</output><output id="frame-label">rate unknown</output><label class="idle-repeat"><input id="idle-repeat" type="checkbox" checked>Repeat idle</label><div id="reading-ticks" class="reading-ticks" hidden aria-label="Reading ticks"></div><div id="idle-blink-strip" class="idle-blink-strip" hidden><button id="idle-blink-marker" type="button" class="idle-blink-marker" aria-label="Blink time">◆</button></div></section><section id="frame-controls" class="frame-controls" hidden aria-label="Frame navigation using the explicit clock"><label for="frame-number">Frame</label><input id="frame-number" type="number" min="0" step="1" inputmode="numeric" aria-label="Frame number to inspect"><button id="frame-jump" type="button">Go</button><input id="frame-scrub" type="range" min="0" max="1" step="1" value="0" aria-label="Scrub by frame"><span id="frame-clock-label" class="hint"></span><p id="frame-outside" class="hint" hidden>The current time is outside the explicit frame range. Enter a frame number to go to its exact time.</p></section><section id="motion-tools"><div class="eyebrow">MOTION · EDITS BUILD THROUGH THE PRODUCER</div><button data-proposal="pose" class="tool">Inspect pose <small>Add feedback</small></button><button data-proposal="timing" class="tool">Inspect interval <small>Add feedback</small></button><button data-proposal="audio" class="tool">Audio links <small>Add feedback</small></button><button data-proposal="angle" class="tool">Angle inspection <small id="axis-state">No inspection axes supplied</small></button></section><section id="angle-panel" class="angle-panel" hidden><label for="axis-select">Rig inspection axis</label><select id="axis-select"></select><label id="axis-label" for="axis-value"></label><input id="axis-value" type="range" disabled><output id="axis-value-label"></output><p class="hint">Maps one inspection axis to its animation time. This does not rotate the camera.</p></section><section id="sound-panel" class="sound-panel"><div class="row"><span class="eyebrow">SOUND CUES</span><label><input id="sync-audio" type="checkbox">Play cue audio in sync</label></div><div id="cues"></div><div class="row"><label for="audio-volume">Audio volume</label><input id="audio-volume" type="range" min="0" max="1" step="0.05" value="0.8"><output id="audio-volume-label">80%</output></div><div id="event-log" class="hint" aria-live="polite">Runtime events: none</div></section><section id="environment" class="environment-panel" aria-labelledby="environment-heading"><div class="row"><span id="environment-heading" class="eyebrow">ENVIRONMENT · INSPECTION ONLY</span></div><p class="hint">Wind, gravity and impulses go to the player while you inspect. Nothing is written to the model or motion; feedback records the state as context.</p><input id="environment-applied" type="checkbox" hidden aria-hidden="true" tabindex="-1"><div class="row"><label for="wind-base">Wind</label><input id="wind-base" type="range" min="-100" max="100" step="1" value="0" disabled><output id="wind-base-label">0</output></div><div class="row"><label><input id="wind-breeze" type="checkbox" disabled>Breeze</label><button id="wind-gust" type="button" disabled>Gust</button><label><input id="wind-repeat" type="checkbox" disabled>Repeat</label></div><div class="row"><label for="wind-gust-peak">Gust peak</label><input id="wind-gust-peak" type="number" min="0" max="200" step="1" value="48" inputmode="decimal" disabled><label for="wind-gust-duration">Duration (s)</label><input id="wind-gust-duration" type="number" min="0.1" max="20" step="0.1" value="2.4" inputmode="decimal" disabled></div><output id="wind-readout" class="environment-readout">No compiled model drawn · environment unavailable</output><div class="row"><label for="gravity">Gravity</label><input id="gravity" type="range" min="0" max="4" step="0.05" value="1" disabled><output id="gravity-label">1.00</output></div><div class="row"><button id="jolt" type="button" disabled>Jolt</button><label title="Dragging the rig pane moves the rig's physics instead of the camera. Hold Space to pan."><input id="drag-trail" type="checkbox" disabled>Drag to trail</label><button id="physics-reset" type="button" disabled>Reset physics</button></div><p class="hint">Drag to trail: dragging the rig pane tells the physics the rig moved; the camera stays. Hold Space to pan. Reset returns the runtime start values and re-poses from the current time, clearing the physics history.</p><div class="row"><label title="Readings and Apply's check read this wind and gravity as a rigc-environment/1 schedule; the wind clock restarts with the clip so you watch the same world"><input id="environment-use" type="checkbox" disabled>Use for readings</label><button id="environment-export" type="button" disabled>Export schedule</button><button id="environment-check" type="button" disabled>Check under this world</button></div><output id="environment-schedule" class="environment-readout">No schedule · the controls are not applied (the player stands at the runtime start values)</output><div id="environment-check-lines" class="environment-check-lines" aria-live="polite"></div><p class="hint">The schedule is the controls as a <code>rigc-environment/1</code> file from the clip's first frame: one step when the wind is steady, else one step per frame at 12 fps (between frames rigc holds the frame's value). A one-off Gust is not in it; Repeat puts gusts in it. Check under this world runs <code>rig-parts check --environment</code> on the build on the stage; the default-world line is that build's own check.</p></section><section id="physics-panel" class="physics-panel" aria-labelledby="physics-panel-heading"><div class="row"><span id="physics-panel-heading" class="eyebrow">PHYSICS TUNING · APPLY REBUILDS</span></div><p id="physics-panel-state" class="hint">No compiled model drawn.</p><p class="hint">Each edit is a config patch op at <code>/constraints/<i>/<field></code> naming its constraint. Apply runs the installed producer (<code>rig --config</code>, then <code>check</code>) into a scratch build and reloads it under the same wind, gust and jolt state; Reset returns to the inspected build and clears the pending ops. Live preview before Apply waits on play#147.</p><div class="row physics-chain-row"><label for="physics-chain">Chain</label><select id="physics-chain" disabled></select><button id="physics-chain-pick" type="button" aria-pressed="false" disabled>Pick on stage</button></div><div class="row physics-ramp-row"><label for="physics-ramp-field">Ramp</label><select id="physics-ramp-field" disabled></select><label for="physics-ramp-root">root</label><input id="physics-ramp-root" type="number" step="any" inputmode="decimal" disabled><label for="physics-ramp-tip">tip</label><input id="physics-ramp-tip" type="number" step="any" inputmode="decimal" disabled><button id="physics-ramp" type="button" disabled>Ramp</button></div><div class="physics-work"><div id="physics-rows" class="physics-table-wrap" role="region" aria-label="Physics constraints the config declares" tabindex="0"></div><div id="physics-row-editor" class="physics-row-editor"></div></div><div id="physics-locked" class="physics-table-wrap physics-locked" role="region" aria-label="Constraints that are not editable" tabindex="0" hidden></div><div class="row"><span class="eyebrow">PENDING OPS</span></div><ol id="physics-pending-list" class="physics-pending-list"></ol><div class="row"><button id="physics-apply" type="button" class="primary" disabled>Apply</button><button id="physics-reset-build" type="button" disabled>Reset</button></div><output id="physics-apply-status" class="environment-readout"></output><div id="physics-readout" class="physics-readout" aria-label="Check readout before and after Apply"></div><textarea id="physics-pending" class="sr-only" hidden aria-hidden="true" tabindex="-1"></textarea></section><section id="idle-tracks" class="physics-panel idle-panel" aria-labelledby="idle-tracks-heading"><div class="row"><span id="idle-tracks-heading" class="eyebrow">IDLE TRACKS · APPLY REBUILDS</span><span id="idle-preview-label" class="idle-preview-label" aria-live="polite" hidden>preview — Apply decides</span></div><p id="idle-tracks-state" class="hint">No compiled model drawn.</p><p class="hint">Each edit is a config patch op at <code>/motion/tracks/<i>/…</code> or <code>/motion/blink/…</code>. Period is offered only among the divisors of <code>motion.duration</code> (the producer refuses any other, <code>CONFIG_PERIOD_DIVIDES_DURATION</code>). While edits are pending the preview drives the edited bones through the player's <code>adjust</code> with the producer's sine, <code>v(t) = base + amp·sin(2π(t/period − phase))</code>, on the bone the idle keys (<code><bone>_ctl</code> under ctl keys); it is never recorded or submitted. Apply runs the installed producer (<code>rig --config</code>, then <code>check</code>) into a scratch build and replaces the preview with the built idle; Reset returns to the inspected build and clears the pending ops.</p><p id="idle-keys-line" class="hint"></p><div class="physics-work"><div id="idle-track-list" class="physics-table-wrap" role="region" aria-label="Idle tracks the config states" tabindex="0"></div><div id="idle-track-editor" class="physics-row-editor"></div></div><datalist id="idle-bones"></datalist><div class="row idle-add-row"><span class="eyebrow">ADD</span><input data-idle-add-bone type="text" list="idle-bones" autocomplete="off" placeholder="bone" aria-label="Bone for a new track" disabled><select data-idle-add-prop aria-label="Property for a new track" disabled></select><button data-idle-add-single type="button" disabled>Add track</button><select data-idle-add-chain aria-label="Chain for a new chain track" disabled></select><button data-idle-add-chain-button type="button" disabled>Add chain track</button></div><div id="idle-blink" class="idle-blink"></div><div class="row"><label class="idle-preview-toggle"><input data-idle-preview type="checkbox" checked>Preview edits (not recorded)</label></div><div id="idle-precheck" class="idle-precheck" aria-label="Review pre-check"></div><div class="row"><span class="eyebrow">PENDING OPS</span></div><ol id="idle-pending-list" class="physics-pending-list"></ol><div class="row"><button id="idle-apply" type="button" class="primary" disabled>Apply</button><button id="idle-reset" type="button" disabled>Reset</button></div><output id="idle-apply-status" class="environment-readout"></output><div id="idle-readout" class="physics-readout" aria-label="Check readout before and after Apply"></div><textarea id="idle-pending" class="sr-only" hidden aria-hidden="true" tabindex="-1"></textarea></section><section id="readings" class="readings-panel" aria-labelledby="readings-heading"><div class="row"><span id="readings-heading" class="eyebrow">READINGS · RIGC RENDER</span></div><p class="hint">Each reading is one <code>rigc render</code> of the drawn animation, on the inspected build and on the latest scratch build. Its frames become ticks under the timeline (<span class="tick-swatch inspected" aria-hidden="true"></span> inspected, <span class="tick-swatch scratch" aria-hidden="true"></span> scratch; click one to seek) and each pair gets its worst-frame line. A reading, not a gate: an unknown slot or bone is refused with rigc's own line.</p><datalist id="readings-slots"></datalist><datalist id="readings-bones"></datalist><div class="row"><label><input id="readings-folds" type="checkbox">Folds</label><label><input id="readings-overlaps" type="checkbox">Overlaps</label></div><div class="row readings-pair"><label><input id="readings-pass-on" type="checkbox">Pass-through</label><input id="readings-pass-cloth" type="text" list="readings-slots" autocomplete="off" placeholder="cloth slot" aria-label="Pass-through cloth slot"><span aria-hidden="true">:</span><input id="readings-pass-leg" type="text" list="readings-slots" autocomplete="off" placeholder="leg slot" aria-label="Pass-through leg slot"></div><div class="row readings-pair"><label><input id="readings-seam-on" type="checkbox">Seam</label><input id="readings-seam-a" type="text" list="readings-slots" autocomplete="off" placeholder="slot" aria-label="Seam first slot"><span aria-hidden="true">:</span><input id="readings-seam-b" type="text" list="readings-slots" autocomplete="off" placeholder="slot" aria-label="Seam second slot"></div><div class="row readings-pair"><label><input id="readings-angle-on" type="checkbox">Angle range</label><input id="readings-angle-bone" type="text" list="readings-bones" autocomplete="off" placeholder="bone" aria-label="Angle range bone"><span aria-hidden="true">vs</span><input id="readings-angle-relative" type="text" list="readings-bones" autocomplete="off" placeholder="relative bone" aria-label="Angle range relative-to bone"></div><div class="row"><button id="readings-run" type="button" disabled>Run readings</button></div><p id="readings-status" class="hint" aria-live="polite">No compiled model drawn.</p><ul id="readings-worst" class="readings-worst" aria-label="Worst frame per reading"></ul></section><section id="requirements" class="readings-panel requirements-panel" aria-labelledby="requirements-heading"><div class="row"><span id="requirements-heading" class="eyebrow">REQUIREMENTS · RIG-PARTS CHECK</span></div><p class="hint">Declare what the scene asks of the motion: a slot (picked on the stage), its mesh or a bone, an animation and a kind, with your own bar. Check writes a <code>spine-parts-requirements/1</code> file into scratch and runs the installed producer's <code>rig-parts check --requirements</code> on the inspected rig or the latest scratch build; each requirement gets one line with its worst frame (click to seek). A refusal is the producer's, by name. Waivers (<code>may_fold</code>), scene targets and the part exception lists are not written here. Submitted feedback carries the file by hash and content.</p><div id="requirements-body"></div></section><section id="retarget" class="readings-panel requirements-panel retarget-panel" aria-labelledby="retarget-heading"><div class="row"><span id="retarget-heading" class="eyebrow">RETARGET · RIG-POSE</span></div><p class="hint">Put a pose sequence the package declares onto this rig: edit the <code>rig-pose-map/1</code> (per chain, the bone that carries each segment), set the clean and retarget flags, and Apply. The installed <code>@rig-c/pose</code> runs <code>rig-pose clean</code> and <code>rig-pose retarget</code> onto the handoff rig, and its <code>rigc</code> compiles the result, all into scratch; the new animation plays here beside the reference (pick the sequence under Reference). Review writes no keys and never writes the package's <code>motion.json</code>. Refusals (<code>POSE_NOT_CLEANED</code>, <code>POSE_BEND_ONE_WAY</code>, <code>RAISED_HEAD</code>, <code>MOTION_NAME_FREE</code>, <code>MAP_*</code>) are the producer's, verbatim. FEET, STANCE, FLIP, SHORT, END-ON, LEG-SCALE and REACH lines seek to their frame.</p><div id="retarget-body"></div></section><section id="report-panel" class="report-panel" aria-labelledby="report-heading"><div class="report-header"><h2 id="report-heading">Report events</h2><span id="report-summary" class="subtle"></span><a id="report-source-link" target="_blank" rel="noopener" hidden>Source report</a></div><p id="report-unavailable" class="hint" hidden>No rig-pose-report/1 events supplied.</p><div id="report-browser"><div class="report-controls"><label for="report-category" class="sr-only">Report event category</label><select id="report-category"><option value="all">All events</option></select><button id="report-previous" type="button" aria-label="Previous event page">Previous</button><output id="report-page"></output><button id="report-next" type="button" aria-label="Next event page">Next</button></div><div id="report-empty-categories" class="report-empty-categories"></div><p id="report-missing" class="hint" hidden></p><ol id="report-events" class="report-event-list" aria-label="Read-only report events"></ol><section id="report-selection" class="report-selection" hidden aria-labelledby="report-selection-heading"><h3 id="report-selection-heading"></h3><p id="report-timing" class="hint"></p><div class="report-controls"><label id="report-time-label" for="report-time" hidden>Select explicit time</label><select id="report-time" hidden></select><button id="report-seek" type="button" disabled>Go to explicit time</button></div><div id="report-target-control" hidden><label for="report-target" class="label space-top">Select a target named in the report</label><select id="report-target"></select></div><p id="report-prefill-note" class="hint"></p><p id="report-provenance" class="hint"></p><details><summary>Source event details</summary><pre id="report-details"></pre></details></section></div></section></section></div></section></main></div><footer class="status-bar"><span id="message" class="status-message" role="status" aria-live="polite">Loading local review package…</span><div class="status-readouts" aria-label="Status"><span id="player-state" class="subtle">No rig supplied</span><span id="layout-state" class="subtle" role="status" aria-label="Effective layout">Loading inputs…</span><span id="renderer-state" class="subtle" aria-label="Last draw backend" title="DOM images for rigid parts; WebGL meshes when available, Canvas2D fallback.">Not rendered</span><span id="layer-state" class="hint">No rig supplied</span><span id="idle-state" class="hint" role="status">Idle preview unavailable</span><span id="control-state" class="hint" role="status"></span><div class="draft-notice"><span id="draft-state" role="status" aria-live="polite"></span><div id="draft-recovery"></div></div></div><div class="status-source"><details class="view-region"><summary>View region</summary><div class="view-region-body"><span class="hint">Numeric source region · opening this link restores a paused inspection</span><pre id="view-coordinates" class="numeric-view" aria-label="Current numeric inspection region"></pre><p class="hint">Sync cameras links fit-relative zoom and viewport-relative pan across visible panes. Turn it off to inspect each pane independently. Toolbar zoom and Fit use the last active pane. Hold Space while dragging a video to pan without taking over its native controls.</p></div></details><span id="source-hash" class="subtle"></span></div></footer><dialog id="review-dialog" class="review-dialog" aria-labelledby="review-dialog-title"><div class="review-dialog-header"><h2 id="review-dialog-title">Write review</h2><span class="hint">Drafts save locally as you type · nothing is submitted until you press Submit</span><button id="review-dialog-close" type="button">Close</button></div><section id="inspector-feedback" class="review-dialog-body" aria-label="Feedback"><div class="review-summary"><div class="eyebrow">REVIEW</div><label for="review-status" class="label">Review status</label><select id="review-status"><option value="pending">Pending</option><option value="approved">Approved</option><option value="changes-requested">Changes requested</option></select><section class="provenance" aria-label="Reviewed source"><div class="eyebrow">REVIEWED SOURCE</div><div class="provenance-row"><span>Package revision</span><strong id="provenance-version">—</strong></div><div class="provenance-row"><span>rig SHA-256</span><code id="provenance-rig">—</code></div><div class="provenance-row"><span>motion SHA-256</span><code id="provenance-motion">—</code></div><details><summary>Full provenance</summary><pre id="provenance-full"></pre></details><div class="source-files"><div class="eyebrow">SOURCE FILES · READ ONLY</div><div id="source-files"></div></div><p class="hint">Submits server-verified file hashes and tool versions. Your selected review status is preserved.</p></section><div class="divider"></div></div><div id="checkpoint-detail"><div class="eyebrow">SELECTED CHECKPOINT</div><h1 id="detail-title">Checkpoint</h1><p id="detail-note" class="hint"></p><fieldset class="outcome"><legend>Checkpoint outcome</legend><label><input type="radio" name="outcome" value="pending" checked>Pending</label><label><input type="radio" name="outcome" value="pass">Pass</label><label><input type="radio" name="outcome" value="fail">Needs changes</label></fieldset><label for="checkpoint-note" class="label space-top">Review notes</label><textarea id="checkpoint-note" rows="5" placeholder="Describe the part, conditions and what looks wrong."></textarea></div><div class="divider"></div><section id="proposal-panel"><div class="eyebrow">STRUCTURED FEEDBACK</div><div class="label">Frame range · inclusive</div><div class="frame-range"><label>Start<input id="feedback-start" type="number" min="0" step="1" inputmode="numeric" placeholder="e.g. 18"></label><label>End<input id="feedback-end" type="number" min="0" step="1" inputmode="numeric" placeholder="e.g. 32"></label></div><p id="feedback-range-hint" class="hint">Checking frame bounds…</p><label for="feedback-target-kind" class="label space-top">Target type</label><select id="feedback-target-kind"><option value="">Select target</option><option value="rig">Entire rig</option><option value="bone">Bone</option><option value="slot">Slot</option></select><label for="feedback-target-name" class="label space-top">Target name</label><input id="feedback-target-name" type="text" list="feedback-target-names" autocomplete="off" placeholder="Actual bone or slot name"><datalist id="feedback-target-names"></datalist><p id="feedback-target-hint" class="hint"></p><label for="feedback-category" class="label space-top">Category</label><select id="feedback-category"><option value="">Select category</option><option value="defect">Defect</option><option value="quality">Quality</option></select><label for="feedback-summary" class="label space-top">One-line summary</label><input id="feedback-summary" type="text" maxlength="1000" placeholder="e.g. The left wrist moves outside the sleeve"><button id="add-proposal" class="outline block">Add feedback</button><p id="proposal-boundary" class="hint">Records the current checkpoint, time, reference, camera and inspection axis. Motion is read only: inspect, mark ranges and export feedback. No keys or source files are edited.</p><p id="feedback-warnings" class="hint" role="status" hidden></p><p id="feedback-applied-check" class="hint error" role="alert" hidden></p><div id="proposal-list"></div></section><details id="strip-panel" class="report-panel"><summary>Export frame strip</summary><p class="hint">Uses the feedback form start and end frames, inclusive. Inspection time and source files remain unchanged.</p><label for="strip-scope" class="label">Export range</label><select id="strip-scope"><option value="interval">Entered start–end frames</option><option value="feedback">All feedback ranges</option></select><label for="strip-reference" class="label space-top">Reference video</label><select id="strip-reference"><option value="">Select reference video</option></select><label for="strip-format" class="label space-top">Format</label><select id="strip-format"><option value="png">PNG strip</option><option value="json">JSON frames and provenance</option></select><p class="hint">PNG requires frameTimes in the selected report. Without it, JSON preserves requested frames with time: null. Batch export requires every feedback item to match the selected animation. Unsubmitted feedback is included; exporting does not submit the review.</p><button id="strip-export" type="button" disabled>Download strip</button><p id="strip-status" class="hint" role="status" aria-live="polite">Loading package…</p></details></section><div class="review-dialog-actions"><span id="review-message" class="review-message" role="status" aria-live="polite"></span><span id="review-draft-state" class="review-draft-state" role="status" aria-live="polite"></span><button id="save" class="primary" disabled>Submit review</button></div></dialog></div>
|
|
10
10
|
<script type="module" src="main.js"></script>
|
|
11
11
|
</body></html>
|
package/web/main.ts
CHANGED
|
@@ -19,7 +19,7 @@ import {bindClothPanel,type ClothPanel} from './cloth-panel.ts';
|
|
|
19
19
|
import {retargetAppliedRecord,retargetOutcomeLine,type AppliedRetarget,type RetargetJobView,type RetargetRequestBody} from './rebuild-client.ts';
|
|
20
20
|
import {bindRetargetPanel,type PoseMap,type RetargetPanel} from './retarget-panel.ts';
|
|
21
21
|
import type {RetargetFlags} from '../src/retarget.ts';
|
|
22
|
-
import {appliedRecord,deltaAppliedRecord,deltaOutcomeLine,designatedRegions,handoffText,landmarksRequest,seedLandmarks,makePatch,outcomeLine,patchAppliedRecord,patchOutcomeLine,pickPart,runRebuild,seedFields,stageModel,type AppliedDelta,type AppliedPatch,type AppliedRecord,type DeltaJobView,type DeltaRequestBody,type ConfigPatch,type PatchJobView,type RebuildJobView,type RebuildRefusal,type RebuildRequestBody,type RebuildStatusView} from './rebuild-client.ts';
|
|
22
|
+
import {historyRow,recordFor,refusedText,undoControl,type AppliedHistoryView,type UndoView,type AnyJobView,appliedRecord,deltaAppliedRecord,deltaOutcomeLine,designatedRegions,handoffText,landmarksRequest,seedLandmarks,makePatch,outcomeLine,patchAppliedRecord,patchOutcomeLine,pickPart,runRebuild,seedFields,stageModel,type AppliedDelta,type AppliedPatch,type AppliedRecord,type DeltaJobView,type DeltaRequestBody,type ConfigPatch,type PatchJobView,type RebuildJobView,type RebuildRefusal,type RebuildRequestBody,type RebuildStatusView} from './rebuild-client.ts';
|
|
23
23
|
import {meshEditBinding,meshEditText,type MeshEditDocument} from '../src/mesh-edit.ts';
|
|
24
24
|
import {chestRegionsBinding,cropWorldMapping,type CropWorldMapping} from '../src/chest-regions.ts';
|
|
25
25
|
import {bindWorkspaceUI,selectedInspector,type WorkspaceUI} from './workspace-ui.ts';
|
|
@@ -1271,6 +1271,7 @@ function bindEnvironment():void {
|
|
|
1271
1271
|
button('wind-gust').onclick=()=>engage(()=>wind.triggerGust());
|
|
1272
1272
|
button('jolt').onclick=()=>{if(!player)return;joltQueue.push(...joltPacket(wind.read().time));if(!playing)message('Jolt queued · it reaches the physics on the next steps; press Play.');};
|
|
1273
1273
|
button('physics-reset').onclick=resetPhysics;
|
|
1274
|
+
button('applied-undo').onclick=()=>{void undoApply();};
|
|
1274
1275
|
input('environment-use').onchange=()=>{
|
|
1275
1276
|
if(!input('environment-use').checked){message('Use for readings is off: readings send the standing wind and gravity, Apply\'s check reads the default world.');renderSchedule();return;}
|
|
1276
1277
|
const world=worldFor(animation);
|
|
@@ -1441,7 +1442,60 @@ function renderHandoff(status:RebuildStatusView):void {
|
|
|
1441
1442
|
if(handoffTimer)clearTimeout(handoffTimer);handoffTimer=null;
|
|
1442
1443
|
if(status.baseline?.status==='running')handoffTimer=setTimeout(()=>{void api<RebuildStatusView>('/api/rebuild').then(next=>{rebuildStatus=next;renderHandoff(next);}).catch(()=>{});},2000);
|
|
1443
1444
|
}
|
|
1444
|
-
function rebuildState():void {chestEditor?.rebuildState({busy:rebuildBusy,applied:!!appliedOnStage});}
|
|
1445
|
+
function rebuildState():void {chestEditor?.rebuildState({busy:rebuildBusy,applied:!!appliedOnStage});renderApplied();if(!rebuildBusy)void refreshApplied();}
|
|
1446
|
+
/** #166: the session's applied records (the server's stack), the last one on the stage when the stage shows it. */
|
|
1447
|
+
let appliedHistory:AppliedHistoryView|null=null;
|
|
1448
|
+
async function refreshApplied():Promise<void> {
|
|
1449
|
+
if(!inspectedModel)return;
|
|
1450
|
+
try{appliedHistory=await api<AppliedHistoryView>('/api/rebuild/history');}catch{return;}
|
|
1451
|
+
renderApplied();
|
|
1452
|
+
}
|
|
1453
|
+
function renderApplied():void {
|
|
1454
|
+
const section=document.getElementById('applied-records');if(!section)return;
|
|
1455
|
+
const h=appliedHistory;section.hidden=!h||h.depth===0;
|
|
1456
|
+
const list=el('applied-list');list.replaceChildren(...(h?.records??[]).map((r,i)=>{const li=document.createElement('li');li.textContent=historyRow(h!,i);li.dataset.buildId=r.id;li.classList.toggle('on-stage',i===h!.records.length-1&&appliedOnStage?.buildId===r.id);return li;}));
|
|
1457
|
+
const control=undoControl(h,appliedOnStage?.buildId??null,rebuildBusy||!!readOnly),undo=button('applied-undo');
|
|
1458
|
+
undo.textContent=control.label;undo.disabled=!control.enabled;undo.title=control.title;
|
|
1459
|
+
}
|
|
1460
|
+
function appliedStatus(text:string,error=false):void {const node=el('applied-status');node.textContent=text;node.classList.toggle('error',error);}
|
|
1461
|
+
/**
|
|
1462
|
+
* #166: Undo last Apply — the last record leaves the server's stack and the stage returns to its predecessor's build
|
|
1463
|
+
* (kept on disk, or re-run from its record when evicted) or to the inspected build, under the same world. The feedback
|
|
1464
|
+
* context then carries the post-undo record (or null), never the reverted one. A refusal leaves the stage as it was.
|
|
1465
|
+
*/
|
|
1466
|
+
async function undoApply():Promise<UndoView|null> {
|
|
1467
|
+
const from=appliedOnStage?.buildId;
|
|
1468
|
+
if(rebuildBusy||readOnly||!from||!inspectedModel)return null;
|
|
1469
|
+
const world=stageWorld();
|
|
1470
|
+
rebuildBusy=true;rebuildState();appliedStatus('Undoing the last Apply…');
|
|
1471
|
+
try{
|
|
1472
|
+
let result=await api<UndoView>('/api/rebuild/undo',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({from})});
|
|
1473
|
+
if(result.status==='running'){
|
|
1474
|
+
// An evicted predecessor re-runs its recorded step; its lines stream like an Apply's.
|
|
1475
|
+
let job=result.job as AnyJobView;const undone=result.undone;let seen=0;
|
|
1476
|
+
while(job.status==='running'){await new Promise(resolve=>setTimeout(resolve,700));job=await api<AnyJobView>(`/api/rebuild/${job.id}`);for(;seen<job.lines.length;seen++)appliedStatus(job.lines[seen]!.trim()||'…');}
|
|
1477
|
+
if(job.status!=='built'){const text=refusedText(job);appliedStatus(text,true);message(text,true);return {status:'refused',error:text,producerLine:job.producerLine};}
|
|
1478
|
+
result={status:'built',undone,job,history:await api<AppliedHistoryView>('/api/rebuild/history')};
|
|
1479
|
+
}
|
|
1480
|
+
if(result.status==='refused'){appliedStatus(result.error,true);message(result.error,true);return result;}
|
|
1481
|
+
let ops:import('../src/config-patch.ts').PatchOp[]|null=null;
|
|
1482
|
+
if(result.status==='inspected'){await swapRig(inspectedModel.url,inspectedModel.prefix,inspectedModel.sha256,world);appliedOnStage=null;stageCheck=null;}
|
|
1483
|
+
else{
|
|
1484
|
+
const job=result.job;if(!job.build)throw new Error('the restored job carries no build');
|
|
1485
|
+
await swapRig(new URL(access.route(job.build.modelUrl),location.href).href,access.route(`/rebuild/${job.id}/`),job.build.modelHash,world);
|
|
1486
|
+
appliedOnStage=recordFor(job);stageCheck=(job as PatchJobView).kind==='patch'?{lines:[...job.lines],check:(job as PatchJobView).check}:null;
|
|
1487
|
+
if((job as PatchJobView).kind==='patch')ops=(job as PatchJobView).request.patch.ops;
|
|
1488
|
+
}
|
|
1489
|
+
// The panels mark the ops the stage's build applied; the pending edits stay as they are.
|
|
1490
|
+
physicsPanel?.applied(ops);idlePanel?.applied(ops);rigFieldsPanel?.applied(ops);bonesPanel?.applied(ops);
|
|
1491
|
+
const draft=el<HTMLTextAreaElement>('rebuild-applied');draft.value=appliedOnStage?JSON.stringify(appliedOnStage):'';draft.dispatchEvent(new Event('input',{bubbles:true}));
|
|
1492
|
+
appliedHistory=result.history;
|
|
1493
|
+
const text=`Undid ${result.undone.slice(0,12)}… · the stage is ${result.status==='inspected'?`the inspected build (model ${inspectedModel.sha256.slice(0,12)}…)`:`record ${result.history.depth} (model ${stageModelHash?.slice(0,12)}…)`} · ${result.history.depth} applied record(s) left`;
|
|
1494
|
+
appliedStatus(text);message(text);redrawOverlay(true);
|
|
1495
|
+
return result;
|
|
1496
|
+
}catch(error){const text=`UNDO_FAILED: ${errorText(error)}`;appliedStatus(text,true);message(text,true);return {status:'refused',error:text,producerLine:null};}
|
|
1497
|
+
finally{rebuildBusy=false;rebuildState();}
|
|
1498
|
+
}
|
|
1445
1499
|
/** Apply · Rebuild: the rows go to the installed producer; on success its scratch build replaces the drawn rig in place. */
|
|
1446
1500
|
async function applyRebuild():Promise<void> {
|
|
1447
1501
|
if(rebuildBusy)return;
|
|
@@ -1504,6 +1558,8 @@ async function applyPatch(patch:ConfigPatch,world:StageWorld|null=null):Promise<
|
|
|
1504
1558
|
/** The client's edit entry point for automation and the edit cards to come: apply a patch, return to the inspected build, read the stage. */
|
|
1505
1559
|
(window as unknown as {rigReviewEditor:unknown}).rigReviewEditor={
|
|
1506
1560
|
applyPatch,reset:()=>resetRebuild(),
|
|
1561
|
+
/** #166: undo the last Apply, and the applied records as the server holds them. */
|
|
1562
|
+
undo:()=>undoApply(),history:()=>appliedHistory?JSON.parse(JSON.stringify(appliedHistory)):null,
|
|
1507
1563
|
status:async()=>rebuildStatus??await loadRebuildStatus(),
|
|
1508
1564
|
physics:{rows:()=>physicsPanel?.rows()??[],chains:()=>physicsPanel?.chains()??[],edits:()=>physicsPanel?.edits()??[],set:(name:string,field:Parameters<PhysicsPanel['set']>[1],value:number)=>physicsPanel?.set(name,field,value),ramp:(id:string,field:Parameters<PhysicsPanel['set']>[1],root:number,tip:number)=>physicsPanel?.ramp(id,field,root,tip),select:(name:string)=>physicsPanel?.select(name),apply:()=>applyPhysics(physicsPanel?.edits().map(e=>({op:e.op,path:e.path,value:e.value,name:e.name}))??[]),reset:()=>resetPhysicsBuild()},
|
|
1509
1565
|
/** Inspection only: one slot's posed world geometry on the drawn frame (what the stage drew), for comparing against `rigc render --geometry`. */
|
package/web/rebuild-client.ts
CHANGED
|
@@ -22,7 +22,7 @@ export interface RetargetStatusView {available:boolean;reason:string|null;produc
|
|
|
22
22
|
export interface RebuildStatusView {retarget?:RetargetStatusView;available:boolean;handoff:HandoffView|null;baseline:BaselineView|null;inputs:{config:string;parts:string|null;rig:string|null;configHash:string}|null;parts:{name:string;regions:unknown[]}[];contour:Record<string,Record<string,unknown>>;meshes?:Record<string,unknown>;constraints?:unknown[]|null;bones?:unknown[]|null;fields?:Record<string,unknown>|null;cuts?:{config:Record<string,unknown>;build:string|null}|null;inspectedCheck?:InspectedCheckView|null;idle?:import('../src/idle-tracks.ts').IdleStatus|null;cloth?:ClothStatusView|null;producer:{package:string;version:string}|null;compiler:{package:string;version:string}|null;reason:string|null}
|
|
23
23
|
/** #111: the cloth recipe form's status — the reader's tags, the recipes as written, each tag's parts and chains, the keypoints, the route and why Apply cannot run. */
|
|
24
24
|
export interface ClothStatusView {targets:string[];targetsRead:boolean;recipes:Record<string,unknown>|null;tags:import('../src/cloth-recipe.ts').ClothTagView[];keypoints:{path:string;sha256:string}|null;route:'build'|'rig';reason:string|null}
|
|
25
|
-
export interface RebuildJobView {id:string;status:'running'|'built'|'refused'|'failed';reused:boolean;lines:string[];producerLine:string|null;exitCode:number|null;wallMs:{remesh:number|null;compile:number|null};build:{modelUrl:string;modelHash:string;files:Record<string,string>}|null;request:{part:string;regions:RegionEntry[];configHash:string;entrySha256:string;bust?:BustSpec};producer:{package:string;version:string};compiler:{package:string;version:string}}
|
|
25
|
+
export interface RebuildJobView {id:string;status:'running'|'built'|'refused'|'failed';reused:boolean;lines:string[];producerLine:string|null;hint?:string;exitCode:number|null;wallMs:{remesh:number|null;compile:number|null};build:{modelUrl:string;modelHash:string;files:Record<string,string>}|null;request:{part:string;regions:RegionEntry[];configHash:string;entrySha256:string;bust?:BustSpec};producer:{package:string;version:string};compiler:{package:string;version:string}}
|
|
26
26
|
export interface RebuildRefusal {status:'refused';error:string;producerLine:string|null}
|
|
27
27
|
/** #99 regions or #105 landmarks: exactly one of `regions` and `bust`. */
|
|
28
28
|
export type RebuildRequestBody={part:string;regions:RegionEntry[];configHash:string}|{part:string;bust:BustSpec;configHash:string};
|
|
@@ -118,11 +118,11 @@ export function parseApplied(text:string):AppliedRecord|AppliedPatch|AppliedDelt
|
|
|
118
118
|
/** A patch Apply's job (#102): the patch and its hash, the op/compile/check wall clocks, check's summary and the rig.json diff. */
|
|
119
119
|
/** #115: a build job's assemble readout (`AssembledView` in src/rebuild.ts). */
|
|
120
120
|
export type AssembledJobView=import('../src/rebuild.ts').AssembledView;
|
|
121
|
-
export interface PatchJobView {id:string;kind:'patch';op?:'rig'|'retune'|'remesh'|'build'|'cloth';assembled?:AssembledJobView;cloth?:import('../src/rebuild.ts').ClothJob;status:'running'|'built'|'refused'|'failed';reused:boolean;lines:string[];producerLine:string|null;exitCode:number|null;wallMs:{op:number|null;compile:number|null;check:number|null};check:CheckSummary|null;diff:{count:number;paths:string[]}|null;files?:FilesDiff|null;remesh?:import('../src/mesh-entry.ts').RemeshCounts&{modelTriangles:number|null}|null;build:{modelUrl:string;modelHash:string;files:Record<string,string>}|null;request:{patch:ConfigPatch;patchSha256:string;configHash:string};producer:{package:string;version:string};compiler:{package:string;version:string};environment?:{sha256:string;steps:number}}
|
|
121
|
+
export interface PatchJobView {id:string;kind:'patch';op?:'rig'|'retune'|'remesh'|'build'|'cloth';assembled?:AssembledJobView;cloth?:import('../src/rebuild.ts').ClothJob;status:'running'|'built'|'refused'|'failed';reused:boolean;lines:string[];producerLine:string|null;hint?:string;exitCode:number|null;wallMs:{op:number|null;compile:number|null;check:number|null};check:CheckSummary|null;diff:{count:number;paths:string[]}|null;files?:FilesDiff|null;remesh?:import('../src/mesh-entry.ts').RemeshCounts&{modelTriangles:number|null}|null;build:{modelUrl:string;modelHash:string;files:Record<string,string>}|null;request:{patch:ConfigPatch;patchSha256:string;configHash:string};producer:{package:string;version:string};compiler:{package:string;version:string};environment?:{sha256:string;steps:number}}
|
|
122
122
|
/** #112: `environment` is the exported `rigc-environment/1` schedule Apply's check reads, when "Use for readings" is on. */
|
|
123
123
|
export interface PatchRequestBody {patch:ConfigPatch;environment?:import('../src/environment-schedule.ts').EnvironmentSchedule}
|
|
124
124
|
/** Edit Mesh's delta Apply (#135): `remesh --delta`, the per-file diff and the part's counts; remesh's own gate holds check. */
|
|
125
|
-
export interface DeltaJobView {id:string;kind:'delta';op:'remesh';status:'running'|'built'|'refused'|'failed';reused:boolean;lines:string[];producerLine:string|null;exitCode:number|null;wallMs:{op:number|null;compile:number|null};files:FilesDiff|null;remesh:import('../src/mesh-entry.ts').RemeshCounts&{modelTriangles:number|null}|null;build:{modelUrl:string;modelHash:string;files:Record<string,string>}|null;request:{part:string;delta:Record<string,unknown>;deltaSha256:string;configHash:string};producer:{package:string;version:string};compiler:{package:string;version:string}}
|
|
125
|
+
export interface DeltaJobView {id:string;kind:'delta';op:'remesh';status:'running'|'built'|'refused'|'failed';reused:boolean;lines:string[];producerLine:string|null;hint?:string;exitCode:number|null;wallMs:{op:number|null;compile:number|null};files:FilesDiff|null;remesh:import('../src/mesh-entry.ts').RemeshCounts&{modelTriangles:number|null}|null;build:{modelUrl:string;modelHash:string;files:Record<string,string>}|null;request:{part:string;delta:Record<string,unknown>;deltaSha256:string;configHash:string};producer:{package:string;version:string};compiler:{package:string;version:string}}
|
|
126
126
|
/** `{part, delta, configHash}`: the server owns every path and flag; the delta is the editor's export as it stands. */
|
|
127
127
|
export interface DeltaRequestBody {part:string;delta:import('../src/mesh-edit.ts').MeshEditDocument;configHash:string}
|
|
128
128
|
/** A retarget Apply's job (#114): clean, retarget, compile; every FAIL line of the step that refused, verbatim; the listed lines. */
|
|
@@ -172,7 +172,7 @@ export const deltaAppliedRecord=(job:DeltaJobView):AppliedDelta=>({schema:APPLIE
|
|
|
172
172
|
/** A delta job's status line: built (wall clocks, counts, the per-file diff) or the producer's line verbatim. */
|
|
173
173
|
export function deltaOutcomeLine(result:DeltaJobView|RebuildRefusal):{text:string;error:boolean} {
|
|
174
174
|
if(!('id' in result))return {text:result.producerLine??result.error,error:true};
|
|
175
|
-
if(result.status!=='built')return {text:result
|
|
175
|
+
if(result.status!=='built')return {text:refusedText(result),error:true};
|
|
176
176
|
const ops=Array.isArray(result.request.delta.ops)?result.request.delta.ops.length:0;
|
|
177
177
|
const wall=result.reused?'reused the scratch build of this exact delta':`remesh --delta ${seconds(result.wallMs.op)} + compile ${seconds(result.wallMs.compile)}`;
|
|
178
178
|
const counts=result.remesh?` · ${result.remesh.part} v ${result.remesh.before.vertices} t ${result.remesh.before.triangles} → v ${result.remesh.after.vertices} t ${result.remesh.after.triangles}`:'';
|
|
@@ -186,11 +186,13 @@ export function stageModel(result:AnyJobView|RebuildRefusal,current:{url:string;
|
|
|
186
186
|
if('id' in result&&result.status==='built'&&result.build)return {url:result.build.modelUrl,hash:result.build.modelHash,id:result.id};
|
|
187
187
|
return {...current,id:null};
|
|
188
188
|
}
|
|
189
|
+
/** #148: a refused job's line — the producer's line as printed, then review's reading of it when the server added one. */
|
|
190
|
+
export const refusedText=(job:{producerLine:string|null;exitCode:number|null;hint?:string}):string=>`${job.producerLine??`REBUILD_FAILED: exit ${job.exitCode}`}${job.hint?` · review: ${job.hint}`:''}`;
|
|
189
191
|
const seconds=(ms:number|null)=>ms===null?'—':`${(ms/1000).toFixed(1)} s`;
|
|
190
192
|
/** A patch job's status line: built (wall clocks, check's verdict, the per-file diff — #125) or the producer's line verbatim. */
|
|
191
193
|
export function patchOutcomeLine(result:PatchJobView|RebuildRefusal):{text:string;error:boolean} {
|
|
192
194
|
if(!('id' in result))return {text:result.producerLine??result.error,error:true};
|
|
193
|
-
if(result.status!=='built')return {text:result
|
|
195
|
+
if(result.status!=='built')return {text:refusedText(result),error:true};
|
|
194
196
|
const check=result.check?(result.check.pass===null?`check refused: ${result.check.line??`exit ${result.check.exitCode}`}`:`check ${result.check.pass?'PASS':'FAIL'}${result.check.source==='retune'?' (retune\'s own check of this revision, not run again)':''}`):'check not run';
|
|
195
197
|
// #115: a build's own wall clock covers assemble, rig and check; the recorded figure is the same number.
|
|
196
198
|
const building=result.op==='build'||(result.op==='cloth'&&result.cloth?.route==='build');
|
|
@@ -201,7 +203,7 @@ export function patchOutcomeLine(result:PatchJobView|RebuildRefusal):{text:strin
|
|
|
201
203
|
/** The status line after a job: built (with the wall clock and whether the scratch build was reused) or the producer's line verbatim. */
|
|
202
204
|
export function outcomeLine(result:RebuildJobView|RebuildRefusal):{text:string;error:boolean} {
|
|
203
205
|
if(!('id' in result))return {text:result.producerLine??result.error,error:true};
|
|
204
|
-
if(result.status!=='built')return {text:result
|
|
206
|
+
if(result.status!=='built')return {text:refusedText(result),error:true};
|
|
205
207
|
const wall=[result.wallMs.remesh,result.wallMs.compile].every(v=>v!==null)?` · remesh ${(result.wallMs.remesh!/1000).toFixed(1)} s + compile ${(result.wallMs.compile!/1000).toFixed(1)} s`:'';
|
|
206
208
|
return {text:`Applied ${result.request.part} with ${result.producer.package} ${result.producer.version}${result.reused?' · reused the scratch build of this exact designation':wall} · Reset returns to the inspected build`,error:false};
|
|
207
209
|
}
|
|
@@ -219,3 +221,31 @@ export function handoffText(status:Pick<RebuildStatusView,'handoff'|'baseline'|'
|
|
|
219
221
|
const base=status.baseline?` · ${status.baseline.wallMs!==null&&status.baseline.status!=='running'?`${status.baseline.line} (${(status.baseline.wallMs/1000).toFixed(1)} s)`:status.baseline.line}`:' · baseline: waits for the producer';
|
|
220
222
|
return {text:`Handoff ${how}: ${h.config.path} sha256 ${h.config.sha256.slice(0,12)}…${parts}${base}`,error:!!status.baseline&&['not-reproduced','refused','failed'].includes(status.baseline.status)};
|
|
221
223
|
}
|
|
224
|
+
|
|
225
|
+
/** #166: `GET /api/rebuild/history` — the session's applied records in order; the last is on the stage; `kept`: its build is on disk. */
|
|
226
|
+
export interface AppliedHistoryView {schema:'rig-review-applied-history/1';depth:number;records:{id:string;kind:'regions'|'patch'|'delta'|'retarget';op:string;what:string;modelHash:string;kept:boolean}[]}
|
|
227
|
+
/** #166: `POST /api/rebuild/undo {from}` — the predecessor's build (kept, or re-run and still running), the inspected build, or a refusal. */
|
|
228
|
+
export type UndoView=
|
|
229
|
+
|{status:'inspected';undone:string;history:AppliedHistoryView}
|
|
230
|
+
|{status:'built';undone:string;job:AnyJobView;history:AppliedHistoryView}
|
|
231
|
+
|{status:'running';undone:string;job:AnyJobView}
|
|
232
|
+
|RebuildRefusal;
|
|
233
|
+
/** #166: the applied record a job carries into `context.applied` (the same shapes each Apply records). */
|
|
234
|
+
export function recordFor(job:AnyJobView):AppliedRecord|AppliedPatch|AppliedDelta|AppliedRetarget {
|
|
235
|
+
const kind=(job as {kind?:string}).kind;
|
|
236
|
+
return kind==='patch'?patchAppliedRecord(job as PatchJobView):kind==='delta'?deltaAppliedRecord(job as DeltaJobView):kind==='retarget'?retargetAppliedRecord(job as RetargetJobView):appliedRecord(job as RebuildJobView);
|
|
237
|
+
}
|
|
238
|
+
/** #166: one record's line in the list: its place, what it applied, the op and the model; the stage's last is marked. */
|
|
239
|
+
export function historyRow(history:AppliedHistoryView,index:number):string {
|
|
240
|
+
const r=history.records[index]!,last=index===history.records.length-1;
|
|
241
|
+
return `${index+1}. ${r.kind} (${r.op}) · ${r.what} · model ${r.modelHash.slice(0,12)}…${last?' · on the stage':r.kept?'':' · build evicted, an undo to it re-runs its step'}`;
|
|
242
|
+
}
|
|
243
|
+
/** #166: the Undo control's label and whether it can run: the depth, and what the stage returns to. */
|
|
244
|
+
export function undoControl(history:AppliedHistoryView|null,stageId:string|null,busy:boolean):{label:string;enabled:boolean;title:string} {
|
|
245
|
+
const depth=history?.depth??0,top=history?.records.at(-1)??null;
|
|
246
|
+
const label=`Undo last Apply (${depth} deep)`;
|
|
247
|
+
if(!top)return {label,enabled:false,title:'No Apply is on the stage'};
|
|
248
|
+
if(stageId!==top.id)return {label,enabled:false,title:'The stage does not show the last Apply (Reset shows the inspected build); undo acts on the last Apply only'};
|
|
249
|
+
const back=depth>1?`record ${depth-1} (model ${history!.records[depth-2]!.modelHash.slice(0,12)}…)`:'the inspected build';
|
|
250
|
+
return {label,enabled:!busy,title:`Return the stage to ${back}; the package is unchanged`};
|
|
251
|
+
}
|