1import * as THREE from 'three'; 2import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'; 3import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; 4 5// Global state 6let allViewers = []; 7let isSyncing = false; 8let isRotating = true; 9let isPaused = false; 10 11// Scale mapping for each example 12const scaleMap = { 13 'example_1': 1.2, 14 'example_2': 1.0, 15 'example_3': 0.8, 16}; 17 18// Sync camera angles across all viewers 19function syncCameras(sourceControls) { 20 if (isSyncing) return; 21 isSyncing = true; 22 23 const cam = sourceControls.object; 24 const target = sourceControls.target.clone(); 25 const offset = new THREE.Vector3().subVectors(cam.position, target); 26 const spherical = new THREE.Spherical().setFromVector3(offset); 27 28 allViewers.forEach(viewer => { 29 if (viewer.controls === sourceControls) return; 30 const newPos = new THREE.Vector3() 31 .setFromSphericalCoords(spherical.radius, spherical.phi, spherical.theta) 32 .add(viewer.controls.target); 33 viewer.camera.position.copy(newPos); 34 viewer.camera.lookAt(viewer.controls.target); 35 viewer.controls.update(); 36 }); 37 38 isSyncing = false; 39} 40 41// Initialize a single viewer 42function initViewer(container, glbPath, showGround = false, scale = 1.0) { 43 const width = container.clientWidth; 44 const height = container.clientHeight; 45 46 // Scene 47 const scene = new THREE.Scene(); 48 scene.background = new THREE.Color(0xFFFFFF); 49 50 // Camera 51 const camera = new THREE.PerspectiveCamera(25, width / height, 0.1, 1000); 52 camera.position.set(0, 1.5, 3); 53 54 // Renderer 55 const renderer = new THREE.WebGLRenderer({ antialias: true }); 56 renderer.setSize(width, height); 57 renderer.shadowMap.enabled = true; 58 renderer.shadowMap.type = THREE.PCFSoftShadowMap; 59 renderer.outputColorSpace = THREE.SRGBColorSpace; 60 renderer.toneMapping = THREE.ACESFilmicToneMapping; 61 renderer.toneMappingExposure = 1.2; 62 container.appendChild(renderer.domElement); 63 64 // Enhanced lighting setup 65 const ambient = new THREE.AmbientLight(0xffffff, 0.4); 66 scene.add(ambient); 67 68 const dirLight = new THREE.DirectionalLight(0xffffff, 2.5); 69 dirLight.position.set(5, 12, 5); 70 dirLight.castShadow = true; 71 dirLight.shadow.mapSize.width = 2048; 72 dirLight.shadow.mapSize.height = 2048; 73 scene.add(dirLight); 74 75 // Add fill lights for better detail visibility 76 const fillLight1 = new THREE.DirectionalLight(0xffffff, 0.8); 77 fillLight1.position.set(-5, 5, -5); 78 scene.add(fillLight1); 79 80 const fillLight2 = new THREE.DirectionalLight(0xffffff, 0.6); 81 fillLight2.position.set(0, -5, 5); 82 scene.add(fillLight2); 83 84 // Ground plane 85 if (showGround) { 86 const planeGeo = new THREE.PlaneGeometry(20, 20); 87 const planeMat = new THREE.ShadowMaterial({ opacity: 0.2 }); 88 const ground = new THREE.Mesh(planeGeo, planeMat); 89 ground.rotateX(-Math.PI / 2); 90 ground.position.y = -0.8; 91 ground.receiveShadow = true; 92 scene.add(ground); 93 } 94 95 // Controls 96 const controls = new OrbitControls(camera, renderer.domElement); 97 controls.enablePan = false; 98 controls.enableZoom = false;
99 controls.autoRotate = true; 100 controls.autoRotateSpeed = 2; 101 102 controls.addEventListener('change', () => { 103 if (controls.userIsInteracting) syncCameras(controls); 104 }); 105 controls.domElement.addEventListener('mousedown', () => { 106 controls.userIsInteracting = true; 107 }); 108 document.addEventListener('mouseup', () => { 109 controls.userIsInteracting = false; 110 }); 111 112 // Loading indicator 113 const loading = document.createElement('div'); 114 loading.className = 'loading'; 115 loading.innerText = 'Loading...'; 116 container.appendChild(loading); 117 118 // Load GLB 119 const loader = new GLTFLoader(); 120 loader.load( 121 glbPath, 122 (gltf) => { 123 const model = gltf.scene; 124 model.scale.setScalar(scale); 125 model.traverse((child) => { 126 if (child.isMesh) { 127 child.castShadow = true; 128 child.receiveShadow = true; 129 130 // Apply plastic-like material properties 131 if (child.material) { 132 child.material.roughness = 0.3; 133 child.material.metalness = 0.1; 134 child.material.envMapIntensity = 1.5; 135 child.material.needsUpdate = true; 136 } 137 } 138 }); 139 scene.add(model); 140 loading.style.display = 'none'; 141 }, 142 undefined, 143 (err) => { 144 console.error('Error loading GLB:', err); 145 loading.innerText = 'Error loading model'; 146 } 147 ); 148 149 window.addEventListener('resize', () => { 150 const w = container.clientWidth; 151 const h = container.clientHeight; 152 renderer.setSize(w, h); 153 camera.aspect = w / h; 154 camera.updateProjectionMatrix(); 155 }); 156 157 return { scene, camera, renderer, controls, container }; 158} 159 160function clearViewers() { 161 allViewers.forEach(viewer => { 162 viewer.renderer.dispose(); 163 viewer.container.innerHTML = ''; 164 }); 165 allViewers = []; 166} 167 168function animate() { 169 requestAnimationFrame(animate); 170 allViewers.forEach(viewer => { 171 viewer.controls.update(); 172 viewer.renderer.render(viewer.scene, viewer.camera); 173 }); 174} 175 176function loadTab(objName) { 177 clearViewers(); 178 179 const tabButton = document.querySelector(`[data-obj="${objName}"]`); 180 const conditionPath = tabButton.dataset.condition; 181 const gen1Path = tabButton.dataset.gen1; 182 const gen2Path = tabButton.dataset.gen2; 183 184 document.querySelectorAll('.tab-button').forEach(btn => { 185 btn.classList.toggle('active', btn.dataset.obj === objName); 186 }); 187 188 // Sample labels dictionary 189 const sampleLabels = { 190 'example_1': ['« Modern chair with slatted back »', '« Baby pacifier with handle »'], 191 'example_2': ['« Industrial screwdriver with cylindrical body and tapered tip »', '« Ceremonial candle with pedestal base »'], 192 'example_3': ['« Wooden chess pawn, polished »', '« Wooden bar stool with cushioned seat »'], 193 }; 194 195 const labels = sampleLabels[objName] || ['Sample 1', 'Sample 2']; 196 const scale = scaleMap[objName] || 1.0; 197 198 const container = document.querySelector('.viewers-container'); 199 container.innerHTML = ` 200 <div style="flex: 1; max-width: 350px;"> 201 <h4 class="title is-6" style="margin-bottom: 0.5rem;">Condition</h4> 202 <p style="color: #666; font-size: 0.9rem; margin-bottom: 0.8rem;">Coarse Bounding Boxes</p> 203 <div id="condition-viewer" style="background: #ffffff; width: 100%; aspect-ratio: 1; border-radius: 4px;"></div> 204 </div> 205 <hr class="flex: 1; is-divider-vertical" style="background: white;height: 100%;"> 206 <div style="flex: 2; display: flex; flex-direction: column; gap: 0rem;"> 207 <div style="margin-bottom: 0.8rem;"> 208 <h4 class="title is-6" style="margin-bottom: 0.5rem;">Generation</h4> 209 <p style="color: #666; font-size: 0.9rem; margin-bottom: 0.8rem;">Part-Separated Shape</p> 210 </div> 211 <div style="display: flex; gap: 1.5rem;"> 212 <div style="flex: 1;"> 213 <div id="gen1-viewer" style="background: #ffffff; width: 100%; aspect-ratio: 1; border-radius: 4px;"></div> 214 <p style="color: #888; font-size: 0.85rem; margin-top: 1rem; text-align: center;">${labels[0]}</p> 215 </div> 216 <div style="flex: 1;"> 217 <div id="gen2-viewer" style="background: #ffffff; width: 100%; aspect-ratio: 1; border-radius: 4px;"></div> 218 <p style="color: #888; font-size: 0.85rem; margin-top: 1rem; text-align: center;">${labels[1]}</p> 219 </div> 220 </div> 221 </div> 222 `; 223 224 const conditionViewer = initViewer(document.getElementById('condition-viewer'), conditionPath, false, scale); 225 const gen1Viewer = initViewer(document.getElementById('gen1-viewer'), gen1Path, true, scale); 226 const gen2Viewer = initViewer(document.getElementById('gen2-viewer'), gen2Path, true, scale); 227 228 allViewers = [conditionViewer, gen1Viewer, gen2Viewer];
229 allViewers.forEach(v => v.controls.autoRotate = isRotating); 230} 231 232window.addEventListener('DOMContentLoaded', () => { 233 document.querySelectorAll('.tab-button').forEach(btn => { 234 btn.addEventListener('click', () => loadTab(btn.dataset.obj)); 235 }); 236 237 document.getElementById('btn-rotate').addEventListener('click', () => { 238 isRotating = !isRotating; 239 allViewers.forEach(v => v.controls.autoRotate = isRotating); 240 document.getElementById('btn-rotate').innerText = isRotating ? 'Rotate: On' : 'Rotate: Off'; 241 }); 242 243 document.getElementById('btn-pause').addEventListener('click', () => { 244 isPaused = !isPaused; 245 if (isPaused) { 246 allViewers.forEach(v => v.controls.autoRotate = false); 247 document.getElementById('btn-pause').innerText = 'Play'; 248 } else { 249 allViewers.forEach(v => v.controls.autoRotate = isRotating); 250 document.getElementById('btn-pause').innerText = 'Pause'; 251 } 252 }); 253 254 loadTab('example_1'); 255 animate(); 256});
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.