PageSourceSearch

https://www.easteine.com/rescene4d/js/pcd-viewer-v5.js

js easteine.com collected 2026-09-28 12:10:02 UTC 40,672 bytes, 1,252 lines download raw bytes

1// 1) IMPORTS (ensure your bundler or <script type="module"> can find these)
2import * as THREE from 'three';
3import { PCDLoader } from 'PCDLoader';
4import { OrbitControls } from 'OrbitControls';
5// PLY loader from Three.js examples for mesh .ply files (used in comparison grid)
6import { PLYLoader } from 'https://unpkg.com/[email protected]/examples/jsm/loaders/PLYLoader.js';
7
8// 2) SCENE MAP (scene_1/2/3 -> existing PCD object names; for now same PCD used in all 6 cells)
9const sceneToPcd = {
10  scene_1: 'partnet_78',
11  scene_2: 'partnet_652',
12  scene_3: 'partnet_680'
13};
14const sampleMap = {
15  partnet_78:  ['1','2','3','4','5','6','7','8','9','10','11','12','13','14','15','16','17','18','19'],
16  partnet_652: ['1','2','3','4','5','6','7','8','9','10','11','12','13','14','15','16','17','18','19'],
17  partnet_680: ['1','2','3','4','5','6','7','8','9','10','11','12','13','14','15','16','17','18','19']
18};
19
20// OUTSIDE—at module scope, create and reuse these:
21const _trajSphereGeo = new THREE.SphereGeometry(0.008, 6, 4);
22const _trajSphereMat = new THREE.MeshBasicMaterial({
23  transparent: true,
24  opacity: 0.35,
25  vertexColors: true,    // allows per‐instance color
26  // Because it's MeshBasicMaterial, it does NOT cast or receive shadows
27});
28
29const viewerParams = {
30  partnet_78:  { groundHeight: -0.8, cameraY: 1.5  },
31  partnet_652: { groundHeight: -0.85, cameraY: 1.5  },
32  partnet_680: { groundHeight: -0.45, cameraY: 1.0  },
33  scene_1:     { groundHeight: -0.8, cameraY: 1.5  },
34  scene_2:     { groundHeight: -0.85, cameraY: 1.5  },
35  scene_3:     { groundHeight: -0.45, cameraY: 1.0  }
36};
37
38const totalFrames   = 20;    // steps per sample
39const frameInterval = 40;    // ms per frame
40const pauseDuration = 3000;  // ms to pause at last frame
41let slowMode = true;         // toggle “slow” vs “normal” playback
42
43// 3) HSV → RGB HELPER (unchanged)
44function hsvToRgb(h, s, v) {
45  let r, g, b;
46  const i = Math.floor(h * 6);
47  const f = h * 6 - i;
48  const p = v * (1 - s);
49  const q = v * (1 - f * s);
50  const t = v * (1 - (1 - f) * s);
51  switch (i % 6) {
52    case 0: r = v; g = t; b = p; break;
53    case 1: r = q; g = v; b = p; break;
54    case 2: r = p; g = v; b = t; break;
55    case 3: r = p; g = q; b = v; break;
56    case 4: r = t; g = p; b = v; break;
57    case 5: r = v; g = p; b = q; break;
58  }
59  return [r, g, b];
60}
61
62// 4) GLOBAL STATE
63let isPaused = false;
64let autoResample = true;
65let currentFrameIdx = 0;
66let playTimeoutId = null;
67let currentSession = 0;         // “session stamp” to cancel stale loads/timeouts
68let isSyncing = false;          // guard for OrbitControl sync
69let inputState = null;
70let sampledStates = [];
71let allStates = [];
72
73const globalLoader = new PCDLoader();
74const globalPlyLoader = new PLYLoader();
75
76// --- TRAJ GLOBALS ---
77let showTrajectories = false;    // tracks whether trajectories should be visible
78// (Each viewer state will keep its own `trajectorySpheres` array.)
79
80// 5) UTILITY: PICK TWO DISTINCT RANDOM SAMPLES (unchanged)
81function pickTwoRandomSamples(objName) {
82  const arr = sampleMap[objName];
83  if (!arr || arr.length < 2) {
84    console.error(`Need ≥2 samples for "${objName}".`);
85    return arr.slice(0, 2);
86  }
87  const copy = arr.slice();
88  for (let i = copy.length - 1; i > 0; i--) {
89    const j = Math.floor(Math.random() * (i + 1));
90    [copy[i], copy[j]] = [copy[j], copy[i]];
91  }
92  return [copy[0], copy[1]];
93}
94
95// 6) SYNC HELPERS FOR ORBITCONTROLS (unchanged)
96function syncAnglesFrom(srcControls) {
97  if (isSyncing) return;
98  isSyncing = true;
99  try {
100    const cam = srcControls.object;
101    const target = srcControls.target.clone();
102    const offset = new THREE.Vector3().copy(cam.position).sub(target);
103    const spherical = new THREE.Spherical().setFromVector3(offset);
104
105    allStates.forEach((st) => {
106      if (st.controls === srcControls) return;
107      const newPos = new THREE.Vector3()
108        .setFromSphericalCoords(spherical.radius, spherical.phi, spherical.theta)
109        .add(st.controls.target);
110      st.camera.position.copy(newPos);
111      st.camera.lookAt(st.controls.target);
112      st.controls.update();
113    });
114  } finally {
115    isSyncing = false;
116  }
117}
118
119// 7) INITIALIZE A STATIC INPUT VIEWER (colored by label)
120function initInputViewer(container, config) {
121  let width  = container.clientWidth;
122  let height = container.clientHeight;
123  if (!width || !height) {
124    width = Math.max(width || 200, 200);
125    height = Math.max(height || 200, 200);
126  }
127
128  const scene = new THREE.Scene();
129  scene.background = new THREE.Color(0xFCFCFC);
130
131  const aspect = height > 0 ? width / height : 1;
132  const camera = new THREE.PerspectiveCamera(25, aspect, 0.1, 1000);
133  // Use Z-up convention so auto-rotation orbits around a horizontal axis
134  camera.up.set(0, 0, 1);
135  camera.position.set(0, 1.4, 5);
136  camera.lookAt(0, 0, 0);
137
138  const renderer = new THREE.WebGLRenderer({ antialias: true });
139  renderer.setSize(width, height);
140  renderer.shadowMap.enabled = true;
141  renderer.shadowMap.type = THREE.PCFSoftShadowMap;
142  container.appendChild(renderer.domElement);
143
144  // Lights
145  const ambient = new THREE.AmbientLight(0xffffff, 1.8);
146  scene.add(ambient);
147  const dirLight = new THREE.DirectionalLight(0xffffff, 1);
148  dirLight.position.set(5, 12, 5);
149  dirLight.castShadow = true;
150  dirLight.shadow.mapSize.width = 1024;
151  dirLight.shadow.mapSize.height = 1024;
152  dirLight.shadow.camera.near = 1;
153  dirLight.shadow.camera.far = 25;
154  dirLight.shadow.camera.left = -10;
155  dirLight.shadow.camera.right = 10;
156  dirLight.shadow.camera.top = 10;
157  dirLight.shadow.camera.bottom = -10;
158  scene.add(dirLight);
159
160  // Handle window resize
161  window.addEventListener('resize', () => {
162    const w = container.clientWidth;
163    const h = container.clientHeight;
164    renderer.setSize(w, h);
165    camera.aspect = w / h;
166    camera.updateProjectionMatrix();
167  });
168
169  // “Loading…” overlay
170  const loading = document.createElement('div');
171  loading.classList.add('loading');
172  loading.innerText = 'Loading…';
173  container.appendChild(loading);
174
175  // OrbitControls (auto‐rotate + drag‐pause + sync)
176  const controls = new OrbitControls(camera, renderer.domElement);
177  controls.target.set(0, 0, 0);
178  controls.enablePan = false;
179  // Allow zooming with mouse wheel / trackpad
180  controls.enableZoom = true;
181  controls.autoRotate = true;
182  // Even slower, more gentle rotation
183  controls.autoRotateSpeed = 0.15;
184  controls.update();
185
186  controls.userIsInteracting = false;
187  controls.domElement.addEventListener('mousedown', () => {
188    controls.userIsInteracting = true;
189  });
190  document.addEventListener('mouseup', () => {
191    controls.userIsInteracting = false;
192  });
193
194  controls.addEventListener('start', () => {
195    allStates.forEach((st) => {
196      st.controls.autoRotate = false;
197    });
198    document.getElementById('btn-rotate').disabled = true;
199  });
200  controls.addEventListener('end', () => {
201    const rotateBtn = document.getElementById('btn-rotate');
202    const isAutoRotate = rotateBtn.innerHTML.includes(': On');
203    document.getElementById('btn-rotate').disabled = false;
204    if (!isAutoRotate) return;
205    allStates.forEach((st) => {
206      st.controls.autoRotate = true;
207    });
208    syncAnglesFrom(controls);
209  });
210  // Sync all viewers whenever any control changes (rotation or zoom)
211  controls.addEventListener('change', () => {
212    syncAnglesFrom(controls);
213  });
214
215  // LOAD 3D mesh PLY files and display them overlapped:
216  // - RGB geometry mesh (underlay)
217  // - Instance mesh (colored overlay), using the same coordinate frame
218  // The specific files for this viewer are provided via config.
219  const rgbUrl = (config && typeof config === 'object' && config.rgbUrl)
220    ? config.rgbUrl
221    : 'pcd/scene0170_00-scene0170_01/scene0170_00_rgb.ply';
222  const instUrl = (config && typeof config === 'object' && config.instUrl)
223    ? config.instUrl
224    : 'pcd/scene0170_00-scene0170_01/scene0170_00_gt_instances_filtered.ply';
225
226  globalPlyLoader.load(
227    rgbUrl,
228    (geometry) => {
229      geometry.computeVertexNormals();
230      geometry.computeBoundingSphere();
231
232      const hasColor = geometry.hasAttribute('color');
233      const geomMat = new THREE.MeshStandardMaterial({
234        // Very bright base so RGB vertex colors appear vivid
235        color: 0xffffff,
236        metalness: 0.0,
237        roughness: 0.4,
238        flatShading: false,
239        vertexColors: hasColor,
240        // Add a small emissive term to boost brightness without washing out colors
241        emissive: new THREE.Color(0.15, 0.15, 0.15),
242        emissiveIntensity: 1.2
243      });
244      const geomMesh = new THREE.Mesh(geometry, geomMat);
245      geomMesh.castShadow = true;
246      geomMesh.receiveShadow = true;
247      scene.add(geomMesh);
248
249      // Fit camera to geometry mesh
250      const bs = geometry.boundingSphere;
251      const center = bs.center;
252      const radius = bs.radius || 1.0;
253      camera.position.set(center.x + 2.5 * radius, center.y + 1.5 * radius, center.z + 2.5 * radius);
254      controls.target.copy(center);
255      controls.update();
256
257      loading.style.display = 'none';
258
259      // After geometry is in place, load instance mesh and overlay it.
260      globalPlyLoader.load(
261        instUrl,
262        (instGeom) => {
263          instGeom.computeVertexNormals();
264          const instHasColor = instGeom.hasAttribute('color');
265          const instMat = new THREE.MeshStandardMaterial({
266            color: 0xffffff,
267            metalness: 0.0,
268            roughness: 0.7,
269            flatShading: false,
270            vertexColors: instHasColor,
271            transparent: true,
272            // Slightly more transparent so underlying geometry is visible
273            opacity: 0.7
274          });
275          const instMesh = new THREE.Mesh(instGeom, instMat);
276          instMesh.castShadow = false;
277          instMesh.receiveShadow = false;
278          scene.add(instMesh);
279        },
280        undefined,
281        (err2) => {
282          console.error(`Error loading instance mesh PLY ${instUrl}:`, err2);
283        }
284      );
285    },
286    undefined,
287    (err) => {
288      console.error(`Error loading geometry mesh PLY ${rgbUrl}:`, err);
289      loading.innerText = 'Error loading mesh';
290    }
291  );
292
293  return {
294    container,
295    scene,
296    camera,
297    renderer,
298    controls,
299    mesh: null,
300    isSampled: false
301  };
302}
303
304// 8) INITIALIZE A “SAMPLED” VIEWER (20‐frame animation)
305function initSampledViewer(container, objName, initialSampleId) {
306  const width  = container.clientWidth;
307  const height = container.clientHeight;
308
309  // Scene, camera, renderer
310  const scene = new THREE.Scene();
311  scene.background = new THREE.Color(0xFCFCFC);
312
313  const camera = new THREE.PerspectiveCamera(25, width / height, 0.1, 1000);
314  camera.position.set(0, viewerParams[objName].cameraY, 5);
315  camera.lookAt(0, 0, 0);
316
317  const renderer = new THREE.WebGLRenderer({ antialias: true });
318  renderer.setSize(width, height);
319  renderer.shadowMap.enabled = true;
320  renderer.shadowMap.type = THREE.PCFSoftShadowMap;
321  container.appendChild(renderer.domElement);
322
323  // Lights
324  const ambient = new THREE.AmbientLight(0xffffff, 1.8);
325  scene.add(ambient);
326  const dirLight = new THREE.DirectionalLight(0xffffff, 1);
327  dirLight.position.set(5, 12, 5);
328  dirLight.castShadow = true;
329  dirLight.shadow.mapSize.width = 1024;
330  dirLight.shadow.mapSize.height = 1024;
331  dirLight.shadow.camera.near = 1;
332  dirLight.shadow.camera.far = 25;
333  dirLight.shadow.camera.left = -10;
334  dirLight.shadow.camera.right = 10;
335  dirLight.shadow.camera.top = 10;
336  dirLight.shadow.camera.bottom = -10;
337  scene.add(dirLight);
338
339  // Ground plane
340  const planeGeo = new THREE.PlaneGeometry(20, 20);
341  const planeMat = new THREE.ShadowMaterial({ opacity: 0.2 });
342  const ground = new THREE.Mesh(planeGeo, planeMat);
343  ground.rotateX(-Math.PI / 2);
344  ground.position.y = viewerParams[objName].groundHeight;
345  ground.receiveShadow = true;
346  scene.add(ground);
347
348  // Handle window resize
349  window.addEventListener('resize', () => {
350    const w = container.clientWidth;
351    const h = container.clientHeight;
352    renderer.setSize(w, h);
353    camera.aspect = w / h;
354    camera.updateProjectionMatrix();
355  });
356
357  // “Loading…” overlay
358  const loading = document.createElement('div');
359  loading.classList.add('loading');
360  loading.innerText = 'Loading…';
361  container.appendChild(loading);
362
363  // OrbitControls (auto‐rotate + drag‐pause + sync)
364  const controls = new OrbitControls(camera, renderer.domElement);
365  controls.target.set(0, 0, 0);
366  controls.enablePan = false;
367  controls.enableZoom = false;
368  controls.autoRotate = true;
369  controls.autoRotateSpeed = 2;
370  controls.update();
371
372  controls.userIsInteracting = false;
373  controls.domElement.addEventListener('mousedown', () => {
374    controls.userIsInteracting = true;
375  });
376  document.addEventListener('mouseup', () => {
377    controls.userIsInteracting = false;
378  });
379
380  controls.addEventListener('start', () => {
381    allStates.forEach((st) => {
382      st.controls.autoRotate = false;
383    });
384    document.getElementById('btn-rotate').disabled = true;
385  });
386  controls.addEventListener('end', () => {
387    allStates.forEach((st) => {
388      st.controls.autoRotate = true;
389    });
390    document.getElementById('btn-rotate').disabled = false;
391    syncAnglesFrom(controls);
392  });
393  controls.addEventListener('change', () => {
394    if (controls.userIsInteracting) {
395      syncAnglesFrom(controls);
396    }
397  });
398
399  // Prepare arrays for 20 frames
400  const frameMeshes = new Array(totalFrames).fill(null);
401  const framePositions = new Array(totalFrames).fill(null);
402  let N_points = 0;  // will be known once frame 0 loads
403
404  // Keep track of trajectory spheres so we can remove them later
405  const trajectorySpheres = [];         // ← TRAJ
406
407  // Assign this state’s “session stamp”
408  const mySession = currentSession;
409
410  const state = {
411    container,
412    scene,
413    camera,
414    renderer,
415    controls,
416    frameMeshes,
417    framePositions,
418    N_points,
419    currentSampleId: null,
420    loadingOverlay: loading,
421    isSampled: true,
422    session: mySession,
423    trajectorySpheres             // ← TRAJ
424  };
425
426  // Helper: pick a random sample ≠ current
427  function pickRandomSample() {
428    const arr = sampleMap[objName];
429    if (arr.length === 1) return arr[0];
430    let choice;
431    do {
432      choice = arr[Math.floor(Math.random() * arr.length)];
433    } while (choice === state.currentSampleId);
434    return choice;
435  }
436
437  // Helper: build an interpolated InstancedMesh for frames (unchanged)
438  function buildInterpolatedMesh(t0, t1) {
439    const pos0 = state.framePositions[t0];
440    const pos1 = state.framePositions[t1];
441    if (!pos0 || !pos1) return null;
442
443    const sphereGeo = new THREE.SphereGeometry(0.01, 6, 4);
444    const mat = new THREE.MeshStandardMaterial({
445      transparent: true,
446      metalness: 0.2,
447      roughness: 0.4,
448      depthWrite: true,
449      flatShading: false
450    });
451
452    const mesh = new THREE.InstancedMesh(sphereGeo, mat, state.N_points);
453    mesh.castShadow = true;
454    mesh.receiveShadow = false;
455
456    const dummyMatrix = new THREE.Matrix4();
457    const color = new THREE.Color();
458    const srcMesh = state.frameMeshes[t0];
459
460    for (let i = 0; i < state.N_points; i++) {
461      const x0 = pos0[3 * i];
462      const y0 = pos0[3 * i + 1];
463      const z0 = pos0[3 * i + 2];
464      const x1 = pos1[3 * i];
465      const y1 = pos1[3 * i + 1];
466      const z1 = pos1[3 * i + 2];
467
468      const xm = 0.5 * (x0 + x1);
469      const ym = 0.5 * (y0 + y1);
470      const zm = 0.5 * (z0 + z1);
471      dummyMatrix.makeTranslation(xm, ym, zm);
472      mesh.setMatrixAt(i, dummyMatrix);
473
474      if (srcMesh.instanceColor) {
475        const cA = srcMesh.instanceColor;
476        const r = cA.getX(i);
477        const g = cA.getY(i);
478        const b = cA.getZ(i);
479        color.setRGB(r, g, b);
480      } else {
481        color.setRGB(0.5, 0.5, 0.5);
482      }
483      mesh.setColorAt(i, color);
484    }
485
486    mesh.visible = false;
487    state.scene.add(mesh);
488    return mesh;
489  }
490
491  // Helper: load all frames for a sample (unchanged except return)
492  async function loadSample(sampleId, showLoading = false) {
493    if (state.session !== currentSession) return null;
494
495    if (showLoading) {
496      state.loadingOverlay.style.display = 'flex';
497    }
498
499    const newMeshes = new Array(totalFrames).fill(null);
500    const newPositions = new Array(totalFrames).fill(null);
501    let sphereGeo = null;
502
503    for (let t = 0; t < totalFrames; t++) {
504      const url = `pcd/${objName}/sample_${sampleId}/step_${t}.pcd`;
505
506      if (state.session !== currentSession) {
507        for (let k = 0; k < t; k++) {
508          if (newMeshes[k]) {
509            newMeshes[k].geometry.dispose();
510            newMeshes[k].material.dispose();
511          }
512        }
513        return null;
514      }
515
516      await new Promise((resolve) => {
517        globalLoader.load(
518          url,
519          (points) => {
520            const geom = points.geometry;
521            const posAttr = geom.attributes.position;
522            const lblAttr = geom.attributes.label;
523            if (!posAttr) {
524              console.error(`No position attribute in ${url}`);
525              newMeshes[t] = null;
526              newPositions[t] = null;
527              resolve();
528              return;
529            }
530
531            const positions = posAttr.array;
532            const N = positions.length / 3;
533            if (t === 0) {
534              state.N_points = N;
535            }
536            newPositions[t] = new Float32Array(positions);
537
538            if (!sphereGeo) sphereGeo = new THREE.SphereGeometry(0.01, 6, 4);
539            const mat = new THREE.MeshStandardMaterial({
540              transparent: true,
541              metalness: 0.2,
542              roughness: 0.4,
543              depthWrite: true,
544              flatShading: false
545            });
546            const instMesh = new THREE.InstancedMesh(sphereGeo, mat, N);
547            instMesh.castShadow = true;
548            instMesh.receiveShadow = false;
549
550            const dummyMatrix = new THREE.Matrix4();
551            const color = new THREE.Color();
552
553            let maxLabel = 0;
554            if (lblAttr) {
555              for (let i = 0; i < N; i++) {
556                if (lblAttr.array[i] > maxLabel) maxLabel = lblAttr.array[i];
557              }
558            }
559
560            for (let i = 0; i < N; i++) {
561              const x = positions[3 * i];
562              const y = positions[3 * i + 1];
563              const z = positions[3 * i + 2];
564              dummyMatrix.makeTranslation(x, y, z);
565              instMesh.setMatrixAt(i, dummyMatrix);
566
567              if (lblAttr) {
568                const lbl = lblAttr.array[i];
569                const hue = maxLabel > 0 ? (lbl / maxLabel) * 0.8 : 0;
570                const hue_remap = (hue + 0.548) % 1;
571                const [r, g, b] = hsvToRgb(hue_remap, 0.62, 0.46);
572                color.setRGB(r, g, b);
573              } else {
574                color.setRGB(0.5, 0.5, 0.5);
575              }
576              instMesh.setColorAt(i, color);
577            }
578
579            instMesh.visible = false;
580            newMeshes[t] = instMesh;
581            state.scene.add(instMesh);
582
583            geom.dispose();
584            points.material.dispose();
585            resolve();
586          },
587          () => { /* ignore progress */ },
588          (err) => {
589            console.error(`Error loading ${url}:`, err);
590            newMeshes[t] = null;
591            newPositions[t] = null;
592            resolve();
593          }
594        );
595      });
596
597      if (state.session !== currentSession) {
598        for (let k = 0; k <= t; k++) {
599          if (newMeshes[k]) {
600            newMeshes[k].geometry.dispose();
601            newMeshes[k].material.dispose();
602          }
603        }
604        return null;
605      }
606    }
607
608    if (state.session !== currentSession) {
609      newMeshes.forEach((m) => {
610        if (m) {
611          m.geometry.dispose();
612          m.material.dispose();
613        }
614      });
615      return null;
616    }
617
618    if (showLoading) {
619      state.loadingOverlay.style.display = 'none';
620
621      // Dispose old frame‐meshes:
622      state.frameMeshes.forEach((oldMesh) => {
623        if (oldMesh) {
624          state.scene.remove(oldMesh);
625          oldMesh.geometry.dispose();
626          oldMesh.material.dispose();
627        }
628      });
629
630      // Swap in the new
631      state.frameMeshes = newMeshes;
632      state.framePositions = newPositions;
633      state.currentSampleId = sampleId;
634
635      const idx = currentFrameIdxMapping();
636      if (idx.isOriginal && state.frameMeshes[idx.frameIndex]) {
637        state.frameMeshes[idx.frameIndex].visible = true;
638      }
639      return null;
640    } else {
641      return { newMeshes, newPositions, sampleId };
642    }
643  }
644
645  state.currentSampleId = initialSampleId;
646  state.pickRandom = pickRandomSample;
647  state.loadSample = loadSample;
648
649  // Initially load the first sample and show frame 0
650  loadSample(initialSampleId, true);
651
652  return state;
653
654  function currentFrameIdxMapping() {
655    if (!slowMode) {
656      return { isOriginal: true, frameIndex: currentFrameIdx, alpha: 0 };
657    } else {
658      const g = currentFrameIdx;
659      if (g % 2 === 0) {
660        return { isOriginal: true, frameIndex: g / 2, alpha: 0 };
661      } else {
662        const i0 = Math.floor(g / 2);
663        const i1 = i0 + 1;
664        return { isOriginal: false, frameIndex: i0, nextIndex: i1, alpha: 0.5 };
665      }
666    }
667  }
668}
669
670// 9) TEARDOWN: CLEAR ALL VIEWERS (disposing Three.js objects)
671function clearAllViewers() {
672  currentSession++;           // bump session so in‐flight promises bail
673  clearTimeout(playTimeoutId);
674
675  allStates.forEach((st) => {
676    // Guard: skip any state object that doesn’t have a scene
677    if (!st || !st.scene) return; // ← UPDATED: avoid calling traverse on undefined
678
679    if (st.isSampled) {
680      // Remove any interpolated meshes (if still present)
681    //   st.scene.traverse((obj) => {
682    //     if (obj.name && obj.name.startsWith(`interp_${st.currentSampleId}_`)) {
683    //       if (obj.geometry) obj.geometry.dispose();
684    //       if (obj.material) obj.material.dispose();
685    //       st.scene.remove(obj);
686    //     }
687    //   });
688
689      // Remove each frame’s InstancedMesh
690      if (Array.isArray(st.frameMeshes)) {
691        st.frameMeshes.forEach((m) => {
692          if (m) {
693            st.scene.remove(m);
694            if (m.geometry) m.geometry.dispose();
695            if (m.material) m.material.dispose();
696          }
697        });
698      }
699
700      // Remove any trajectory spheres
701      if (Array.isArray(st.trajectorySpheres)) {
702        st.trajectorySpheres.forEach((sphere) => {
703          if (sphere.geometry) sphere.geometry.dispose();
704          if (sphere.material) sphere.material.dispose();
705          st.scene.remove(sphere);
706        });
707        st.trajectorySpheres.length = 0;
708      }
709    } else {
710      if (st.mesh) {
711        st.scene.remove(st.mesh);
712        if (st.mesh.geometry) st.mesh.geometry.dispose();
713        if (st.mesh.material) st.mesh.material.dispose();
714      }
715    }
716
717    if (st.renderer && st.renderer.domElement) {
718      st.container.removeChild(st.renderer.domElement);
719      st.renderer.dispose();
720    }
721  });
722
723  // Clear the grid container
724  const containerEl = document.querySelector('.viewers-container');
725  if (containerEl) {
726    containerEl.innerHTML = '';
727  }
728
729  inputState = null;
730  sampledStates = [];
731  allStates = [];
732}
733
734// 9b) Build 3×2 grid: columns = GT | Baseline | Ours, rows = t₀ | t₁ (6 viewers total)
735// Only this function builds the viewer layout; no "Condition/Generation" layout.
736// Each cell displays an RGB mesh underlay and an instance mesh overlay for a given scene + time step.
737
738// Scene configuration: folder path and timestep stems (t0, t1) for each scene button.
739const comparisonScenes = {
740  scene_1: {
741    pairDir: 'pcd/scene0170_00-scene0170_01',
742    timesteps: ['scene0170_00', 'scene0170_01']
743  },
744  scene_2: {
745    pairDir: 'pcd/scene0029_01-scene0029_00',
746    timesteps: ['scene0029_01', 'scene0029_00']
747  },
748  scene_3: {
749    pairDir: 'pcd/scene0269_00-scene0269_02',
750    timesteps: ['scene0269_00', 'scene0269_02']
751  }
752};
753
754function buildComparisonGrid(objName) {
755  clearAllViewers();
756  document.querySelectorAll('.tab-button').forEach((btn) => {
757    btn.classList.toggle('active', btn.dataset.obj === objName);
758  });
759  currentSession++;
760
761  const vc = document.querySelector('.viewers-container');
762  if (!vc) return;
763
764  const grid = document.createElement('div');
765  grid.className = 'comparison-grid-3x2';
766  grid.style.display = 'grid';
767  grid.style.gridTemplateColumns = '1fr 1fr 1fr';
768  grid.style.gridTemplateRows = 'auto 1fr 1fr';
769  grid.style.gap = '8px';
770  grid.style.width = '100%';
771  grid.style.height = '520px';
772
773  // We use a single fixed baseline: Mask3D with semantic matching
774  const baselineName = 'mask3d_sem';
775  const colHeaders = ['GT', 'Baseline', 'ReScene4D'];
776
777  colHeaders.forEach((h, c) => {
778    const cap = document.createElement('div');
779    cap.className = 'grid-col-caption';
780    cap.style.gridColumn = c + 1;
781    cap.style.textAlign = 'center';
782    cap.style.fontWeight = 'bold';
783    cap.style.fontSize = '1.4rem';
784    cap.textContent = c === 1 ? `Baseline` : h;
785    grid.appendChild(cap);
786  });
787
788  // Define grid cells with logical roles
789  const cellOrder = [
790    { col: 0, row: 1, label: 't₀', role: 'gt' },
791    { col: 0, row: 2, label: 't₁', role: 'gt' },
792    { col: 1, row: 1, label: 't₀', role: 'baseline' },
793    { col: 1, row: 2, label: 't₁', role: 'baseline' },
794    { col: 2, row: 1, label: 't₀', role: 'ours' },
795    { col: 2, row: 2, label: 't₁', role: 'ours' }
796  ];
797
798  const sceneCfg = comparisonScenes[objName] || comparisonScenes.scene_1;
799
800  const viewerConfigs = [];
801  cellOrder.forEach(({ col, row, label, role }) => {
802    const wrap = document.createElement('div');
803    wrap.style.display = 'flex';
804    wrap.style.flexDirection = 'column';
805    wrap.style.minHeight = '0';
806    wrap.style.gridColumn = col + 1;
807    wrap.style.gridRow = row + 1;
808
809    const rowCap = document.createElement('div');
810    rowCap.style.fontSize = '1.2rem';
811    rowCap.style.marginBottom = '4px';
812    rowCap.textContent = label;
813    wrap.appendChild(rowCap);
814
815    const viewerDiv = document.createElement('div');
816    viewerDiv.className = 'viewer comparison-cell';
817    viewerDiv.style.flex = '1';
818    viewerDiv.style.minHeight = '200px';
819    viewerDiv.style.borderRadius = '6px';
820    viewerDiv.style.overflow = 'hidden';
821    viewerDiv.style.background = '#FCFCFC';
822    wrap.appendChild(viewerDiv);
823    grid.appendChild(wrap);
824
825    // Determine timestep index (0 -> t₀, 1 -> t₁)
826    const tIndex = (label === 't₀') ? 0 : 1;
827    const stem = sceneCfg.timesteps[tIndex];
828    const basePath = `${sceneCfg.pairDir}/${stem}`;
829
830    const rgbUrl = `${basePath}_rgb.ply`;
831
832    let suffix;
833    if (role === 'gt') {
834      suffix = 'gt_instances_filtered';
835    } else if (role === 'baseline') {
836      suffix = 'mask3d_feature_instances';
837    } else {
838      // ours / ReScene4D
839      suffix = 'concerto_temporal_sharing_instances';
840    }
841    const instUrl = `${basePath}_${suffix}.ply`;
842
843    viewerConfigs.push({ viewerDiv, rgbUrl, instUrl });
844  });
845
846  vc.appendChild(grid);
847
848  viewerConfigs.forEach(({ viewerDiv, rgbUrl, instUrl }) => {
849    const state = initInputViewer(viewerDiv, { rgbUrl, instUrl });
850    allStates.push(state);
851  });
852  inputState = allStates[0];
853
854  // Sync rotation across all 6
855  allStates.forEach((st) => {
856    if (st.controls) st.controls.autoRotate = true;
857  });
858  const rotateBtn = document.getElementById('btn-rotate');
859  if (rotateBtn) rotateBtn.innerText = 'Rotate: On';
860}
861
862// 10) ADVANCE FRAMES (two sampled viewers in sync)
863function advanceAllFrames(session) {
864  if (session !== currentSession || sampledStates.length === 0) return;
865  if (isPaused) return;
866
867  const globalTotal = slowMode ? (totalFrames * 2 - 1) : totalFrames;
868  const g = currentFrameIdx;
869  const prevG = (g - 1 + globalTotal) % globalTotal;
870
871  sampledStates.forEach((st) => {
872    const prevMap = mapGlobalToLocal(prevG, st);
873    const currMap = mapGlobalToLocal(g, st);
874
875    // Hide whatever was visible at prevG:
876    if (prevMap.isOriginal) {
877      const oldIdx = prevMap.frameIndex;
878      if (st.frameMeshes[oldIdx]) st.frameMeshes[oldIdx].visible = false;
879    } else {
880      const name = `interp_${st.currentSampleId}_${prevMap.frameIndex}_${prevMap.nextIndex}_${prevG}`;
881      const obj = st.scene.getObjectByName(name);
882      if (obj) obj.visible = false;
883    }
884
885    // Show whatever should be visible at g:
886    if (currMap.isOriginal) {
887      const idx = currMap.frameIndex;
888      if (st.frameMeshes[idx]) st.frameMeshes[idx].visible = true;
889    } else {
890      const i0 = currMap.frameIndex;
891      const i1 = currMap.nextIndex;
892      const interpName = `interp_${st.currentSampleId}_${i0}_${i1}_${g}`;
893      let mesh = st.scene.getObjectByName(interpName);
894      if (!mesh) {
895        mesh = buildInterpolatedMeshForState(st, i0, i1, g);
896      }
897      if (mesh) {
898        mesh.visible = true;
899      }
900    }
901  });
902
903  if (g === globalTotal - 1) {
904    // At the last frame: start prefetching new samples immediately
905    const curr0 = sampledStates[0].currentSampleId;
906    const curr1 = sampledStates[1].currentSampleId;
907
908    let newId0;
909    do {
910      newId0 = sampledStates[0].pickRandom();
911    } while (newId0 === curr1);
912
913    let newId1;
914    do {
915      newId1 = sampledStates[1].pickRandom();
916    } while (newId1 === curr0 || newId1 === newId0);
917
918    // Kick off prefetch (showLoading=false)
919    const prefetchPromise0 = sampledStates[0].loadSample(newId0, false);
920    const prefetchPromise1 = sampledStates[1].loadSample(newId1, false);
921
922    // Wait pauseDuration, then swap everything in one go
923    playTimeoutId = setTimeout(async () => {
924      if (session !== currentSession) return;
925      const [res0, res1] = await Promise.all([prefetchPromise0, prefetchPromise1]);
926      if (session !== currentSession) return;
927
928      // For each sampled viewer, remove old trajectories & old meshes before swapping
929      [res0, res1].forEach((res, i) => {
930        const st = sampledStates[i];
931        if (!res) return;
932
933        // Remove & dispose ALL interpolated meshes (any name starting with 'interp_<oldSampleId>_')
934        const toRemove = [];
935        st.scene.traverse((obj) => {
936          if (obj.name && obj.name.startsWith(`interp_${st.currentSampleId}_`)) {
937            toRemove.push(obj);
938          }
939        });
940        toRemove.forEach((obj) => {
941          if (obj.geometry) obj.geometry.dispose();
942          if (obj.material) obj.material.dispose();
943          st.scene.remove(obj);
944        });
945
946        // Remove & dispose old frame‐meshes
947        st.frameMeshes.forEach((oldMesh) => {
948          if (oldMesh) {
949            st.scene.remove(oldMesh);
950            oldMesh.geometry.dispose();
951            oldMesh.material.dispose();
952          }
953        });
954
955        // ← TRAJ: Remove any trajectory spheres for the old sample
956        st.trajectorySpheres.forEach((sphere) => {
957          if (sphere.geometry) sphere.geometry.dispose();
958          if (sphere.material) sphere.material.dispose();
959          st.scene.remove(sphere);
960        });
961        st.trajectorySpheres.length = 0;
962
963        // Swap in the newly-loaded frames & positions
964        st.frameMeshes = res.newMeshes;
965        st.framePositions = res.newPositions;
966        st.currentSampleId = res.sampleId;
967
968        // Show frame 0 of the new sample
969        if (st.frameMeshes[0]) {
970          st.frameMeshes[0].visible = true;
971        }
972
973        // ← TRAJ: If the toggle is ON, immediately draw new spheres for the new sample
974        if (showTrajectories) {
975          drawTrajectoriesForState(st);
976        }
977      });
978
979      // 7) Restart from frame 0 if still playing
980      if (!isPaused) {
981        currentFrameIdx = 0;
982        advanceAllFrames(session);
983      }
984    }, pauseDuration);
985  } else {
986    // Normal case: simply advance to g+1 after frameInterval
987    playTimeoutId = setTimeout(() => {
988      if (session !== currentSession) return;
989      if (!isPaused) {
990        currentFrameIdx = (currentFrameIdx + 1) % globalTotal;
991        advanceAllFrames(session);
992      }
993    }, frameInterval);
994  }
995
996  // --- helper to map “global frame index” → {isOriginal, frameIndex, nextIndex, alpha} ---
997  function mapGlobalToLocal(globalIdx, st) {
998    if (!slowMode) {
999      return { isOriginal: true, frameIndex: globalIdx, alpha: 0 };
1000    } else {
1001      if (globalIdx % 2 === 0) {
1002        return { isOriginal: true, frameIndex: globalIdx / 2, alpha: 0 };
1003      } else {
1004        const i0 = Math.floor(globalIdx / 2);
1005        const i1 = i0 + 1;
1006        return { isOriginal: false, frameIndex: i0, nextIndex: i1, alpha: 0.5 };
1007      }
1008    }
1009  }
1010
1011  // Helper: build (and name) an interpolated mesh in `st` for indices (i0, i1) (unchanged)
1012  function buildInterpolatedMeshForState(st, i0, i1, globalIdx) {
1013    const name = `interp_${st.currentSampleId}_${i0}_${i1}_${globalIdx}`;
1014    const pos0 = st.framePositions[i0];
1015    const pos1 = st.framePositions[i1];
1016    if (!pos0 || !pos1) return null;
1017
1018    const sphereGeo = new THREE.SphereGeometry(0.01, 6, 4);
1019    const mat = new THREE.MeshStandardMaterial({
1020      transparent: true,
1021      metalness: 0.2,
1022      roughness: 0.4,
1023      depthWrite: true,
1024      flatShading: false
1025    });
1026    const mesh = new THREE.InstancedMesh(sphereGeo, mat, st.N_points);
1027    mesh.castShadow = true;
1028    mesh.receiveShadow = false;
1029    mesh.name = name;
1030
1031    const dummyMatrix = new THREE.Matrix4();
1032    const color = new THREE.Color();
1033    const srcMesh = st.frameMeshes[i0];
1034
1035    for (let i = 0; i < st.N_points; i++) {
1036      const x0 = pos0[3 * i], y0 = pos0[3 * i + 1], z0 = pos0[3 * i + 2];
1037      const x1 = pos1[3 * i], y1 = pos1[3 * i + 1], z1 = pos1[3 * i + 2];
1038      const xm = 0.5 * (x0 + x1), ym = 0.5 * (y0 + y1), zm = 0.5 * (z0 + z1);
1039      dummyMatrix.makeTranslation(xm, ym, zm);
1040      mesh.setMatrixAt(i, dummyMatrix);
1041
1042      if (srcMesh.instanceColor) {
1043        const cA = srcMesh.instanceColor;
1044        const r = cA.getX(i), g = cA.getY(i), b = cA.getZ(i);
1045        color.setRGB(r, g, b);
1046      } else {
1047        color.setRGB(0.5, 0.5, 0.5);
1048      }
1049      mesh.setColorAt(i, color);
1050    }
1051
1052    mesh.visible = false;
1053    st.scene.add(mesh);
1054    return mesh;
1055  }
1056}
1057
1058// 11) RENDER LOOP (calls controls.update + render each scene) (unchanged)
1059function animateAll() {
1060  requestAnimationFrame(animateAll);
1061  allStates.forEach((st) => {
1062    st.controls.update();
1063    st.renderer.render(st.scene, st.camera);
1064  });
1065}
1066
1067// 12) DRAW TRAJECTORIES FOR ONE VIEWER STATE (sampled viewer only) ← TRAJ
1068function drawTrajectoriesForState(st) {
1069  const N = st.N_points;
1070  if (N === 0 || !st.framePositions[0]) return;
1071
1072  // 1) Pick 5% of indices at random
1073  const numToSample = Math.max(1, Math.floor(N * 0.075));
1074  const indices = new Set();
1075  while (indices.size < numToSample) {
1076    indices.add(Math.floor(Math.random() * N));
1077  }
1078  const selectedIndices = Array.from(indices);
1079
1080  // 2) We will have totalInstances = numToSample * totalFrames
1081  const totalInstances = numToSample * totalFrames;
1082  const instancedMesh = new THREE.InstancedMesh(
1083    _trajSphereGeo,
1084    _trajSphereMat,
1085    totalInstances
1086  );
1087  // Enable per‐instance color
1088  instancedMesh.instanceColor = new THREE.InstancedBufferAttribute(
1089    new Float32Array(totalInstances * 3),
1090    3
1091  );
1092  instancedMesh.count = totalInstances; // yep, exactly this many
1093  instancedMesh.castShadow = false;
1094  instancedMesh.receiveShadow = false;
1095
1096  // 3) Fill in each instance’s matrix and color
1097  const dummyMatrix = new THREE.Matrix4();
1098  const color = new THREE.Color();
1099
1100  let instanceIdx = 0;
1101  for (let pi = 0; pi < selectedIndices.length; pi++) {
1102    const ptIdx = selectedIndices[pi];
1103
1104    // Pre‐grab colorSeries[ptIdx] for all frames if you want—this avoids
1105    // re‐reading instanceColor on each frame. But we can just read from meshAtT.instanceColor as below.
1106    for (let t = 0; t < totalFrames; t++) {
1107      const posArr = st.framePositions[t]; // Float32Array length=N*3
1108      const x = posArr[3 * ptIdx];
1109      const y = posArr[3 * ptIdx + 1];
1110      const z = posArr[3 * ptIdx + 2];
1111
1112      // Build translation for this instance
1113      dummyMatrix.makeTranslation(x, y, z);
1114      instancedMesh.setMatrixAt(instanceIdx, dummyMatrix);
1115
1116      // Determine the color from the original instanced‐mesh at frame t
1117      const meshAtT = st.frameMeshes[t];
1118      if (meshAtT.instanceColor) {
1119        const cA = meshAtT.instanceColor;
1120        const r = cA.getX(ptIdx);
1121        const g = cA.getY(ptIdx);
1122        const b = cA.getZ(ptIdx);
1123        color.setRGB(r, g, b);
1124      } else {
1125        color.setRGB(0.5, 0.5, 0.5);
1126      }
1127      instancedMesh.setColorAt(instanceIdx, color);
1128
1129      instanceIdx++;
1130    }
1131  }
1132
1133  // Make sure the InstancedMesh knows its colors were updated
1134  instancedMesh.instanceColor.needsUpdate = true;
1135  instancedMesh.instanceMatrix.needsUpdate = true;
1136
1137  // 4) Add the single InstancedMesh to the scene
1138  st.scene.add(instancedMesh);
1139
1140  // 5) Push it into st.trajectorySpheres so the teardown logic still works
1141  // (Later, removeTrajectoriesForState can simply loop over trajectorySpheres,
1142  //  dispose geometry/material, and scene.remove(obj).)
1143  st.trajectorySpheres.push(instancedMesh);
1144}
1145
1146// 13) REMOVE ALL TRAJECTORIES FOR ONE VIEWER STATE ← TRAJ
1147function removeTrajectoriesForState(st) {
1148  st.trajectorySpheres.forEach((sphere) => {
1149    if (sphere.geometry) sphere.geometry.dispose();
1150    if (sphere.material) sphere.material.dispose();
1151    st.scene.remove(sphere);
1152  });
1153  st.trajectorySpheres.length = 0;
1154}
1155
1156// 14) Only build the 3x2 comparison grid (GT | Baseline | Ours x t0, t1). No other layout.
1157function selectObject(objName) {
1158  buildComparisonGrid(objName);
1159}
1160
1161// 15) HOOK UP TAB BUTTONS + BASELINE + ROTATE ONCE DOM IS READY
1162window.addEventListener('DOMContentLoaded', () => {
1163  // Tab buttons:
1164  document.querySelectorAll('.tab-button').forEach((btn) => {
1165    btn.addEventListener('click', () => {
1166      selectObject(btn.dataset.obj);
1167    });
1168  });
1169
1170  // “Rotate” button toggles autoRotate:
1171  const rotateBtn = document.getElementById('btn-rotate');
1172  if (rotateBtn) rotateBtn.addEventListener('click', () => {
1173    const anyAuto = allStates.some((st) => st.controls?.autoRotate);
1174    if (anyAuto) {
1175      allStates.forEach((st) => {
1176        if (st.controls) st.controls.autoRotate = false;
1177      });
1178    } else {
1179      allStates.forEach((st) => {
1180        if (st.controls) st.controls.autoRotate = true;
1181      });
1182    }
1183    // Enable the button again (in case it was disabled during drag)
1184    rotateBtn.disabled = false;
1185
1186    const anyAuto2 = allStates.some((st) => st.controls?.autoRotate);
1187    rotateBtn.innerText = anyAuto2
1188      ? 'Rotate: On'
1189      : 'Rotate: Off';
1190  });
1191
1192  // “Show Trajectories” button (toggle):
1193  const trajBtn = document.getElementById('btn-traj');
1194  if (trajBtn) {
1195  trajBtn.innerText = 'Trajectories: Off';     // initially off
1196  trajBtn.addEventListener('click', () => {
1197    showTrajectories = !showTrajectories;
1198    if (showTrajectories) {
1199      trajBtn.innerText = 'Trajectories: On';
1200      // Draw trajectories for each sampled viewer if frames are already loaded:
1201      sampledStates.forEach((st) => {
1202        const allLoaded = st.framePositions.every((arr) => arr !== null);
1203        if (allLoaded) {
1204          drawTrajectoriesForState(st);
1205        }
1206      });
1207    } else {
1208      trajBtn.innerText = 'Trajectories: Off';
1209      // Remove any existing trajectories
1210      sampledStates.forEach((st) => {
1211        removeTrajectoriesForState(st);
1212      });
1213    }
1214  });
1215  }
1216
1217  // ← UPDATED: “Play/Pause” button wiring (now also stops/starts autoRotate)
1218  const pauseBtn = document.getElementById('btn-pause');
1219  if (pauseBtn) {
1220    pauseBtn.innerText = 'Pause';    // default: playing
1221    pauseBtn.addEventListener('click', () => {
1222      isPaused = !isPaused;
1223
1224      if (isPaused) {
1225        // 1) Switch button label
1226        pauseBtn.innerText = 'Play';
1227        // 2) Stop the frame‐advance loop
1228        clearTimeout(playTimeoutId);
1229        // 3) Force all viewers to stop rotating
1230        allStates.forEach((st) => {
1231          if (st.controls) st.controls.autoRotate = false;
1232        });
1233      } else {
1234        // 1) Switch button label
1235        pauseBtn.innerText = 'Pause';
1236        // 2) Restore autoRotate only if "Rotate" is currently set to On:
1237        const rotateIsOn = rotateBtn.innerText.includes('On');
1238        allStates.forEach((st) => {
1239          if (st.controls) st.controls.autoRotate = rotateIsOn;
1240        });
1241        // 3) Re‐start frame‐advance from the current frame
1242        advanceAllFrames(currentSession);
1243      }
1244    });
1245  }
1246
1247  // Show Scene 2 by default
1248  selectObject('scene_2');
1249
1250  // Start the render loop
1251  animateAll();
1252});

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.