PageSourceSearch

https://anon-compose.github.io/js/mesh-viewer.js

js anon-compose.github.io collected 2026-10-03 08:37:54 UTC 8,742 bytes, 256 lines download raw bytes

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.