@rig-c/review 0.14.0 → 0.15.1
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 +1 -0
- package/docs/DESIGN.md +1 -1
- package/docs/PANELS.md +12 -0
- package/docs/REVIEW_PACKAGE.md +19 -1
- package/docs/WORKFLOWS.md +6 -0
- package/package.json +1 -1
- package/src/cli.ts +5 -3
- package/src/comment-crops.ts +30 -0
- package/src/compare-session.ts +5 -2
- package/src/feedback-export.ts +24 -6
- package/src/feedback-validation.ts +9 -0
- package/src/manifest.ts +2 -2
- package/src/server.ts +1 -1
- package/src/session.ts +39 -7
- package/src/stage-comment.ts +89 -0
- package/web/compare.css +7 -4
- package/web/compare.html +1 -1
- package/web/compare.ts +34 -11
- package/web/index.html +1 -1
- package/web/inspection-persistence.ts +5 -1
- package/web/main.ts +57 -22
- package/web/stage-comments.ts +107 -0
- package/web/styles.css +7 -4
- package/web/turn-pad.ts +2 -2
- package/web/viewport-capture.ts +73 -0
package/web/compare.css
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/* Theme tokens: site values copied from rig-c.org styles.css (dark); light and --rv-* are review-local derivations. No literal colour outside these blocks. */
|
|
2
2
|
:root{--font-sans:Inter,system-ui,sans-serif;--font-display:Inter,system-ui,sans-serif;--font-mono:"JetBrains Mono",ui-monospace,monospace;--radius:10px;--bg:#070b18;--fg:#c9d4e8;--fg-strong:#f3f6fc;--fg-soft:#aebbd4;--fg-muted:#7f8ba6;--border:#18213a;--border-strong:#2a3657;--hover-bg:#0f1730;--active-bg:#142042;--link:#7cc8ff;--link-hover:#b3e0ff;--btn-fg:#08101f;--code-bg:#050814;--code-fg:#d6e2f5;--code-border:#18213a;--code-inline-bg:#121a30;--footer-bg:#050813;--pass:#30d158;--warn:#ffcc00;--fail:#ff453a;--rv-bar:#0b1124;--rv-panel:#0d152b;--rv-raised:var(--code-inline-bg);--rv-control:var(--code-inline-bg);--rv-control-hover:var(--active-bg);--rv-field:var(--code-bg);--rv-segmented:var(--code-bg);--rv-pressed-bg:#133733;--rv-pressed-border:#207c44;--rv-pressed-fg:var(--fg-strong);--rv-focus:var(--link-hover);--rv-backdrop:rgba(7,11,24,.82);--rv-pane-hi:#142042;--rv-pane-lo:#070b18;--rv-pane-label-bg:rgba(7,11,24,.85);--rv-stage-fg:#aebbd4;--rv-grid-dot:rgba(127,139,166,.2);--rv-media-bg:#0b1020}
|
|
3
3
|
:root[data-theme=light]{--bg:#f5f7fb;--fg:#26324a;--fg-strong:#0c1324;--fg-soft:#3b4864;--fg-muted:#5d6982;--border:#d5dcea;--border-strong:#b3bed3;--hover-bg:#e8edf6;--active-bg:#dbe3f3;--link:#1a6fc0;--link-hover:#0f528f;--btn-fg:#ffffff;--code-bg:#eef1f7;--code-fg:#1d2940;--code-border:#d5dcea;--code-inline-bg:#e3e8f2;--footer-bg:#eceff6;--pass:#1e8a3c;--warn:#8a6400;--fail:#b3261e;--rv-bar:#edf1f7;--rv-panel:#e9eef6;--rv-raised:var(--code-inline-bg);--rv-control:var(--code-inline-bg);--rv-control-hover:#d5ddec;--rv-field:#ffffff;--rv-segmented:var(--code-inline-bg);--rv-pressed-bg:#c9ded8;--rv-pressed-border:#6fb286;--rv-pressed-fg:var(--fg-strong);--rv-focus:var(--link);--rv-backdrop:rgba(12,19,36,.45);--rv-pane-hi:#142042;--rv-pane-lo:#070b18;--rv-pane-label-bg:rgba(7,11,24,.85);--rv-stage-fg:#aebbd4;--rv-grid-dot:rgba(127,139,166,.2);--rv-media-bg:#0b1020}
|
|
4
|
-
:root{font:13px/1.5 var(--font-sans);color:var(--fg);background:var(--bg);color-scheme:dark}*{box-sizing:border-box}body{margin:0}button,input,select{font:inherit}button{padding:7px 10px;border:1px solid var(--border-strong);border-radius:6px;color:inherit;background:var(--rv-control);cursor:pointer}button:hover{background:var(--rv-control-hover)}button:disabled{opacity:.4;cursor:default}.primary{background:var(--link);color:var(--btn-fg);border-color:var(--link)}.primary:hover{background:var(--link-hover)}input,select{border:1px solid var(--border-strong);background:var(--rv-field);border-radius:6px;color:inherit;padding:8px;max-width:100%;width:100%}input[type=range],input[type=checkbox]{accent-color:var(--link);padding:0}input[type=checkbox]{width:auto}input[type=range]{min-width:60px}button:focus-visible,input:focus-visible,select:focus-visible,summary:focus-visible{outline:2px solid var(--rv-focus);outline-offset:3px}header{padding:15px 21px;display:flex;align-items:center;gap:24px;border-bottom:1px solid var(--border);background:var(--rv-bar);flex-wrap:wrap}.brand{font-size:19px;letter-spacing:-.5px;white-space:nowrap}.brand span{font-size:10px;letter-spacing:.08em;display:block;color:var(--fg-muted)}h1{font-size:14px;font-weight:500;margin:0}h2{font-size:13px;font-weight:500;margin:0 0 13px}.hint{font-size:11px;color:var(--fg-muted)}.actions{display:flex;align-items:center;gap:7px;margin-left:auto;flex-wrap:wrap}.actions button{font-size:11px}.layout{display:grid;grid-template-columns:minmax(0,1fr) 300px;min-height:calc(100vh - 118px)}main{min-width:0;display:flex;flex-direction:column}.toolbar{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;padding:12px 16px;border-bottom:1px solid var(--border);font-size:11px;color:var(--fg-muted)}.row{display:flex;gap:5px;align-items:center;flex-wrap:wrap}.row button{font-size:11px;padding:4px 8px}.row output{font:11px var(--font-mono);padding:0 4px}.stages{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px;padding:12px;flex:1;min-height:500px}.pane{display:flex;flex-direction:column;min-width:0;border:1px solid var(--border-strong);border-radius:8px;overflow:hidden;background:var(--rv-panel)}.pane-head{display:flex;align-items:center;gap:8px;padding:
|
|
4
|
+
:root{font:13px/1.5 var(--font-sans);color:var(--fg);background:var(--bg);color-scheme:dark}*{box-sizing:border-box}body{margin:0}button,input,select{font:inherit}button{padding:7px 10px;border:1px solid var(--border-strong);border-radius:6px;color:inherit;background:var(--rv-control);cursor:pointer}button:hover{background:var(--rv-control-hover)}button:disabled{opacity:.4;cursor:default}.primary{background:var(--link);color:var(--btn-fg);border-color:var(--link)}.primary:hover{background:var(--link-hover)}input,select{border:1px solid var(--border-strong);background:var(--rv-field);border-radius:6px;color:inherit;padding:8px;max-width:100%;width:100%}input[type=range],input[type=checkbox]{accent-color:var(--link);padding:0}input[type=checkbox]{width:auto}input[type=range]{min-width:60px}button:focus-visible,input:focus-visible,select:focus-visible,summary:focus-visible{outline:2px solid var(--rv-focus);outline-offset:3px}header{padding:15px 21px;display:flex;align-items:center;gap:24px;border-bottom:1px solid var(--border);background:var(--rv-bar);flex-wrap:wrap}.brand{font-size:19px;letter-spacing:-.5px;white-space:nowrap}.brand span{font-size:10px;letter-spacing:.08em;display:block;color:var(--fg-muted)}h1{font-size:14px;font-weight:500;margin:0}h2{font-size:13px;font-weight:500;margin:0 0 13px}.hint{font-size:11px;color:var(--fg-muted)}.actions{display:flex;align-items:center;gap:7px;margin-left:auto;flex-wrap:wrap}.actions button{font-size:11px}.layout{display:grid;grid-template-columns:minmax(0,1fr) 300px;min-height:calc(100vh - 118px)}main{min-width:0;display:flex;flex-direction:column}.toolbar{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;padding:12px 16px;border-bottom:1px solid var(--border);font-size:11px;color:var(--fg-muted)}.row{display:flex;gap:5px;align-items:center;flex-wrap:wrap}.row button{font-size:11px;padding:4px 8px}.row output{font:11px var(--font-mono);font-variant-numeric:tabular-nums;padding:0 4px}.stages{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px;padding:12px;flex:1;min-height:500px}.pane{display:flex;flex-direction:column;min-width:0;border:1px solid var(--border-strong);border-radius:8px;overflow:hidden;background:var(--rv-panel)}.pane-head{display:flex;align-items:center;gap:8px;padding:0 12px;border-bottom:1px solid var(--border);font-size:11px;color:var(--fg-soft);height:32px;min-height:32px;max-height:32px;box-sizing:border-box;flex:none;flex-wrap:nowrap;overflow:hidden}.pane-head>span{min-width:0;flex:1 1 0%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.pane-head strong{font-weight:500}.side-label{display:inline-grid;place-items:center;width:23px;height:23px;border:1px solid var(--rv-pressed-border);border-radius:5px;color:var(--rv-pressed-fg);flex:none}.rig-viewport,.reference-viewport{position:relative;min-height:420px;overflow:hidden;flex:1;background:radial-gradient(ellipse at 50% 40%,var(--rv-pane-hi),var(--rv-pane-lo) 75%)}.rig-viewport{background-image:radial-gradient(var(--rv-grid-dot) .7px,transparent .7px);background-size:22px 22px}.rig-root{position:absolute;transform-origin:0 0}video{width:100%;height:100%;position:absolute;inset:0;object-fit:contain;background:var(--rv-media-bg)}.empty{position:absolute;inset:50px 16px 30px;display:grid;place-items:center;text-align:center;font-size:12px;color:var(--rv-stage-fg);line-height:1.7;overflow-wrap:anywhere}.pane-foot{display:flex;justify-content:space-between;align-items:center;gap:8px;flex-wrap:nowrap;padding:0 11px;border-top:1px solid var(--border);color:var(--fg-muted);font:11px var(--font-mono);height:30px;min-height:30px;max-height:30px;box-sizing:border-box;flex:none;overflow:hidden}.pane-foot>*{min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.pane-foot label{display:flex;align-items:center;gap:4px;flex:none;font:11px -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}#reference-time,#a-time,#b-time{flex:none;width:11ch;font-variant-numeric:tabular-nums}#a-frame,#b-frame,#a-renderer,#b-renderer{flex:1 1 0%;min-width:0}#a-renderer,#b-renderer{text-align:right}.transport{display:flex;gap:11px;align-items:center;flex-wrap:wrap;padding:13px 17px;border-top:1px solid var(--border);background:var(--rv-panel)}.transport input[type=range]{flex:1}.transport input[type=number]{width:100px}.transport output{font:11px var(--font-mono);font-variant-numeric:tabular-nums;white-space:nowrap;color:var(--fg-muted)}#time{min-width:16ch;display:inline-block}#sync-status{font-variant-numeric:tabular-nums}.compare-info{display:flex;justify-content:space-between;gap:9px;flex-wrap:wrap;padding:9px 17px;color:var(--fg-muted);font-size:11px}.trace{font-size:11px;color:var(--fg-muted);padding:4px 17px}.trace summary,.provenance summary{cursor:pointer}.trace pre{font:11px/1.6 var(--font-mono);white-space:pre-wrap;overflow-wrap:anywhere}.boundary{font-size:11px;color:var(--fg-muted);padding:7px 17px;margin:0}aside{background:var(--rv-panel);padding:21px 17px;border-left:1px solid var(--border)}.eyebrow{font-size:11px;letter-spacing:.08em;color:var(--fg-muted);margin-bottom:15px}aside label{display:block;font-size:11px;color:var(--fg-muted);margin:12px 0 6px}.provenance{padding:13px 11px;margin-top:22px;background:var(--rv-raised);border:1px solid var(--border-strong);border-radius:7px;font-size:11px}.provenance p{margin:7px 0;color:var(--fg-muted);overflow-wrap:anywhere}.provenance details{margin-top:12px}.provenance pre{font:11px/1.6 var(--font-mono);overflow-wrap:anywhere;white-space:pre-wrap}.feedback-form{border-top:1px solid var(--border);margin-top:24px;padding-top:21px}.frame-range{display:grid;grid-template-columns:1fr 1fr;gap:9px}.block{width:100%;margin-top:14px}.feedback-item{font-size:11px;overflow-wrap:anywhere;padding:12px 0;border-top:1px solid var(--border);margin-top:13px}.feedback-item strong{display:block;font-weight:500;color:var(--fg-strong)}.feedback-item button{padding:3px 7px;font-size:11px;margin-top:7px}.dirty{color:var(--warn)!important}.error{color:var(--fail)!important}footer{display:flex;justify-content:space-between;align-items:center;gap:13px;flex-wrap:nowrap;height:40px;min-height:40px;max-height:40px;box-sizing:border-box;overflow:hidden;padding:0 18px;background:var(--footer-bg);border-top:1px solid var(--border);font-size:11px;color:var(--fg-soft)}footer>*{min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}footer>span:last-child{flex:none}#message{flex:1 1 auto;min-width:0}[hidden]{display:none!important}.sr-only{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)}
|
|
5
5
|
@media(min-width:1600px){.rig-viewport,.reference-viewport{min-height:620px}.layout{grid-template-columns:minmax(0,1fr) 320px}}
|
|
6
|
-
@media(max-width:1100px){.layout{grid-template-columns:minmax(0,1fr) 260px}aside{padding:17px 12px}.stages{gap:7px;padding:9px;min-height:430px}.rig-viewport,.reference-viewport{min-height:380px}.pane-head{padding:9px}.pane-foot{padding:
|
|
6
|
+
@media(max-width:1100px){.layout{grid-template-columns:minmax(0,1fr) 260px}aside{padding:17px 12px}.stages{gap:7px;padding:9px;min-height:430px}.rig-viewport,.reference-viewport{min-height:380px}.pane-head{padding:0 9px}.pane-foot{padding:0 7px}}
|
|
7
7
|
@media(max-width:850px){.layout{grid-template-columns:1fr}aside{border-left:0;border-top:1px solid var(--border);display:grid;grid-template-columns:1fr 1fr;gap:12px 24px}aside>.eyebrow,aside>label,aside>select{grid-column:1/-1}.feedback-form,.provenance{margin-top:0}.feedback-form{padding-top:0;border-top:0}.actions{margin-left:0}.rig-viewport,.reference-viewport{min-height:420px}}
|
|
8
|
-
@media(max-width:550px){header{gap:12px;padding:13px}.stages{grid-template-columns:repeat(2,minmax(0,1fr));gap:7px}.pane:first-child{grid-column:1/-1}.reference-viewport{min-height:240px}.rig-viewport{min-height:365px}.pane-head{padding:7px}.pane-foot{font-size:11px}aside{display:block}.feedback-form{margin-top:23px;padding-top:20px;border-top:1px solid var(--border)}.toolbar{padding:11px 13px}.transport{padding:12px;gap:8px}.transport input[type=range]{min-width:130px}.transport input[type=number]{width:84px}.provenance{margin-top:18px}}
|
|
8
|
+
@media(max-width:550px){header{gap:12px;padding:13px}.stages{grid-template-columns:repeat(2,minmax(0,1fr));gap:7px}.pane:first-child{grid-column:1/-1}.reference-viewport{min-height:240px}.rig-viewport{min-height:365px}.pane-head{padding:0 7px}.pane-foot{font-size:11px}aside{display:block}.feedback-form{margin-top:23px;padding-top:20px;border-top:1px solid var(--border)}.toolbar{padding:11px 13px}.transport{padding:12px;gap:8px}.transport input[type=range]{min-width:130px}.transport input[type=number]{width:84px}.provenance{margin-top:18px}footer{padding:0 12px}}
|
|
9
9
|
@media(pointer:coarse){button{min-height:42px}input,select{font-size:16px}.row button{min-width:38px}}
|
|
10
10
|
|
|
11
11
|
/* Inspection layout is bounded independently of pane content and review forms. */
|
|
@@ -66,7 +66,7 @@ main{min-height:0;overflow:hidden;display:flex;flex-direction:column}
|
|
|
66
66
|
@media(max-width:780px){.layout{grid-template-columns:130px minmax(0,1fr)!important}.inspector{height:36dvh}.stage-meta{padding:6px 10px 0}.stage-footer{padding:5px 10px}.inspector-heading>.hint{display:none}}
|
|
67
67
|
@media(max-width:540px){.layout{grid-template-columns:minmax(0,1fr)!important}.left-panel{display:block;padding:8px;font-size:11px}.layout{grid-template-columns:105px minmax(0,1fr)!important}.topbar,header{padding:7px 10px;gap:8px}.topbar .brand,header .brand{font-size:14px}.top-actions,.actions{gap:4px}.top-actions button,.actions button{padding:4px 6px;font-size:10px}.workspaces button{padding:6px}.inspector{height:40dvh;min-height:150px}.inspector-heading{gap:8px;padding:5px 8px}.inspector-heading button{padding:4px 7px}.stage-footer{display:none}}
|
|
68
68
|
|
|
69
|
-
body{display:grid;grid-template-rows:auto minmax(0,1fr) auto}.layout{grid-template-columns:minmax(0,1fr)!important}.toolbar{padding:7px 12px}.pane-head{padding:
|
|
69
|
+
body{display:grid;grid-template-rows:auto minmax(0,1fr) auto}.layout{grid-template-columns:minmax(0,1fr)!important}.toolbar{padding:7px 12px}.pane-head{padding:0 9px;height:32px;min-height:32px;max-height:32px}.pane-foot{padding:0 8px;height:30px;min-height:30px;max-height:30px}.inspector{height:clamp(205px,32dvh,340px)}
|
|
70
70
|
|
|
71
71
|
@media(max-width:540px){.layout{grid-template-columns:minmax(0,1fr)!important}}
|
|
72
72
|
|
|
@@ -131,3 +131,6 @@ body{display:grid;grid-template-rows:auto minmax(0,1fr) auto}.layout{grid-templa
|
|
|
131
131
|
.turn-pad output{font-variant-numeric:tabular-nums;color:var(--fg-strong)}
|
|
132
132
|
.turn-pad button[aria-pressed=true]{border-color:var(--pass);color:var(--pass)}
|
|
133
133
|
#turn-pad-host>p{margin:0 12px 6px}
|
|
134
|
+
|
|
135
|
+
/* Source-bound stage comments; the layer never intercepts ordinary inspection. */
|
|
136
|
+
main{position:relative}.stage-comment-mode{cursor:crosshair}.stage-comment-mode video{pointer-events:none}.stage-comment-layer{position:absolute;inset:0;pointer-events:none;z-index:20;overflow:hidden}.stage-comment-region{position:absolute;border:2px solid var(--link);pointer-events:none}.stage-comment-pin{position:absolute;transform:translate(-50%,-50%);border-radius:50%;min-width:26px;height:26px;padding:2px 5px;background:var(--link);color:var(--btn-fg);pointer-events:auto}.stage-comment-composer{position:absolute;z-index:30;width:286px;padding:10px;background:var(--rv-panel);border:1px solid var(--link);border-radius:6px;box-shadow:0 4px 20px var(--rv-backdrop)}.stage-comment-composer input{width:100%}.stage-comment-composer small{display:block;margin-top:6px;color:var(--fg-muted)}.stage-comment-composer p{color:var(--fail);margin:4px 0}.stage-comment-list{position:absolute;z-index:40;right:8px;top:55px;width:310px;max-width:90%;max-height:60%;overflow:auto;background:var(--rv-panel);border:1px solid var(--border-strong);padding:12px;border-radius:6px}.stage-comment-item{padding:8px 0;border-bottom:1px solid var(--border)}.stage-comment-item>button:first-child{display:block;width:100%;text-align:left;overflow-wrap:anywhere}.stage-comment-item span{display:block;color:var(--fg-muted)}
|
package/web/compare.html
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<!doctype html>
|
|
2
2
|
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>rig review · compare</title><link rel="stylesheet" href="styles.css"></head><body>
|
|
3
3
|
<header><div class="brand">rig review <span>COMPARE</span></div><div><h1 id="title">A / B comparison</h1><span id="animation" class="hint"></span></div><div class="actions"><span id="save-state" class="hint">Loading…</span><button id="export-json">Feedback JSON</button><button id="export-markdown">Issue Markdown</button><button id="submit" class="primary" disabled>Submit review</button></div></header>
|
|
4
|
-
<div class="layout"><main><div class="toolbar"><span>Inspect A and B at the reference video time</span><div class="row"><label class="camera-sync"><input id="camera-sync" type="checkbox" checked>Sync cameras</label><button id="zoom-out" aria-label="Zoom active view out">−</button><output id="zoom-label">100%</output><button id="zoom-in" aria-label="Zoom active view in">+</button><button id="fit">Fit synchronized views</button></div></div>
|
|
4
|
+
<div class="layout"><main><div class="toolbar"><button id="stage-comment" type="button" aria-pressed="false" title="Comment · C (active mesh editor keeps C)">Comment <kbd>C</kbd></button><button id="stage-comment-list-toggle" type="button" aria-expanded="false" aria-controls="stage-comment-list">Comments (0)</button><span>Inspect A and B at the reference video time</span><div class="row"><label class="camera-sync"><input id="camera-sync" type="checkbox" checked>Sync cameras</label><button id="zoom-out" aria-label="Zoom active view out">−</button><output id="zoom-label">100%</output><button id="zoom-in" aria-label="Zoom active view in">+</button><button id="fit">Fit synchronized views</button></div></div>
|
|
5
5
|
<div id="stage-layout" class="stages"><section class="pane"><div class="pane-head"><strong>REFERENCE</strong><span id="reference-name"></span></div><div id="reference-viewport" class="reference-viewport"><video id="reference-video" preload="auto" playsinline muted></video><p id="reference-empty" class="empty">Loading reference video…</p></div><div class="pane-foot"><span id="reference-time">— s</span><label><input id="reference-audio" type="checkbox">Video audio</label></div></section><section class="pane"><div class="pane-head"><strong class="side-label">A</strong><span id="a-title"></span></div><div id="a-viewport" class="rig-viewport"><div id="a-root" class="rig-root"></div><canvas id="a-overlay" class="rig-overlay" aria-label="A actual rig mesh and bone overlay"></canvas><p id="a-empty" class="empty">Loading rig…</p></div><div class="pane-foot"><span id="a-time">— s</span><span id="a-frame">rate unknown</span><span id="a-renderer" aria-label="A last draw backend" title="DOM images for rigid parts; WebGL meshes when available, Canvas2D fallback.">Not rendered</span></div></section><section class="pane"><div class="pane-head"><strong class="side-label">B</strong><span id="b-title"></span></div><div id="b-viewport" class="rig-viewport"><div id="b-root" class="rig-root"></div><canvas id="b-overlay" class="rig-overlay" aria-label="B actual rig mesh and bone overlay"></canvas><p id="b-empty" class="empty">Loading rig…</p></div><div class="pane-foot"><span id="b-time">— s</span><span id="b-frame">rate unknown</span><span id="b-renderer" aria-label="B last draw backend" title="DOM images for rigid parts; WebGL meshes when available, Canvas2D fallback.">Not rendered</span></div></section></div>
|
|
6
6
|
<section class="inspector" aria-label="Inspection controls"><div class="inspector-heading"><strong>INSPECTOR</strong><div role="tablist" aria-label="Inspector sections"><button role="tab" id="tab-view" data-inspector="view" aria-controls="inspector-view" aria-selected="true" tabindex="0">View</button><button role="tab" id="tab-rig" data-inspector="rig" aria-controls="inspector-rig" aria-selected="false" tabindex="-1">Rig</button><button role="tab" id="tab-playback" data-inspector="playback" aria-controls="inspector-playback" aria-selected="false" tabindex="-1">Playback</button><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="draft-notice"><span id="draft-state" role="status" aria-live="polite"></span><div id="draft-recovery"></div></div><div class="inspector-body"><section id="inspector-view" role="tabpanel" aria-labelledby="tab-view" data-inspector-panel="view"><div class="view-link-controls"><button id="copy-view" type="button">Copy view link</button><span class="hint">Numeric source region · opening this link restores a paused inspection</span></div><pre id="view-coordinates" class="numeric-view" aria-label="Current numeric inspection region"></pre><div class="inspection-controls" 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><span id="idle-state" class="hint" role="status">Idle preview unavailable</span><span id="control-state" class="hint" role="status"></span></div><p class="hint">Sync cameras links fit-relative zoom and viewport-relative pan across visible panes. Turn it off for independent Reference, A and B views. Toolbar zoom and Fit use the last active pane.</p></section><section id="inspector-rig" role="tabpanel" aria-labelledby="tab-rig" data-inspector-panel="rig" hidden><section id="selected-part" aria-label="Selected part" aria-live="polite"></section><div class="rig-layer-controls"><fieldset id="layer-controls"><legend>Rig layers</legend><label><input id="layer-images" type="checkbox" checked disabled>Images</label><label><input id="layer-mesh" type="checkbox" disabled>Mesh edges</label><label><input id="layer-bones" type="checkbox" disabled>Bone points</label></fieldset><span id="layer-state" class="hint">No rig supplied</span></div><p class="hint">These controls show actual images, last-drawn mesh edges and named bone points. Both builds use the fixed animation named above. This inspector does not author geometry or motion.</p></section><section id="inspector-playback" role="tabpanel" aria-labelledby="tab-playback" data-inspector-panel="playback" hidden><section class="transport"><button id="play" aria-label="Play reference video and both rigs" disabled>▶</button><input id="scrub" type="range" min="0" max="1" step="0.0001" value="0" aria-label="Shared comparison time" disabled><output id="time">— / — s</output><label for="time-input" class="sr-only">Reference video time to inspect</label><input id="time-input" type="number" min="0" step="0.0001" placeholder="Seconds" disabled><button id="seek" disabled>Go to time</button></section><div class="compare-info"><output id="sync-status">Presentation pending</output><span id="sync-warning"></span></div><div class="compare-info"><label for="frame-basis">Review frame</label><select id="frame-basis"><option value="a">A explicit samples</option><option value="b">B explicit samples</option></select><input id="frame-input" type="number" min="0" step="1" aria-label="Explicit producer frame" disabled><button id="frame-seek" type="button" disabled>Inspect frame</button><span id="frame-index-status">Loading reference timestamp index…</span></div><div class="compare-info"><span id="domain">Checking shared playback range…</span><span id="clock-source">Waiting for video time</span></div><details class="trace"><summary>Synchronization trace</summary><pre id="trace">No frame presented yet.</pre></details><p class="boundary">Motion is read only. The reference video FPS is not used to infer the pose frame clock.</p></section><section id="inspector-feedback" role="tabpanel" aria-labelledby="tab-feedback" data-inspector-panel="feedback" hidden><div class="review-summary"><div class="eyebrow">COMPARE REVIEW</div><label for="status">Review status</label><select id="status"><option value="pending">Pending</option><option value="approved">Approved</option><option value="changes-requested">Changes requested</option></select><section class="provenance"><h2>Source provenance</h2><div id="provenance-brief"></div><details><summary>Both builds, versions and reference video hash</summary><pre id="provenance-full"></pre></details></section></div><section class="feedback-form"><h2>Frame-range feedback</h2><label for="side">Target build</label><select id="side"><option value="">Select A or B</option><option value="a">A</option><option value="b">B</option></select><div class="frame-range"><label>Start frame<input id="frame-start" type="number" min="0" step="1" inputmode="numeric"></label><label>End frame<input id="frame-end" type="number" min="0" step="1" inputmode="numeric"></label></div><p id="feedback-range-hint" class="hint">Select A or B to check frame bounds.</p><label for="target-kind">Target type</label><select id="target-kind"><option value="">Select</option><option value="bone">Bone</option><option value="slot">Slot</option><option value="rig">Entire rig</option></select><label for="target-name">Actual target name</label><input id="target-name" type="text" list="target-names" autocomplete="off"><datalist id="target-names"></datalist><p id="target-hint" class="hint">Select a target build and type.</p><label for="category">Category</label><select id="category"><option value="">Select</option><option value="defect">Defect</option><option value="quality">Quality</option></select><label for="summary">One-line summary</label><input id="summary" type="text" maxlength="1000" placeholder="What looks wrong, and over which frames?"><button id="add-feedback" class="block" disabled>Add feedback</button><p id="feedback-warnings" class="hint" role="status" hidden></p><div id="feedback-list"></div></section><details id="strip-panel" class="trace"><summary>Export frame strip</summary><p class="hint">Uses the feedback form start and end frames, inclusive. Exports Reference / A / B rows without changing inspection time.</p><label for="strip-scope">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-basis">Report defining the range frames</label><select id="strip-basis"><option value="">Select A or B as frame basis</option><option value="a">A report frameTimes</option><option value="b">B report frameTimes</option></select><label for="strip-format">Format</label><select id="strip-format"><option value="png">PNG strip</option><option value="json">JSON frames and provenance</option></select><p class="hint">The same frame number in A and B is not assumed to mean the same time. Batch export uses each feedback side as its frame basis. Without frameTimes, PNG is refused and JSON preserves time: null.</p><button id="strip-export" type="button" disabled>Download strip</button><p id="strip-status" class="hint" role="status" aria-live="polite">Loading comparison provenance…</p></details></section></div></section></main><aside id="parts-explorer" class="parts-explorer" aria-label="Parts explorer"><h2>PARTS</h2><button type="button" data-part-whole aria-pressed="true">Whole rig</button><label for="part-search">Find slot</label><input id="part-search" data-part-search type="search" autocomplete="off" placeholder="Exact slot name or text"><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>
|
|
7
7
|
</div><footer><span id="message" role="status" aria-live="polite">Loading comparison provenance from the server…</span><span>Source files remain unchanged.</span></footer><script type="module" src="main.js"></script></body></html>
|
package/web/compare.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import {requireFeedbackFrames} from '../src/stage-comment.ts';
|
|
2
|
+
import {bindStageComments,type CommentPose} from './stage-comments.ts';
|
|
1
3
|
import {sharedTurnDials,type TurnDial} from '../src/turn-pad.ts';
|
|
2
4
|
import {bindTurnPad,type TurnPad} from './turn-pad.ts';
|
|
3
5
|
import {PartExplorer,loadPartCatalog} from './part-selection.ts';
|
|
4
6
|
import {cameraFrame,cameraSignature,cameraFromSignature,assertCameraView,boundsFitSignature,dragSignature,zoomSignature,type CameraFrame,type CameraSignature,type PaneCamera} from './camera-view.ts';
|
|
5
|
-
import { loadModel, RigPlayer, type ModelAssets, type PoseBounds } from '@rig-c/play';
|
|
7
|
+
import { CanvasRenderer, loadModel, RigPlayer, type ModelAssets, type PoseBounds } from '@rig-c/play';
|
|
6
8
|
import type { CompareResponse, CompareSide } from '../src/compare.ts';
|
|
7
9
|
import type { CompareFeedback, CompareSession } from '../src/compare-session.ts';
|
|
8
10
|
import { clockForAnimation, lastFrame, timeAtFrame } from '../src/frame-clock.ts';
|
|
@@ -17,7 +19,7 @@ import {connectControl} from './control.ts';
|
|
|
17
19
|
import {pageInstanceAccess,readOnlyReason} from './instance-access.ts';
|
|
18
20
|
import {controlComparison,type ControlCommand,type ControlInspection,type ControlView} from '../src/control.ts';
|
|
19
21
|
import {pairEligibility,type InputState,type PairDomain} from '../src/comparison-eligibility.ts';
|
|
20
|
-
import {connectInspectionPersistence,serializableView,numericCamera,restoredZoom,restoreTransaction,captureInputs,restoreInputs,type InspectionBookmark,type InspectionBinding,type PersistenceConnection} from './inspection-persistence.ts';
|
|
22
|
+
import {parseBookmark,connectInspectionPersistence,serializableView,numericCamera,restoredZoom,restoreTransaction,captureInputs,restoreInputs,type InspectionBookmark,type InspectionBinding,type PersistenceConnection} from './inspection-persistence.ts';
|
|
21
23
|
import {RendererBadge} from './renderer-status.ts';
|
|
22
24
|
import {ComparePresentation,type PresentationState} from '../src/compare-presentation.ts';
|
|
23
25
|
import {nextReferenceInterval,ReferenceSelectionUnavailable,inspectionReferenceFrame,lastSupportedReferenceFrame,producerFrameAssociations,referenceFrameAtTime,referenceSeekPosition,type ReferenceFrameIndex,type VideoInfo,type VideoFrame} from '../src/reference-frames.ts';
|
|
@@ -26,7 +28,8 @@ const el = <T extends HTMLElement = HTMLElement>(id: string): T => {
|
|
|
26
28
|
const node = document.getElementById(id); if (!node) throw new Error(`Missing compare UI element: ${id}`); return node as T;
|
|
27
29
|
};
|
|
28
30
|
const input = (id: string) => el<HTMLInputElement>(id), select = (id: string) => el<HTMLSelectElement>(id), button = (id: string) => el<HTMLButtonElement>(id);
|
|
29
|
-
const
|
|
31
|
+
const titledIds = new Set(['reference-time', 'a-time', 'b-time', 'a-frame', 'b-frame', 'a-renderer', 'b-renderer', 'reference-name', 'a-title', 'b-title', 'message']);
|
|
32
|
+
const text = (id: string, value: string) => { const node = el(id); node.textContent = value; if (titledIds.has(id)) node.title = value; };
|
|
30
33
|
const describe = (value: unknown) => value instanceof Error ? value.message : String(value);
|
|
31
34
|
const video = el<HTMLVideoElement>('reference-video');
|
|
32
35
|
let data: CompareResponse, session: CompareSession;
|
|
@@ -44,6 +47,7 @@ let idleHeld=true,loopRestarting=false,suspendedPreview=false;
|
|
|
44
47
|
let appearance:ReturnType<typeof bindAppearance>|null=null,controlDispose:(()=>void)|null=null,workspace:WorkspaceUI|null=null;
|
|
45
48
|
const viewDisposers:(()=>void)[]=[];
|
|
46
49
|
let parts:PartExplorer|null=null,turnPad:TurnPad|null=null;
|
|
50
|
+
let comments:ReturnType<typeof bindStageComments>|null=null;
|
|
47
51
|
const overlays:Partial<Record<Side,RigOverlays>>={};
|
|
48
52
|
function inspectionPlayer(side:Side):InspectionPlayer {const player=players[side];if(!player)throw new Error('Part player unavailable');return {get time(){return player.time;},pause:()=>player.pause(),seek:time=>player.seek(time),draw:()=>{player.draw();parts?.present(side);}};}
|
|
49
53
|
const layers={images:true,mesh:false,bones:false,meshOpacity:.65};
|
|
@@ -134,7 +138,7 @@ function renderPresentation(state:PresentationState):void {
|
|
|
134
138
|
el('sync-status').classList.toggle('error',state.status==='held'||state.lastLatenessMs>0||state.status==='failed');
|
|
135
139
|
if(!state.inspection){
|
|
136
140
|
trace=null;updateFeedbackAvailability();
|
|
137
|
-
for(const side of ['a','b'] as const){text(`${side}-time`,last?`${last[side].toFixed(6)} s
|
|
141
|
+
for(const side of ['a','b'] as const){text(`${side}-time`,last?`${last[side].toFixed(6)} s`:'— s');const decoded=last&&referenceFrames?referenceFrameAtTime(referenceFrames,last.reference):null,frame=decoded?associations[side].get(decoded.index)??null:null;text(`${side}-frame`,last?`${state.status==='held'?'Held':'Drawn; feedback unavailable'} · review F${frame??'—'}`:'Review frame unknown');}
|
|
138
142
|
text('clock-source','Feedback requires a pair submitted before the known next-reference estimate or paused inspection.');
|
|
139
143
|
}
|
|
140
144
|
text('sync-warning',state.deadlineKind==='unknown'?'Next-reference timing unknown: an adjacent decoded timestamp and positive playback rate are required. Playback continues; pause for exact inspection.':state.lastLatenessMs>0?'The pair finished CPU draw submission after the estimated next reference display boundary. Pause for exact inspection.':'');
|
|
@@ -290,6 +294,7 @@ function renderFeedbackSelection(): void {
|
|
|
290
294
|
}
|
|
291
295
|
|
|
292
296
|
function renderFeedback(): void {
|
|
297
|
+
comments?.refresh();
|
|
293
298
|
const list = el('feedback-list'); list.replaceChildren();
|
|
294
299
|
const warnings = el('feedback-warnings'); warnings.hidden = session.feedbackWarnings.length === 0;
|
|
295
300
|
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. JSON and Markdown exports preserve both the original records and warnings.` : '';
|
|
@@ -299,7 +304,7 @@ function renderFeedback(): void {
|
|
|
299
304
|
for (const warning of session.feedbackWarnings.filter(value => value.id === feedback.id)) { const note = document.createElement('p'); note.className = 'hint'; note.textContent = `Validation warning · ${warning.error}`; row.append(note); }
|
|
300
305
|
const details = document.createElement('details'), heading = document.createElement('summary'), source = document.createElement('pre'); heading.textContent = 'Saved feedback and inspection context'; source.textContent = JSON.stringify(feedback, null, 2); details.append(heading, source);
|
|
301
306
|
const remove = document.createElement('button'); remove.type = 'button'; remove.textContent = 'Delete'; remove.onclick = () => { session.feedback = session.feedback.filter(value => value.id !== feedback.id); session.feedbackWarnings = session.feedbackWarnings.filter(warning => warning.id !== feedback.id); renderFeedback(); markDirty(); };
|
|
302
|
-
row.append(details
|
|
307
|
+
row.append(details);if((feedback.context as any)?.comment?.schema!=='rig-review-comment/1'||!submittedBaseline?.feedback.some(f=>f.id===feedback.id))row.append(remove); list.append(row);
|
|
303
308
|
}
|
|
304
309
|
savedState();
|
|
305
310
|
}
|
|
@@ -324,7 +329,7 @@ async function exportStrip(): Promise<void> {
|
|
|
324
329
|
for (const feedback of session.feedback) {
|
|
325
330
|
const context = feedback.context;
|
|
326
331
|
if (!context || typeof context !== 'object' || Array.isArray(context) || context.animation !== data.animation) throw new Error(`Feedback ${feedback.id}: Its animation is missing or differs from the comparison animation. The range was not reassigned to another animation.`);
|
|
327
|
-
intervals.push({ id: feedback.id, feedbackId: feedback.id, basis: feedback.side,
|
|
332
|
+
intervals.push({ id: feedback.id, feedbackId: feedback.id, basis: feedback.side, ...requireFeedbackFrames(feedback) });
|
|
328
333
|
}
|
|
329
334
|
} else {
|
|
330
335
|
const basis = select('strip-basis').value, start = input('frame-start').value.trim(), end = input('frame-end').value.trim();
|
|
@@ -383,7 +388,7 @@ function bind(): void {
|
|
|
383
388
|
session.feedback.push({ id: crypto.randomUUID(), side, frameStart, frameEnd, target, category: category as CompareFeedback['category'], summary, context: { ...(turnPad?{turn:JSON.parse(JSON.stringify(turnPad.snapshot()))}:{}),interval: { frameStart, frameEnd, inclusive: true }, animation: data.animation, mediaTime: inspected.reference, clockSource: inspected.source, frame: currentFrame(side), frames: { a: currentFrame('a'), b: currentFrame('b') }, playheads: { reference: inspected.reference, a: inspected.a, b: inspected.b },reviewFrames:{a:currentReviewFrame('a'),b:currentReviewFrame('b')},requestedProducer,part:parts?.feedback()??null,view:{...serializableView(numericView()),theme:appearance?.get()??'system',layers:{...layers}},decodedIndex:referenceFrames?referenceFrameAtTime(referenceFrames,inspected.reference)?.index??null:null } });
|
|
384
389
|
input('summary').value = ''; markDirty(); renderFeedback(); message('Added feedback bound to the target build and both reviewed sources.');
|
|
385
390
|
};
|
|
386
|
-
button('submit').onclick = async () => { if(readOnly){message(readOnly,true);return;}if(draftConflict){message('Resolve the saved local draft conflict before submitting.',true);return;}if (session.feedbackWarnings.length) { message('Resolve validation warnings in existing feedback before submitting. Delete the marked entries and add valid replacements.', true); return; } saving = true; savedState(); const payload = JSON.stringify(session); try { const saved = await api<CompareSession>('/api/compare/session', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: payload }); if (JSON.stringify(session) === payload) { session = saved; dirty = false; } else session.updatedAt = saved.updatedAt;await persistence?.submitted(saved); message(dirty ? 'Changes were made during submission. Submit again.' : 'Submitted the selected review status and A/B/reference-video provenance.'); } catch (error) { message(`Submission failed: ${describe(error)}`, true); } finally { saving = false; savedState(); } };
|
|
391
|
+
button('submit').onclick = async () => { if(readOnly){message(readOnly,true);return;}if(draftConflict){message('Resolve the saved local draft conflict before submitting.',true);return;}if (session.feedbackWarnings.length) { message('Resolve validation warnings in existing feedback before submitting. Delete the marked entries and add valid replacements.', true); return; } saving = true; savedState(); const payload = JSON.stringify(session); try { const saved = await api<CompareSession>('/api/compare/session', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: payload }); if (JSON.stringify(session) === payload) { session = saved; dirty = false; } else session.updatedAt = saved.updatedAt;submittedBaseline=structuredClone(saved);comments?.refresh();await persistence?.submitted(saved);renderFeedback(); message(dirty ? 'Changes were made during submission. Submit again.' : 'Submitted the selected review status and A/B/reference-video provenance.'); } catch (error) { message(`Submission failed: ${describe(error)}`, true); } finally { saving = false; savedState(); } };
|
|
387
392
|
button('export-json').onclick = () => { download(`${session.id}-feedback.json`, JSON.stringify({ ...session, schema: 'rig-review-compare-feedback/1', issues: session.feedback.map(issue) }, null, 2) + '\n', 'application/json'); };
|
|
388
393
|
button('export-markdown').onclick = () => { download(`${session.id}-issues.md`, [`# ${md(data.title)}`, '', 'Ordered A/B and reference provenance:', ...JSON.stringify(session.binding, null, 2).split('\n').map(line => ' ' + line), '', 'Validation warnings:', ...JSON.stringify(session.feedbackWarnings, null, 2).split('\n').map(line => ' ' + line), '', ...session.feedback.flatMap(feedback => { const entry = issue(feedback); return [`## ${md(entry.title)}`, '', entry.body, '', '---', '']; })].join('\n'), 'text/markdown'); };
|
|
389
394
|
// Saved browser defaults seed a new inspection; explicit links and source drafts restore over them.
|
|
@@ -391,7 +396,7 @@ function bind(): void {
|
|
|
391
396
|
cameraSync=workspace.defaults().defaultCameraSync;input('camera-sync').checked=cameraSync;input('idle-repeat').checked=workspace.defaults().defaultIdleRepeat;
|
|
392
397
|
appearance=bindAppearance(select('appearance'),workspace.defaults().theme);
|
|
393
398
|
for(const side of ['a','b'] as const)overlays[side]=new RigOverlays({canvas:el<HTMLCanvasElement>(side+'-overlay'),rigRoot:el(side+'-root')});
|
|
394
|
-
for(const target of ['a','b','reference'] as const)viewDisposers.push(bindPan(el(target+'-viewport'),(dx,dy)=>cameraAction(()=>panCamera(target,dx,dy))));
|
|
399
|
+
for(const target of ['a','b','reference'] as const)viewDisposers.push(bindPan(el(target+'-viewport'),(dx,dy)=>cameraAction(()=>panCamera(target,dx,dy)),()=>!comments?.active()));
|
|
395
400
|
viewDisposers.push(bindStageLayout(el('stage-layout'),fit));
|
|
396
401
|
for(const [id,key] of [['layer-images','images'],['layer-mesh','mesh'],['layer-bones','bones']] as const)input(id).onchange=()=>{layers[key]=input(id).checked;redrawOverlays(false);};
|
|
397
402
|
input('idle-repeat').onchange=()=>{renderIdle();if(canRepeatIdle())void playComparison().catch(error=>message(describe(error),true));};
|
|
@@ -400,11 +405,12 @@ function bind(): void {
|
|
|
400
405
|
document.addEventListener('visibilitychange',()=>{if(document.hidden){suspendedPreview=!video.paused;stop();}else if(suspendedPreview&&!idleHeld){suspendedPreview=false;void playComparison().catch(error=>message(describe(error),true));}});
|
|
401
406
|
const observer = new ResizeObserver(fit); observer.observe(el('a-viewport')); observer.observe(el('b-viewport'));observer.observe(el('reference-viewport'));
|
|
402
407
|
window.addEventListener('beforeunload', event => { if (dirty) { event.preventDefault(); event.returnValue = ''; } });
|
|
403
|
-
window.addEventListener('pagehide', () => { persistence?.dispose();turnPad?.dispose();workspace?.dispose();disposed = true; seekAbort?.abort();controlDispose?.();appearance?.dispose();viewDisposers.forEach(dispose=>dispose());for(const overlay of Object.values(overlays))overlay.dispose();stop(); if (videoCallback !== null) video.cancelVideoFrameCallback(videoCallback); if (fallbackCallback !== null) cancelAnimationFrame(fallbackCallback); for (const side of ['a', 'b'] as const) { players[side]?.dispose(); assets[side]?.dispose(); } observer.disconnect(); });
|
|
408
|
+
window.addEventListener('pagehide', () => { comments?.dispose();persistence?.dispose();turnPad?.dispose();workspace?.dispose();disposed = true; seekAbort?.abort();controlDispose?.();appearance?.dispose();viewDisposers.forEach(dispose=>dispose());for(const overlay of Object.values(overlays))overlay.dispose();stop(); if (videoCallback !== null) video.cancelVideoFrameCallback(videoCallback); if (fallbackCallback !== null) cancelAnimationFrame(fallbackCallback); for (const side of ['a', 'b'] as const) { players[side]?.dispose(); assets[side]?.dispose(); } observer.disconnect(); });
|
|
404
409
|
}
|
|
405
410
|
|
|
406
411
|
async function start(): Promise<void> {
|
|
407
412
|
try {
|
|
413
|
+
for (const id of titledIds) { const node = document.getElementById(id); if (node && node.textContent) node.title = node.textContent; }
|
|
408
414
|
let writes: { capabilityRequired: boolean; control?: boolean };
|
|
409
415
|
[data, session, writes] = await Promise.all([api<CompareResponse>('/api/compare'), api<CompareSession>('/api/compare/session'), api<{ capabilityRequired: boolean; control?: boolean }>('/api/access')]);
|
|
410
416
|
readOnly = readOnlyReason(access, writes.capabilityRequired); if (readOnly) message(readOnly, true); controlServed = writes.control !== false;
|
|
@@ -421,7 +427,7 @@ async function start(): Promise<void> {
|
|
|
421
427
|
void loadFrameIndex();
|
|
422
428
|
input('scrub').max = String(domain.end); input('time-input').max = String(domain.end); text('domain', `Shared range 0–${domain.end} s · Neither side is independently clamped or looped`); ready = true;
|
|
423
429
|
input('scrub').disabled = false; input('time-input').disabled = false; button('seek').disabled = false; button('play').disabled = domain.end === 0;
|
|
424
|
-
await seekTo(0);configureTurnPad();activeCamera='a';if(cameraSync)alignCameras();fit();for(const id of ['layer-images','layer-mesh','layer-bones'])input(id).disabled=false;renderIdle();const restored=await setupPersistence();if(!restored&&canRepeatIdle())void playComparison().catch(error=>message(describe(error),true));
|
|
430
|
+
await seekTo(0);configureTurnPad();workspace?.refresh();activeCamera='a';if(cameraSync)alignCameras();fit();for(const id of ['layer-images','layer-mesh','layer-bones'])input(id).disabled=false;renderIdle();const restored=await setupPersistence();setupComments();if(!restored&&canRepeatIdle())void playComparison().catch(error=>message(describe(error),true));
|
|
425
431
|
if(!readOnly&&controlServed)controlDispose=connectControl({sourceHash:data.sourceHash,mode:'compare',route:access.route,headers:()=>access.headers(),observe:controlState,apply:applyControl,onStatus:value=>text('control-state',value)});
|
|
426
432
|
} catch (error) { failPair(error); for (const side of ['a', 'b'] as const) if (!players[side]) text(`${side}-empty`, `${side.toUpperCase()} Could not open comparison rig.`); text('reference-empty', 'Check the reference video and comparison inputs.'); }
|
|
427
433
|
}
|
|
@@ -499,7 +505,7 @@ async function restartIdle():Promise<void> {
|
|
|
499
505
|
try{await seekTo(0,null,null,true);if(!idleHeld&&canRepeatIdle()&&!disposed&&!document.hidden&&trace)await playComparison();}
|
|
500
506
|
catch(error){idleHeld=true;message(describe(error),true);}finally{loopRestarting=false;}
|
|
501
507
|
}
|
|
502
|
-
function localDraft():boolean{return dirty||!!input('summary').value.trim()||!!input('frame-start').value.trim()||!!input('frame-end').value.trim()||!!input('target-name').value.trim()||!!select('category').value;}
|
|
508
|
+
function localDraft():boolean{return !!comments?.draft()||dirty||!!input('summary').value.trim()||!!input('frame-start').value.trim()||!!input('frame-end').value.trim()||!!input('target-name').value.trim()||!!select('category').value;}
|
|
503
509
|
function controlState():ControlInspection {
|
|
504
510
|
const css=(target:CameraTarget)=>{const frame=paneFrame(target,true);if(!frame)return {zoom:1,pan:{x:0,y:0}};const value=cameraSignature(cameras[target],frame);return {zoom:value.zoom,pan:{x:value.pan.x*frame.width,y:value.pan.y*frame.height}};};
|
|
505
511
|
return {part:parts?.value()??null,partGeneration:parts?.observation()?.generation??null,view:numericView(),theme:appearance?.get()??'system',layout:'auto',effectiveLayout:el('stage-layout').dataset.orientation==='vertical'?'column':'row',cameras:{rig:css('a'),a:css('a'),b:css('b'),reference:css('reference')},animation:data.animation,time:trace?.reference??null,playing:!video.paused,loop:canRepeatIdle()?'idle':'off',layers:{images:!!players.a&&layers.images,meshes:!!players.a&&layers.mesh,bones:!!players.a&&layers.bones},ready,seeking:seeking||video.seeking||!!persistence?.restoring(),dirty:localDraft(),busy:saving||stripBusy||!!persistence?.restoring(),feedbackEligible:!persistence?.restoring()&&pairEligibility(pairInputs).feedback&&!!feedbackTrace(ready,seeking||video.seeking,trace),comparison:controlComparison(pairEligibility(pairInputs)),capabilities:{camera:!!players.a&&!!players.b,referenceCamera:video.readyState>=1,animation:false,time:ready,playback:ready&&pairEligibility(pairInputs).playback,layers:{images:!!players.a&&!!players.b,meshes:!!players.a&&!!players.b,bones:!!players.a&&!!players.b}}};
|
|
@@ -557,4 +563,21 @@ async function setupPersistence():Promise<boolean> {
|
|
|
557
563
|
ui:()=>({inspectorScroll:workspace?.scroll()??'{}',frameBasis:select('frame-basis').value}),restoreUI:value=>{if(value.frameBasis==='a'||value.frameBasis==='b')select('frame-basis').value=value.frameBasis;frameControls();workspace?.restoreScroll(value.inspectorScroll);},afterInputs:()=>{video.muted=!input('reference-audio').checked;const name=input('target-name').value;renderFeedbackSelection();input('target-name').value=name;},onConflict:value=>{draftConflict=value;savedState();},onStatus:value=>{text('draft-state',value);el('draft-state').title=value;}});
|
|
558
564
|
return persistence.restored;
|
|
559
565
|
}
|
|
566
|
+
|
|
567
|
+
function setupComments():void {
|
|
568
|
+
comments=bindStageComments({toolbar:el('stage-comment').parentElement!,viewports:{a:el('a-viewport'),b:el('b-viewport'),reference:el('reference-viewport')},
|
|
569
|
+
pose:view=>{
|
|
570
|
+
if(view==='rig'||!trace)return null;const side=view==='b'?'b':'a',camera=numericView().cameras[view];if(!camera||hostHidden(el(view+'-viewport')))return null;
|
|
571
|
+
const raw=currentReviewFrame(side)??currentFrame(side),decoded=referenceFrames?referenceFrameAtTime(referenceFrames,trace.reference):null;
|
|
572
|
+
const renderer=players[side]?.renderer;const index=view==='reference'?decoded?.index:raw??decoded?.index,basis=view==='reference'||raw===null?'reference':'producer';
|
|
573
|
+
return {mode:'compare',capture:{pane:el(view+'-viewport'),root:view==='reference'?undefined:el(side+'-root'),images:view==='reference'||layers.images,canvas:view!=='reference'&&renderer instanceof CanvasRenderer?renderer.canvas:undefined,backend:renderer instanceof CanvasRenderer?renderer.backendActive:renderer?.meshBackendActive??'canvas2d'},camera,frame:index===null||index===undefined?null:{index,basis},time:trace.reference,animation:data.animation,turn:turnPad?.snapshot()??null,sourceHash:data[side].sourceHash,modelHash:view==='reference'?null:data[side].reviewedSource.rig?.sha256??null,referenceHash:data.reference.sha256,referenceId:data.reference.sha256,rig:data[side].manifest.id,side,...(view==='reference'?{media:video}:{posed:parts?.frame(side),assets:assets[side]})} as CommentPose;
|
|
574
|
+
},
|
|
575
|
+
pause:async()=>{idleHeld=true;const running=!video.paused,at=trace?.reference??lastPresented?.time;stop();if(running&&at!==undefined){await seekTo(at);if(!trace)throw new Error('COMMENT_INSPECTION_UNAVAILABLE');}if(turnPad)turnPad.restore({...turnPad.snapshot(),held:true},false);},
|
|
576
|
+
bookmark:inspectionBookmark,restore:b=>restoreInspection(parseBookmark(b,inspectionBinding()),new AbortController().signal),feedback:()=>session.feedback,
|
|
577
|
+
locked:id=>!!submittedBaseline?.updatedAt&&submittedBaseline.feedback.some(f=>f.id===id),
|
|
578
|
+
add:(f,side)=>session.feedback.push({...f,side:side??'a'}),changed:()=>{markDirty();renderFeedback();persistence?.save();},error:value=>message(value,true),blocked:()=>saving||seeking||video.seeking||!!persistence?.restoring()||!ready
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
function hostHidden(node:HTMLElement):boolean{return node.clientWidth<=0||node.clientHeight<=0;}
|
|
582
|
+
|
|
560
583
|
void start();
|