Spaces:
Running
Running
darren.zhang
Claude Opus 5 (1M context)
teleopSample: cut what a pick-and-place page loads by ~60%
350de76 Download teleopSample/lib/viewer.js from faradayfuture/SampleViewer: direct link, hf CLI and curl.
- Browser
- Download file 12.5 kB
-
https://huggingface.co/spaces/faradayfuture/SampleViewer/resolve/main/teleopSample/lib/viewer.js
- Command line
-
hf download hf://spaces/faradayfuture/SampleViewer/teleopSample/lib/viewer.js
-
curl -L -o viewer.js https://huggingface.co/spaces/faradayfuture/SampleViewer/resolve/main/teleopSample/lib/viewer.js
12.5 kB
| /* Page logic for the pick-and-place sample viewers: camera grid, synchronised playback with | |
| * autoplay + loop, the state readout, the joint-effort strip and the charts. | |
| * The instruction header, step timeline and current-step panel come from steps-ui.js, which | |
| * the older sample pages use as well. All page data lives in the JSON blocks of the page. */ | |
| (function () { | |
| const J = id => { const e = document.getElementById(id); return e ? JSON.parse(e.textContent) : null; }; | |
| const S = J('series'), STEPS = J('steps') || [], CAMS = J('cams') || [], | |
| FILES = J('files') || [], TASK = J('task') || {}; | |
| const has = k => S[k] !== undefined; | |
| const DUR = S.dur != null ? S.dur : (S.t ? S.t[S.n - 1] : (S.n - 1) / S.fps); | |
| const END_A = S.side === 'L' ? S.end_l : S.end_r; // active arm, derived | |
| const cmdDelta = i => { // |command - state|, derived | |
| let m = 0; | |
| for (const c of S.joint_idx) m = Math.max(m, Math.abs(S.act_joints[i][c] - S.joints[i][c])); | |
| return m; | |
| }; | |
| const frameAt = t => Math.max(0, Math.min(S.n - 1, Math.round(t * S.fps))); | |
| const SIDE = S.side, SIDEL = SIDE === 'L' ? 'left' : 'right'; | |
| if (window.self !== window.top) { // embedded in the gallery shell | |
| const b = document.getElementById('backlink'); | |
| if (b) b.style.display = 'none'; | |
| } | |
| // ---- file list | |
| const fm = document.getElementById('fileManifest'); | |
| FILES.forEach(f => { | |
| const d = document.createElement('span'); | |
| d.className = 'file-chip'; | |
| d.innerHTML = f.name + '<span class="sz">' + f.size + '</span>'; | |
| fm.appendChild(d); | |
| }); | |
| // ---- cameras | |
| const camgrid = document.getElementById('camgrid'), videos = []; | |
| CAMS.forEach(c => { | |
| const d = document.createElement('div'); | |
| d.className = 'cam'; | |
| d.innerHTML = `<div class="lbl">${c.label}</div>` + | |
| `<video muted playsinline preload="auto" src="${c.src}"></video>`; | |
| camgrid.appendChild(d); | |
| videos.push(d.querySelector('video')); | |
| }); | |
| const master = videos[Math.max(0, CAMS.findIndex(c => c.key === 'head_color'))]; | |
| // ---- playback: autoplay once every tile can render, then loop | |
| const playBtn = document.getElementById('play'), seek = document.getElementById('seek'), | |
| tlabel = document.getElementById('tlabel'); | |
| let playing = false; | |
| const seekTo = t => videos.forEach(v => { try { v.currentTime = t; } catch (e) {} }); | |
| function setPlaying(p) { | |
| playing = p; | |
| playBtn.textContent = p ? '❚❚' : '▶︎'; | |
| if (p) { videos.forEach(v => v.play().catch(() => {})); requestAnimationFrame(loop); } | |
| else videos.forEach(v => v.pause()); | |
| } | |
| playBtn.onclick = () => setPlaying(!playing); | |
| let started = false, iv = setInterval(tryStart, 400); | |
| function tryStart() { | |
| if (started || playing || !videos.length || !videos.every(v => v.readyState >= 2)) return; | |
| started = true; clearInterval(iv); | |
| videos.forEach(v => v.muted = true); | |
| setPlaying(true); | |
| } | |
| videos.forEach(v => { v.addEventListener('canplay', tryStart); v.addEventListener('loadeddata', tryStart); }); | |
| window.addEventListener('load', tryStart); | |
| setTimeout(() => clearInterval(iv), 90000); | |
| setInterval(() => { | |
| if (playing && master.currentTime >= DUR - 0.08) { | |
| seekTo(0); videos.forEach(v => v.play().catch(() => {})); | |
| } | |
| }, 200); | |
| seek.oninput = () => { const t = seek.value / 1000 * DUR; seekTo(t); update(); }; | |
| function resync() { | |
| const t = master.currentTime; | |
| videos.forEach(v => { if (v !== master && Math.abs(v.currentTime - t) > 0.2) { try { v.currentTime = t; } catch (e) {} } }); | |
| } | |
| master.addEventListener('seeked', resync); | |
| setInterval(() => { if (!master.paused) resync(); }, 1000); | |
| function loop() { update(); if (playing) requestAnimationFrame(loop); } | |
| // ---- shared step UI: instruction header, timeline, current-step panel, step list | |
| const list = document.getElementById('stepList'); | |
| const items = STEPS.map(() => { | |
| const d = document.createElement('div'); | |
| d.className = 'step-item'; | |
| list.appendChild(d); | |
| return d; | |
| }); | |
| const steps = StepsUI.mount({ | |
| series: S, steps: STEPS, task: TASK, colorBy: 'token', listItems: items, | |
| seek: t => { seekTo(t); update(); }, | |
| hosts: { | |
| instr: document.getElementById('suInstr'), | |
| timeline: document.getElementById('suTl'), | |
| panel: document.getElementById('suPanel'), | |
| }, | |
| }); | |
| // ---- chart labels that depend on which arm moved | |
| document.getElementById('lblJoints').textContent = | |
| SIDEL + '-arm joint positions (rad) · 7 DoF (j1 shoulder → j7 wrist)' + | |
| (has('act_joints') ? ' · solid = state, dashed = teleop command' : ''); | |
| document.getElementById('lblEnd').innerHTML = SIDEL + | |
| '-arm end-effector position (m) — <i style="background:#e0616f"></i>x ' + | |
| '<i style="background:#41c76b"></i>y <i style="background:#4f8ff7"></i>z'; | |
| if (has('act_grip_l')) document.getElementById('lblGrip').innerHTML += ' · dashed = command'; | |
| // ---- joint-effort strip | |
| function heat(v) { | |
| v = Math.max(0, Math.min(1, v)); | |
| const s = [[30, 40, 120], [40, 140, 200], [60, 200, 120], [230, 210, 60], [220, 60, 50]]; | |
| const x = v * (s.length - 1), i = Math.floor(x), f = x - i, | |
| a = s[i], b = s[Math.min(i + 1, s.length - 1)]; | |
| return `rgb(${Math.round(a[0] + (b[0] - a[0]) * f)},${Math.round(a[1] + (b[1] - a[1]) * f)},` + | |
| `${Math.round(a[2] + (b[2] - a[2]) * f)})`; | |
| } | |
| const cEff = document.getElementById('cEff'); | |
| if (has('eff')) { | |
| document.getElementById('effInfo').textContent = | |
| '(7 joints/arm · both arms shown · peak ' + S.eff_max + ' Nm)'; | |
| document.getElementById('effPeak').textContent = S.eff_max; | |
| } | |
| function drawEffort(i) { | |
| if (!has('eff')) return; | |
| const ctx = cEff.getContext('2d'), dpr = devicePixelRatio || 1, | |
| w = cEff.clientWidth, h = cEff.clientHeight || 46; | |
| cEff.width = w * dpr; cEff.height = h * dpr; | |
| ctx.setTransform(dpr, 0, 0, dpr, 0, 0); ctx.clearRect(0, 0, w, h); | |
| const vals = S.eff[i] || [], K = vals.length, gap = 18, cw = (w - gap - 4) / K; | |
| for (let k = 0; k < K; k++) { | |
| const x = 2 + k * cw + (k >= 7 ? gap : 0), v = Math.abs(vals[k]) / Math.max(1e-6, S.eff_max); | |
| ctx.fillStyle = v > 0.02 ? heat(v) : '#1b1f2a'; | |
| ctx.fillRect(x, 4, cw - 1.5, h - 16); | |
| ctx.strokeStyle = '#0b0d12'; ctx.lineWidth = 1; | |
| ctx.strokeRect(x, 4, cw - 1.5, h - 16); | |
| } | |
| ctx.font = '10px ui-monospace'; | |
| ctx.fillStyle = SIDE === 'L' ? '#41c76b' : '#6b7686'; ctx.fillText('ARM L', 2, h - 3); | |
| ctx.fillStyle = SIDE === 'R' ? '#e0616f' : '#6b7686'; ctx.fillText('ARM R', 2 + 7 * cw + gap, h - 3); | |
| } | |
| // ---- charts | |
| const fmt = v => (Math.abs(v) >= 100 ? v.toFixed(0) : Math.abs(v) >= 10 ? v.toFixed(1) : v.toFixed(2)); | |
| function mkDraw(canvas, series, cols, yMin, yMax, opt) { | |
| opt = opt || {}; | |
| const fill = opt.fill, dash = opt.dash || []; | |
| let off = null, ow = 0, oh = 0; // the curves never change: draw them once | |
| function paint(w, h, dpr) { | |
| off = document.createElement('canvas'); | |
| off.width = w * dpr; off.height = h * dpr; | |
| const ctx = off.getContext('2d'); | |
| ctx.setTransform(dpr, 0, 0, dpr, 0, 0); | |
| STEPS.forEach((st, i) => { | |
| ctx.fillStyle = steps.colorOf(st, i) + '18'; | |
| ctx.fillRect(st.start_s / DUR * w, 0, (st.end_s - st.start_s) / DUR * w, h); | |
| }); | |
| ctx.strokeStyle = '#232733'; ctx.lineWidth = 1; | |
| for (let g = 0; g <= 4; g++) { | |
| const y = (h - 14) * g / 4 + 7; | |
| ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(w, y); ctx.stroke(); | |
| } | |
| const X = k => k / (S.n - 1) * w, | |
| Y = v => (h - 14) - ((v - yMin) / Math.max(1e-9, yMax - yMin)) * (h - 14) + 7; | |
| series.forEach((c, ci) => { | |
| ctx.strokeStyle = cols[ci % cols.length]; | |
| ctx.lineWidth = dash[ci] ? 1 : 1.4; | |
| ctx.setLineDash(dash[ci] ? [3, 3] : []); | |
| ctx.beginPath(); | |
| c.forEach((v, k) => { const x = X(k), y = Y(v); k ? ctx.lineTo(x, y) : ctx.moveTo(x, y); }); | |
| ctx.stroke(); | |
| ctx.setLineDash([]); | |
| if (fill) { | |
| ctx.lineTo(X(c.length - 1), Y(yMin)); ctx.lineTo(X(0), Y(yMin)); ctx.closePath(); | |
| ctx.fillStyle = cols[ci % cols.length] + '33'; ctx.fill(); | |
| } | |
| }); | |
| ctx.font = '9px ui-monospace'; ctx.fillStyle = '#6b7686'; ctx.textAlign = 'right'; | |
| ctx.fillText(fmt(yMax), w - 3, 10); ctx.fillText(fmt(yMin), w - 3, h - 3); | |
| } | |
| return function (t) { | |
| const dpr = devicePixelRatio || 1; | |
| const w = canvas.clientWidth, h = canvas.clientHeight || 90; | |
| if (!off || ow !== w || oh !== h) { paint(w, h, dpr); ow = w; oh = h; } | |
| canvas.width = w * dpr; canvas.height = h * dpr; | |
| const ctx = canvas.getContext('2d'); | |
| ctx.setTransform(1, 0, 0, 1, 0, 0); | |
| ctx.drawImage(off, 0, 0); // cached curves | |
| ctx.setTransform(dpr, 0, 0, dpr, 0, 0); | |
| const px = (t / DUR) * w; // only the cursor is drawn per frame | |
| ctx.strokeStyle = '#ffffffaa'; ctx.lineWidth = 1; | |
| ctx.beginPath(); ctx.moveTo(px, 0); ctx.lineTo(px, h); ctx.stroke(); | |
| }; | |
| } | |
| const rng = ss => { | |
| let a = Infinity, b = -Infinity; | |
| ss.forEach(c => c.forEach(v => { a = Math.min(a, v); b = Math.max(b, v); })); | |
| return [a, b]; | |
| }; | |
| const draws = []; | |
| const jcols = ['#e0616f', '#e0955f', '#e0d15f', '#7fe05f', '#5fe0c0', '#5f9fe0', '#b46fe0']; | |
| let jser = S.joint_idx.map(c => S.joints.map(r => r[c])), jdash = jser.map(() => false); | |
| if (has('act_joints')) S.joint_idx.forEach(c => { jser.push(S.act_joints.map(r => r[c])); jdash.push(true); }); | |
| draws.push(mkDraw(document.getElementById('cJoints'), jser, jcols, ...rng(jser), { dash: jdash })); | |
| if (has('end_l')) { | |
| const er = [0, 1, 2].map(c => END_A.map(r => r[c])); | |
| draws.push(mkDraw(document.getElementById('cEnd'), er, ['#e0616f', '#41c76b', '#4f8ff7'], ...rng(er))); | |
| } | |
| if (has('grip_l')) { | |
| let gs = [S.grip_l, S.grip_r], gd = [false, false]; | |
| if (has('act_grip_l')) { gs.push(S.act_grip_l, S.act_grip_r); gd.push(true, true); } | |
| draws.push(mkDraw(document.getElementById('cGrip'), gs, ['#41c76b', '#e0616f'], 0, 1, { dash: gd })); | |
| } | |
| if (has('fmag_l')) { | |
| const [, fx] = rng([S.fmag_l, S.fmag_r]); | |
| draws.push(mkDraw(document.getElementById('cForce'), [S.fmag_l, S.fmag_r], ['#41c76b', '#e0616f'], 0, fx || 1)); | |
| } | |
| if (has('src')) draws.push(mkDraw(document.getElementById('cSrc'), [S.src], ['#e0a53e'], 0, 1.05, { fill: true })); | |
| ['cForce', 'cJoints', 'cEnd', 'cGrip', 'cSrc'].forEach(id => { | |
| const c = document.getElementById(id); | |
| if (!c) return; | |
| c.addEventListener('click', e => { | |
| const r = c.getBoundingClientRect(); | |
| seekTo((e.clientX - r.left) / r.width * DUR); update(); | |
| }); | |
| }); | |
| // ---- per-frame readout | |
| function update() { | |
| const t = master.currentTime, i = frameAt(t); | |
| const si = steps.update(t), st = STEPS[si]; | |
| const set = (id, v) => { const e = document.getElementById(id); if (e) e.textContent = v; }; | |
| set('roTime', t.toFixed(2) + 's'); | |
| set('roFrame', i + ' / ' + (S.n - 1)); | |
| set('roStep', st.id + '. ' + st.token); | |
| set('roGrip', has('grip_l') ? S.grip_l[i].toFixed(2) + ' / ' + S.grip_r[i].toFixed(2) : '–'); | |
| set('roEndL', has('end_l') ? S.end_l[i].map(x => x.toFixed(3)).join(' / ') : '–'); | |
| set('roEndR', has('end_l') ? S.end_r[i].map(x => x.toFixed(3)).join(' / ') : '–'); | |
| set('roF', has('fmag_l') ? S.fmag_l[i].toFixed(1) + ' / ' + S.fmag_r[i].toFixed(1) : '–'); | |
| set('roSrc', has('src') ? (S.src[i] === 1 ? 'OPERATOR' : 'idle / other') : '–'); | |
| set('roSide', SIDEL + ' arm'); | |
| set('roCmd', has('act_joints') ? cmdDelta(i).toFixed(3) : '–'); | |
| if (has('eff')) { | |
| const e = S.eff[i]; | |
| let bk = 0; | |
| for (let k = 0; k < e.length; k++) if (Math.abs(e[k]) > Math.abs(e[bk])) bk = k; | |
| set('roTau', S.joint_names[bk] + ' ' + e[bk].toFixed(1) + ' Nm'); | |
| } | |
| set('roProg', Math.round((t - st.start_s) / Math.max(1e-6, st.end_s - st.start_s) * 100) + | |
| '% of step ' + st.id); | |
| seek.value = Math.round(t / DUR * 1000); | |
| tlabel.textContent = t.toFixed(2) + ' / ' + DUR.toFixed(2) + 's'; | |
| drawEffort(i); | |
| draws.forEach(d => d(t)); | |
| } | |
| window.addEventListener('resize', update); | |
| master.addEventListener('loadedmetadata', update); | |
| master.addEventListener('seeked', update); | |
| window.addEventListener('load', update); | |
| update(); | |
| })(); | |