File size: 12,517 Bytes
81299b2
 
 
 
 
 
 
 
 
350de76
 
 
 
 
 
 
81299b2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
350de76
 
 
 
 
 
81299b2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
350de76
 
 
 
 
 
 
 
 
 
 
81299b2
 
 
 
 
 
 
 
 
 
 
 
 
 
350de76
 
81299b2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
350de76
 
81299b2
 
 
350de76
81299b2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
/* 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();
})();