Spaces:
Running
Running
egoSample/faberu: split point cloud into map (0.5 MB, first) / scans (1.8 MB) / depth-hover grid (0.9 MB, last); delta-encoded
d01ec1a verified Download egoSample/faberu.html from faradayfuture/SampleViewer: direct link, hf CLI and curl.
- Browser
- Download file 39.2 kB
-
https://huggingface.co/spaces/faradayfuture/SampleViewer/resolve/main/egoSample/faberu.html
- Command line
-
hf download hf://spaces/faradayfuture/SampleViewer/egoSample/faberu.html
-
curl -L -o faberu.html https://huggingface.co/spaces/faradayfuture/SampleViewer/resolve/main/egoSample/faberu.html
39.2 kB
| <html lang="en"><head><meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width,initial-scale=1"> | |
| <title>Faber U Teleop Viewer</title> | |
| <style> | |
| :root{color-scheme:dark} | |
| *{box-sizing:border-box} | |
| html,body{margin:0;height:100%} | |
| body{background:#07090d;color:#e6e8ee;font:14px/1.5 -apple-system,"Helvetica Neue",Arial,sans-serif} | |
| a{color:#6f92c8} | |
| .main{height:100vh;overflow-y:auto} | |
| header{padding:14px 20px 10px} | |
| header h1{margin:0 0 3px;font-size:19px} | |
| .sub{color:#8890a0;font-size:12.5px}.sub code{color:#c7ccd6;background:#12151c;padding:1px 5px;border-radius:4px} | |
| .badges{display:flex;flex-wrap:wrap;gap:6px;margin-top:7px} | |
| .bdg{background:#12151c;border:1px solid #1c2028;border-radius:999px;padding:2px 9px;font-size:11px;color:#9aa1ad} | |
| .bdg b{color:#e6e8ee} | |
| .bdg.tag{color:#07090d;background:#6f9fe0;border-color:#6f9fe0;font-weight:700} | |
| .layout{display:grid;grid-template-columns:minmax(0,1fr) 320px;gap:14px;padding:0 20px} | |
| @media(max-width:1360px){.layout{grid-template-columns:1fr}} | |
| .panel{background:#0e1118;border:1px solid #1c2028;border-radius:8px} | |
| .vgrid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px;padding:8px} | |
| @media(max-width:700px){.vgrid{grid-template-columns:1fr}} | |
| .vp{position:relative;background:#000;border-radius:4px;overflow:hidden;aspect-ratio:5/4;min-width:0} | |
| .vp.big{grid-column:span 2;grid-row:span 2} | |
| .vp video{width:100%;height:100%;object-fit:contain;display:block} | |
| .vp .lbl{position:absolute;top:5px;left:6px;font:10.5px ui-monospace,monospace;color:#bcd2e6;background:rgba(0,0,0,.5);padding:1px 5px;border-radius:3px;pointer-events:none} | |
| .vp .rd{position:absolute;bottom:5px;right:6px;font:11px ui-monospace,monospace;color:#fff;background:rgba(0,0,0,.6);padding:1px 6px;border-radius:3px;pointer-events:none} | |
| .three{position:relative;height:clamp(260px,38vh,420px);margin:0 8px 8px;background:#0b0d12;border:1px solid #1c2028;border-radius:6px;overflow:hidden} | |
| .three canvas{display:block} | |
| .three .lbl{position:absolute;top:6px;left:9px;font:12px ui-monospace,monospace;color:#9aa1ad;pointer-events:none;z-index:1} | |
| .three .hint{position:absolute;bottom:6px;right:9px;font-size:10px;color:#556;pointer-events:none;z-index:1} | |
| .chk{display:flex;flex-wrap:wrap;gap:4px 14px;padding:0 12px 8px;font-size:11.5px;color:#9aa1ad} | |
| .chk label{cursor:pointer;display:flex;gap:4px;align-items:center} | |
| .spans-head{padding:12px 14px 8px;font-size:12px;letter-spacing:.04em;color:#9aa1ad;text-transform:uppercase} | |
| .spans-head b{color:#6f9fe0} | |
| .tl{height:9px;border-radius:4px;margin:0 14px 6px;display:flex;overflow:hidden;cursor:pointer;background:#12151c} | |
| .tl i{height:100%} | |
| .chips{display:flex;flex-wrap:wrap;gap:5px;padding:0 14px 10px} | |
| .chip{min-width:24px;height:21px;padding:0 5px;display:flex;align-items:center;justify-content:center;border-radius:5px;font-size:10.5px;font-weight:700;cursor:pointer;color:#07090d} | |
| .chip.on{outline:2px solid #fff} | |
| .slist{max-height:560px;overflow-y:auto} | |
| .si{border-left:3px solid transparent;border-radius:4px;padding:7px 10px;margin:0 10px 5px;cursor:pointer;background:#12151c} | |
| .si:hover{background:#171b24}.si.on{background:#182339;border-left-color:#6f9fe0} | |
| .si .n{display:inline-flex;min-width:17px;height:17px;padding:0 3px;border-radius:9px;color:#07090d;font-size:10px;font-weight:700;align-items:center;justify-content:center;margin-right:6px} | |
| .si .k{font-size:13px}.si .m{font-size:10.5px;color:#6f92c8;font-family:ui-monospace,monospace;margin-top:2px} | |
| .si .lb{font-size:11px;color:#8890a0} | |
| .legend{display:flex;flex-wrap:wrap;gap:6px 12px;padding:10px 20px 0;color:#8890a0;font-size:11.5px} | |
| .legend .it{display:flex;align-items:center;gap:5px} | |
| .sw{width:11px;height:11px;border-radius:3px;display:inline-block} | |
| .controls{display:flex;align-items:center;gap:12px;padding:9px 16px;margin:14px 20px 0} | |
| .controls button{background:#1b2740;color:#cfe0ff;border:1px solid #2b3654;border-radius:6px;padding:5px 13px;cursor:pointer;font-size:15px} | |
| .controls input[type=range]{flex:1;min-width:0} | |
| .controls .t{font-variant-numeric:tabular-nums;color:#c7ccd6;font-size:13px;white-space:nowrap} | |
| .controls select{background:#12151c;color:#c7ccd6;border:1px solid #2b3654;border-radius:5px;font-size:12px;padding:3px 4px} | |
| .note{margin:10px 20px 0;padding:9px 14px;font-size:12px;color:#9aa1ad;border-left:3px solid #b8860b;background:#14130c;border-radius:4px} | |
| .note b{color:#d9c48a} | |
| .readout{margin:14px 20px 0;padding:13px 18px} | |
| .rt{font-size:11px;letter-spacing:.04em;color:#9aa1ad;text-transform:uppercase;margin-bottom:9px} | |
| .rg{display:grid;grid-template-columns:repeat(3,1fr);gap:8px 26px} | |
| @media(max-width:900px){.rg{grid-template-columns:1fr 1fr}} | |
| .ro{display:flex;justify-content:space-between;border-bottom:1px dashed #1c2028;padding-bottom:4px;gap:8px} | |
| .ro .k{color:#9aa1ad}.ro .v{font-variant-numeric:tabular-nums;text-align:right}.ro .v.b{color:#7fb2ff}.ro .v.hi{color:#6fe08a} | |
| .charts{margin:12px 20px 0;display:grid;grid-template-columns:1fr 1fr;gap:8px} | |
| @media(max-width:1100px){.charts{grid-template-columns:1fr}} | |
| .chp{padding:6px 14px 8px} | |
| .chp .l{font-size:11px;color:#9aa1ad}.chp .u{color:#5a6373;font-size:10px;margin-left:6px} | |
| .chp canvas{width:100%;height:84px;display:block;cursor:pointer} | |
| .leg{display:inline-flex;flex-wrap:wrap;gap:4px 9px;margin-left:8px;font-size:10.5px} | |
| .leg i{width:9px;height:9px;border-radius:2px;display:inline-block;margin-right:3px;vertical-align:-1px} | |
| .files{display:flex;flex-wrap:wrap;gap:7px} | |
| .fc{background:#12151c;border:1px solid #1c2028;border-radius:5px;padding:4px 9px;font:11.5px ui-monospace,monospace;color:#c7ccd6;text-decoration:none;display:inline-block} | |
| .fc.dl{color:#7fe0a0;border-color:#24402f}.fc.dl:hover{background:#16211a;border-color:#3d9970} | |
| .cpy{background:#12151c;border:1px solid #2b3654;color:#9fc0ff;border-radius:5px;padding:4px 9px;font:11.5px ui-monospace,monospace;cursor:pointer} | |
| </style> | |
| <script type="importmap">{"imports":{"three":"https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js","three/addons/":"https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/"}}</script> | |
| </head> | |
| <body> | |
| <div class="main" id="main"></div> | |
| <link rel="modulepreload" href="https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js"><link rel="modulepreload" href="https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/controls/OrbitControls.js"> | |
| <script id="D" type="application/json">{"eps": [{"slug": "restock_20260916_ep000", "dataset_dir": "faberu_restock_20260916_ep000", "task": "restock_20260916", "leaf": "Restocking a retail shelf", "leaf_instr": "Fetch a bottle from the table and restock the shelf", "mode": "VR teleoperation, mobile manipulation", "perspective": "Robot egocentric", "planned": true, "rationale": "We can collect data from robots deployed in real-world working scenes. This episode is included in Ego Sample to showcase that sensor capability: a deployed robot captures rich, synchronized multi-sensor data (stereo, fisheye and color cameras, metric depth, dual lidar, IMU, odometry, joint and teleop streams). Robot-egocentric capture is a planned data line.", "ep": 0, "task_name": "Retail shelf restocking (mobile manipulation)", "instruction": "Drive from the shelf to the table, pick up the bottle with the right gripper, carry it back and place it on the shelf", "duration": 80.4, "nframes": 2412, "fps": 30, "hw": "Faber U", "vendor": "FF", "region": "US", "tags": ["US"], "taxo": {"environment_l1": "Retail and Consumer Goods", "environment_l2": "Grocery Store", "environment_l3": "Shelf Aisle", "naics_primary_code": "445110", "naics_primary_title": "Supermarkets and Other Grocery Retailers", "taxonomy_source": "OpenAI Robotics Data Requirements 08-21-2026 (Environment/Skill Group/Jobs taxonomies)", "collection_setup": "staged retail shelf in a lab space", "skill_group": "Inventory & Stock Management", "job_family": "Stock clerk", "task_difficulty": "Medium"}, "n_segments": 22, "skills": {"Navigate": 7, "Wait": 3, "Rotate": 3, "Open": 2, "Retract": 2, "Close": 1, "Pick": 1, "Lift": 1, "Reach": 1, "Place": 1}, "skill_list": ["Close", "Lift", "Navigate", "Open", "Pick", "Place", "Reach", "Retract", "Rotate", "Wait"], "qc": {"status": "pass", "total": 10, "passed": 8, "warnings": 2, "failures": 0}, "state_dim": 27, "action_dim": 19, "depth_valid": 0.847, "lidar_scans": [810, 810], "segments": [{"skill": "Wait", "label": "The robot stands in front of the retail shelf while the teleoperator gets ready", "ts": 0.0, "te": 13.0, "mistake": false}, {"skill": "Close", "label": "The right gripper closes to its travel position", "ts": 13.0, "te": 14.25, "mistake": false}, {"skill": "Navigate", "label": "The robot backs away from the shelf", "ts": 14.25, "te": 16.75, "mistake": false}, {"skill": "Rotate", "label": "The robot turns about 95 degrees right to face the work table", "ts": 16.75, "te": 20.0, "mistake": false}, {"skill": "Wait", "label": "The robot pauses facing the table", "ts": 20.0, "te": 21.25, "mistake": false}, {"skill": "Navigate", "label": "The robot drives forward toward the table", "ts": 21.25, "te": 23.25, "mistake": false}, {"skill": "Rotate", "label": "The robot corrects its heading toward the table", "ts": 23.25, "te": 25.25, "mistake": false}, {"skill": "Navigate", "label": "The robot drives up to the table edge and stops", "ts": 25.25, "te": 32.0, "mistake": false}, {"skill": "Open", "label": "The right gripper opens in front of the bottle on the table", "ts": 32.0, "te": 34.0, "mistake": false}, {"skill": "Pick", "label": "The right arm reaches the bottle on the table and the gripper closes on it", "ts": 34.0, "te": 40.25, "mistake": false}, {"skill": "Lift", "label": "The right arm lifts the bottle and retracts it close to the body", "ts": 40.25, "te": 44.25, "mistake": false}, {"skill": "Navigate", "label": "The robot drives back toward the shelf while holding the bottle", "ts": 44.25, "te": 48.5, "mistake": false}, {"skill": "Rotate", "label": "The robot turns about 95 degrees left to face the shelf", "ts": 48.5, "te": 52.0, "mistake": false}, {"skill": "Navigate", "label": "The robot aligns itself with the shelf", "ts": 52.0, "te": 54.75, "mistake": false}, {"skill": "Reach", "label": "The right arm raises the bottle toward the middle shelf", "ts": 54.75, "te": 61.0, "mistake": false}, {"skill": "Navigate", "label": "The robot repositions sideways along the shelf to reach a free slot", "ts": 61.0, "te": 65.75, "mistake": false}, {"skill": "Place", "label": "The right arm moves the bottle onto the middle shelf", "ts": 65.75, "te": 70.25, "mistake": false}, {"skill": "Open", "label": "The right gripper opens to release the bottle on the shelf", "ts": 70.25, "te": 71.5, "mistake": false}, {"skill": "Retract", "label": "The right arm withdraws from the shelf", "ts": 71.5, "te": 74.0, "mistake": false}, {"skill": "Navigate", "label": "The robot backs away from the shelf and straightens up", "ts": 74.0, "te": 76.75, "mistake": false}, {"skill": "Retract", "label": "Both arms return to the home pose", "ts": 76.75, "te": 78.5, "mistake": false}, {"skill": "Wait", "label": "The robot stops; episode ends", "ts": 78.5, "te": 80.4, "mistake": false}]}], "skc": {"Clip": "#4f83cc", "Close": "#e0913a", "CloseBox": "#5aa469", "Cut": "#c0504d", "Dip": "#8064a2", "Drop": "#4bacc6", "Flip": "#d98594", "Fold": "#9b8f2e", "HandOver": "#7f8fa6", "Hang": "#674ea7", "Insert": "#3d9970", "Knead": "#b8860b", "Open": "#a0522d", "Paint": "#2e8b8b", "Pick": "#b5651d", "Place": "#6a5acd", "Plug": "#556b2f", "Pour": "#8b3a62", "Press": "#487eb0", "PressButton": "#cc8e35", "Pull": "#6b8e23", "Rinse": "#9370db", "Rotate": "#20b2aa", "Scan": "#cd853f", "Scoop": "#4f83cc", "Screw (tighten)": "#e0913a", "Smear": "#5aa469", "Stack": "#c0504d", "Stir": "#8064a2", "Straighten": "#4bacc6", "Takedown": "#d98594", "Takeout (remove)": "#9b8f2e", "Tear": "#7f8fa6", "Wipe": "#674ea7", "dip": "#3d9970", "plug": "#b8860b", "Wait": "#5d6574", "Navigate": "#3fa7d6", "Lift": "#e07a5f", "Reach": "#81b29a", "Retract": "#b388eb"}, "hf": "https://huggingface.co/datasets/faradayfuture/egoSample"}</script> | |
| <script> | |
| const D=JSON.parse(document.getElementById('D').textContent); | |
| const EPS=D.eps, SKC=D.skc, HF=D.hf; | |
| const CAMS=['head_stereo_left','head_color','head_stereo_right','head_left_fisheye','head_depth','head_right_fisheye']; | |
| const AX={x:'#e0616f',y:'#41c76b',z:'#4f8ff7'}; | |
| const PAL=['#e0616f','#41c76b','#4f8ff7','#e0a33a','#b57be0','#3fc1c9','#d98594']; | |
| let EP=null, SIG=null, vids=[], master=null, playing=false, raf=0, syncT=0, _lastCi=-2, _toggle=null, setCloudT=null; | |
| const esc=s=>String(s??'').replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c])); | |
| const fmt=t=>{t=Math.max(0,t);const m=Math.floor(t/60),s=t%60;return m+':'+s.toFixed(2).padStart(5,'0');}; | |
| const skc=s=>SKC[s]||'#666'; | |
| function select(i){ EP=EPS[i]; SIG=null; cancelAnimationFrame(raf); clearInterval(syncT); playing=false; render(); loadSig(EP.slug); } | |
| function loadSig(slug){ window.EPDATA=window.EPDATA||{}; | |
| // start every download at once; core first-priority, map next, scans next, depth-hover grid last | |
| const P=n=>'epdata/'+slug+'.'+n; | |
| const mapP=fetchBin(P('map'),'map'), scansP=fetchBin(P('scans'),'scans'); | |
| const merge=(k,fn)=>b=>{ if(!b||!SIG) return; fn(parseFUP(b)); if(window._addClouds) window._addClouds(); }; | |
| const done=()=>{ SIG=window.EPDATA[slug]; if(!SIG) return; sizeCharts(); drawCharts(); init3D(); | |
| mapP.then(merge('map',F=>{ SIG.map=decPts(F,0,F.hdr.n); })); | |
| scansP.then(merge('scans',F=>{ let c=0; SIG.clouds={}; Object.entries(F.hdr.clouds).forEach(([k,fr])=>{ SIG.clouds[k]=fr.map(f=>Object.assign({t:f.t},decPts(F,c++,f.n))); }); })) | |
| .then(()=>fetchBin(P('dgrid'),null,'low')).then(b=>{ if(!b||!SIG) return; const F=parseFUP(b), n=F.hdr.w*F.hdr.h; SIG.depthGrid=F.hdr.offs.map((o,k)=>new Uint16Array(F.buf.buffer,F.base+o,n)); }); | |
| updateUI(master.currentTime||0); | |
| const go=()=>{ if(_toggle&&!playing) _toggle(); }; if(master.readyState>=3) go(); else master.addEventListener('canplay',go,{once:true}); }; | |
| if(window.EPDATA[slug]){ done(); return; } | |
| fetchBin(P('core'),null).then(b=>{ window.EPDATA[slug]=JSON.parse(new TextDecoder().decode(b)); done(); }) | |
| .catch(()=>{ const s=document.createElement('script'); s.src='epdata/'+slug+'.js'; s.onload=done; document.body.appendChild(s); }); } | |
| const _prog={}; | |
| function showProg(){ const el=document.getElementById('cloudStat'); if(!el) return; | |
| const a=Object.entries(_prog).filter(([k,v])=>!v.done); el.textContent=a.length?'loading '+a.map(([k,v])=>`${k} ${(v.got/1e6).toFixed(1)}${v.total?'/'+(v.total/1e6).toFixed(1):''} MB`).join(' · '):''; } | |
| async function fetchBin(url,label,prio){ | |
| try{ | |
| const r=await fetch(url,{priority:prio||'high'}); if(!r.ok) throw new Error('HTTP '+r.status); | |
| let b; | |
| if(label){ const total=+r.headers.get('content-length')||0, rd=r.body.getReader(), parts=[]; let got=0; _prog[label]={got:0,total}; | |
| for(;;){ const {done,value}=await rd.read(); if(done) break; parts.push(value); got+=value.length; _prog[label].got=got; showProg(); } | |
| b=new Uint8Array(got); let o=0; for(const p of parts){ b.set(p,o); o+=p.length; } _prog[label].done=1; showProg(); } | |
| else b=new Uint8Array(await r.arrayBuffer()); | |
| if(b[0]===0x1f&&b[1]===0x8b) b=new Uint8Array(await new Response(new Blob([b]).stream().pipeThrough(new DecompressionStream('gzip'))).arrayBuffer()); | |
| return b; | |
| }catch(e){ if(label){ const el=document.getElementById('cloudStat'); if(el) el.textContent=label+' failed to load: '+e.message; } return null; } | |
| } | |
| function parseFUP(buf){ const dv=new DataView(buf.buffer,buf.byteOffset); if(String.fromCharCode(...buf.slice(0,4))!=='FUP2') throw new Error('bad pack'); | |
| const hl=dv.getUint32(4,true), hdr=JSON.parse(new TextDecoder().decode(buf.slice(8,8+hl))); let base=8+hl; base+=(4-base%4)%4; return {buf,hdr,base}; } | |
| /* planar per-axis deltas (int16 cm) + intensity -> interleaved Int16 xyz (cm) */ | |
| function decPts(F,k,n){ const o=F.base+F.hdr.offs[k], d=new Int16Array(F.buf.buffer,F.buf.byteOffset+o,n*3), q=new Int16Array(n*3); | |
| for(let a=0;a<3;a++){ let acc=0; for(let m=0;m<n;m++){ acc+=d[a*n+m]; q[3*m+a]=acc; } } | |
| return {q, I:new Uint8Array(F.buf.buffer,F.buf.byteOffset+o+n*6,n)}; } | |
| function render(){ | |
| const e=EP, dur=e.duration, segs=e.segments, T=e.taxo; | |
| const tile=(c,big)=>`<div class="vp${big?' big':''}" ><video data-c="${c}" src="media/${e.slug}/${c}.mp4" muted playsinline preload="auto"></video><span class="lbl">${c}${c==='head_depth'?' · filtered preview · raw 16-bit mm in dataset':''}</span>${c==='head_depth'?'<span class="rd" id="drd">hover for depth</span>':''}</div>`; | |
| const chips=segs.map((s,i)=>`<div class="chip" data-i="${i}" style="background:${skc(s.skill)}" title="${esc(s.skill)} — ${esc(s.label)}">${i+1}</div>`).join(''); | |
| const slist=segs.map((s,i)=>`<div class="si" data-i="${i}"><div><span class="n" style="background:${skc(s.skill)}">${i+1}</span><span class="k">${esc(s.skill)}</span><span class="lb"> · ${s.ts.toFixed(2)}–${s.te.toFixed(2)}s</span></div><div class="m">${esc(s.label)}</div></div>`).join(''); | |
| const legend=Object.keys(e.skills).sort().map(s=>`<span class="it"><span class="sw" style="background:${skc(s)}"></span>${esc(s)} <b style="color:#e6e8ee">${e.skills[s]}</b></span>`).join(''); | |
| const HFDIR=HF+'/tree/main/'+e.dataset_dir, raw=f=>HF+'/resolve/main/'+e.dataset_dir+'/'+f; | |
| const camDl=CAMS.map(c=>`<a class="fc dl" href="media/${e.slug}/${c}.mp4" download="${e.slug}_${c}.mp4">${c}.mp4</a>`).join(''); | |
| const refs=['data/file-000.parquet','meta/info.json','meta/ff/segments.parquet','meta/ff/episode_meta.parquet','meta/ff/quality.json','meta/ff/camera_params.json','depth/head_depth/file-000.mkv','pointclouds/lidar_front/file-000.parquet','pointclouds/lidar_back/file-000.parquet','sensors/imu_chassis.parquet','sensors/odom.parquet','sensors/vr_teleop_device.parquet'] | |
| .map(f=>`<a class="fc dl" href="${raw(f)}" target="_blank" rel="noopener">${f}</a>`).join(''); | |
| document.getElementById('main').innerHTML=` | |
| <header> | |
| <h1>${esc(e.task_name)}</h1> | |
| <div class="sub"><code>${esc(e.task)}</code> · ${esc(e.vendor)} ${esc(e.hw)} · <b style="color:#e0a33a">robot egocentric</b> · ${esc(e.mode)} · episode ${e.ep} · ${e.duration}s @ ${e.fps}fps</div> | |
| <div class="badges">${e.tags.map(t=>`<span class="bdg tag">${esc(t)}</span>`).join('')}<span class="bdg tag" style="background:#e0a33a;border-color:#e0a33a">Robot egocentric</span>${e.planned?'<span class="bdg tag" style="background:#b388eb;border-color:#b388eb">Planned</span>':''}<span class="bdg">instruction: <b>${esc(e.instruction)}</b></span></div> | |
| <div class="badges"><span class="bdg">environment <b>${esc(T.environment_l1)} › ${esc(T.environment_l2)} › ${esc(T.environment_l3)}</b></span><span class="bdg">skill group <b>${esc(T.skill_group)}</b></span><span class="bdg">job <b>${esc(T.job_family)}</b></span><span class="bdg">difficulty <b>${esc(T.task_difficulty)}</b></span><span class="bdg">NAICS <b>${esc(T.naics_primary_code)}</b></span></div> | |
| </header> | |
| <div class="layout"> | |
| <div class="panel"> | |
| <div class="vgrid">${CAMS.map(c=>tile(c)).join('')}</div> | |
| <div class="chk" id="chk"></div> | |
| <div class="three" id="three"><span class="lbl">world (odom) · robot links · map (all scans, dim) · current scan front <span style="color:#ff8a70">■</span> back <span style="color:#6ea8ff">■</span> · base path · ceiling (>2.3 m) and >10 m hidden</span><span class="hint">drag to orbit · scroll to zoom</span><span id="cloudStat" style="position:absolute;top:26px;left:9px;font:11px ui-monospace,monospace;color:#e0a33a;z-index:1">loading map…</span></div> | |
| </div> | |
| <div class="panel"> | |
| <div class="spans-head">temporal spans (${e.n_segments}) · <b>${esc(e.task_name)}</b></div> | |
| <div class="tl" id="tl"></div><div class="chips" id="chips">${chips}</div><div class="slist" id="slist">${slist}</div> | |
| </div> | |
| </div> | |
| <div class="legend">${legend}</div> | |
| <div class="panel controls"><button id="play">▶</button><input type="range" id="seek" min="0" max="10000" value="0"><span class="t" id="tlab">0:00.00 / ${fmt(dur)}</span> | |
| <select id="rate"><option value="1">1×</option><option value="2">2×</option><option value="4">4×</option></select></div> | |
| <div class="badges" style="padding:0 20px;margin-top:8px"> | |
| <span class="bdg"><b>${e.nframes}</b> frames</span><span class="bdg"><b>${e.n_segments}</b> skill spans</span><span class="bdg">state dim <b>${e.state_dim}</b> · action dim <b>${e.action_dim}</b></span> | |
| <span class="bdg">5 RGB cams + depth</span><span class="bdg">2 lidars · ${e.lidar_scans[0]}+${e.lidar_scans[1]} scans</span><span class="bdg">IMU 200 Hz</span> | |
| <span class="bdg">depth valid <b>${(e.depth_valid*100).toFixed(0)}%</b></span> | |
| <span class="bdg">QC <b style="color:${e.qc.failures?'#ff5c5c':'#6fe08a'}">${e.qc.passed}/${e.qc.total}</b>${e.qc.warnings?` · ${e.qc.warnings} warn`:''}</span></div> | |
| <div class="note"><b>Why a robot episode in Ego Sample.</b> ${esc(e.rationale||'')}</div> | |
| <div class="panel readout"><div class="rt">current-time readout</div><div class="rg"> | |
| <div class="ro"><span class="k">time</span><span class="v" id="roT">–</span></div> | |
| <div class="ro"><span class="k">current skill</span><span class="v b" id="roSk">–</span></div> | |
| <div class="ro"><span class="k">span</span><span class="v" id="roSp">–</span></div> | |
| <div class="ro"><span class="k">base x / y / yaw</span><span class="v" id="roB">–</span></div> | |
| <div class="ro"><span class="k">base speed (odom)</span><span class="v" id="roV">–</span></div> | |
| <div class="ro"><span class="k">gripper L / R (rad)</span><span class="v" id="roG">–</span></div> | |
| <div class="ro"><span class="k">teleop active</span><span class="v hi" id="roA">–</span></div> | |
| <div class="ro"><span class="k">right end xyz (base_link)</span><span class="v" id="roE">–</span></div> | |
| <div class="ro"><span class="k">max |target − measured| arm</span><span class="v" id="roTr">–</span></div></div></div> | |
| <div class="charts"> | |
| <div class="panel chp"><div class="l">left arm joints <span class="u">rad · solid measured, dim target</span></div><canvas id="chL"></canvas></div> | |
| <div class="panel chp"><div class="l">right arm joints <span class="u">rad · solid measured, dim target</span></div><canvas id="chR"></canvas></div> | |
| <div class="panel chp"><div class="l">grippers <span class="u">rad</span><span class="leg"><span><i style="background:#e0616f"></i>L pos</span><span><i style="background:#41c76b"></i>R pos</span><span><i style="background:#7a3a42"></i>L cmd</span><span><i style="background:#2a6b3e"></i>R cmd</span></span></div><canvas id="chG"></canvas></div> | |
| <div class="panel chp"><div class="l">base velocity <span class="u">m/s, rad/s · solid odom, dim teleop cmd</span><span class="leg"><span><i style="background:${AX.x}"></i>vx</span><span><i style="background:${AX.y}"></i>vy</span><span><i style="background:${AX.z}"></i>wz</span></span></div><canvas id="chV"></canvas></div> | |
| <div class="panel chp"><div class="l">chassis IMU · gyro <span class="u">rad/s</span><span class="leg"><span><i style="background:${AX.x}"></i>x</span><span><i style="background:${AX.y}"></i>y</span><span><i style="background:${AX.z}"></i>z</span></span></div><canvas id="chI"></canvas></div> | |
| <div class="panel chp"><div class="l">end-effector wrench · force <span class="u">N</span><span class="leg"><span><i style="background:#e0616f"></i>L fx fy fz</span><span><i style="background:#41c76b"></i>R fx fy fz</span></span></div><canvas id="chW"></canvas></div> | |
| </div> | |
| <div class="panel" style="margin:14px 20px 30px;padding:11px 16px"> | |
| <div class="rt">episode clips — click to download (preview encodes)</div><div class="files" style="padding:0 0 6px">${camDl}</div> | |
| <div class="rt" style="margin-top:8px">dataset files · <a href="${HFDIR}" target="_blank" rel="noopener" style="text-transform:none;color:#7fe0a0">full episode on HuggingFace →</a> | |
| <button class="cpy" id="cpy" style="margin-left:8px">📋 copy: hf download (all files)</button></div> | |
| <div class="files">${refs}</div></div>`; | |
| wire(); | |
| } | |
| function wire(){ | |
| const e=EP, dur=e.duration, segs=e.segments; | |
| vids=[...document.querySelectorAll('.vp video')]; master=vids[0]; | |
| const playBtn=document.getElementById('play'), seek=document.getElementById('seek'), rate=document.getElementById('rate'); | |
| function toggle(){ playing=!playing; playBtn.innerHTML=playing?'❙❙':'▶'; | |
| if(playing){ vids.forEach(v=>{ v.playbackRate=+rate.value; v.play().catch(()=>{}); }); tick(); clearInterval(syncT); syncT=setInterval(()=>syncSlaves(false),250); } | |
| else { vids.forEach(v=>v.pause()); clearInterval(syncT); syncSlaves(true); } } | |
| playBtn.onclick=toggle; _toggle=toggle; | |
| rate.onchange=()=>vids.forEach(v=>v.playbackRate=+rate.value); | |
| seek.oninput=()=>setTime(seek.value/10000*dur); | |
| const tl=document.getElementById('tl'); tl.innerHTML=''; | |
| segs.forEach(s=>{ const b=document.createElement('i'); b.style.width=((s.te-s.ts)/dur*100)+'%'; b.style.background=skc(s.skill); b.title=`${s.skill} — ${s.label}`; | |
| b.onclick=ev=>{ ev.stopPropagation(); setTime(s.ts+0.01); }; tl.appendChild(b); }); | |
| document.querySelectorAll('#chips .chip,#slist .si').forEach(c=>c.onclick=()=>setTime(segs[+c.dataset.i].ts+0.01)); | |
| ['chL','chR','chG','chV','chI','chW'].forEach(id=>{ const c=document.getElementById(id); c.onclick=ev=>{ const r=c.getBoundingClientRect(); setTime((ev.clientX-r.left)/r.width*dur); }; }); | |
| master.addEventListener('seeked',()=>syncSlaves(true)); | |
| document.getElementById('cpy').onclick=()=>{ const cmd=`hf download ${HF.replace('https://huggingface.co/datasets/','')} --repo-type dataset --include "${e.dataset_dir}/*" --local-dir .`; | |
| navigator.clipboard.writeText(cmd).then(()=>{ document.getElementById('cpy').textContent='✓ copied'; setTimeout(()=>document.getElementById('cpy').textContent='📋 copy: hf download (all files)',1500); }); }; | |
| document.onkeydown=ev=>{ if(ev.target.tagName==='SELECT') return; | |
| if(ev.code==='Space'){ ev.preventDefault(); toggle(); } | |
| else if(ev.code==='ArrowRight'){ ev.preventDefault(); const n=segs.find(s=>s.ts>master.currentTime+0.05); if(n) setTime(n.ts+0.01); } | |
| else if(ev.code==='ArrowLeft'){ ev.preventDefault(); const p=[...segs].reverse().find(s=>s.ts<master.currentTime-0.3); setTime(p?p.ts+0.01:0); } }; | |
| // depth hover readout | |
| const dv=vids.find(v=>v.dataset.c==='head_depth'), rd=document.getElementById('drd'), cache={}; | |
| dv.parentElement.addEventListener('mousemove',ev=>{ if(!SIG) return; const r=dv.getBoundingClientRect(), ar=640/400; let w=r.width,h=w/ar; if(h>r.height){h=r.height;w=h*ar;} | |
| const x=(ev.clientX-r.left-(r.width-w)/2)/w, y=(ev.clientY-r.top-(r.height-h)/2)/h; if(x<0||x>1||y<0||y>1){ rd.textContent='—'; return; } | |
| if(!SIG.depthGrid){ rd.textContent='loading…'; return; } | |
| const [gw,gh]=SIG.depth_grid_wh, fi=Math.min(SIG.depthGrid.length-1,Math.round(dv.currentTime*30/SIG.depth_grid_every)); | |
| const g=SIG.depthGrid[fi]; const v=g[Math.floor(y*gh)*gw+Math.floor(x*gw)]; | |
| rd.textContent=v?`${(v/100).toFixed(2)} m`:'invalid'; }); | |
| dv.parentElement.addEventListener('mouseleave',()=>rd.textContent='hover for depth'); | |
| setTime(0); | |
| } | |
| function inBuf(v,t){ const b=v.buffered; for(let i=0;i<b.length;i++) if(t>=b.start(i)-0.2&&t<=b.end(i)+0.2) return true; return false; } | |
| function syncSlaves(hard){ const t=master.currentTime, base=+document.getElementById('rate').value; | |
| for(const v of vids){ if(v===master||v.readyState<1) continue; | |
| if(hard){ v.playbackRate=base; if(!v.seeking&&Math.abs(v.currentTime-t)>0.04){ try{v.currentTime=t;}catch(e){} } continue; } | |
| if(!playing) continue; if(v.paused&&!v.ended) v.play().catch(()=>{}); | |
| const d=v.currentTime-t, ad=Math.abs(d); | |
| if(ad>0.75){ if(!v.seeking&&inBuf(v,t)){ try{v.currentTime=t;}catch(e){} } v.playbackRate=base; } | |
| else v.playbackRate=ad>0.035?base*Math.max(.5,Math.min(1.6,1-d*3)):base; } } | |
| function setTime(t){ t=Math.max(0,Math.min(EP.duration-0.01,t)); try{ if(Math.abs(master.currentTime-t)>0.03) master.currentTime=t; }catch(e){} syncSlaves(true); updateUI(t); } | |
| function tick(){ if(!playing) return; const t=master.currentTime; updateUI(t); | |
| if(t>=EP.duration-0.05){ try{master.currentTime=0;}catch(e){} syncSlaves(true); vids.forEach(v=>v.play().catch(()=>{})); } | |
| raf=requestAnimationFrame(tick); } | |
| function curSpan(t){ const s=EP.segments; for(let i=0;i<s.length;i++) if(t>=s[i].ts&&t<s[i].te) return i; return -1; } | |
| function updateUI(t){ | |
| const dur=EP.duration, segs=EP.segments; | |
| document.getElementById('seek').value=t/dur*10000; document.getElementById('tlab').textContent=fmt(t)+' / '+fmt(dur); | |
| document.getElementById('roT').textContent=t.toFixed(2)+'s'; | |
| const ci=curSpan(t); document.getElementById('roSk').textContent=ci>=0?segs[ci].skill:'–'; document.getElementById('roSp').textContent=ci>=0?`${ci+1} / ${segs.length}`:'–'; | |
| if(ci!==_lastCi){ const sl=document.querySelectorAll('#slist .si'); sl.forEach((el,k)=>el.classList.toggle('on',k===ci)); | |
| document.querySelectorAll('#chips .chip').forEach((el,k)=>el.classList.toggle('on',k===ci)); | |
| if(ci>=0){ const box=document.getElementById('slist'), el=sl[ci], top=el.offsetTop-box.offsetTop; if(top<box.scrollTop||top+el.offsetHeight>box.scrollTop+box.clientHeight) box.scrollTop=top-box.clientHeight/2; } | |
| _lastCi=ci; } | |
| if(!SIG) return; | |
| const j=Math.min(SIG.n-1,Math.round(t*SIG.fps)), b=SIG.base[j], bv=SIG.base_vel[j], g=SIG.grip[j], ee=SIG.ee[j]; | |
| document.getElementById('roB').textContent=`${b[0].toFixed(2)} / ${b[1].toFixed(2)} m / ${(b[2]*180/Math.PI).toFixed(0)}°`; | |
| document.getElementById('roV').textContent=`${Math.hypot(bv[0],bv[1]).toFixed(2)} m/s · ${(bv[2]*180/Math.PI).toFixed(0)}°/s`; | |
| document.getElementById('roG').textContent=`${g[0].toFixed(2)} / ${g[1].toFixed(2)}`; | |
| document.getElementById('roA').textContent=SIG.active[j]?'yes':'no'; | |
| document.getElementById('roE').textContent=ee.slice(7,10).map(v=>v.toFixed(2)).join(', '); | |
| document.getElementById('roTr').textContent=Math.max(...SIG.arm[j].map((v,k)=>Math.abs(v-SIG.arm_target[j][k]))).toFixed(3)+' rad'; | |
| drawPlayheads(t); if(setCloudT) setCloudT(t); | |
| } | |
| /* ---------- charts ---------- */ | |
| const CH={}; | |
| function sizeCharts(){ document.querySelectorAll('.chp canvas').forEach(c=>{ c.width=c.clientWidth*devicePixelRatio; c.height=84*devicePixelRatio; }); } | |
| function plot(id,series){ const c=document.getElementById(id); if(!c) return; const g=c.getContext('2d'), w=c.width, h=c.height, dpr=devicePixelRatio; | |
| g.fillStyle='#0b0e13'; g.fillRect(0,0,w,h); let lo=Infinity,hi=-Infinity; series.forEach(s=>s.d.forEach(v=>{ if(v<lo)lo=v; if(v>hi)hi=v; })); if(lo===hi){lo-=1;hi+=1;} | |
| const n=series[0].d.length, Y=v=>h-4*dpr-(v-lo)/(hi-lo)*(h-8*dpr); | |
| if(lo<0&&hi>0){ g.strokeStyle='#1c2028'; g.beginPath(); g.moveTo(0,Y(0)); g.lineTo(w,Y(0)); g.stroke(); } | |
| series.forEach(s=>{ g.strokeStyle=s.c; g.globalAlpha=s.a||1; g.lineWidth=dpr; g.beginPath(); s.d.forEach((v,i)=>{ const x=i/(n-1)*w; i?g.lineTo(x,Y(v)):g.moveTo(x,Y(v)); }); g.stroke(); }); | |
| g.globalAlpha=1; g.fillStyle='#5a6373'; g.font=`${10*dpr}px ui-monospace`; g.fillText(hi.toFixed(2),4*dpr,11*dpr); g.fillText(lo.toFixed(2),4*dpr,h-3*dpr); | |
| CH[id]=g.getImageData(0,0,w,h); } | |
| const colOf=(a,k)=>a.map(r=>r[k]); | |
| function drawCharts(){ if(!SIG) return; sizeCharts(); | |
| const arm=(off)=>{ const s=[]; for(let k=0;k<7;k++){ s.push({d:colOf(SIG.arm_target,off+k),c:PAL[k],a:.3}); s.push({d:colOf(SIG.arm,off+k),c:PAL[k]}); } return s; }; | |
| plot('chL',arm(0)); plot('chR',arm(7)); | |
| plot('chG',[{d:colOf(SIG.grip_cmd,0),c:'#7a3a42'},{d:colOf(SIG.grip_cmd,1),c:'#2a6b3e'},{d:colOf(SIG.grip,0),c:'#e0616f'},{d:colOf(SIG.grip,1),c:'#41c76b'}]); | |
| plot('chV',[{d:colOf(SIG.base_cmd,0),c:AX.x,a:.35},{d:colOf(SIG.base_cmd,1),c:AX.y,a:.35},{d:colOf(SIG.base_cmd,2),c:AX.z,a:.35},{d:colOf(SIG.base_vel,0),c:AX.x},{d:colOf(SIG.base_vel,1),c:AX.y},{d:colOf(SIG.base_vel,2),c:AX.z}]); | |
| plot('chI',[3,4,5].map((k,i)=>({d:colOf(SIG.imu,k),c:[AX.x,AX.y,AX.z][i]}))); | |
| plot('chW',[0,1,2].map(k=>({d:colOf(SIG.wrench,k),c:'#e0616f',a:.5+k*.25})).concat([6,7,8].map(k=>({d:colOf(SIG.wrench,k),c:'#41c76b',a:.5+(k-6)*.25})))); | |
| drawPlayheads(master?master.currentTime:0); } | |
| function drawPlayheads(t){ const f=t/EP.duration; Object.entries(CH).forEach(([id,img])=>{ const c=document.getElementById(id); if(!c) return; const g=c.getContext('2d'); g.putImageData(img,0,0); | |
| g.strokeStyle='rgba(255,255,255,.6)'; g.lineWidth=devicePixelRatio; g.beginPath(); g.moveTo(f*c.width,0); g.lineTo(f*c.width,c.height); g.stroke(); }); } | |
| window.addEventListener('resize',()=>{ clearTimeout(window._rz); window._rz=setTimeout(drawCharts,150); }); | |
| /* ---------- 3D world view ---------- */ | |
| async function init3D(){ | |
| const THREE=await import('three'); const {OrbitControls}=await import('three/addons/controls/OrbitControls.js'); | |
| const el=document.getElementById('three'); el.querySelectorAll('canvas').forEach(c=>c.remove()); | |
| const R=new THREE.WebGLRenderer({antialias:true}); R.setPixelRatio(devicePixelRatio); el.appendChild(R.domElement); | |
| const scene=new THREE.Scene(); scene.background=new THREE.Color(0x0b0d12); | |
| const cam=new THREE.PerspectiveCamera(50,1,.05,200); cam.up.set(0,0,1); | |
| const B0=SIG.base; let cx=0,cy=0; B0.forEach(b=>{cx+=b[0];cy+=b[1];}); cx/=B0.length; cy/=B0.length; | |
| let x0=1e9,x1=-1e9,y0=1e9,y1=-1e9; B0.forEach(b=>{x0=Math.min(x0,b[0]);x1=Math.max(x1,b[0]);y0=Math.min(y0,b[1]);y1=Math.max(y1,b[1]);}); const span=Math.max(x1-x0,y1-y0,2)+3; | |
| cam.position.set(cx-span*.75,cy-span*.75,span*.7); const ctl=new OrbitControls(cam,R.domElement); ctl.target.set(cx,cy,.6); ctl.update(); | |
| const grid=new THREE.GridHelper(16,32,0x2a3a5a,0x1a2030); grid.rotation.x=Math.PI/2; grid.position.set(cx,cy,0); scene.add(grid); | |
| // base path | |
| const path=new THREE.Line(new THREE.BufferGeometry().setFromPoints(B0.map(b=>new THREE.Vector3(b[0],b[1],.01))),new THREE.LineBasicMaterial({color:0x6ea8ff})); scene.add(path); | |
| const robot=new THREE.Group(); scene.add(robot); | |
| const body=new THREE.LineSegments(new THREE.BufferGeometry(),new THREE.LineBasicMaterial({color:0xd6dbe6})); robot.add(body); | |
| const joints=new THREE.Points(new THREE.BufferGeometry(),new THREE.PointsMaterial({color:0xffc857,size:.045})); robot.add(joints); | |
| const foot=new THREE.Mesh(new THREE.BoxGeometry(.62,.52,.25),new THREE.MeshBasicMaterial({color:0x4a5570,wireframe:true})); foot.position.set(.1,0,.14); robot.add(foot); | |
| const ax=new THREE.AxesHelper(.35); robot.add(ax); | |
| const L=SIG.links, idx=n=>L.indexOf(n); | |
| const bones=[['chassis_link','body_link1'],['body_link1','body_link2'],['body_link2','body_link3'],['body_link3','body_link5'],['body_link5','head_link3'],['body_link5','arm_base_link']]; | |
| ['l','r'].forEach(s=>{ bones.push(['arm_base_link',`arm_${s}_link1`]); for(let i=1;i<7;i++) bones.push([`arm_${s}_link${i}`,`arm_${s}_link${i+1}`]); bones.push([`arm_${s}_link7`,`arm_${s}_end_link`],[`arm_${s}_end_link`,`gripper_${s}_center_link`]); }); | |
| const clouds={}; const CC={lidar_front:[1,.54,.44],lidar_back:[.43,.66,1]}; | |
| let follow=false; | |
| const buildChk=()=>{ document.getElementById('chk').innerHTML=Object.keys(clouds).map(k=>`<label><input type="checkbox" data-k="${k}" checked>${k==='__map'?'environment map (all scans)':k+' (current scan)'}</label>`).join('')+`<label><input type="checkbox" data-k="__path" checked>base path</label><label><input type="checkbox" data-k="__follow">camera follows robot</label>`; | |
| document.querySelectorAll('#chk input').forEach(i=>i.onchange=()=>{ const k=i.dataset.k; if(k==='__path') path.visible=i.checked; else if(k==='__follow') follow=i.checked; else clouds[k].p.visible=i.checked; }); }; | |
| let mapPts=null; | |
| window._addClouds=()=>{ | |
| if(SIG.map&&!mapPts){ const q=SIG.map.q, I=SIG.map.I, n=I.length, pos=new Float32Array(n*3), col=new Float32Array(n*3); | |
| for(let m=0;m<n;m++){ pos[3*m]=q[3*m]/100; pos[3*m+1]=q[3*m+1]/100; const z=q[3*m+2]/100; pos[3*m+2]=z; | |
| const h=Math.max(0,Math.min(1,z/2.3)), s=.30+.35*I[m]/255; col[3*m]=(.35+.25*h)*s*1.6; col[3*m+1]=(.45+.2*h)*s*1.6; col[3*m+2]=(.6+.15*h)*s*1.6; } | |
| const g=new THREE.BufferGeometry(); g.setAttribute('position',new THREE.BufferAttribute(pos,3)); g.setAttribute('color',new THREE.BufferAttribute(col,3)); | |
| mapPts=new THREE.Points(g,new THREE.PointsMaterial({size:.03,vertexColors:true,transparent:true,opacity:.35})); scene.add(mapPts); clouds.__map={p:mapPts,cur:0,static:true}; } if(SIG.clouds) Object.keys(SIG.clouds).forEach(k=>{ if(clouds[k]) return; const p=new THREE.Points(new THREE.BufferGeometry(),new THREE.PointsMaterial({size:.05,vertexColors:true})); scene.add(p); clouds[k]={p,cur:-1}; }); | |
| buildChk(); if(setCloudT) setCloudT(master.currentTime||0); }; | |
| buildChk(); | |
| let last=null; | |
| setCloudT=t=>{ const j=Math.min(SIG.n-1,Math.round(t*SIG.fps)), b=SIG.base[j]; | |
| robot.position.set(b[0],b[1],0); robot.rotation.set(0,0,b[2]); | |
| if(follow&&last){ const dx=b[0]-last[0], dy=b[1]-last[1]; cam.position.x+=dx; cam.position.y+=dy; ctl.target.x+=dx; ctl.target.y+=dy; } last=b; | |
| const lt=SIG.link_t; let li=0; while(li<lt.length-1&<[li+1]<=t) li++; const P=SIG.link_p[li]; | |
| const seg=[]; bones.forEach(([a,c])=>{ const ia=idx(a), ic=idx(c); if(ia>=0&&ic>=0) seg.push(...P[ia],...P[ic]); }); | |
| body.geometry.setAttribute('position',new THREE.Float32BufferAttribute(seg,3)); joints.geometry.setAttribute('position',new THREE.Float32BufferAttribute(P.flat(),3)); | |
| Object.entries(clouds).forEach(([k,C])=>{ if(C.static) return; const fr=SIG.clouds&&SIG.clouds[k]; if(!fr) return; let i=0; while(i<fr.length-1&&fr[i+1].t<=t) i++; if(i===C.cur) return; C.cur=i; | |
| const f=fr[i], q=f.q, I=f.I, n=I.length, bj=Math.max(0,Math.min(SIG.n-1,Math.round(f.t*SIG.fps))), bb=SIG.base[bj], cs=Math.cos(bb[2]), sn=Math.sin(bb[2]); | |
| const pos=new Float32Array(n*3), col=new Float32Array(n*3), base=CC[k]||[.8,.8,.8]; | |
| for(let m=0;m<n;m++){ const x=q[3*m]/100,y=q[3*m+1]/100,z=q[3*m+2]/100; pos[3*m]=bb[0]+cs*x-sn*y; pos[3*m+1]=bb[1]+sn*x+cs*y; pos[3*m+2]=z; | |
| const s=.35+.65*I[m]/255; col[3*m]=base[0]*s; col[3*m+1]=base[1]*s; col[3*m+2]=base[2]*s; } | |
| C.p.geometry.setAttribute('position',new THREE.BufferAttribute(pos,3)); C.p.geometry.setAttribute('color',new THREE.BufferAttribute(col,3)); C.p.geometry.computeBoundingSphere(); }); }; | |
| const resize=()=>{ const w=el.clientWidth,h=el.clientHeight; if(!w) return; R.setSize(w,h); cam.aspect=w/h; cam.updateProjectionMatrix(); }; | |
| new ResizeObserver(resize).observe(el); resize(); | |
| (function loop(){ requestAnimationFrame(loop); ctl.update(); R.render(scene,cam); })(); | |
| setCloudT(master.currentTime||0); window._addClouds(); | |
| } | |
| function fromHash(){ const s=decodeURIComponent(location.hash.slice(1)); const i=EPS.findIndex(e=>e.slug===s); select(i>=0?i:0); } | |
| window.addEventListener('hashchange',fromHash); fromHash(); | |
| </script> | |
| </body></html> | |