MySafeCode commited on
Commit
40de165
·
verified ·
1 Parent(s): 621fe2f

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +636 -18
index.html CHANGED
@@ -1,19 +1,637 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </html>
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Fixed VOX Color Viewer</title>
7
+ <style>
8
+ body {
9
+ margin: 0;
10
+ padding: 0;
11
+ overflow: hidden;
12
+ font-family: Arial, sans-serif;
13
+ background: #1a1a1a;
14
+ }
15
+ #container {
16
+ width: 100vw;
17
+ height: 100vh;
18
+ }
19
+ #controls {
20
+ position: absolute;
21
+ top: 10px;
22
+ left: 10px;
23
+ color: white;
24
+ background: rgba(0,0,0,0.8);
25
+ padding: 15px;
26
+ border-radius: 8px;
27
+ max-width: 380px;
28
+ }
29
+ #controls h3 {
30
+ margin-top: 0;
31
+ color: #4CAF50;
32
+ }
33
+ .control-group {
34
+ margin: 12px 0;
35
+ }
36
+ label {
37
+ display: inline-block;
38
+ width: 140px;
39
+ font-size: 12px;
40
+ }
41
+ input[type="file"] {
42
+ display: none;
43
+ }
44
+ .btn {
45
+ background: #4CAF50;
46
+ color: white;
47
+ border: none;
48
+ padding: 8px 12px;
49
+ border-radius: 4px;
50
+ cursor: pointer;
51
+ font-size: 11px;
52
+ margin: 2px;
53
+ }
54
+ .btn:hover {
55
+ background: #45a049;
56
+ }
57
+ .btn.active {
58
+ background: #2196F3;
59
+ }
60
+ .btn:disabled {
61
+ background: #666;
62
+ cursor: not-allowed;
63
+ }
64
+ #info {
65
+ position: absolute;
66
+ bottom: 10px;
67
+ left: 10px;
68
+ color: white;
69
+ background: rgba(0,0,0,0.7);
70
+ padding: 10px;
71
+ border-radius: 5px;
72
+ font-size: 12px;
73
+ }
74
+ #loading {
75
+ position: absolute;
76
+ top: 50%;
77
+ left: 50%;
78
+ transform: translate(-50%, -50%);
79
+ color: white;
80
+ font-size: 18px;
81
+ display: none;
82
+ }
83
+ .model-info {
84
+ font-size: 11px;
85
+ color: #ccc;
86
+ margin-top: 8px;
87
+ min-height: 30px;
88
+ }
89
+ .render-mode {
90
+ display: flex;
91
+ gap: 5px;
92
+ margin: 8px 0;
93
+ }
94
+ .render-mode button {
95
+ flex: 1;
96
+ }
97
+ .file-types {
98
+ font-size: 10px;
99
+ color: #aaa;
100
+ margin-top: 4px;
101
+ }
102
+ .status-indicator {
103
+ display: inline-block;
104
+ width: 8px;
105
+ height: 8px;
106
+ border-radius: 50%;
107
+ margin-right: 6px;
108
+ }
109
+ .status-indicator.glb { background: #FF9800; }
110
+ .status-indicator.vox { background: #4CAF50; }
111
+ .status-indicator.none { background: #666; }
112
+ .rotation-controls {
113
+ background: rgba(255,255,255,0.1);
114
+ padding: 8px;
115
+ border-radius: 4px;
116
+ margin: 8px 0;
117
+ }
118
+ </style>
119
+ </head>
120
+ <body>
121
+ <div id="container"></div>
122
+
123
+ <div id="controls">
124
+ <h3>VOX & GLB Viewer</h3>
125
+
126
+ <div class="control-group">
127
+ <button class="btn" onclick="document.getElementById('glbFileInput').click()">Load GLB/GLTF</button>
128
+ <input type="file" id="glbFileInput" accept=".glb,.gltf">
129
+ <div class="file-types">GLB, GLTF files</div>
130
+ </div>
131
+
132
+ <div class="control-group">
133
+ <button class="btn" onclick="document.getElementById('voxFileInput').click()">Load VOX File</button>
134
+ <input type="file" id="voxFileInput" accept=".vox">
135
+ <div class="file-types">MagicaVoxel files</div>
136
+ </div>
137
+
138
+ <div class="control-group" id="voxControls" style="display: none;">
139
+ <label>VOX Render Mode:</label>
140
+ <div class="render-mode">
141
+ <button class="btn active" id="boxMode" onclick="setRenderMode('box')">Boxes</button>
142
+ <button class="btn" id="ballMode" onclick="setRenderMode('ball')">Balls</button>
143
+ </div>
144
+ </div>
145
+
146
+ <div class="control-group" id="voxelSizeControl" style="display: none;">
147
+ <label>Voxel Size:</label>
148
+ <input type="range" id="voxelSize" min="0.3" max="1.5" step="0.1" value="0.8">
149
+ <span id="voxelSizeValue">0.8</span>
150
+ </div>
151
+
152
+ <div class="control-group" id="ballSegmentsControl" style="display: none;">
153
+ <label>Ball Segments:</label>
154
+ <input type="range" id="ballSegments" min="4" max="16" step="1" value="8">
155
+ <span id="ballSegmentsValue">8</span>
156
+ </div>
157
+
158
+ <div class="rotation-controls">
159
+ <div class="control-group">
160
+ <label>Rotation Center:</label>
161
+ <button class="btn" onclick="centerRotationOnModel()">Center on Model</button>
162
+ </div>
163
+ <div class="control-group">
164
+ <label>Default Position:</label>
165
+ <button class="btn" onclick="resetToDefaultPosition()">Reset Position</button>
166
+ </div>
167
+ </div>
168
+
169
+ <div class="control-group">
170
+ <label>Rotation Speed:</label>
171
+ <input type="range" id="rotationSpeed" min="0" max="0.02" step="0.001" value="0.005">
172
+ </div>
173
+
174
+ <div class="control-group">
175
+ <label>Auto Rotate:</label>
176
+ <input type="checkbox" id="autoRotate" checked>
177
+ </div>
178
+
179
+ <div class="control-group">
180
+ <button class="btn" onclick="resetView()">Reset View</button>
181
+ <button class="btn" onclick="loadSampleModel('chest')">Load Sample Chest</button>
182
+ </div>
183
+
184
+ <div class="model-info">
185
+ <span class="status-indicator" id="statusIndicator"></span>
186
+ <span id="modelInfo">No model loaded</span>
187
+ </div>
188
+ </div>
189
+
190
+ <div id="loading">Loading...</div>
191
+ <div id="info">Click and drag to rotate • Scroll to zoom • Switch between balls and boxes for VOX!</div>
192
+
193
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
194
+ <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/loaders/GLTFLoader.js"></script>
195
+ <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script>
196
+
197
+ <script>
198
+ // Fixed VOX parser with correct color handling
199
+ class VOXParser {
200
+ static parse(buffer) {
201
+ const view = new DataView(buffer);
202
+ let offset = 0;
203
+
204
+ const header = new TextDecoder().decode(buffer.slice(0, 4));
205
+ if (header !== 'VOX ') {
206
+ throw new Error('Invalid VOX file');
207
+ }
208
+
209
+ offset = 8; // Skip version
210
+
211
+ const chunks = {};
212
+ while (offset < buffer.byteLength) {
213
+ const chunkId = new TextDecoder().decode(buffer.slice(offset, offset + 4));
214
+ const chunkSize = view.getUint32(offset + 4, true);
215
+ const childSize = view.getUint32(offset + 8, true);
216
+
217
+ offset += 12;
218
+
219
+ if (chunkId === 'SIZE') {
220
+ const sizeX = view.getUint32(offset, true);
221
+ const sizeY = view.getUint32(offset + 4, true);
222
+ const sizeZ = view.getUint32(offset + 8, true);
223
+ chunks.size = { x: sizeX, y: sizeY, z: sizeZ };
224
+ } else if (chunkId === 'XYZI') {
225
+ const numVoxels = view.getUint32(offset, true);
226
+ const voxels = [];
227
+ for (let i = 0; i < numVoxels; i++) {
228
+ const x = view.getUint8(offset + 4 + i * 4);
229
+ const y = view.getUint8(offset + 4 + i * 4 + 1);
230
+ const z = view.getUint8(offset + 4 + i * 4 + 2);
231
+ const colorIndex = view.getUint8(offset + 4 + i * 4 + 3);
232
+ voxels.push({ x, y, z, colorIndex });
233
+ }
234
+ chunks.voxels = voxels;
235
+ } else if (chunkId === 'RGBA') {
236
+ const colors = [];
237
+ for (let i = 0; i < 256; i++) {
238
+ const r = view.getUint8(offset + i * 4);
239
+ const g = view.getUint8(offset + i * 4 + 1);
240
+ const b = view.getUint8(offset + i * 4 + 2);
241
+ const a = view.getUint8(offset + i * 4 + 3);
242
+ colors.push({ r, g, b, a });
243
+ }
244
+ chunks.palette = colors;
245
+ }
246
+
247
+ offset += chunkSize;
248
+ }
249
+
250
+ return chunks;
251
+ }
252
+
253
+ static createInstancedGeometry(voxData, renderMode = 'box', voxelSize = 0.8, ballSegments = 8) {
254
+ if (!voxData.voxels || !voxData.size) return null;
255
+
256
+ const voxels = voxData.voxels;
257
+ const palette = voxData.palette || this.generateDefaultPalette();
258
+
259
+ // Create base geometry based on render mode
260
+ let baseGeometry;
261
+ if (renderMode === 'ball') {
262
+ baseGeometry = new THREE.SphereGeometry(voxelSize * 0.5, ballSegments, ballSegments);
263
+ } else {
264
+ baseGeometry = new THREE.BoxGeometry(voxelSize, voxelSize, voxelSize);
265
+ }
266
+
267
+ // Create instanced mesh for performance - use a material that supports vertex colors
268
+ const instancedMesh = new THREE.InstancedMesh(
269
+ baseGeometry,
270
+ new THREE.MeshPhongMaterial({
271
+ vertexColors: true,
272
+ shininess: 30
273
+ }),
274
+ voxels.length
275
+ );
276
+
277
+ // Set up instance data
278
+ const dummy = new THREE.Object3D();
279
+ const color = new THREE.Color();
280
+
281
+ // Calculate offsets to center the model
282
+ const offsetX = voxData.size.x / 2;
283
+ const offsetY = voxData.size.y / 2;
284
+ const offsetZ = voxData.size.z / 2;
285
+
286
+ for (let i = 0; i < voxels.length; i++) {
287
+ const voxel = voxels[i];
288
+
289
+ // Position
290
+ dummy.position.set(
291
+ voxel.x - offsetX,
292
+ voxel.y - offsetY,
293
+ voxel.z - offsetZ
294
+ );
295
+ dummy.updateMatrix();
296
+ instancedMesh.setMatrixAt(i, dummy.matrix);
297
+
298
+ // Color - fix the color index mapping
299
+ const colorIndex = voxel.colorIndex - 1; // VOX color indices start at 1
300
+ if (colorIndex >= 0 && colorIndex < palette.length) {
301
+ const colorData = palette[colorIndex];
302
+ color.setRGB(colorData.r / 255, colorData.g / 255, colorData.b / 255);
303
+ instancedMesh.setColorAt(i, color);
304
+ }
305
+ }
306
+
307
+ instancedMesh.castShadow = true;
308
+ instancedMesh.receiveShadow = true;
309
+
310
+ return instancedMesh;
311
+ }
312
+
313
+ static generateDefaultPalette() {
314
+ const palette = [];
315
+ for (let i = 0; i < 256; i++) {
316
+ palette.push({
317
+ r: Math.random() * 255,
318
+ g: Math.random() * 255,
319
+ b: Math.random() * 255,
320
+ a: 255
321
+ });
322
+ }
323
+ return palette;
324
+ }
325
+ }
326
+ </script>
327
+
328
+ <script>
329
+ // Scene setup
330
+ const scene = new THREE.Scene();
331
+ scene.background = new THREE.Color(0x2a2a2a);
332
+
333
+ const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
334
+ camera.position.set(8, 8, 12);
335
+
336
+ const renderer = new THREE.WebGLRenderer({ antialias: true });
337
+ renderer.setSize(window.innerWidth, window.innerHeight);
338
+ renderer.shadowMap.enabled = true;
339
+ renderer.shadowMap.type = THREE.PCFSoftShadowMap;
340
+ document.getElementById('container').appendChild(renderer.domElement);
341
+
342
+ // Controls
343
+ const controls = new THREE.OrbitControls(camera, renderer.domElement);
344
+ controls.enableDamping = true;
345
+ controls.dampingFactor = 0.05;
346
+
347
+ // Store original camera position for reset
348
+ const defaultCameraPosition = new THREE.Vector3(8, 8, 12);
349
+ const defaultCameraLookAt = new THREE.Vector3(0, 0, 0);
350
+
351
+ // Lighting setup - make it brighter for better color visibility
352
+ const ambientLight = new THREE.AmbientLight(0xffffff, 0.7);
353
+ scene.add(ambientLight);
354
+
355
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 0.9);
356
+ directionalLight.position.set(10, 10, 5);
357
+ directionalLight.castShadow = true;
358
+ directionalLight.shadow.mapSize.width = 2048;
359
+ directionalLight.shadow.mapSize.height = 2048;
360
+ scene.add(directionalLight);
361
+
362
+ // Current state
363
+ let currentModel = null;
364
+ let currentVOXData = null;
365
+ let currentFileType = 'none'; // 'none', 'glb', 'vox'
366
+ let autoRotateEnabled = true;
367
+ let currentRenderMode = 'box';
368
+ let modelCenter = new THREE.Vector3(0, 0, 0); // Store model center for rotation
369
+
370
+ // File input handlers
371
+ document.getElementById('glbFileInput').addEventListener('change', handleGLBLoad);
372
+ document.getElementById('voxFileInput').addEventListener('change', handleVOXLoad);
373
+
374
+ function handleGLBLoad(event) {
375
+ const file = event.target.files[0];
376
+ if (!file) return;
377
+
378
+ showLoading(true);
379
+ const reader = new FileReader();
380
+
381
+ reader.onload = function(e) {
382
+ const loader = new THREE.GLTFLoader();
383
+ loader.parse(e.target.result, '', function(gltf) {
384
+ const model = gltf.scene;
385
+
386
+ // Calculate model center
387
+ const box = new THREE.Box3().setFromObject(model);
388
+ modelCenter = box.getCenter(new THREE.Vector3());
389
+
390
+ loadModel(model, 'glb');
391
+ showVOXControls(false);
392
+ showLoading(false);
393
+ updateModelInfo(`GLB/GLTF: ${file.name}`, 'glb');
394
+ }, function(error) {
395
+ console.error('Error loading GLTF:', error);
396
+ showLoading(false);
397
+ });
398
+ };
399
+
400
+ reader.readAsArrayBuffer(file);
401
+ }
402
+
403
+ function handleVOXLoad(event) {
404
+ const file = event.target.files[0];
405
+ if (!file) return;
406
+
407
+ showLoading(true);
408
+ const reader = new FileReader();
409
+
410
+ reader.onload = function(e) {
411
+ try {
412
+ const voxData = VOXParser.parse(e.target.result);
413
+ currentVOXData = voxData;
414
+
415
+ // Calculate model center
416
+ modelCenter.set(
417
+ voxData.size.x / 2 - voxData.size.x / 2,
418
+ voxData.size.y / 2 - voxData.size.y / 2,
419
+ voxData.size.z / 2 - voxData.size.z / 2
420
+ );
421
+
422
+ const instancedMesh = VOXParser.createInstancedGeometry(
423
+ voxData,
424
+ currentRenderMode,
425
+ parseFloat(document.getElementById('voxelSize').value),
426
+ parseInt(document.getElementById('ballSegments').value)
427
+ );
428
+
429
+ if (instancedMesh) {
430
+ loadModel(instancedMesh, 'vox');
431
+ showVOXControls(true);
432
+ updateModelInfo(`VOX: ${file.name} (${voxData.voxels.length} voxels)`, 'vox');
433
+ }
434
+ } catch (error) {
435
+ console.error('Error loading VOX:', error);
436
+ }
437
+ showLoading(false);
438
+ };
439
+
440
+ reader.readAsArrayBuffer(file);
441
+ }
442
+
443
+ function loadModel(model, fileType) {
444
+ if (currentModel) {
445
+ scene.remove(currentModel);
446
+ if (currentModel.dispose) currentModel.dispose();
447
+ }
448
+
449
+ currentModel = model;
450
+ currentFileType = fileType;
451
+ scene.add(model);
452
+
453
+ // Center and scale model
454
+ const box = new THREE.Box3().setFromObject(model);
455
+ const center = box.getCenter(new THREE.Vector3());
456
+ const size = box.getSize(new THREE.Vector3());
457
+
458
+ // Update model center for rotation
459
+ modelCenter.copy(center);
460
+
461
+ model.position.sub(center);
462
+
463
+ const maxDim = Math.max(size.x, size.y, size.z);
464
+ const scale = 6 / maxDim;
465
+ model.scale.multiplyScalar(scale);
466
+
467
+ // Enable shadows
468
+ model.traverse(function(child) {
469
+ if (child.isMesh) {
470
+ child.castShadow = true;
471
+ child.receiveShadow = true;
472
+ }
473
+ });
474
+ }
475
+
476
+ function showVOXControls(show) {
477
+ const voxControls = document.getElementById('voxControls');
478
+ const voxelSizeControl = document.getElementById('voxelSizeControl');
479
+ const ballSegmentsControl = document.getElementById('ballSegmentsControl');
480
+
481
+ voxControls.style.display = show ? 'block' : 'none';
482
+ voxelSizeControl.style.display = show ? 'block' : 'none';
483
+ ballSegmentsControl.style.display = show && currentRenderMode === 'ball' ? 'block' : 'none';
484
+ }
485
+
486
+ function setRenderMode(mode) {
487
+ currentRenderMode = mode;
488
+
489
+ // Update button states
490
+ document.getElementById('boxMode').classList.toggle('active', mode === 'box');
491
+ document.getElementById('ballMode').classList.toggle('active', mode === 'ball');
492
+
493
+ // Show/hide ball segments control
494
+ const ballSegmentsControl = document.getElementById('ballSegmentsControl');
495
+ ballSegmentsControl.style.display = mode === 'ball' ? 'block' : 'none';
496
+
497
+ // Rebuild geometry if we have VOX data
498
+ if (currentVOXData && currentFileType === 'vox') {
499
+ const instancedMesh = VOXParser.createInstancedGeometry(
500
+ currentVOXData,
501
+ mode,
502
+ parseFloat(document.getElementById('voxelSize').value),
503
+ parseInt(document.getElementById('ballSegments').value)
504
+ );
505
+
506
+ if (instancedMesh) {
507
+ loadModel(instancedMesh, 'vox');
508
+ }
509
+ }
510
+ }
511
+
512
+ function updateVoxelSize() {
513
+ const value = parseFloat(document.getElementById('voxelSize').value);
514
+ document.getElementById('voxelSizeValue').textContent = value.toFixed(1);
515
+
516
+ if (currentVOXData && currentFileType === 'vox') {
517
+ setRenderMode(currentRenderMode);
518
+ }
519
+ }
520
+
521
+ function updateBallSegments() {
522
+ const value = parseInt(document.getElementById('ballSegments').value);
523
+ document.getElementById('ballSegmentsValue').textContent = value;
524
+
525
+ if (currentVOXData && currentFileType === 'vox' && currentRenderMode === 'ball') {
526
+ setRenderMode('ball');
527
+ }
528
+ }
529
+
530
+ function centerRotationOnModel() {
531
+ if (!currentModel) return;
532
+
533
+ // Move controls target to model center
534
+ controls.target.copy(modelCenter);
535
+ controls.update();
536
+
537
+ // Optional: Move camera to look at the center
538
+ camera.lookAt(modelCenter);
539
+ }
540
+
541
+ function resetToDefaultPosition() {
542
+ // Reset camera to default position
543
+ camera.position.copy(defaultCameraPosition);
544
+ camera.lookAt(defaultCameraLookAt);
545
+
546
+ // Reset controls target to origin
547
+ controls.target.copy(defaultCameraLookAt);
548
+ controls.update();
549
+ }
550
+
551
+ function loadSampleModel(type) {
552
+ if (type === 'chest') {
553
+ const chestGroup = new THREE.Group();
554
+
555
+ const woodMaterial = new THREE.MeshPhongMaterial({ color: 0x8B4513 });
556
+ const metalMaterial = new THREE.MeshPhongMaterial({ color: 0x444444 });
557
+
558
+ const baseGeometry = new THREE.BoxGeometry(2, 1, 1.2);
559
+ const base = new THREE.Mesh(baseGeometry, woodMaterial);
560
+ base.position.y = -0.3;
561
+ base.castShadow = true;
562
+ chestGroup.add(base);
563
+
564
+ const lidGeometry = new THREE.BoxGeometry(2, 0.8, 1.2);
565
+ const lid = new THREE.Mesh(lidGeometry, woodMaterial);
566
+ lid.position.y = 0.2;
567
+ lid.castShadow = true;
568
+ chestGroup.add(lid);
569
+
570
+ const bandGeometry = new THREE.BoxGeometry(2.05, 0.08, 1.25);
571
+ const band = new THREE.Mesh(bandGeometry, metalMaterial);
572
+ band.position.y = -0.1;
573
+ chestGroup.add(band);
574
+
575
+ // Calculate center
576
+ const box = new THREE.Box3().setFromObject(chestGroup);
577
+ modelCenter = box.getCenter(new THREE.Vector3());
578
+
579
+ loadModel(chestGroup, 'glb');
580
+ showVOXControls(false);
581
+ updateModelInfo('Sample Chest Model (GLB)', 'glb');
582
+ }
583
+ }
584
+
585
+ function showLoading(show) {
586
+ document.getElementById('loading').style.display = show ? 'block' : 'none';
587
+ }
588
+
589
+ function updateModelInfo(info, fileType) {
590
+ const statusIndicator = document.getElementById('statusIndicator');
591
+ const modelInfo = document.getElementById('modelInfo');
592
+
593
+ statusIndicator.className = `status-indicator ${fileType || 'none'}`;
594
+ modelInfo.textContent = info;
595
+ }
596
+
597
+ function resetView() {
598
+ resetToDefaultPosition();
599
+ }
600
+
601
+ // Control event listeners
602
+ document.getElementById('autoRotate').addEventListener('change', function(e) {
603
+ autoRotateEnabled = e.target.checked;
604
+ });
605
+
606
+ document.getElementById('voxelSize').addEventListener('input', updateVoxelSize);
607
+ document.getElementById('ballSegments').addEventListener('input', updateBallSegments);
608
+
609
+ // Animation loop
610
+ function animate() {
611
+ requestAnimationFrame(animate);
612
+
613
+ controls.update();
614
+
615
+ if (currentModel && autoRotateEnabled) {
616
+ currentModel.rotation.y += parseFloat(document.getElementById('rotationSpeed').value);
617
+ }
618
+
619
+ renderer.render(scene, camera);
620
+ }
621
+
622
+ // Handle window resize
623
+ window.addEventListener('resize', () => {
624
+ camera.aspect = window.innerWidth / window.innerHeight;
625
+ camera.updateProjectionMatrix;
626
+ renderer.setSize(window.innerWidth, window.innerHeight);
627
+ });
628
+
629
+ // Initialize
630
+ updateVoxelSize();
631
+ updateBallSegments();
632
+ updateModelInfo('Ready to load models', 'none');
633
+
634
+ animate();
635
+ </script>
636
+ </body>
637
  </html>