/* Step UI shared by every sample viewer on this Space. * * Builds the three blocks that describe a teleop episode's steps — the instruction / * object / target / scene header, the segmented step timeline over the scrubber, and the * current-step annotation panel — plus the per-step measurements behind them. * * The pick-and-place pages hand it their own containers ('inline' mode) and pre-computed * per-step measurements; the older pages let it insert the blocks itself ('inject' mode) * and it derives the measurements from the page's series. Both get the same layout. * * StepsUI.mount({series, steps, task, mode, hosts, numbering}) -> {update(t), stepAt(t)} */ (function (global) { const COLORS = ["#8a8f98", "#4f8ff7", "#41c76b", "#e0a53e", "#b46fe0", "#e0616f", "#e08f5f", "#5fc9e0"]; const TOKC = { idle: '#8a8f98', reach_to_object: '#4f8ff7', fine_align: '#6f9fd8', grasp_close: '#41c76b', lift: '#e0a53e', transport: '#b46fe0', align_over_target: '#e0d15f', lower_onto_target: '#e0955f', release: '#e0616f', retreat: '#5fe0c0', carry_away: '#b46fe0', hold: '#8a8f98', }; const esc = s => String(s == null ? '' : s) .replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])); const CSS = ` .su-instr{margin:10px 22px 0;padding:11px 15px;background:#12151c;border:1px solid #232733;border-radius:8px;font-size:13px;line-height:1.65} .su-instr .k{font-size:10.5px;letter-spacing:.04em;text-transform:uppercase;color:#9aa1ad;margin-right:8px} .su-instr .obj{color:#7fb2ff}.su-instr .tgt{color:#7fe0a0} .su-tlwrap{margin:12px 22px 0;padding:9px 14px;background:#12151c;border:1px solid #232733;border-radius:8px} .su-tl{position:relative;display:flex;height:26px;border-radius:5px;overflow:hidden;cursor:pointer} .su-tl .seg{position:relative;opacity:.85;transition:opacity .12s} .su-tl .seg.on{opacity:1;box-shadow:inset 0 0 0 2px #fff} .su-tl .seg span{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:10.5px;color:#0b0d12;font-weight:700} .su-tl .cur{position:absolute;top:0;bottom:0;width:2px;background:#fff;pointer-events:none} .su-tlnums{display:flex;justify-content:space-between;font-size:10.5px;color:#6b7686;margin-top:4px;font-variant-numeric:tabular-nums} .su-annot{margin:12px 22px 0;padding:14px 18px;background:#12151c;border:1px solid #232733;border-radius:8px} .su-annot .t{font-size:11px;letter-spacing:.04em;color:#9aa1ad;text-transform:uppercase;margin-bottom:10px} .su-annot h3{margin:0 0 2px;font-size:16px} .su-annot h3 .tok{font-size:11.5px;color:#6f92c8;font-family:ui-monospace,monospace;margin-left:8px} .su-annot .when{font-size:12px;color:#8890a0;font-variant-numeric:tabular-nums;margin-bottom:8px} .su-annot p{margin:0 0 6px;line-height:1.6;font-size:13.5px;color:#dfe4ec} .su-sgrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));gap:7px 26px;margin-top:10px} .su-sg{display:flex;justify-content:space-between;border-bottom:1px dashed #232733;padding-bottom:3px;font-size:12.5px} .su-sg .k{color:#9aa1ad}.su-sg .v{font-variant-numeric:tabular-nums} .su-item .t{font-size:11px;color:#8890a0;font-variant-numeric:tabular-nums} .su-item .title{font-size:13.5px;margin:2px 0;line-height:1.35} .su-item .tok{font-size:10.5px;color:#6f92c8;font-family:ui-monospace,monospace}`; function styles() { if (document.getElementById('su-css')) return; const e = document.createElement('style'); e.id = 'su-css'; e.textContent = CSS; document.head.appendChild(e); } /** End-effector / gripper measurements over a frame range, from the page's own series. */ function measure(S, a, b) { const END = S.end || S.end_pos; // [n][6] = left xyz, right xyz const LR = S.end_l && S.end_r; // the newer pages ship them apart a = Math.max(0, a); b = Math.min(S.n - 1, b); const of = (i, o) => (LR ? (o ? S.end_r[i][o - 3] : S.end_l[i][o]) : END[i][o]); const arm = o => { let path = 0, vmax = 0; for (let i = a + 1; i <= b; i++) { const d = Math.hypot(of(i, o) - of(i - 1, o), of(i, o + 1) - of(i - 1, o + 1), of(i, o + 2) - of(i - 1, o + 2)); path += d; vmax = Math.max(vmax, d * S.fps); } return { path, vmax, dz: of(b, o + 2) - of(a, o + 2), dxy: Math.hypot(of(b, o) - of(a, o), of(b, o + 1) - of(a, o + 1)), vmean: path / Math.max(1e-6, (b - a) / S.fps), }; }; const L = arm(0), R = arm(3); return { L, R, act: L.path >= R.path ? L : R, side: L.path >= R.path ? 'left' : 'right' }; } function rows(S, st, a, b) { const out = []; const f = (v, n) => (v == null ? '–' : Number(v).toFixed(n == null ? 2 : n)); if (st.gripper) { out.push(['gripper open', `${f(st.gripper.start)} → ${f(st.gripper.end)} (min ${f(st.gripper.min)})`]); } else if (S.grip_l) { out.push(['gripper L', `${f(S.grip_l[a])} → ${f(S.grip_l[b])}`]); out.push(['gripper R', `${f(S.grip_r[a])} → ${f(S.grip_r[b])}`]); } const ee = st.ee || measure(S, a, b).act; out.push(['ee path', f(ee.travel_m != null ? ee.travel_m : ee.path, 3) + ' m']); const dz = ee.dz_m != null ? ee.dz_m : ee.dz; out.push(['ee Δz', (dz >= 0 ? '+' : '') + f(dz, 3) + ' m']); out.push(['ee Δxy', f(ee.dxy_m != null ? ee.dxy_m : ee.dxy, 3) + ' m']); out.push(['speed mean / max', `${f(ee.v_mean != null ? ee.v_mean : ee.vmean)} / ${f(ee.v_max != null ? ee.v_max : ee.vmax)} m/s`]); if (st.contact) out.push(['contact |F| peak', f(st.contact.force_peak_N, 1) + ' N']); if (st.effort) out.push(['peak |τ|', `${st.effort.peak_joint} ${f(st.effort.peak_Nm, 1)} Nm`]); if (st.tracking) out.push(['cmd err mean / max', `${f(st.tracking.cmd_err_mean_rad, 3)} / ${f(st.tracking.cmd_err_max_rad, 3)} rad`]); if (!st.ee && S.intervened) { const n = S.intervened.slice(a, b + 1).reduce((p, c) => p + (c ? 1 : 0), 0); out.push(['operator-sourced frames', Math.round(100 * n / (b - a + 1)) + '%']); } return out; } function mount(cfg) { styles(); const S = cfg.series, STEPS = cfg.steps || [], task = cfg.task || {}; if (!S || !STEPS.length) return null; const DUR = S.dur != null ? S.dur : (S.t ? S.t[S.n - 1] : (S.n - 1) / S.fps); const frameAt = t => Math.max(0, Math.min(S.n - 1, Math.round(t * S.fps))); const num = (st, i) => (cfg.numbering === 'id' && st.id != null ? st.id : i + 1); const col = (st, i) => (cfg.colorBy === 'token' && TOKC[st.token]) || COLORS[i % COLORS.length]; const seek = cfg.seek || (t => document.querySelectorAll('video') .forEach(v => { try { v.currentTime = t; } catch (e) {} })); // ---- instruction header const instr = document.createElement('div'); instr.className = 'su-instr'; instr.innerHTML = `