@rig-c/review 0.4.0 → 0.5.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 +46 -12
- package/docs/DESIGN.md +10 -6
- package/package.json +1 -1
- package/src/chest-regions.ts +253 -0
- package/web/chest-regions-tool.ts +51 -0
- package/web/compare.css +1 -0
- package/web/index.html +3 -6
- package/web/main.ts +69 -38
- package/web/rig-overlays.ts +10 -2
- package/web/styles.css +78 -21
- package/web/view-controls.ts +11 -5
- package/web/workspace-layout.ts +2 -1
- package/web/workspace-ui.ts +23 -11
package/web/index.html
CHANGED
|
@@ -3,12 +3,9 @@
|
|
|
3
3
|
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>rig review</title><link rel="stylesheet" href="styles.css"></head>
|
|
4
4
|
<body>
|
|
5
5
|
<div id="app">
|
|
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="top-actions"><span id="dirty" class="subtle">Loading…</span><button id="
|
|
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
|
-
<aside class="left-panel"><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"><
|
|
9
|
-
<main><div class="viewbar"><div class="segmented" aria-label="View"><button data-view="compare" aria-pressed="true">Side by side</button><button data-view="reference" aria-pressed="false">Reference</button><button data-view="rig" aria-pressed="false">Rig</button></div><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="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><div class="view-options" aria-label="Inspection display controls"><label>Appearance <select id="appearance"><option value="system">System</option><option value="dark">Dark</option><option value="light">Light</option></select></label><label><input id="idle-repeat" type="checkbox" checked>Repeat idle</label><button id="copy-view" type="button">Copy view link</button></div></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 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="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"><strong>INSPECTOR</strong><div role="tablist" aria-label="Inspector sections">
|
|
10
|
-
<button role="tab" id="tab-rig" data-inspector="rig" aria-controls="inspector-rig" aria-selected="true" tabindex="0">Setup</button>
|
|
11
|
-
<button role="tab" id="tab-playback" data-inspector="playback" aria-controls="inspector-playback" aria-selected="false" tabindex="-1">Animate</button>
|
|
12
|
-
<button role="tab" id="tab-feedback" data-inspector="feedback" aria-controls="inspector-feedback" aria-selected="false" tabindex="-1">Feedback</button></div><span class="hint">Local view controls · source files stay unchanged</span></div><div class="inspector-body"><section id="inspector-rig" role="tabpanel" aria-labelledby="tab-rig" data-inspector-panel="rig"><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><button data-proposal="angle" class="tool">Angle inspection <small id="axis-state">No inspection axes supplied</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="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><section id="inspector-playback" role="tabpanel" aria-labelledby="tab-playback" data-inspector-panel="playback" hidden><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></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 · READ ONLY</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></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="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><section id="inspector-feedback" role="tabpanel" aria-labelledby="tab-feedback" data-inspector-panel="feedback" hidden><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><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></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></div>
|
|
8
|
+
<aside class="left-panel"><div class="left-header parts-header"><h2>PARTS</h2><label for="part-search">Find slot</label><input id="part-search" data-part-search type="search" autocomplete="off" placeholder="Exact slot name or text"></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. Type icons and main preview represent actual slots; 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="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="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 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="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"><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 · DESIGNATION ONLY</div><p class="hint">Crop pixels, x screen-right and y down (<code>family-chest-regions/1</code>). Numeric fields only; the rig pane draws each core 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><section id="inspector-playback" aria-label="Animate" data-inspector-panel="playback" hidden><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></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 · READ ONLY</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="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><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>
|
|
13
10
|
<script type="module" src="main.js"></script>
|
|
14
11
|
</body></html>
|
package/web/main.ts
CHANGED
|
@@ -3,8 +3,10 @@ import { loadModel, RigPlayer, type ModelAssets, type PoseBounds } from '@rig-c/
|
|
|
3
3
|
import {cameraFrame,cameraSignature,cameraFromSignature,assertCameraView,boundsFitSignature,dragSignature,zoomSignature,type CameraFrame,type CameraSignature} from './camera-view.ts';
|
|
4
4
|
import {rendererStatus} from './renderer-status.ts';
|
|
5
5
|
import {bindAppearance,bindPan,bindStageLayout,syncStageLayout,bindInspectorTabs,idleAvailable,initialIdleAnimation,automaticIdlePreview,bindLayerShortcuts,describeLayerToggles,type ViewPoint} from './view-controls.ts';
|
|
6
|
-
import {RigOverlays} from './rig-overlays.ts';
|
|
7
|
-
import {
|
|
6
|
+
import {RigOverlays,type OverlayRegion} from './rig-overlays.ts';
|
|
7
|
+
import {chestOverlayPolygons,chestRegionRows,chestRowId,evaluateChestForm} from './chest-regions-tool.ts';
|
|
8
|
+
import {chestRegionsBinding,cropWorldMapping,type CropWorldMapping} from '../src/chest-regions.ts';
|
|
9
|
+
import {bindWorkspaceUI,selectedInspector,type WorkspaceUI} from './workspace-ui.ts';
|
|
8
10
|
import {connectControl} from './control.ts';
|
|
9
11
|
import {pageInstanceAccess,readOnlyReason} from './instance-access.ts';
|
|
10
12
|
import {controlComparison,type ControlCommand,type ControlInspection,type ControlView} from '../src/control.ts';
|
|
@@ -74,6 +76,7 @@ let suspendedPreview=false;
|
|
|
74
76
|
let controlDispose:(()=>void)|null=null;
|
|
75
77
|
const viewDisposers:(()=>void)[]=[];
|
|
76
78
|
const layers={images:true,mesh:false,bones:false,meshOpacity:.65};
|
|
79
|
+
let chestMapping:CropWorldMapping={available:false,reason:'CHEST_REGION_MAPPING_UNAVAILABLE: no compiled model drawn'};
|
|
77
80
|
let referenceSize: { width: number; height: number } | null = null;
|
|
78
81
|
let video: HTMLVideoElement | null = null;
|
|
79
82
|
let controlledVideoSample:{media:HTMLVideoElement;position:number;time:number}|null=null;
|
|
@@ -94,6 +97,8 @@ function message(value: string, error = false): void {
|
|
|
94
97
|
text('message', value);
|
|
95
98
|
el('message').title = value; // the footer truncates long messages
|
|
96
99
|
el('message').classList.toggle('error', error);
|
|
100
|
+
// The modal makes the footer inert; the review dialog repeats the latest message for its reader.
|
|
101
|
+
text('review-message', value); el('review-message').classList.toggle('error', error);
|
|
97
102
|
}
|
|
98
103
|
|
|
99
104
|
async function api<T>(path: string, init?: RequestInit): Promise<T> {
|
|
@@ -183,6 +188,7 @@ function layoutEligibility(){return singleEligibility({rig:rigInput,reference:re
|
|
|
183
188
|
function renderLayout():void {
|
|
184
189
|
const state=layoutEligibility();effectiveView=state.effective;
|
|
185
190
|
for(const item of document.querySelectorAll<HTMLButtonElement>('[data-view]')){const layout=item.dataset.view as SingleLayout,reason=state.reasons[layout]??'';item.setAttribute('aria-pressed',String(layout===state.effective));item.disabled=!!reason;item.title=reason;if(reason)item.setAttribute('aria-description',reason);else item.removeAttribute('aria-description');}
|
|
191
|
+
const menuLabels={compare:'Side by side',reference:'Reference',rig:'Rig'};text('view-menu-label',menuLabels[state.effective??state.requested]);
|
|
186
192
|
el('reference-pane').hidden=state.effective!=='compare'&&state.effective!=='reference';el('rig-pane').hidden=state.effective!=='compare'&&state.effective!=='rig';
|
|
187
193
|
const shown={compare:'Side by side',reference:'Reference only',rig:'Rig only'};
|
|
188
194
|
text('layout-state',state.effective===null?`No input rendered · ${state.reasons.compare??''}`:state.effective===state.requested?shown[state.effective]:`${shown[state.effective]} · ${shown[state.requested]} unavailable: ${state.reasons[state.requested]??''}`);
|
|
@@ -575,7 +581,7 @@ function applyAxis(): void {
|
|
|
575
581
|
function renderProposals(): void {
|
|
576
582
|
const area = el('proposal-list'); area.replaceChildren();
|
|
577
583
|
const warnings = el('feedback-warnings'); warnings.hidden = session.feedbackWarnings.length === 0;
|
|
578
|
-
warnings.textContent = session.feedbackWarnings.length ? `${session.feedbackWarnings.length} validation warnings in existing feedback · original records are preserved. Review the marked entries, delete them and add valid replacements before submitting.
|
|
584
|
+
warnings.textContent = session.feedbackWarnings.length ? `${session.feedbackWarnings.length} validation warnings in existing feedback · original records are preserved. Review the marked entries, delete them and add valid replacements before submitting. The saved session that rig-review feedback exports keeps both the original records and warnings.` : '';
|
|
579
585
|
for (const feedback of session.feedback) {
|
|
580
586
|
const item = document.createElement('div'); item.className = 'proposal-item'; const title = document.createElement('strong'); title.textContent = `${feedback.category} · ${feedback.target.kind}:${feedback.target.name} · F${feedback.frameStart}–${feedback.frameEnd}`; const description = document.createElement('span'); description.textContent = feedback.summary;
|
|
581
587
|
item.append(title, description);
|
|
@@ -588,7 +594,7 @@ function renderProposals(): void {
|
|
|
588
594
|
}
|
|
589
595
|
|
|
590
596
|
function propose(tool: string): void {
|
|
591
|
-
if (tool === 'angle' && !input('axis-value').disabled) { showInspector('
|
|
597
|
+
if (tool === 'angle' && !input('axis-value').disabled) { showInspector('playback');chooseAxis(); return; }
|
|
592
598
|
const choices: Record<string, { target: Feedback['target']['kind']; hint: string }> = {
|
|
593
599
|
mesh: { target: 'slot', hint: 'One-line feedback on mesh density' }, contour: { target: 'slot', hint: 'One-line feedback on contour curvature' }, chest: { target: 'rig', hint: 'One-line feedback on chest position' },
|
|
594
600
|
angle: { target: 'rig', hint: 'One-line feedback on face or torso angle deformation' }, pose: { target: 'bone', hint: 'One-line feedback on differences from the input pose' }, timing: { target: 'rig', hint: 'One-line feedback on timing in the selected range' }, audio: { target: 'rig', hint: 'One-line feedback on event or audio links' },
|
|
@@ -599,10 +605,10 @@ function propose(tool: string): void {
|
|
|
599
605
|
showInspector('feedback');input('feedback-summary').focus(); message('Specify a frame range and actual bone or slot name to add feedback. Motion keys are not edited.');
|
|
600
606
|
}
|
|
601
607
|
|
|
608
|
+
|
|
602
609
|
function download(name: string, content: string, mime: string): void {
|
|
603
610
|
const url = URL.createObjectURL(new Blob([content], { type: mime })); const anchor = document.createElement('a'); anchor.href = url; anchor.download = name; anchor.click(); setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
604
611
|
}
|
|
605
|
-
|
|
606
612
|
function downloadStrip(name: string, content: Blob): void {
|
|
607
613
|
const url = URL.createObjectURL(content), link = document.createElement('a'); link.href = url; link.download = name; link.click(); setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
608
614
|
}
|
|
@@ -651,26 +657,6 @@ async function exportStrip(): Promise<void> {
|
|
|
651
657
|
finally { stripBusy = false; updateStripControls(); }
|
|
652
658
|
}
|
|
653
659
|
|
|
654
|
-
const markdown = (value: string) => value.replace(/[\\`*_{}\[\]<>]/g, '\\$&');
|
|
655
|
-
function issueFields(feedback: Feedback): { title: string; body: string; labels: string[] } {
|
|
656
|
-
const title = `[${feedback.category}] ${feedback.target.kind}:${feedback.target.name} F${feedback.frameStart}–${feedback.frameEnd} · ${feedback.summary}`;
|
|
657
|
-
const body = [
|
|
658
|
-
`- Package: ${markdown(session.id)} / ${markdown(session.revision)}`,
|
|
659
|
-
`- Source hash: ${session.sourceHash}`, ...(session.referenceBinding?[`- Reference binding: ${JSON.stringify(session.referenceBinding)}`]:[]),
|
|
660
|
-
`- Package revision: ${markdown(session.reviewedSource.packageRevision)}`,
|
|
661
|
-
`- Viewer version: ${markdown(session.reviewedSource.viewerVersion)}`,
|
|
662
|
-
`- Runtime version: ${markdown(session.reviewedSource.runtimeVersion)}`,
|
|
663
|
-
`- Rig: ${session.reviewedSource.rig ? `${markdown(session.reviewedSource.rig.path)} / SHA-256 ${session.reviewedSource.rig.sha256}` : 'not supplied'}`,
|
|
664
|
-
`- Motion: ${session.reviewedSource.motion ? `${markdown(session.reviewedSource.motion.path)} / SHA-256 ${session.reviewedSource.motion.sha256}` : 'not supplied'}`,
|
|
665
|
-
`- Frames: ${feedback.frameStart}–${feedback.frameEnd} (inclusive; producer frame numbers; no inferred FPS)`,
|
|
666
|
-
`- Target: ${feedback.target.kind} / ${markdown(feedback.target.name)}`,
|
|
667
|
-
`- Category: ${feedback.category}`, '', markdown(feedback.summary), '', 'Inspection context:',
|
|
668
|
-
...JSON.stringify(feedback.context ?? null, null, 2).split('\n').map(line => ' ' + line),
|
|
669
|
-
'', 'Validation warnings (must be corrected before submission):',
|
|
670
|
-
...JSON.stringify(session.feedbackWarnings.filter(warning => warning.id === feedback.id), null, 2).split('\n').map(line => ' ' + line),
|
|
671
|
-
].join('\n');
|
|
672
|
-
return { title, body, labels: [feedback.category] };
|
|
673
|
-
}
|
|
674
660
|
|
|
675
661
|
function paneFrame(target:CameraTarget,hidden=false):CameraFrame|null {
|
|
676
662
|
const viewport=el(target+'-viewport'),pane=el(target+'-pane');
|
|
@@ -708,8 +694,12 @@ function zoomViewport(target:CameraTarget,deltaY:number):void {zoomCamera(deltaY
|
|
|
708
694
|
|
|
709
695
|
function bind(): void {
|
|
710
696
|
button('strip-export').onclick = () => { void exportStrip(); };
|
|
697
|
+
button('chest-export').onclick = () => { void exportChest(); };
|
|
698
|
+
for (const event of ['input', 'change']) el('chest-regions').addEventListener(event, renderChest);
|
|
699
|
+
// A discarded local draft restores the form after its button's own handler; re-read the tool then.
|
|
700
|
+
el('draft-recovery').addEventListener('click', () => renderChest());
|
|
711
701
|
for (const id of ['strip-scope', 'strip-reference', 'strip-format']) select(id).onchange = () => stripStatus('Selection changed. Check the ranges, animation and reference video before downloading.');
|
|
712
|
-
for (const item of document.querySelectorAll<HTMLButtonElement>('[data-view]')) item.onclick = () => setView(item.dataset.view as typeof view);
|
|
702
|
+
for (const item of document.querySelectorAll<HTMLButtonElement>('[data-view]')) item.onclick = () => {setView(item.dataset.view as typeof view);const menu=document.getElementById('view-menu') as HTMLDetailsElement|null;if(menu)menu.open=false;};
|
|
713
703
|
for (const item of document.querySelectorAll<HTMLButtonElement>('[data-proposal]')) item.onclick = () => propose(item.dataset.proposal ?? '');
|
|
714
704
|
select('reference-select').onchange = () => { void showReference(select('reference-select').value); };
|
|
715
705
|
select('person-select').onchange = () => { selectedPerson = select('person-select').value; lastPoseFrame = -1; renderSequenceFrame(); };
|
|
@@ -775,12 +765,10 @@ function bind(): void {
|
|
|
775
765
|
message(dirty ? 'Changes were made during submission. Submit again.' : 'Submitted the selected review status, feedback and source provenance.');
|
|
776
766
|
} catch (error) { message(`Submission failed: ${errorText(error)}`, true); } finally { saving = false; updateSavedState(); }
|
|
777
767
|
};
|
|
778
|
-
button('export').onclick = () => { download(`${session.id}-feedback.json`, JSON.stringify({ schema: 'rig-review-feedback/1', id: session.id, revision: session.revision, sourceHash: session.sourceHash, referenceBinding:session.referenceBinding, reviewedSource: session.reviewedSource, status: session.status, checkpoints: session.checkpoints, feedback: session.feedback, feedbackWarnings: session.feedbackWarnings, issues: session.feedback.map(issueFields) }, null, 2) + '\n', 'application/json'); message('Exported source provenance, feedback, validation warnings and issue fields as JSON. Source files remain unchanged.'); };
|
|
779
|
-
button('export-markdown').onclick = () => { const content = [`# ${markdown(session.id)} review feedback`, '', 'Reviewed source:', ...JSON.stringify({ sourceHash: session.sourceHash, referenceBinding:session.referenceBinding, reviewedSource: session.reviewedSource }, null, 2).split('\n').map(line => ' ' + line), '', 'Validation warnings:', ...JSON.stringify(session.feedbackWarnings, null, 2).split('\n').map(line => ' ' + line), '', ...session.feedback.flatMap(item => { const issue = issueFields(item); return [`## ${markdown(issue.title)}`, '', issue.body, '', '---', '']; })].join('\n'); download(`${session.id}-issues.md`, content, 'text/markdown'); message('Exported source provenance, validation warnings and structured issue content as Markdown.'); };
|
|
780
768
|
// Saved browser defaults seed a new inspection; explicit links and source drafts restore over them.
|
|
781
|
-
workspace=bindWorkspaceUI({changed:fitCamera,defaultTheme:theme=>appearance?.set(theme)});
|
|
769
|
+
workspace=bindWorkspaceUI({changed:fitCamera,defaultTheme:theme=>appearance?.set(theme),currentTheme:()=>appearance?.get()});
|
|
782
770
|
cameraSync=workspace.defaults().defaultCameraSync;input('camera-sync').checked=cameraSync;input('idle-repeat').checked=workspace.defaults().defaultIdleRepeat;
|
|
783
|
-
appearance=bindAppearance(select('
|
|
771
|
+
appearance=bindAppearance(select('settings-theme'),workspace.defaults().theme);
|
|
784
772
|
overlays=new RigOverlays({canvas:el<HTMLCanvasElement>('rig-overlay'),rigRoot:el('rig-root')});
|
|
785
773
|
viewDisposers.push(bindStageLayout(el('stage-layout'),fitCamera));
|
|
786
774
|
for(const target of ['rig','reference'] as const)viewDisposers.push(bindPan(el(target+'-viewport'),(dx,dy)=>cameraAction(()=>panCamera(target,dx,dy))));
|
|
@@ -788,7 +776,7 @@ function bind(): void {
|
|
|
788
776
|
viewDisposers.push(bindLayerShortcuts(document));describeLayerToggles(document,el('layer-state').textContent??'');
|
|
789
777
|
input('idle-repeat').onchange=()=>{renderIdle();if(canRepeatIdle())void playPreview();};
|
|
790
778
|
for(const id of ['proposal-panel','checkpoint-detail'])el(id).addEventListener('focusin',pause);
|
|
791
|
-
bindInspector();
|
|
779
|
+
bindInspector();bindReviewDialog();
|
|
792
780
|
document.addEventListener('visibilitychange',()=>{if(document.hidden){const resume=playing;pause();suspendedPreview=resume;}else if(suspendedPreview){suspendedPreview=false;void playPreview();}});
|
|
793
781
|
const resize = new ResizeObserver(fitCamera); resize.observe(el('rig-viewport')); resize.observe(el('reference-viewport'));
|
|
794
782
|
window.addEventListener('beforeunload', event => { if (dirty) { event.preventDefault(); event.returnValue = ''; } });
|
|
@@ -808,12 +796,13 @@ async function start(): Promise<void> {
|
|
|
808
796
|
// Checkpoints and reference fold away when the package supplies neither.
|
|
809
797
|
(el('package-context') as HTMLDetailsElement).open = data.manifest.checkpoints.length > 0 || data.manifest.references.length > 0;
|
|
810
798
|
const sourceFiles = el('source-files'); for (const file of data.manifest.authoring) { const a = document.createElement('a'); a.href = assetUrl(file.path); a.textContent = file.label; a.className = 'source-link'; a.target = '_blank'; a.rel = 'noopener'; sourceFiles.append(a); } if (!data.manifest.authoring.length) sourceFiles.textContent = 'No source files supplied';
|
|
811
|
-
bind(); parts=new PartExplorer(el('
|
|
799
|
+
populateChestBones(); bind(); parts=new PartExplorer(el('workspace-left'),el('selected-part'),previous=>{redrawOverlay(true);const part=parts?.value();if(part){activeCamera='rig';if(view==='reference')setView('rig');showInspector('rig');renderCameraControls();}if(!cameraRestoring&&(previous?.side!==part?.side||previous?.slot!==part?.slot))cameraAction(()=>fitActiveCamera(part?'rig':availableCamera()));persistence?.save();},part=>{select('feedback-target-kind').value='slot';renderFeedbackTargets(true);input('feedback-target-name').value=part.slot;showInspector('feedback');}); updateStripControls(); stripStatus('Choose the entered start–end frames or all feedback ranges to download. Unsubmitted feedback is included; exporting does not submit the review.'); renderFeedbackTargets(true); renderFeedbackRange(); renderCheckpoints(); renderProposals(); renderProvenance(); updateSavedState(); renderCues(); initReport();
|
|
812
800
|
const requestedId = new URLSearchParams(location.search).get('checkpoint'),initial=initialReviewSelection(data.manifest,requestedId,data.explicitReference?.id,data.initialAnimation??undefined),first=initial.checkpoint;
|
|
813
801
|
rigInput=data.manifest.model?{state:'pending'}:{state:'absent'};renderLayout();
|
|
814
802
|
if(initial.referenceId){select('reference-select').value=initial.referenceId;await showReference(initial.referenceId);}
|
|
815
803
|
await initPlayer(first);
|
|
816
804
|
// initPlayer keeps `player` only after its first draw succeeded; a declared model path alone never counts.
|
|
805
|
+
updateChestMapping();
|
|
817
806
|
rigInput=player?{state:'loaded'}:data.manifest.model?{state:'failed',reason:el('rig-empty').textContent||'compiled player unavailable'}:{state:'absent'};renderLayout();
|
|
818
807
|
if (first) await chooseCheckpoint(first,initial); else renderCheckpoint();
|
|
819
808
|
activeCamera=player?'rig':'reference';if(cameraSync)alignCameras();renderTransport();fitCamera();
|
|
@@ -821,17 +810,27 @@ async function start(): Promise<void> {
|
|
|
821
810
|
else if (readOnly) message(readOnly, true);
|
|
822
811
|
else message(data.manifest.model ? 'Record review status and structured feedback while preserving sources. Motion is read only.' : 'No rig supplied. Review the available references and checkpoints.');
|
|
823
812
|
requestAnimationFrame(tick);
|
|
824
|
-
const restored=await setupPersistence();renderIdle();if(!restored&&canRepeatIdle()&&automaticIdlePreview(assets?.animations.map(item=>item.name)??[],animation,first,!!requestedId))void playPreview();
|
|
813
|
+
const restored=await setupPersistence();renderChest();renderIdle();if(!restored&&canRepeatIdle()&&automaticIdlePreview(assets?.animations.map(item=>item.name)??[],animation,first,!!requestedId))void playPreview();
|
|
825
814
|
if(!readOnly)controlDispose=connectControl({sourceHash:data.sourceHash,mode:'single',route:access.route,headers:()=>access.headers(),observe:controlState,apply:applyControl,onStatus:value=>text('control-state',value)});
|
|
826
815
|
} catch (error) { message(`Could not open review package: ${errorText(error)}`, true); text('dirty', 'Load failed'); }
|
|
827
816
|
}
|
|
828
|
-
|
|
829
|
-
function
|
|
817
|
+
/** `feedback` is the review dialog, not an Inspector section: a user action opens it; a restored older bookmark that saved it selects nothing. */
|
|
818
|
+
function showInspector(name:string,reveal=true):void {if(name==='feedback'){if(reveal)openReview();return;}if(reveal)workspace?.revealInspector();document.querySelector<HTMLButtonElement>(`[data-inspector="${name}"]`)?.click();}
|
|
819
|
+
function bindInspector():void {bindInspectorTabs(()=>{workspace?.afterTab();fitCamera();},()=>workspace?.beforeTab());}
|
|
820
|
+
/** Opening the review pauses playback, as selecting Feedback did. Its inputs stay in the document whether or not it is open, so the draft keeps them. */
|
|
821
|
+
function openReview():void {const dialog=el<HTMLDialogElement>('review-dialog');for(const drawer of document.querySelectorAll<HTMLDialogElement>('dialog.workspace-drawer[open]'))drawer.close();pause();if(!dialog.open)dialog.showModal();}
|
|
822
|
+
function bindReviewDialog():void {
|
|
823
|
+
const dialog=el<HTMLDialogElement>('review-dialog');
|
|
824
|
+
button('write-review').onclick=openReview;button('review-dialog-close').onclick=()=>dialog.close();
|
|
825
|
+
// Escape closes the native modal; every close returns focus to the button that opens it.
|
|
826
|
+
dialog.addEventListener('close',()=>{button('write-review').setAttribute('aria-expanded','false');button('write-review').focus();});
|
|
827
|
+
dialog.addEventListener('toggle',()=>{if(dialog.open)button('write-review').setAttribute('aria-expanded','true');});
|
|
828
|
+
}
|
|
830
829
|
|
|
831
830
|
function redrawOverlay(fresh:boolean):void {
|
|
832
831
|
if(!overlays||!player)return;
|
|
833
832
|
const root=el('rig-root'),project=(x:number,y:number)=>({x:parseFloat(root.style.left||'0')+x*rigScale,y:parseFloat(root.style.top||'0')-y*rigScale});
|
|
834
|
-
const value=fresh?overlays.refresh({player,frame:parts?.frame('single'),selectedSlot:parts?.value()?.slot,bones:(data.feedbackCatalog.bones??[]).map(name=>({name})),project,settings:layers}):overlays.redraw({project,settings:layers});
|
|
833
|
+
const value=fresh?overlays.refresh({player,frame:parts?.frame('single'),selectedSlot:parts?.value()?.slot,bones:(data.feedbackCatalog.bones??[]).map(name=>({name})),project,settings:layers,regions:chestRegions()}):overlays.redraw({project,settings:layers,regions:chestRegions()});
|
|
835
834
|
text('layer-state',value.warnings.join(' · ')||'Actual last-drawn mesh edges / bone points');if(!fresh&&value.needsCapture&&!playing&&!seekingControl)drawRig(()=>player!.draw());
|
|
836
835
|
}
|
|
837
836
|
function canRepeatIdle():boolean{return !!player&&idleAvailable(assets?.animations.map(a=>a.name)??[],animation)&&!axisInspection&&input('idle-repeat').checked&&duration>0;}
|
|
@@ -908,7 +907,7 @@ function numericView():ControlView {
|
|
|
908
907
|
}
|
|
909
908
|
function renderNumericView():void {const node=document.getElementById('view-coordinates');if(node)node.textContent=JSON.stringify(numericView(),null,2);}
|
|
910
909
|
function inspectionBinding():InspectionBinding{return {mode:'single',sourceHash:data.sourceHash,rigs:[data.reviewedSource.rig?.sha256??null]};}
|
|
911
|
-
function inspectionBookmark():InspectionBookmark {return {schema:'rig-review-inspection/1',binding:inspectionBinding(),animation:animation||null,time:player?.time??time,theme:appearance?.get()??'system',layers:{images:layers.images,meshes:layers.mesh,bones:layers.bones},view:numericView(),selection:{part:parts?.value()??null,view,checkpoint:checkpoint?.id??null,reference:select('reference-select').value||null,person:selectedPerson||null,inspector:
|
|
910
|
+
function inspectionBookmark():InspectionBookmark {return {schema:'rig-review-inspection/1',binding:inspectionBinding(),animation:animation||null,time:player?.time??time,theme:appearance?.get()??'system',layers:{images:layers.images,meshes:layers.mesh,bones:layers.bones},view:numericView(),selection:{part:parts?.value()??null,view,checkpoint:checkpoint?.id??null,reference:select('reference-select').value||null,person:selectedPerson||null,inspector:selectedInspector()??'view',partSet:select('part-set').disabled?null:select('part-set').value,axis:axisInspection?{id:select('axis-select').value,value:Number(input('axis-value').value)}:null}};}
|
|
912
911
|
async function restoreInspection(bookmark:InspectionBookmark,signal:AbortSignal):Promise<void> {
|
|
913
912
|
await restoreTransaction(()=>prepareInspection(bookmark,signal),()=>({bookmark:inspectionBookmark(),inputs:captureInputs()}),()=>applyInspection(bookmark,signal),async previous=>{await applyInspection(previous.bookmark,new AbortController().signal);restoreInputs(previous.inputs);renderFeedbackRange();});
|
|
914
913
|
}
|
|
@@ -971,7 +970,39 @@ async function applyInspection(bookmark:InspectionBookmark,signal:AbortSignal):P
|
|
|
971
970
|
async function waitReferenceMetadata(media:HTMLVideoElement,signal:AbortSignal):Promise<void> {if(media.readyState>=1)return;await new Promise<void>((resolve,reject)=>{const clean=()=>{clearTimeout(timer);media.removeEventListener('loadedmetadata',ready);media.removeEventListener('error',fail);signal.removeEventListener('abort',fail);};const ready=()=>{clean();resolve();},fail=()=>{clean();reject(new Error('Saved reference metadata unavailable'));};const timer=setTimeout(fail,10000);media.addEventListener('loadedmetadata',ready,{once:true});media.addEventListener('error',fail,{once:true});signal.addEventListener('abort',fail,{once:true});});}
|
|
972
971
|
async function setupPersistence():Promise<boolean> {
|
|
973
972
|
persistence=await connectInspectionPersistence({scope:'single:'+data.manifest.id+':'+data.sourceHash,binding:inspectionBinding(),baseline:submittedBaseline!,session:()=>session,reviewDirty:()=>dirty,restoreSession:(value,isDirty)=>{session=value;dirty=isDirty;select('review-status').value=session.status;renderProposals();renderCheckpoint();renderCheckpoints();updateSavedState();},bookmark:inspectionBookmark,restoreBookmark:restoreInspection,
|
|
974
|
-
ui:()=>({inspectorScroll:workspace?.scroll()??'{}',reportCategory:select('report-category').value,reportPage,reportEvent:selectedReportEvent?.id??null,reportTime:selectedReportTime??null,reportTarget:selectedReportTarget??null}),restoreUI:value=>{if(typeof value.reportCategory==='string'&&[...select('report-category').options].some(o=>o.value===value.reportCategory))select('report-category').value=value.reportCategory;if(typeof value.reportPage==='number'&&Number.isSafeInteger(value.reportPage)&&value.reportPage>=0)reportPage=value.reportPage;selectedReportEvent=typeof value.reportEvent==='string'?data.report?.categories.flatMap(c=>c.events).find(e=>e.id===value.reportEvent)??null:null;selectedReportTime=typeof value.reportTime==='number'?value.reportTime:undefined;selectedReportTarget=typeof value.reportTarget==='number'?value.reportTarget:undefined;renderReportEvents();renderReportSelection();workspace?.restoreScroll(value.inspectorScroll);},afterInputs:()=>{const target=input('feedback-target-name').value;renderFeedbackTargets(true);if(select('feedback-target-kind').value!=='rig')input('feedback-target-name').value=target;renderFeedbackRange();text('audio-volume-label',`${Math.round(Number(input('audio-volume').value)*100)}%`);},onConflict:value=>{draftConflict=value;updateSavedState();},onStatus:value=>{text('draft-state',value);el('draft-state').title=value;}});
|
|
973
|
+
ui:()=>({inspectorScroll:workspace?.scroll()??'{}',reportCategory:select('report-category').value,reportPage,reportEvent:selectedReportEvent?.id??null,reportTime:selectedReportTime??null,reportTarget:selectedReportTarget??null}),restoreUI:value=>{if(typeof value.reportCategory==='string'&&[...select('report-category').options].some(o=>o.value===value.reportCategory))select('report-category').value=value.reportCategory;if(typeof value.reportPage==='number'&&Number.isSafeInteger(value.reportPage)&&value.reportPage>=0)reportPage=value.reportPage;selectedReportEvent=typeof value.reportEvent==='string'?data.report?.categories.flatMap(c=>c.events).find(e=>e.id===value.reportEvent)??null:null;selectedReportTime=typeof value.reportTime==='number'?value.reportTime:undefined;selectedReportTarget=typeof value.reportTarget==='number'?value.reportTarget:undefined;renderReportEvents();renderReportSelection();workspace?.restoreScroll(value.inspectorScroll);},afterInputs:()=>{const target=input('feedback-target-name').value;renderFeedbackTargets(true);if(select('feedback-target-kind').value!=='rig')input('feedback-target-name').value=target;renderFeedbackRange();text('audio-volume-label',`${Math.round(Number(input('audio-volume').value)*100)}%`);},onConflict:value=>{draftConflict=value;updateSavedState();},onStatus:value=>{text('draft-state',value);el('draft-state').title=value;text('review-draft-state',value);}});
|
|
975
974
|
return persistence.restored;
|
|
976
975
|
}
|
|
976
|
+
/** Setup's Chest regions tool: designation values, an overlay and a two-file client download. Nothing reaches the session's feedback or the package. */
|
|
977
|
+
function chestField(id:string){const node=document.getElementById(id) as HTMLInputElement|HTMLSelectElement|HTMLTextAreaElement|null;return node?{value:node.value,...(node instanceof HTMLInputElement&&node.type==='checkbox'?{checked:node.checked}:{})}:undefined;}
|
|
978
|
+
function chestEvaluation(){return evaluateChestForm(chestField,chestMapping,data.feedbackCatalog.bones);}
|
|
979
|
+
function chestRegions():OverlayRegion[]{
|
|
980
|
+
const style=getComputedStyle(document.documentElement),token=(name:string)=>style.getPropertyValue(name).trim();
|
|
981
|
+
return chestOverlayPolygons(chestEvaluation(),chestMapping).map((region,i)=>({...region,stroke:token(i?'--rv-region-b':'--rv-region-a'),fill:token(i?'--rv-region-b-fill':'--rv-region-a-fill')}));
|
|
982
|
+
}
|
|
983
|
+
function populateChestBones():void {
|
|
984
|
+
const bones=data.feedbackCatalog.bones;
|
|
985
|
+
for(const row of chestRegionRows){const node=select(chestRowId(row,'bone'));node.replaceChildren(option('',bones?'Select bone':'No model bones'));for(const name of bones??[])node.append(option(name,name));node.disabled=!bones;}
|
|
986
|
+
}
|
|
987
|
+
function updateChestMapping():void {
|
|
988
|
+
const root=data.feedbackCatalog.bones?.[0];let at:{x:number;y:number}|null=null;
|
|
989
|
+
if(player&&root){try{const bone=player.bone(root);at={x:bone.x,y:bone.y};}catch{at=null;}}
|
|
990
|
+
chestMapping=player?cropWorldMapping(assets?.stage,at):{available:false,reason:'CHEST_REGION_MAPPING_UNAVAILABLE: no compiled model drawn'};
|
|
991
|
+
renderChest();
|
|
992
|
+
}
|
|
993
|
+
function renderChest():void {
|
|
994
|
+
for(const id of ['chest-crop-w','chest-crop-h']){const node=input(id);node.readOnly=chestMapping.available;if(chestMapping.available)node.value=String(id==='chest-crop-w'?chestMapping.crop.w:chestMapping.crop.h);}
|
|
995
|
+
text('chest-mapping',chestMapping.available?`Crop → rig world from the model: ${chestMapping.basis}.`:`${chestMapping.reason}. No overlay is drawn; enter the crop size to export.`);
|
|
996
|
+
el('chest-mapping').classList.toggle('error',!chestMapping.available);
|
|
997
|
+
const value=chestEvaluation();text('chest-status',value.ok?`${value.document.mesh.value.contour.regions.map(r=>`${r.name} → ${r.bone}`).join(' · ')} · valid for export`:value.error);
|
|
998
|
+
el('chest-status').classList.toggle('error',!value.ok);
|
|
999
|
+
redrawOverlay(false);
|
|
1000
|
+
}
|
|
1001
|
+
async function exportChest():Promise<void> {
|
|
1002
|
+
const value=chestEvaluation();if(!value.ok){message(`Chest regions not exported: ${value.error}`,true);return;}
|
|
1003
|
+
const digest=[...new Uint8Array(await crypto.subtle.digest('SHA-256',new TextEncoder().encode(value.text)))].map(b=>b.toString(16).padStart(2,'0')).join('');
|
|
1004
|
+
const binding=chestRegionsBinding({fileSha256:digest,sourceHash:data.sourceHash,reviewedSource:data.reviewedSource,exportedAt:new Date().toISOString(),mapping:chestMapping.available?chestMapping.basis:chestMapping.reason});
|
|
1005
|
+
download('chest-regions.json',value.text,'application/json');download('chest-regions.binding.json',JSON.stringify(binding,null,2)+'\n','application/json');
|
|
1006
|
+
message('Downloaded chest-regions.json and chest-regions.binding.json. The regions are review evidence, not an approval; the package is unchanged.');
|
|
1007
|
+
}
|
|
977
1008
|
void start();
|
package/web/rig-overlays.ts
CHANGED
|
@@ -14,7 +14,9 @@ export interface OverlaySnapshot {
|
|
|
14
14
|
available:boolean;needsCapture:boolean;meshEdges:number;boneCount:number;
|
|
15
15
|
capabilities:{images:boolean;mesh:boolean;bones:boolean};warnings:string[];
|
|
16
16
|
}
|
|
17
|
-
|
|
17
|
+
/** Designation polygons in rig world coordinates (Setup's Chest regions tool); colours are theme token values. */
|
|
18
|
+
export interface OverlayRegion {name:string;points:OverlayPoint[];stroke:string;fill:string}
|
|
19
|
+
export interface OverlayView {project:OverlayProject;settings:RigOverlaySettings;dpr?:number;regions?:readonly OverlayRegion[]}
|
|
18
20
|
export interface OverlayRefresh extends OverlayView {player:OverlayPlayer|null;bones:readonly BoneOverlaySpec[];frame?:PosedFrame;selectedSlot?:string|null}
|
|
19
21
|
const point=(x:number,y:number):OverlayPoint=>({x,y});
|
|
20
22
|
const finite=(p:OverlayPoint)=>Number.isFinite(p.x)&&Number.isFinite(p.y);
|
|
@@ -91,7 +93,7 @@ export class RigOverlays {
|
|
|
91
93
|
this.geometry=input.player?captureRigGeometry(input.player,input.bones,input.settings,input.frame,input.selectedSlot):null;
|
|
92
94
|
return this.redraw(input);
|
|
93
95
|
}
|
|
94
|
-
redraw({project,settings,dpr=globalThis.devicePixelRatio??1}:OverlayView):OverlaySnapshot{
|
|
96
|
+
redraw({project,settings,dpr=globalThis.devicePixelRatio??1,regions=[]}:OverlayView):OverlaySnapshot{
|
|
95
97
|
if(this.disposed)return this.unavailable('Overlay has been disposed');
|
|
96
98
|
this.host.rigRoot.style.visibility=settings.images?this.visibility:'hidden';
|
|
97
99
|
const warnings=[...(this.geometry?.warnings??[])],ctx=this.host.canvas.getContext('2d');
|
|
@@ -132,6 +134,12 @@ export class RigOverlays {
|
|
|
132
134
|
ctx.fillStyle='#ffe26b';ctx.strokeStyle='#44310d';ctx.lineWidth=1;ctx.beginPath();ctx.arc(center.x,center.y,2.8,0,Math.PI*2);ctx.fill();ctx.stroke();
|
|
133
135
|
}
|
|
134
136
|
}
|
|
137
|
+
// Designations are drawn over the drawn rig only; they are values the reviewer typed, not rig geometry.
|
|
138
|
+
if(this.playerAvailable)for(const region of regions){
|
|
139
|
+
const points=region.points.map(projected);if(points.length<3||points.some(p=>!p))continue;
|
|
140
|
+
ctx.globalAlpha=1;ctx.setLineDash([]);ctx.beginPath();ctx.moveTo(points[0]!.x,points[0]!.y);for(const p of points.slice(1))ctx.lineTo(p!.x,p!.y);ctx.closePath();
|
|
141
|
+
ctx.fillStyle=region.fill;ctx.fill();ctx.strokeStyle=region.stroke;ctx.lineWidth=1.6;ctx.stroke();
|
|
142
|
+
}
|
|
135
143
|
if(this.geometry?.selection.length){ctx.globalAlpha=1;ctx.strokeStyle='#ffbb47';ctx.lineWidth=2;ctx.setLineDash([5,3]);ctx.beginPath();for(const edge of this.geometry.selection)line(edge);ctx.stroke();ctx.setLineDash([]);}
|
|
136
144
|
ctx.restore();
|
|
137
145
|
return this.snapshot(true,needsCapture,warnings);
|