@rig-c/review 0.0.0-stage → 0.1.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 +285 -2
- package/docs/DESIGN.md +43 -0
- package/docs/REVIEW_PACKAGE.md +607 -0
- package/examples/demo/SOURCE.md +5 -0
- package/examples/demo/chime.wav +0 -0
- package/examples/demo/lantern.svg +10 -0
- package/examples/demo/pose.json +244 -0
- package/examples/demo/review.json +67 -0
- package/package.json +47 -4
- package/src/animation-selection.ts +29 -0
- package/src/build.ts +8 -0
- package/src/cli.ts +114 -0
- package/src/compare-playhead.ts +55 -0
- package/src/compare-presentation.ts +43 -0
- package/src/compare-server.ts +60 -0
- package/src/compare-session.ts +26 -0
- package/src/compare.ts +57 -0
- package/src/control-server.ts +83 -0
- package/src/control.ts +103 -0
- package/src/feedback-export.ts +197 -0
- package/src/feedback-validation.ts +38 -0
- package/src/frame-clock.ts +36 -0
- package/src/manifest.ts +140 -0
- package/src/open-protocol.ts +14 -0
- package/src/open-selection.ts +11 -0
- package/src/open-shell.ts +2 -0
- package/src/open-worker.ts +2 -0
- package/src/open.ts +126 -0
- package/src/pose.ts +27 -0
- package/src/probe-reference.ts +16 -0
- package/src/reference-frame-index.ts +16 -0
- package/src/reference-frames.ts +62 -0
- package/src/reference.ts +63 -0
- package/src/report.ts +88 -0
- package/src/server.ts +61 -0
- package/src/session-history.ts +99 -0
- package/src/session.ts +58 -0
- package/src/strip-export.ts +69 -0
- package/src/strip-render.ts +171 -0
- package/src/strip.ts +51 -0
- package/src/versions.ts +20 -0
- package/web/camera-view.ts +34 -0
- package/web/compare.css +96 -0
- package/web/compare.html +7 -0
- package/web/compare.ts +502 -0
- package/web/control.ts +65 -0
- package/web/index.html +13 -0
- package/web/inspection-persistence.ts +200 -0
- package/web/main.ts +928 -0
- package/web/renderer-status.ts +31 -0
- package/web/report-view.ts +51 -0
- package/web/rig-overlays.ts +147 -0
- package/web/styles.css +98 -0
- package/web/view-controls.ts +67 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** Public last-render telemetry only; neither draws, poses nor probes GL. */
|
|
2
|
+
import type {DomRenderer,CanvasRenderer} from '@rig-c/play';
|
|
3
|
+
export type RendererTelemetry=Pick<DomRenderer,'meshBackend'|'meshBackendActive'|'meshCount'|'meshReuseCount'|'triangleCount'>|Pick<CanvasRenderer,'backend'|'backendActive'|'framesReused'|'trianglesDrawn'>;
|
|
4
|
+
export interface RasterStatus {kind:'pending'|'failed'|'fresh'|'cached'|'no-mesh';mode:'dom'|'canvas'|null;backend:'webgl'|'canvas2d'|null;fresh:number;reused:number;triangles:number;label:string}
|
|
5
|
+
const empty=(kind:'pending'|'failed'):RasterStatus=>({kind,mode:null,backend:null,fresh:0,reused:0,triangles:0,label:kind==='failed'?'Rendering failed':'Not rendered'});
|
|
6
|
+
/** Call only after successful draw. DOM actual-flush reporting requires play>=1.11.1. */
|
|
7
|
+
export function rendererStatus(renderer:RendererTelemetry|null,draw:'pending'|'succeeded'|'failed'):RasterStatus {
|
|
8
|
+
if(draw!=='succeeded'||!renderer)return empty(draw==='failed'?'failed':'pending');
|
|
9
|
+
if('meshBackendActive' in renderer){
|
|
10
|
+
const base={mode:'dom' as const,fresh:renderer.meshCount,reused:renderer.meshReuseCount,triangles:renderer.triangleCount};
|
|
11
|
+
if(renderer.meshCount===0)return {...base,kind:renderer.meshReuseCount?'cached':'no-mesh',backend:null,label:renderer.meshReuseCount?'Cached mesh rasters reused':'No mesh · DOM images'};
|
|
12
|
+
const backend=renderer.meshBackendActive,label=backend==='webgl'?'WebGL':renderer.meshBackend==='webgl'?'Canvas2D fallback':'Canvas2D';
|
|
13
|
+
return {...base,kind:'fresh',backend,label:`Fresh meshes · ${label}${renderer.meshReuseCount?' · Cached rasters also reused':''}`};
|
|
14
|
+
}
|
|
15
|
+
const base={mode:'canvas' as const,fresh:renderer.framesReused?0:1,reused:renderer.framesReused,triangles:renderer.trianglesDrawn};
|
|
16
|
+
if(renderer.framesReused)return {...base,kind:'cached',backend:null,label:'Cached canvas frame reused'};
|
|
17
|
+
if(!renderer.trianglesDrawn)return {...base,kind:'no-mesh',backend:null,label:'Canvas frame · No drawable triangles'};
|
|
18
|
+
return {...base,kind:'fresh',backend:renderer.backendActive,label:`Canvas frame · ${renderer.backendActive==='webgl'?'WebGL':renderer.backend==='webgl'?'Canvas2D fallback':'Canvas2D'}`};
|
|
19
|
+
}
|
|
20
|
+
export interface RendererBadgeState {phase:'pending'|'failed'|'drawn'|'held';raster:RasterStatus;label:string}
|
|
21
|
+
/** A skipped preparation cannot resample stale counters as a new visible draw. */
|
|
22
|
+
export class RendererBadge {
|
|
23
|
+
private last:RasterStatus|null=null;
|
|
24
|
+
private value:RendererBadgeState={phase:'pending',raster:empty('pending'),label:'Not rendered'};
|
|
25
|
+
observe(phase:'unknown'|'drawn'|'held'|'failed',renderer:RendererTelemetry|null):RendererBadgeState {return phase==='drawn'?this.succeeded(renderer):phase==='held'?this.held():phase==='failed'?this.failed():this.pending();}
|
|
26
|
+
snapshot():RendererBadgeState{return {...this.value,raster:{...this.value.raster}};}
|
|
27
|
+
pending():RendererBadgeState {this.value={phase:'pending',raster:empty('pending'),label:this.last?'Previous draw · synchronizing':'Not rendered'};return this.snapshot();}
|
|
28
|
+
failed():RendererBadgeState {this.last=null;this.value={phase:'failed',raster:empty('failed'),label:'Rendering failed · pair unavailable'};return this.snapshot();}
|
|
29
|
+
succeeded(renderer:RendererTelemetry|null):RendererBadgeState {const raster=rendererStatus(renderer,'succeeded');this.last=raster;this.value={phase:'drawn',raster,label:raster.label};return this.snapshot();}
|
|
30
|
+
held():RendererBadgeState {const raster=this.last??empty('pending');this.value={phase:'held',raster,label:this.last?`Last drawn · ${raster.label} · held`:'Not rendered · held'};return this.snapshot();}
|
|
31
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { reportFields, type ReportEvent, type ReportSeeds, type ReportTarget } from '../src/report.ts';
|
|
2
|
+
|
|
3
|
+
/** Presentation values only. This never derives seconds from frame count or duration. */
|
|
4
|
+
export function eventSelection(event: ReportEvent, targetIndex?: number) {
|
|
5
|
+
const target = targetIndex === undefined
|
|
6
|
+
? event.targets.length === 1 ? event.targets[0]! : null
|
|
7
|
+
: Number.isInteger(targetIndex) ? event.targets[targetIndex] ?? null : null;
|
|
8
|
+
return {
|
|
9
|
+
frames: event.frames ? [...event.frames] as [number, number] : null,
|
|
10
|
+
target: target ? { ...target } : null,
|
|
11
|
+
targets: event.targets.map(value => ({ ...value })),
|
|
12
|
+
exactTimes: event.times ? [...event.times] : [],
|
|
13
|
+
seek: event.seek ? { ...event.seek } : null,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** A chosen sample is one exact supplied timestamp, never the span between samples. */
|
|
18
|
+
export function eventSeekTime(event: ReportEvent, timeIndex?: number): number | null {
|
|
19
|
+
if (timeIndex === undefined) return event.seek?.time ?? null;
|
|
20
|
+
return Number.isInteger(timeIndex) && timeIndex >= 0 ? event.times?.[timeIndex] ?? null : null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function eventTimingLabel(event: ReportEvent): string {
|
|
24
|
+
if (!event.seek) return event.frames ? 'No time mapping — cannot seek' : 'No time data — cannot seek';
|
|
25
|
+
const source = { event: 'Explicit time', frameTimes: 'frameTimes mapping', fps: 'Explicit fps + frame0 mapping' }[event.seek.source];
|
|
26
|
+
return `${source} · ${event.seek.time} s`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function eventCoordinates(event: ReportEvent): string {
|
|
30
|
+
const labels: string[] = [];
|
|
31
|
+
if (event.frames) labels.push(`F${event.frames[0]}–${event.frames[1]}`);
|
|
32
|
+
if (event.field === 'uncovered' && event.times) labels.push(`${event.times.length} discrete times`);
|
|
33
|
+
else if (event.timeInterval) labels.push(`${event.timeInterval.start}–${event.timeInterval.end} s (${event.timeInterval.endKind === 'boundary' ? 'end boundary' : 'frame time'})`);
|
|
34
|
+
else if (event.seek) labels.push(`${event.seek.time} s`);
|
|
35
|
+
return labels.join(' · ');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function reportCounts(report: ReportSeeds) {
|
|
39
|
+
return {
|
|
40
|
+
events: report.categories.reduce((sum, category) => sum + category.events.length, 0),
|
|
41
|
+
empty: report.categories.filter(category => category.state === 'empty').map(category => category.field),
|
|
42
|
+
missing: reportFields.filter(field => !report.categories.some(category => category.field === field)),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function targetLabel(target: ReportTarget): string { return `${target.kind}:${target.name}`; }
|
|
47
|
+
|
|
48
|
+
/** A new finding needs its own human judgment; sub-selections keep that draft. */
|
|
49
|
+
export function reportDraftForMarker(previousId: string | null, nextId: string, draft: { category: string; summary: string }) {
|
|
50
|
+
return previousId === nextId ? { ...draft } : { category: '', summary: '' };
|
|
51
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/** Inspection graphics from the last successfully drawn public player pose. */
|
|
2
|
+
import type {BoneWorld,PosedFrame,RigPlayer} from '@rig-c/play';
|
|
3
|
+
|
|
4
|
+
export interface OverlayPoint {x:number;y:number}
|
|
5
|
+
export type OverlayProject=(x:number,y:number)=>OverlayPoint;
|
|
6
|
+
export interface BoneOverlaySpec {name:string;parent?:string;length?:number}
|
|
7
|
+
export interface RigOverlaySettings {images:boolean;mesh:boolean;bones:boolean;meshOpacity:number}
|
|
8
|
+
export interface OverlayPlayer extends Pick<RigPlayer,'posedFrame'|'bone'> {}
|
|
9
|
+
export interface OverlayLine {from:OverlayPoint;to:OverlayPoint;slot?:string}
|
|
10
|
+
export interface OverlayBone {name:string;point:OverlayPoint;xAxis:OverlayPoint;yAxis:OverlayPoint}
|
|
11
|
+
export interface RigOverlayGeometry {mesh:OverlayLine[];bones:OverlayLine[];joints:OverlayBone[];warnings:string[];captured:{mesh:boolean;bones:boolean}}
|
|
12
|
+
export interface OverlaySnapshot {
|
|
13
|
+
available:boolean;needsCapture:boolean;meshEdges:number;boneCount:number;
|
|
14
|
+
capabilities:{images:boolean;mesh:boolean;bones:boolean};warnings:string[];
|
|
15
|
+
}
|
|
16
|
+
export interface OverlayView {project:OverlayProject;settings:RigOverlaySettings;dpr?:number}
|
|
17
|
+
export interface OverlayRefresh extends OverlayView {player:OverlayPlayer|null;bones:readonly BoneOverlaySpec[]}
|
|
18
|
+
const point=(x:number,y:number):OverlayPoint=>({x,y});
|
|
19
|
+
const finite=(p:OverlayPoint)=>Number.isFinite(p.x)&&Number.isFinite(p.y);
|
|
20
|
+
const message=(error:unknown)=>error instanceof Error?error.message:String(error);
|
|
21
|
+
|
|
22
|
+
function meshLines(frame:PosedFrame,warnings:string[]):OverlayLine[]{
|
|
23
|
+
const lines:OverlayLine[]=[];
|
|
24
|
+
for(const slot of frame.order){
|
|
25
|
+
if(slot.kind!=='mesh')continue;
|
|
26
|
+
const edges=new Set<string>();
|
|
27
|
+
if(slot.vertices.length%2||slot.triangles.length%3){warnings.push(`Mesh overlay ${slot.slot}: incomplete coordinates or triangle`);continue;}
|
|
28
|
+
for(let i=0;i<slot.triangles.length;i+=3){
|
|
29
|
+
const ids=slot.triangles.slice(i,i+3);
|
|
30
|
+
if(ids.some(index=>!Number.isSafeInteger(index)||index<0||index*2+1>=slot.vertices.length)){warnings.push(`Mesh overlay ${slot.slot}: triangle index outside posed vertices`);continue;}
|
|
31
|
+
const points=ids.map(index=>point(slot.vertices[index*2]!,slot.vertices[index*2+1]!));
|
|
32
|
+
if(points.some(p=>!finite(p))){warnings.push(`Mesh overlay ${slot.slot}: nonfinite posed vertex`);continue;}
|
|
33
|
+
for(let edge=0;edge<3;edge++){
|
|
34
|
+
const a=ids[edge]!,b=ids[(edge+1)%3]!,key=a<b?`${a}:${b}`:`${b}:${a}`;
|
|
35
|
+
if(edges.has(key))continue;edges.add(key);
|
|
36
|
+
lines.push({from:{...points[edge]!},to:{...points[(edge+1)%3]!},slot:slot.slot});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return lines;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Takes copies; later seeks cannot turn an old overlay into a prepared pose. */
|
|
44
|
+
export function captureRigGeometry(player:OverlayPlayer,specs:readonly BoneOverlaySpec[],layers:Pick<RigOverlaySettings,'mesh'|'bones'>):RigOverlayGeometry{
|
|
45
|
+
const geometry:RigOverlayGeometry={mesh:[],bones:[],joints:[],warnings:[],captured:{mesh:false,bones:false}};
|
|
46
|
+
if(layers.mesh){
|
|
47
|
+
try{geometry.mesh=meshLines(player.posedFrame(),geometry.warnings);geometry.captured.mesh=true;}
|
|
48
|
+
catch(error){geometry.warnings.push(`Mesh overlay unavailable: ${message(error)}`);}
|
|
49
|
+
}
|
|
50
|
+
if(layers.bones){
|
|
51
|
+
const found=new Map<string,BoneWorld>();
|
|
52
|
+
const read=(name:string):BoneWorld|null=>{
|
|
53
|
+
if(found.has(name))return found.get(name)!;
|
|
54
|
+
try{const value=player.bone(name);if(![value.x,value.y,value.a,value.b,value.c,value.d].every(Number.isFinite))throw new Error('nonfinite world transform');const copied={...value};found.set(name,copied);return copied;}
|
|
55
|
+
catch(error){geometry.warnings.push(`Bone overlay ${name}: ${message(error)}`);return null;}
|
|
56
|
+
};
|
|
57
|
+
const names=new Set<string>();
|
|
58
|
+
for(const spec of specs){
|
|
59
|
+
if(!spec.name||names.has(spec.name)){geometry.warnings.push('Bone overlay: missing or duplicate declared name');continue;}names.add(spec.name);
|
|
60
|
+
const bone=read(spec.name);if(!bone?.active)continue;
|
|
61
|
+
const from=point(bone.x,bone.y);
|
|
62
|
+
geometry.joints.push({name:spec.name,point:from,xAxis:point(bone.a,bone.c),yAxis:point(bone.b,bone.d)});
|
|
63
|
+
if(spec.length!==undefined&&(!Number.isFinite(spec.length)||spec.length<0)){geometry.warnings.push(`Bone overlay ${spec.name}: invalid declared length`);continue;}
|
|
64
|
+
if(spec.length!==undefined&&spec.length>0){geometry.bones.push({from:{...from},to:point(bone.x+bone.a*spec.length,bone.y+bone.c*spec.length)});}
|
|
65
|
+
else if(spec.parent){const parent=read(spec.parent);if(parent?.active)geometry.bones.push({from:point(parent.x,parent.y),to:{...from}});}
|
|
66
|
+
}
|
|
67
|
+
geometry.captured.bones=true;
|
|
68
|
+
}
|
|
69
|
+
return geometry;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The canvas must be a sibling above rigRoot. refresh follows an actual successful
|
|
74
|
+
* draw; redraw is for camera/layer changes and never samples a prepared player.
|
|
75
|
+
*/
|
|
76
|
+
export class RigOverlays {
|
|
77
|
+
private geometry:RigOverlayGeometry|null=null;
|
|
78
|
+
private playerAvailable=false;
|
|
79
|
+
private disposed=false;
|
|
80
|
+
private readonly visibility:string;
|
|
81
|
+
private readonly pointerEvents:string;
|
|
82
|
+
constructor(private readonly host:{canvas:HTMLCanvasElement;rigRoot:HTMLElement}){
|
|
83
|
+
this.visibility=host.rigRoot.style.visibility;this.pointerEvents=host.canvas.style.pointerEvents;
|
|
84
|
+
host.canvas.style.pointerEvents='none';
|
|
85
|
+
}
|
|
86
|
+
refresh(input:OverlayRefresh):OverlaySnapshot{
|
|
87
|
+
if(this.disposed)return this.unavailable('Overlay has been disposed');
|
|
88
|
+
this.playerAvailable=!!input.player;
|
|
89
|
+
this.geometry=input.player?captureRigGeometry(input.player,input.bones,input.settings):null;
|
|
90
|
+
return this.redraw(input);
|
|
91
|
+
}
|
|
92
|
+
redraw({project,settings,dpr=globalThis.devicePixelRatio??1}:OverlayView):OverlaySnapshot{
|
|
93
|
+
if(this.disposed)return this.unavailable('Overlay has been disposed');
|
|
94
|
+
this.host.rigRoot.style.visibility=settings.images?this.visibility:'hidden';
|
|
95
|
+
const warnings=[...(this.geometry?.warnings??[])],ctx=this.host.canvas.getContext('2d');
|
|
96
|
+
const captured=this.geometry?.captured;
|
|
97
|
+
const needsCapture=this.playerAvailable&&((settings.mesh&&!captured?.mesh)||(settings.bones&&!captured?.bones));
|
|
98
|
+
if(needsCapture)warnings.push('Overlay pending: a successful rig draw is required for the selected layers');
|
|
99
|
+
if(!this.playerAvailable&&(settings.mesh||settings.bones))warnings.push('Overlay unavailable: no successfully drawn rig pose');
|
|
100
|
+
if(!ctx){warnings.push('Overlay unavailable: Canvas2D context missing');return this.snapshot(false,needsCapture,warnings);}
|
|
101
|
+
const width=this.host.canvas.clientWidth,height=this.host.canvas.clientHeight;
|
|
102
|
+
if(!Number.isFinite(dpr)||dpr<=0){warnings.push('Overlay unavailable: positive finite pixel ratio required');ctx.clearRect(0,0,this.host.canvas.width,this.host.canvas.height);return this.snapshot(false,needsCapture,warnings);}
|
|
103
|
+
if(width<=0||height<=0){ctx.clearRect(0,0,this.host.canvas.width,this.host.canvas.height);return this.snapshot(true,needsCapture,warnings);}
|
|
104
|
+
const pw=Math.max(1,Math.round(width*dpr)),ph=Math.max(1,Math.round(height*dpr));
|
|
105
|
+
if(this.host.canvas.width!==pw)this.host.canvas.width=pw;
|
|
106
|
+
if(this.host.canvas.height!==ph)this.host.canvas.height=ph;
|
|
107
|
+
ctx.setTransform(dpr,0,0,dpr,0,0);ctx.clearRect(0,0,width,height);
|
|
108
|
+
const projected=(p:OverlayPoint):OverlayPoint|null=>{
|
|
109
|
+
const value=project(p.x,p.y);if(finite(value))return value;
|
|
110
|
+
if(!warnings.includes('Overlay projection returned nonfinite coordinates'))warnings.push('Overlay projection returned nonfinite coordinates');return null;
|
|
111
|
+
};
|
|
112
|
+
const line=(value:OverlayLine)=>{const from=projected(value.from),to=projected(value.to);if(from&&to){ctx.moveTo(from.x,from.y);ctx.lineTo(to.x,to.y);}};
|
|
113
|
+
ctx.save();
|
|
114
|
+
if(settings.mesh&&captured?.mesh){
|
|
115
|
+
ctx.strokeStyle='#007d98';ctx.lineWidth=2.3;ctx.globalAlpha=Math.max(0,Math.min(1,Number.isFinite(settings.meshOpacity)?settings.meshOpacity:0));
|
|
116
|
+
ctx.beginPath();for(const edge of this.geometry!.mesh)line(edge);ctx.stroke();
|
|
117
|
+
ctx.strokeStyle='#69f1ff';ctx.lineWidth=.9;ctx.beginPath();for(const edge of this.geometry!.mesh)line(edge);ctx.stroke();
|
|
118
|
+
}
|
|
119
|
+
if(settings.bones&&captured?.bones){
|
|
120
|
+
ctx.globalAlpha=1;ctx.strokeStyle='#5c3b00';ctx.lineWidth=3;ctx.beginPath();for(const edge of this.geometry!.bones)line(edge);ctx.stroke();
|
|
121
|
+
ctx.strokeStyle='#ffe26b';ctx.lineWidth=1.3;ctx.beginPath();for(const edge of this.geometry!.bones)line(edge);ctx.stroke();
|
|
122
|
+
for(const bone of this.geometry!.joints){
|
|
123
|
+
const center=projected(bone.point);if(!center)continue;
|
|
124
|
+
// Axis ticks are display glyphs, not invented bone lengths.
|
|
125
|
+
for(const [axis,color] of [[bone.xAxis,'#ff876f'],[bone.yAxis,'#80e9a2']] as const){
|
|
126
|
+
const end=projected(point(bone.point.x+axis.x,bone.point.y+axis.y));if(!end)continue;
|
|
127
|
+
const dx=end.x-center.x,dy=end.y-center.y,length=Math.hypot(dx,dy);if(length===0)continue;
|
|
128
|
+
ctx.strokeStyle=color;ctx.lineWidth=1.5;ctx.beginPath();ctx.moveTo(center.x,center.y);ctx.lineTo(center.x+dx/length*9,center.y+dy/length*9);ctx.stroke();
|
|
129
|
+
}
|
|
130
|
+
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();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
ctx.restore();
|
|
134
|
+
return this.snapshot(true,needsCapture,warnings);
|
|
135
|
+
}
|
|
136
|
+
clear():void{
|
|
137
|
+
this.geometry=null;this.playerAvailable=false;
|
|
138
|
+
const ctx=this.host.canvas.getContext('2d');if(ctx){ctx.save();ctx.setTransform(1,0,0,1,0,0);ctx.clearRect(0,0,this.host.canvas.width,this.host.canvas.height);ctx.restore();}
|
|
139
|
+
}
|
|
140
|
+
dispose():void{
|
|
141
|
+
if(this.disposed)return;this.clear();this.host.rigRoot.style.visibility=this.visibility;this.host.canvas.style.pointerEvents=this.pointerEvents;this.disposed=true;
|
|
142
|
+
}
|
|
143
|
+
private snapshot(canvasAvailable:boolean,needsCapture:boolean,warnings:string[]):OverlaySnapshot{
|
|
144
|
+
return {available:this.playerAvailable&&canvasAvailable,needsCapture,meshEdges:this.geometry?.mesh.length??0,boneCount:this.geometry?.joints.length??0,capabilities:{images:this.playerAvailable,mesh:this.playerAvailable&&canvasAvailable,bones:this.playerAvailable&&canvasAvailable},warnings};
|
|
145
|
+
}
|
|
146
|
+
private unavailable(warning:string):OverlaySnapshot{return {available:false,needsCapture:false,meshEdges:0,boneCount:0,capabilities:{images:false,mesh:false,bones:false},warnings:[warning]};}
|
|
147
|
+
}
|
package/web/styles.css
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
:root{font-family:Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color:#e3eceb;background:#10181c;font-size:13px;color-scheme:dark;font-synthesis:none}*{box-sizing:border-box}body{margin:0}button,input,select,textarea{font:inherit}button{color:inherit;background:#26383f;border:1px solid #3a535f;border-radius:6px;padding:7px 10px;cursor:pointer}button:hover{background:#344d58}button:disabled{opacity:.45;cursor:default}button[aria-pressed=true]{background:#355445;border-color:#729985;color:#dcf4e5}button.primary{background:#b1dec6;border-color:#b1dec6;color:#163728;font-weight:600}button.outline{background:transparent}button.block{width:100%;margin-top:10px}a{color:#b2dccb}input,select,textarea{color:#e3eceb;background:#12242d;border:1px solid #486472;border-radius:6px;padding:8px;max-width:100%}select,input[type=text],textarea{width:100%}textarea{resize:vertical;line-height:1.6}input[type=range]{accent-color:#b2dec7;padding:0;min-width:0}input[type=checkbox],input[type=radio]{accent-color:#b2dec7}button:focus-visible,a:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible{outline:2px solid #c6e7d7;outline-offset:3px}[hidden]{display:none!important}.topbar{min-height:72px;padding:15px 22px;display:flex;align-items:center;gap:25px;border-bottom:1px solid #30464f;flex-wrap:wrap;background:#15232a}.brand{font-size:19px;letter-spacing:-.6px;white-space:nowrap}.brand-mark{display:inline-grid;place-items:center;width:24px;height:24px;border:1px solid #afdcc5;color:#afdcc5;border-radius:50%;margin-right:9px;font-size:14px}.topbar strong{font-weight:500;font-size:13px}.topbar #revision{margin-left:9px}.top-actions{display:flex;align-items:center;gap:8px;margin-left:auto;flex-wrap:wrap}.subtle,.hint{color:#a2bac4;font-size:11px}.hint{line-height:1.6}.workspaces{display:flex;gap:10px;padding:0 18px;border-bottom:1px solid #30464f;background:#142129}.workspaces button{border:0;border-bottom:2px solid transparent;border-radius:0;background:transparent;padding:15px 13px;font-size:12px;letter-spacing:.06em;color:#a6bec8}.workspaces button[aria-pressed=true]{border-bottom-color:#b1dec6;color:#c8ead8;background:transparent}.workspaces button span{font-size:11px;margin-left:8px;letter-spacing:0;color:#8faab6}.layout{display:grid;grid-template-columns:190px minmax(250px,1fr) 286px;min-height:calc(100vh - 160px)}aside{background:#182a33;padding:20px 15px}.left-panel{border-right:1px solid #30464f}.right-panel{border-left:1px solid #30464f}.eyebrow{font-size:11px;letter-spacing:.08em;color:#aac0c8;margin:0 0 11px}.label{display:block;color:#acbec8;font-size:11px;margin:0 0 7px}.space-top{margin-top:15px}.left-panel section{margin-top:26px}.tool{width:100%;display:flex;justify-content:space-between;align-items:center;text-align:left;gap:5px;border-color:transparent;background:transparent;padding:9px 6px;margin:3px 0}.tool small{font-size:11px;color:#9bb4bf}.checkpoint{display:block;width:100%;background:transparent;border:1px solid transparent;text-align:left;margin:4px 0;padding:10px 9px;white-space:normal;line-height:1.5}.checkpoint[aria-current=true]{background:#294638;border-color:#658d76}.checkpoint small{display:block;font-size:11px;color:#9eb8aa;margin-top:3px}.source-link{display:block;font-size:11px;overflow-wrap:anywhere;margin:8px 0}main{min-width:0;display:flex;flex-direction:column;background:#12232c}.viewbar{display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap;padding:12px;border-bottom:1px solid #2e4855}.segmented{display:flex;gap:3px;padding:3px;border:1px solid #3a5663;border-radius:8px;background:#11202a}.segmented button{border:0;background:transparent;font-size:11px;padding:5px 9px}.segmented button[aria-pressed=true]{background:#395560;color:#e3eceb}.zoom-controls{display:flex;gap:4px;align-items:center}.zoom-controls button{padding:4px 8px;font-size:12px}.zoom-controls output{font:11px ui-monospace,SFMono-Regular,monospace;min-width:39px;text-align:center;color:#abc1cc}.stage-meta{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:14px 16px 0;font-size:11px;flex-wrap:wrap}.stages{display:flex;gap:10px;padding:12px 12px 0;flex:1;min-height:460px}.stage-pane{position:relative;min-width:0;flex:1;border:1px solid #35535f;border-radius:8px;overflow:hidden;background:radial-gradient(ellipse at 50% 40%,#233d48,#10222c 74%)}.pane-label{position:absolute;left:10px;top:10px;z-index:2;background:#152a35d9;border-radius:4px;padding:4px 7px;color:#c0d4db;font-size:11px;pointer-events:none}.viewport{position:relative;overflow:hidden;width:100%;height:100%;min-height:450px;touch-action:none;background-image:radial-gradient(#8eb2c333 .7px,transparent .7px);background-size:22px 22px}.empty{position:absolute;inset:65px 22px 30px;display:grid;place-items:center;text-align:center;color:#9dbac7;font-size:12px;line-height:1.8}.reference-content,#rig-root{position:absolute;transform-origin:0 0}.reference-content img,.reference-content video{display:block;max-width:none}.reference-content svg{display:block;max-width:none}.reference-content video{object-fit:contain}.stage-footer{display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap;padding:11px 16px;color:#90afbd;font-size:11px}.transport{display:flex;align-items:center;gap:11px;padding:13px;border-top:1px solid #35515e;background:#162e3b}.transport select{width:auto;max-width:150px;font-size:11px}.transport input{flex:1}.transport output{font:11px ui-monospace,SFMono-Regular,monospace;white-space:nowrap;color:#c1d7df}.sound-panel,.angle-panel{padding:15px;border-top:1px solid #35515e;background:#172f3b}.row{display:flex;align-items:center;gap:11px;flex-wrap:wrap}.row .eyebrow{margin:0}.sound-panel .row{font-size:11px;margin-bottom:9px}.sound-panel input[type=range]{flex:1;max-width:160px}.cue{display:flex;gap:9px;align-items:center;justify-content:space-between;border-top:1px solid #35505c;padding:9px 0;font-size:11px;flex-wrap:wrap}.cue button{padding:4px 7px;font-size:11px}.angle-panel{display:flex;gap:10px;align-items:center;flex-wrap:wrap;font-size:11px}.angle-panel select{width:auto;max-width:150px}.angle-panel .hint{width:100%;margin:0}.angle-panel input{flex:1}.divider{height:1px;background:#38515e;margin:22px 0}h1{font-size:15px;font-weight:500;line-height:1.6;margin:0 0 7px}.outcome{border:0;padding:0;margin:18px 0 0;display:flex;gap:11px;flex-wrap:wrap}.outcome legend{font-size:11px;color:#aec6cf;margin-bottom:8px;padding:0}.outcome label{display:flex;align-items:center;gap:3px;font-size:11px}.proposal-item{border-top:1px solid #3b5664;padding:11px 0;margin-top:12px;font-size:11px;overflow-wrap:anywhere}.proposal-item strong{display:block;font-weight:500;margin-bottom:5px}.proposal-item button{font-size:11px;padding:3px 6px;margin-top:7px}footer{display:flex;justify-content:space-between;gap:15px;flex-wrap:wrap;padding:11px 18px;border-top:1px solid #344e5a;background:#14242c;font-size:11px;color:#b4cad3}.error{color:#f0baac!important}.dirty{color:#e4c99a!important}.sr-only{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)}
|
|
2
|
+
@media(min-width:1500px){.layout{grid-template-columns:218px minmax(250px,1fr) 310px}.stages{min-height:600px}}
|
|
3
|
+
@media(max-width:1000px){.layout{grid-template-columns:158px minmax(250px,1fr) 250px}aside{padding:17px 12px}.topbar{gap:14px}.topbar #revision{display:block;margin-left:0}.stages{min-height:400px}.viewport{min-height:400px}.workspaces span{display:none}}
|
|
4
|
+
@media(max-width:780px){.layout{grid-template-columns:150px minmax(0,1fr)}.right-panel{grid-column:1/-1;border-left:0;border-top:1px solid #30464f;display:grid;grid-template-columns:1fr 1fr;gap:12px 24px}.right-panel>.eyebrow,.right-panel>.label,.right-panel>#review-status{grid-column:1/-1}.right-panel>.divider{display:none}#checkpoint-detail,#proposal-panel{min-width:0}.top-actions{margin-left:0}.stages{min-height:420px}.viewport{min-height:420px}.transport{flex-wrap:wrap}.transport input{min-width:130px}.topbar{padding:14px 16px}}
|
|
5
|
+
@media(max-width:540px){.layout{grid-template-columns:1fr}.left-panel{border-right:0;border-bottom:1px solid #30464f;display:grid;grid-template-columns:1fr 1fr;gap:12px}.left-panel>.eyebrow,.left-panel>#checkpoints{grid-column:1/-1}.left-panel section{margin-top:8px}#checkpoints{display:flex;gap:5px;flex-wrap:wrap}.checkpoint{width:auto;flex:1;min-width:120px}.right-panel{display:block}#proposal-panel{margin-top:25px}.stages{gap:6px;padding:8px}.stage-pane{border-radius:5px}.viewport{min-height:365px}.stages{min-height:365px}.pane-label{left:5px;top:6px;font-size:11px;padding:3px}.viewbar{gap:8px}.workspaces{padding:0 8px}.workspaces button{padding:13px 9px}.stage-footer{padding:9px 12px}.source-link{font-size:11px}footer{padding:11px 12px}}
|
|
6
|
+
@media(pointer:coarse){button{min-height:42px}input,select,textarea{font-size:16px}.outcome label{min-height:40px}.zoom-controls button{min-width:38px}}
|
|
7
|
+
|
|
8
|
+
.frame-range{display:grid;grid-template-columns:1fr 1fr;gap:9px}.frame-range label{font-size:11px;color:#aec6cf}.frame-range input{width:100%;margin-top:6px}
|
|
9
|
+
|
|
10
|
+
.provenance{margin-top:20px;padding:12px;background:#1d3540;border:1px solid #45616c;border-radius:7px}.provenance-row{display:flex;gap:9px;justify-content:space-between;font-size:11px;margin:6px 0}.provenance-row span{color:#a9c0ca}.provenance-row strong{font-weight:500;overflow-wrap:anywhere}.provenance code{font:11px ui-monospace,SFMono-Regular,monospace}.provenance details{margin-top:10px;font-size:11px;color:#b9d0d9}.provenance summary{cursor:pointer}.provenance pre{white-space:pre-wrap;overflow-wrap:anywhere;font:11px/1.6 ui-monospace,SFMono-Regular,monospace}.right-panel>.provenance{grid-column:1/-1}
|
|
11
|
+
|
|
12
|
+
.report-panel{padding:15px;border-top:1px solid #35515e;background:#142a35}.report-header{display:flex;align-items:center;gap:12px;flex-wrap:wrap}.report-header h2{font-size:13px;font-weight:500;margin:0}.report-header a{font-size:11px;margin-left:auto}.report-controls{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-top:12px}.report-controls select{width:auto;max-width:250px;flex:1;font-size:11px}.report-controls button{font-size:11px;padding:5px 9px}.report-controls output{font:11px ui-monospace,SFMono-Regular,monospace;color:#b3cbd5}.report-empty-categories{display:flex;gap:6px;flex-wrap:wrap;margin-top:11px}.report-empty-categories span{font-size:11px;color:#a8c2ce;background:#213943;padding:2px 6px;border-radius:4px}.report-event-list{list-style:none;padding:0;margin:10px 0 0;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:5px}.report-event-list li{min-width:0}.report-event-list button{display:flex;flex-direction:column;gap:2px;text-align:left;width:100%;height:100%;padding:8px 9px;border-color:#304b58;background:#192f3b;white-space:normal;overflow-wrap:anywhere;line-height:1.5}.report-event-list button[aria-current=true]{border-color:#88b39a;background:#294537}.report-event-list strong{font-size:11px;font-weight:500}.report-event-list small{font-size:11px;color:#a4bdc8}.report-event-list .no-time{color:#dfc49c}.report-selection{border-top:1px solid #3e5b68;margin-top:12px;padding-top:12px}.report-selection h3{font-size:12px;font-weight:500;margin:0}.report-selection details{font-size:11px;color:#aac3ce;margin-top:10px}.report-selection summary{cursor:pointer}.report-selection pre{white-space:pre-wrap;overflow-wrap:anywhere;font:11px/1.6 ui-monospace,SFMono-Regular,monospace;background:#11232d;padding:10px;border-radius:5px}.report-selection #report-time{max-width:100%}@media(max-width:1000px){.report-event-list{grid-template-columns:1fr}}@media(max-width:540px){.report-controls select{min-width:140px}.report-header a{margin-left:0}}
|
|
13
|
+
|
|
14
|
+
.transport #frame-label{color:#c6e6d4;font-size:11px}.frame-controls{display:flex;align-items:center;gap:9px;flex-wrap:wrap;padding:10px 14px;background:#17303b;border-top:1px solid #35515e;font-size:11px}.frame-controls label{color:#b1c9d2}.frame-controls input[type=number]{width:86px;font-variant-numeric:tabular-nums}.frame-controls input[type=range]{flex:1;min-width:100px}.frame-controls button{font-size:11px;padding:5px 9px}.frame-controls .hint{margin:0}.frame-controls #frame-outside{width:100%}
|
|
15
|
+
|
|
16
|
+
/* Inspection layout is bounded independently of pane content and review forms. */
|
|
17
|
+
#stage-layout{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));grid-template-rows:minmax(0,1fr);height:clamp(480px,calc(100dvh - 300px),900px);min-height:0;flex:none;align-items:stretch}
|
|
18
|
+
#stage-layout[data-orientation=vertical]{grid-template-columns:minmax(0,1fr);grid-template-rows:repeat(2,minmax(0,1fr))}
|
|
19
|
+
#stage-layout>.stage-pane,#stage-layout>.pane{grid-column:auto;min-height:0;min-width:0;display:flex;flex-direction:column}
|
|
20
|
+
#stage-layout .viewport,#stage-layout .rig-viewport,#stage-layout .reference-viewport{min-height:0;min-width:0;flex:1;touch-action:none;cursor:grab;user-select:none}
|
|
21
|
+
#stage-layout [data-panning=true]{cursor:grabbing!important}
|
|
22
|
+
#stage-layout img{-webkit-user-drag:none;user-select:none}
|
|
23
|
+
.rig-overlay{position:absolute;inset:0;pointer-events:none;width:100%;height:100%;z-index:1}
|
|
24
|
+
.inspection-controls{display:flex;align-items:center;gap:8px 14px;flex-wrap:wrap;padding:9px 14px;border-bottom:1px solid #35515e;font-size:11px;background:#172f3b}
|
|
25
|
+
.inspection-controls label{display:inline-flex;align-items:center;gap:5px;margin:0;white-space:nowrap}
|
|
26
|
+
.inspection-controls select{width:auto;padding:4px;font-size:11px}
|
|
27
|
+
.inspection-controls input[type=checkbox]{width:auto;margin:0}
|
|
28
|
+
.inspection-controls fieldset{display:flex;gap:10px;align-items:center;border:0;padding:0;margin:0;min-width:0;flex-wrap:wrap}
|
|
29
|
+
.inspection-controls legend{float:left;margin-right:9px;font-size:11px}
|
|
30
|
+
#control-state:empty{display:none}
|
|
31
|
+
:root[data-theme=light]{color-scheme:light;color:#233c47;background:#f4f7f9}
|
|
32
|
+
:root[data-theme=dark]{color-scheme:dark}
|
|
33
|
+
[data-theme=light] body,[data-theme=light] main{background:#f4f7f9;color:#233c47}
|
|
34
|
+
[data-theme=light] header,[data-theme=light] .topbar,[data-theme=light] .workspaces,[data-theme=light] footer{background:#edf3f5;border-color:#bacbd3;color:#294552}
|
|
35
|
+
[data-theme=light] aside,[data-theme=light] .transport,[data-theme=light] .frame-controls,[data-theme=light] .inspection-controls,[data-theme=light] .sound-panel,[data-theme=light] .angle-panel,[data-theme=light] .report-panel{background:#e9f0f3;border-color:#b7c9d1;color:#294552}
|
|
36
|
+
[data-theme=light] button{background:#e2ebef;border-color:#a6bdc8;color:#264652}
|
|
37
|
+
[data-theme=light] button:hover{background:#d0e2e8}
|
|
38
|
+
[data-theme=light] button.primary,[data-theme=light] .primary{background:#24694d;border-color:#24694d;color:#fff}
|
|
39
|
+
[data-theme=light] input,[data-theme=light] select,[data-theme=light] textarea{background:#fff;border-color:#8faab7;color:#243e4a;accent-color:#276c50}
|
|
40
|
+
[data-theme=light] input::placeholder,[data-theme=light] textarea::placeholder{color:#607986}
|
|
41
|
+
[data-theme=light] .hint,[data-theme=light] .subtle,[data-theme=light] .label,[data-theme=light] .eyebrow,[data-theme=light] aside label,[data-theme=light] .tool small,[data-theme=light] .stage-footer,[data-theme=light] .compare-info,[data-theme=light] .boundary,[data-theme=light] .trace,[data-theme=light] .transport output,[data-theme=light] .zoom-controls output,[data-theme=light] .frame-range label,[data-theme=light] .frame-controls label{color:#496774}
|
|
42
|
+
[data-theme=light] .workspaces button,[data-theme=light] .segmented button{color:#3e5c69}
|
|
43
|
+
[data-theme=light] .workspaces button[aria-pressed=true]{color:#185738;border-bottom-color:#276c50}
|
|
44
|
+
[data-theme=light] .workspaces button span{color:#58727e}
|
|
45
|
+
[data-theme=light] .segmented,[data-theme=light] .provenance,[data-theme=light] .report-selection pre{background:#e1ebef;border-color:#9cb5c0;color:#294552}
|
|
46
|
+
[data-theme=light] .segmented button[aria-pressed=true],[data-theme=light] .checkpoint[aria-current=true],[data-theme=light] .report-event-list button[aria-current=true]{background:#cde4d8;border-color:#56866e;color:#1f4937}
|
|
47
|
+
[data-theme=light] .checkpoint small{color:#4b6c59}
|
|
48
|
+
[data-theme=light] .provenance p,[data-theme=light] .provenance-row span,[data-theme=light] .provenance details,[data-theme=light] .report-selection details,[data-theme=light] .report-event-list small{color:#486470}
|
|
49
|
+
[data-theme=light] .report-event-list button,[data-theme=light] .report-empty-categories span{background:#e5eef2;border-color:#acbfca;color:#294552}
|
|
50
|
+
[data-theme=light] .feedback-item strong{color:#1e4e3b}
|
|
51
|
+
[data-theme=light] a{color:#175f48}
|
|
52
|
+
[data-theme=light] .error{color:#a03225!important}
|
|
53
|
+
[data-theme=light] .dirty,[data-theme=light] .report-event-list .no-time{color:#875800!important}
|
|
54
|
+
[data-theme=light] :focus-visible{outline-color:#24694d}
|
|
55
|
+
/* Source pixels retain their original color; the neutral stage stays dark in both themes. */
|
|
56
|
+
|
|
57
|
+
/* The application shell never scrolls; each Inspector tab and the source list owns overflow. */
|
|
58
|
+
html,body{height:100%;overflow:hidden}body{height:100dvh}#app{height:100dvh;display:grid;grid-template-rows:auto minmax(0,1fr) auto;overflow:hidden}
|
|
59
|
+
.layout{min-height:0!important;overflow:hidden;grid-template-columns:170px minmax(0,1fr)!important}
|
|
60
|
+
main{min-height:0;overflow:hidden;display:flex;flex-direction:column}
|
|
61
|
+
.left-panel{min-height:0;overflow:auto;padding:13px 10px}
|
|
62
|
+
#stage-layout{height:auto;min-height:0;flex:1;overflow:hidden;padding:8px;gap:7px}
|
|
63
|
+
.inspector{height:clamp(205px,32dvh,340px);min-height:180px;flex:none;display:flex;flex-direction:column;border-top:1px solid #49616d;background:#182d38;overflow:hidden}
|
|
64
|
+
.inspector-heading{display:flex;align-items:center;gap:16px;flex-wrap:wrap;padding:7px 12px;border-bottom:1px solid #3e5c69;flex:none;font-size:11px}
|
|
65
|
+
.inspector-heading strong{font-size:10px;letter-spacing:.08em;font-weight:500}
|
|
66
|
+
.inspector-heading [role=tablist]{display:flex;gap:4px}
|
|
67
|
+
.inspector-heading button{padding:4px 9px;font-size:11px}
|
|
68
|
+
.inspector-heading button[aria-selected=true]{background:#3d5d4d;color:#e0f3e7;border-color:#7fa98e}
|
|
69
|
+
.inspector-body{flex:1;min-height:0;overflow:hidden}
|
|
70
|
+
.inspector-body>[role=tabpanel]{height:100%;overflow:auto;padding:12px 15px;min-height:0}
|
|
71
|
+
.inspector .inspection-controls{padding:0;border:0;background:transparent}
|
|
72
|
+
.inspector .provenance{margin-top:10px}
|
|
73
|
+
.inspector .transport{padding:0 0 10px;border:0;background:transparent;flex-wrap:wrap}
|
|
74
|
+
.inspector .report-panel{padding:10px 0}
|
|
75
|
+
.inspector .feedback-form{border:0;padding:0;margin:0}
|
|
76
|
+
#inspector-feedback{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(270px,100%),1fr));gap:20px;align-items:start}
|
|
77
|
+
#inspector-feedback>section,#inspector-feedback>#checkpoint-detail,#inspector-feedback>#proposal-panel{break-inside:avoid}
|
|
78
|
+
#inspector-feedback .divider{display:none}
|
|
79
|
+
#inspector-feedback>label,#inspector-feedback>select{display:block;max-width:300px}
|
|
80
|
+
#inspector-rig #modeling-tools{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin:0}
|
|
81
|
+
#inspector-rig .tool{width:auto}
|
|
82
|
+
#inspector-rig #part-set{max-width:190px}
|
|
83
|
+
#inspector-playback #motion-tools{display:flex;gap:8px;flex-wrap:wrap}
|
|
84
|
+
#inspector-playback .tool{width:auto}
|
|
85
|
+
[data-theme=light] .inspector{background:#edf3f5;border-color:#a6bec8;color:#294552}
|
|
86
|
+
[data-theme=light] .inspector-heading{border-color:#b3c6d0}
|
|
87
|
+
[data-theme=light] .inspector-heading button[aria-selected=true]{background:#cde4d8;color:#1e4d36;border-color:#59836b}
|
|
88
|
+
@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}}
|
|
89
|
+
@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}}
|
|
90
|
+
|
|
91
|
+
.inspector .feedback-form>label,.inspector .review-summary>label{display:block;font-size:11px;margin:10px 0 5px}.inspector .frame-range input{width:100%}.inspector [role=tabpanel]{overscroll-behavior:contain}.left-panel{overscroll-behavior:contain}#stage-layout .pane-label{max-width:calc(100% - 15px);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
92
|
+
|
|
93
|
+
.rig-layer-controls{display:flex;gap:14px;align-items:center;flex-wrap:wrap;margin-bottom:12px;font-size:11px}.rig-layer-controls fieldset{display:flex;gap:12px;align-items:center;flex-wrap:wrap;border:0;margin:0;padding:0}.rig-layer-controls legend{float:left;margin-right:10px}.rig-layer-controls label{display:inline-flex;gap:5px;align-items:center}.rig-layer-controls input[type=checkbox]{width:auto}
|
|
94
|
+
|
|
95
|
+
.numeric-view{font:11px/1.5 ui-monospace,SFMono-Regular,monospace;white-space:pre-wrap;overflow-wrap:anywhere;max-width:100%;padding:8px;background:#14232c;color:#c9dfe7;border-radius:5px}.view-link-controls{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:8px}.draft-notice{flex:none;font-size:11px;padding:0 12px;color:#e0c58e}.draft-notice:has(#draft-state:empty){display:none}.draft-notice #draft-recovery{display:inline-flex;gap:8px;margin-left:8px;flex-wrap:wrap}.draft-notice button{font-size:11px;padding:2px 6px}[data-theme=light] .numeric-view{background:#e1eaee;color:#264653}[data-theme=light] .draft-notice{color:#80521a}[data-theme=light] .outcome legend,[data-theme=light] .brand span,[data-theme=light] .toolbar{color:#496774}
|
|
96
|
+
|
|
97
|
+
.camera-sync{display:inline-flex;align-items:center;gap:5px;font-size:11px;white-space:nowrap}.camera-sync input[type=checkbox]{width:auto;flex:none;margin:0}
|
|
98
|
+
#stage-layout:has(>.stage-pane[hidden]){grid-template-columns:minmax(0,1fr);grid-template-rows:minmax(0,1fr)}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/** Local inspection controls; these never write source or review state. */
|
|
2
|
+
export type ThemePreference = 'system' | 'dark' | 'light';
|
|
3
|
+
export interface ViewPoint { x:number; y:number }
|
|
4
|
+
export function stageOrientation(width:number,height:number):'horizontal'|'vertical' {return width>=height?'horizontal':'vertical';}
|
|
5
|
+
export function panCenter(center:ViewPoint,dx:number,dy:number,scale:number,space:'world'|'image'):ViewPoint {
|
|
6
|
+
if(![center.x,center.y,dx,dy,scale].every(Number.isFinite)||scale<=0)throw new Error('Pan requires finite coordinates and a positive scale');
|
|
7
|
+
return {x:center.x-dx/scale,y:center.y+(space==='world'?dy:-dy)/scale};
|
|
8
|
+
}
|
|
9
|
+
export function idleAvailable(names:readonly string[],selected:string):boolean {return selected==='idle'&&names.includes('idle');}
|
|
10
|
+
export function initialIdleAnimation(names:readonly string[],fallback:string|null,explicit:boolean):string|null {return !explicit&&names.includes('idle')?'idle':fallback;}
|
|
11
|
+
/** An automatic idle-at-start checkpoint is preview context, not a manual hold. */
|
|
12
|
+
export function automaticIdlePreview(names:readonly string[],selected:string,checkpoint:{animation?:string;time?:number;axis?:unknown}|undefined,explicitCheckpoint:boolean):boolean {
|
|
13
|
+
return idleAvailable(names,selected)&&!explicitCheckpoint&&(!checkpoint||(checkpoint.animation==='idle'&&(checkpoint.time??0)===0&&!checkpoint.axis));
|
|
14
|
+
}
|
|
15
|
+
export function themePreference(value:unknown):ThemePreference {return value==='dark'||value==='light'?value:'system';}
|
|
16
|
+
|
|
17
|
+
export function bindAppearance(control:HTMLSelectElement):{get:()=>ThemePreference;set:(value:ThemePreference)=>void;dispose:()=>void} {
|
|
18
|
+
let current:ThemePreference='system';try{current=themePreference(localStorage.getItem('rig-review-appearance/1'));}catch{/* Storage is optional. */}
|
|
19
|
+
const system=matchMedia('(prefers-color-scheme: light)');
|
|
20
|
+
const render=()=>{document.documentElement.dataset.theme=current==='system'?(system.matches?'light':'dark'):current;control.value=current;};
|
|
21
|
+
const set=(value:ThemePreference)=>{current=value;try{localStorage.setItem('rig-review-appearance/1',current);}catch{/* Storage is optional. */}render();};
|
|
22
|
+
const change=()=>set(themePreference(control.value));control.addEventListener('change',change);system.addEventListener('change',render);render();
|
|
23
|
+
return {get:()=>current,set,dispose:()=>{control.removeEventListener('change',change);system.removeEventListener('change',render);}};
|
|
24
|
+
}
|
|
25
|
+
export function syncStageLayout(area:Pick<HTMLElement,'clientWidth'|'clientHeight'|'dataset'>):boolean {const next=stageOrientation(area.clientWidth,area.clientHeight);if(area.dataset.orientation===next)return false;area.dataset.orientation=next;return true;}
|
|
26
|
+
export function bindStageLayout(area:HTMLElement,changed:()=>void):()=>void {
|
|
27
|
+
const render=()=>{if(syncStageLayout(area))changed();};
|
|
28
|
+
const observer=new ResizeObserver(render);observer.observe(area);render();return ()=>observer.disconnect();
|
|
29
|
+
}
|
|
30
|
+
// Nested or separately disposed pan binders must not restore another binder's override.
|
|
31
|
+
const panVideoOverrides=new WeakMap<HTMLVideoElement,{owners:number;value:string;priority:string}>();
|
|
32
|
+
export function bindPan(viewport:HTMLElement,move:(dx:number,dy:number)=>void,enabled:()=>boolean=()=>true):()=>void {
|
|
33
|
+
let pointer:number|null=null,x=0,y=0,space=false;
|
|
34
|
+
const videos=new Set<HTMLVideoElement>();
|
|
35
|
+
const releaseVideo=(video:HTMLVideoElement)=>{
|
|
36
|
+
const previous=panVideoOverrides.get(video);if(!previous)return;
|
|
37
|
+
if(--previous.owners===0){if(previous.value)video.style.setProperty('pointer-events',previous.value,previous.priority);else video.style.removeProperty('pointer-events');panVideoOverrides.delete(video);}
|
|
38
|
+
};
|
|
39
|
+
const syncVideos=()=>{
|
|
40
|
+
const current=new Set(viewport.querySelectorAll<HTMLVideoElement>('video[controls]'));
|
|
41
|
+
for(const video of videos)if(!current.has(video)){releaseVideo(video);videos.delete(video);}
|
|
42
|
+
for(const video of current)if(!videos.has(video)){
|
|
43
|
+
const previous=panVideoOverrides.get(video);
|
|
44
|
+
if(previous)previous.owners++;else{panVideoOverrides.set(video,{owners:1,value:video.style.getPropertyValue('pointer-events'),priority:video.style.getPropertyPriority('pointer-events')});video.style.setProperty('pointer-events','none','important');}
|
|
45
|
+
videos.add(video);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
const observer=new MutationObserver(()=>{if(space)syncVideos();});
|
|
49
|
+
const releaseSpace=()=>{space=false;observer.disconnect();for(const video of videos)releaseVideo(video);videos.clear();};
|
|
50
|
+
const releasePointer=()=>{const id=pointer;pointer=null;delete viewport.dataset.panning;if(id!==null&&viewport.hasPointerCapture(id))viewport.releasePointerCapture(id);};
|
|
51
|
+
const keyDown=(event:KeyboardEvent)=>{if(event.code==='Space'&&enabled()&&!(event.target instanceof Element&&event.target.closest('input,textarea,select,button'))){
|
|
52
|
+
if(!space){space=true;syncVideos();observer.observe(viewport,{childList:true,subtree:true,attributes:true,attributeFilter:['controls']});}event.preventDefault();
|
|
53
|
+
}};
|
|
54
|
+
const keyUp=(event:KeyboardEvent)=>{if(event.code==='Space')releaseSpace();};const blur=()=>{releaseSpace();releasePointer();};
|
|
55
|
+
const start=(event:PointerEvent)=>{if(!enabled()||!event.isPrimary||event.button!==0)return;const target=event.target;if(target instanceof Element&&(target.closest('button,input,select,textarea,a')||target.closest('video[controls]')&&!space))return;pointer=event.pointerId;x=event.clientX;y=event.clientY;viewport.setPointerCapture(pointer);viewport.dataset.panning='true';event.preventDefault();};
|
|
56
|
+
const drag=(event:PointerEvent)=>{if(event.pointerId!==pointer)return;const dx=event.clientX-x,dy=event.clientY-y;x=event.clientX;y=event.clientY;move(dx,dy);event.preventDefault();};
|
|
57
|
+
const end=(event:PointerEvent)=>{if(event.pointerId===pointer)releasePointer();};
|
|
58
|
+
const native=(event:DragEvent)=>event.preventDefault();
|
|
59
|
+
window.addEventListener('blur',blur);document.addEventListener('keydown',keyDown,true);window.addEventListener('keyup',keyUp,true);viewport.addEventListener('pointerdown',start);viewport.addEventListener('pointermove',drag);viewport.addEventListener('pointerup',end);viewport.addEventListener('pointercancel',end);viewport.addEventListener('lostpointercapture',end);viewport.addEventListener('dragstart',native);
|
|
60
|
+
return ()=>{blur();window.removeEventListener('blur',blur);document.removeEventListener('keydown',keyDown,true);window.removeEventListener('keyup',keyUp,true);viewport.removeEventListener('pointerdown',start);viewport.removeEventListener('pointermove',drag);viewport.removeEventListener('pointerup',end);viewport.removeEventListener('pointercancel',end);viewport.removeEventListener('lostpointercapture',end);viewport.removeEventListener('dragstart',native);};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function bindInspectorTabs(changed:()=>void):void {
|
|
64
|
+
const tabs=[...document.querySelectorAll<HTMLButtonElement>('[data-inspector]')];
|
|
65
|
+
const activate=(tab:HTMLButtonElement)=>{for(const item of tabs){const active=item===tab;item.setAttribute('aria-selected',String(active));item.tabIndex=active?0:-1;}for(const panel of document.querySelectorAll<HTMLElement>('[data-inspector-panel]'))panel.hidden=panel.dataset.inspectorPanel!==tab.dataset.inspector;changed();};
|
|
66
|
+
for(const [index,tab] of tabs.entries()){tab.onclick=()=>activate(tab);tab.onkeydown=event=>{const next=event.key==='ArrowRight'?(index+1)%tabs.length:event.key==='ArrowLeft'?(index+tabs.length-1)%tabs.length:event.key==='Home'?0:event.key==='End'?tabs.length-1:null;if(next===null)return;event.preventDefault();const target=tabs[next];if(target){activate(target);target.focus();}};}
|
|
67
|
+
}
|