Spaces:
Running
Running
| // Render per-frame signals (contact_l, contact_r, grasp, motion, trust) into 5 mini SVGs. | |
| // Each SVG is 200x18 viewBox. We scale to 0..1 vertically (motion uses log(1+x)/log(1+max)). | |
| const VB_W = 200, VB_H = 18; | |
| function clearSvg(svg) { | |
| while (svg.firstChild) svg.removeChild(svg.firstChild); | |
| } | |
| function pathFromSeries(values, normalize) { | |
| if (!values || values.length === 0) return ''; | |
| const n = values.length; | |
| let pts = []; | |
| for (let i = 0; i < n; i++) { | |
| const x = (i / Math.max(n - 1, 1)) * VB_W; | |
| const v = normalize(values[i]); | |
| const y = VB_H - v * VB_H; | |
| pts.push(`${i === 0 ? 'M' : 'L'}${x.toFixed(2)},${y.toFixed(2)}`); | |
| } | |
| return pts.join(' '); | |
| } | |
| function fillBars(svg, values, normalize, color) { | |
| // For binary trust: render as horizontal bars where True | |
| const n = values.length; | |
| if (n === 0) return; | |
| const w = VB_W / n; | |
| let i = 0; | |
| while (i < n) { | |
| if (values[i]) { | |
| let j = i; | |
| while (j < n && values[j]) j++; | |
| const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); | |
| rect.setAttribute('x', (i * w).toFixed(2)); | |
| rect.setAttribute('y', '4'); | |
| rect.setAttribute('width', ((j - i) * w).toFixed(2)); | |
| rect.setAttribute('height', String(VB_H - 8)); | |
| rect.setAttribute('fill', color); | |
| rect.setAttribute('opacity', '0.7'); | |
| svg.appendChild(rect); | |
| i = j; | |
| } else { | |
| i++; | |
| } | |
| } | |
| } | |
| function drawArea(svg, values, normalize, fill, stroke) { | |
| const n = values.length; | |
| if (n === 0) return; | |
| let pts = [`M0,${VB_H}`]; | |
| for (let i = 0; i < n; i++) { | |
| const x = (i / Math.max(n - 1, 1)) * VB_W; | |
| const v = Math.max(0, Math.min(1, normalize(values[i]))); | |
| const y = VB_H - v * VB_H; | |
| pts.push(`L${x.toFixed(2)},${y.toFixed(2)}`); | |
| } | |
| pts.push(`L${VB_W},${VB_H}Z`); | |
| const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); | |
| path.setAttribute('d', pts.join(' ')); | |
| path.setAttribute('fill', fill); | |
| path.setAttribute('stroke', stroke); | |
| path.setAttribute('stroke-width', '0.8'); | |
| svg.appendChild(path); | |
| } | |
| function addPlayhead(svg, frac) { | |
| const x = frac * VB_W; | |
| const line = document.createElementNS('http://www.w3.org/2000/svg', 'line'); | |
| line.setAttribute('x1', x.toFixed(2)); | |
| line.setAttribute('x2', x.toFixed(2)); | |
| line.setAttribute('y1', '0'); | |
| line.setAttribute('y2', String(VB_H)); | |
| line.setAttribute('class', 'signal-playhead'); | |
| line.setAttribute('stroke', 'var(--accent)'); | |
| line.setAttribute('stroke-width', '1'); | |
| svg.appendChild(line); | |
| return line; | |
| } | |
| export class SignalPanel { | |
| constructor() { | |
| this.svgs = { | |
| contact_l: document.getElementById('sig-contact-l'), | |
| contact_r: document.getElementById('sig-contact-r'), | |
| grasp: document.getElementById('sig-grasp'), | |
| motion: document.getElementById('sig-motion'), | |
| trust: document.getElementById('sig-trust'), | |
| }; | |
| this.vals = { | |
| contact_l: document.getElementById('sig-val-contact-l'), | |
| contact_r: document.getElementById('sig-val-contact-r'), | |
| grasp: document.getElementById('sig-val-grasp'), | |
| motion: document.getElementById('sig-val-motion'), | |
| trust: document.getElementById('sig-val-trust'), | |
| }; | |
| this.playheads = {}; | |
| this.data = null; | |
| } | |
| setData(timeseries) { | |
| this.data = timeseries; | |
| if (!timeseries) { | |
| Object.values(this.svgs).forEach(clearSvg); | |
| Object.values(this.vals).forEach(v => v.textContent = '-'); | |
| return; | |
| } | |
| const scene = timeseries.scene || {}; | |
| this._render('contact_l', scene.contact_l || [], v => v, 'rgba(217,119,87,0.25)', 'oklch(0.68 0.18 40)'); | |
| this._render('contact_r', scene.contact_r || [], v => v, 'rgba(106,148,96,0.25)', 'oklch(0.58 0.12 150)'); | |
| this._render('grasp', scene.grasp || [], v => v, 'rgba(160,88,72,0.30)', 'oklch(0.58 0.18 30)'); | |
| // motion: log scale | |
| const motion = scene.motion || []; | |
| const maxMotion = Math.max(1, ...motion.map(v => v || 0)); | |
| this._render('motion', motion, v => Math.log1p(v) / Math.log1p(maxMotion), 'rgba(120,120,160,0.22)', 'oklch(0.55 0.10 240)'); | |
| // trust: union over per-object trust booleans (if any object trusted that frame, mark trusted) | |
| const perObj = timeseries.per_object || {}; | |
| const n = timeseries.n_frames || (motion.length || 0); | |
| const trustUnion = new Array(n).fill(false); | |
| Object.values(perObj).forEach(o => { | |
| const arr = o.trust || []; | |
| for (let i = 0; i < n; i++) if (arr[i]) trustUnion[i] = true; | |
| }); | |
| clearSvg(this.svgs.trust); | |
| fillBars(this.svgs.trust, trustUnion, v => v ? 1 : 0, 'oklch(0.68 0.18 40)'); | |
| // playhead overlay | |
| this.playheads.trust = addPlayhead(this.svgs.trust, 0); | |
| this._lastTrust = trustUnion; | |
| } | |
| _render(key, series, normalize, fill, stroke) { | |
| const svg = this.svgs[key]; | |
| clearSvg(svg); | |
| drawArea(svg, series, normalize, fill, stroke); | |
| this.playheads[key] = addPlayhead(svg, 0); | |
| } | |
| setPlayheadFrame(frameIdx) { | |
| if (!this.data) return; | |
| const n = this.data.n_frames || 1; | |
| const frac = Math.max(0, Math.min(1, frameIdx / Math.max(n - 1, 1))); | |
| Object.values(this.playheads).forEach(line => { | |
| if (!line) return; | |
| const x = (frac * VB_W).toFixed(2); | |
| line.setAttribute('x1', x); | |
| line.setAttribute('x2', x); | |
| }); | |
| // update value labels | |
| const scene = this.data.scene || {}; | |
| const f = Math.max(0, Math.min(n - 1, Math.round(frameIdx))); | |
| const fmt = (v) => (v == null ? '-' : (typeof v === 'number' ? v.toFixed(2) : String(v))); | |
| this.vals.contact_l.textContent = fmt(scene.contact_l && scene.contact_l[f]); | |
| this.vals.contact_r.textContent = fmt(scene.contact_r && scene.contact_r[f]); | |
| this.vals.grasp.textContent = fmt(scene.grasp && scene.grasp[f]); | |
| this.vals.motion.textContent = fmt(scene.motion && scene.motion[f]); | |
| this.vals.trust.textContent = (this._lastTrust && this._lastTrust[f]) ? 'yes' : 'no'; | |
| } | |
| } | |
| // βββ Per-object state timeline (static / grasped / moving) ββββββββ | |
| // Rendered into #state-timeline-rows from signals.json.per_object[oid].state | |
| // One row per object: prompt label + horizontal SVG strip + per-frame value. | |
| // Colors must match the .state-chip classes in styles.css. | |
| const STATE_COLOR = { | |
| static: 'oklch(0.78 0.01 240)', // light gray | |
| grasped_l: 'oklch(0.70 0.16 50)', // warm orange = left hand | |
| grasped_r: 'oklch(0.60 0.20 20)', // red-orange = right hand | |
| grasped_both: 'oklch(0.42 0.20 15)', // deep red = both hands | |
| moving: 'oklch(0.55 0.10 240)', // cool blue | |
| // back-compat: clips processed before the L/R split write plain "grasped" | |
| grasped: 'oklch(0.58 0.18 30)', | |
| }; | |
| export class StateTimelinePanel { | |
| constructor() { | |
| this.container = document.getElementById('state-timeline-rows'); | |
| this.rows = []; // [{oid, svg, valEl, playhead, states}] | |
| this.nFrames = 0; | |
| } | |
| setData(timeseries, scene) { | |
| if (!this.container) return; | |
| this.container.innerHTML = ''; | |
| this.rows = []; | |
| this.nFrames = (timeseries && timeseries.n_frames) || 0; | |
| const perObj = (timeseries && timeseries.per_object) || {}; | |
| const objs = (scene && scene.reconstruction && scene.reconstruction.objects) || []; | |
| // Need at least one object with a state array to render anything | |
| const haveState = Object.values(perObj).some(o => Array.isArray(o.state)); | |
| if (!haveState) { | |
| const empty = document.createElement('div'); | |
| empty.style.cssText = 'font-size:10px;color:var(--ink-3);padding:4px 0;'; | |
| empty.textContent = 'No state data; clip may pre-date the state-machine refresh.'; | |
| this.container.appendChild(empty); | |
| return; | |
| } | |
| objs.forEach(o => { | |
| const oidKey = String(o.id); | |
| const po = perObj[oidKey] || {}; | |
| const states = po.state || []; | |
| if (!states.length) return; | |
| const row = document.createElement('div'); | |
| row.className = 'state-row'; | |
| const label = document.createElement('div'); | |
| label.className = 'state-label'; | |
| label.style.color = o.color_hex || 'inherit'; | |
| label.title = o.prompt || `obj ${o.id}`; | |
| label.textContent = (o.prompt || `obj ${o.id}`).slice(0, 18); | |
| row.appendChild(label); | |
| const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); | |
| svg.setAttribute('preserveAspectRatio', 'none'); | |
| svg.setAttribute('viewBox', `0 0 ${VB_W} ${VB_H}`); | |
| // Paint segments | |
| const n = states.length; | |
| let i = 0; | |
| const w = VB_W / Math.max(n, 1); | |
| while (i < n) { | |
| const s = states[i]; | |
| let j = i; | |
| while (j < n && states[j] === s) j++; | |
| const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); | |
| rect.setAttribute('x', (i * w).toFixed(2)); | |
| rect.setAttribute('y', '2'); | |
| rect.setAttribute('width', ((j - i) * w).toFixed(2)); | |
| rect.setAttribute('height', String(VB_H - 4)); | |
| rect.setAttribute('fill', STATE_COLOR[s] || 'oklch(0.78 0.01 240)'); | |
| svg.appendChild(rect); | |
| i = j; | |
| } | |
| const playhead = addPlayhead(svg, 0); | |
| row.appendChild(svg); | |
| const valEl = document.createElement('div'); | |
| valEl.className = 'state-value'; | |
| valEl.textContent = '-'; | |
| row.appendChild(valEl); | |
| this.container.appendChild(row); | |
| this.rows.push({ oid: o.id, svg, valEl, playhead, states }); | |
| }); | |
| } | |
| setPlayheadFrame(frameIdx) { | |
| if (!this.rows.length) return; | |
| const n = this.nFrames || 1; | |
| const frac = Math.max(0, Math.min(1, frameIdx / Math.max(n - 1, 1))); | |
| const x = (frac * VB_W).toFixed(2); | |
| const f = Math.max(0, Math.min(n - 1, Math.round(frameIdx))); | |
| for (const r of this.rows) { | |
| if (r.playhead) { | |
| r.playhead.setAttribute('x1', x); | |
| r.playhead.setAttribute('x2', x); | |
| } | |
| r.valEl.textContent = r.states[f] || '-'; | |
| } | |
| } | |
| } | |