@littlecarlito/blorktools 0.50.3 → 0.51.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/bin/cli.js +69 -0
  2. package/package.json +13 -7
  3. package/src/asset_debugger/axis-indicator/axis-indicator.css +6 -0
  4. package/src/asset_debugger/axis-indicator/axis-indicator.html +20 -0
  5. package/src/asset_debugger/axis-indicator/axis-indicator.js +822 -0
  6. package/src/asset_debugger/debugger-scene/debugger-scene.css +142 -0
  7. package/src/asset_debugger/debugger-scene/debugger-scene.html +80 -0
  8. package/src/asset_debugger/debugger-scene/debugger-scene.js +791 -0
  9. package/src/asset_debugger/header/header.css +73 -0
  10. package/src/asset_debugger/header/header.html +24 -0
  11. package/src/asset_debugger/header/header.js +224 -0
  12. package/src/asset_debugger/index.html +76 -0
  13. package/src/asset_debugger/landing-page/landing-page.css +396 -0
  14. package/src/asset_debugger/landing-page/landing-page.html +81 -0
  15. package/src/asset_debugger/landing-page/landing-page.js +611 -0
  16. package/src/asset_debugger/loading-splash/loading-splash.css +195 -0
  17. package/src/asset_debugger/loading-splash/loading-splash.html +22 -0
  18. package/src/asset_debugger/loading-splash/loading-splash.js +59 -0
  19. package/src/asset_debugger/loading-splash/preview-loading-splash.js +66 -0
  20. package/src/asset_debugger/main.css +14 -0
  21. package/src/asset_debugger/modals/examples-modal/examples-modal.css +41 -0
  22. package/src/asset_debugger/modals/examples-modal/examples-modal.html +18 -0
  23. package/src/asset_debugger/modals/examples-modal/examples-modal.js +111 -0
  24. package/src/asset_debugger/modals/examples-modal/examples.js +125 -0
  25. package/src/asset_debugger/modals/html-editor-modal/html-editor-modal.css +452 -0
  26. package/src/asset_debugger/modals/html-editor-modal/html-editor-modal.html +87 -0
  27. package/src/asset_debugger/modals/html-editor-modal/html-editor-modal.js +675 -0
  28. package/src/asset_debugger/modals/mesh-info-modal/mesh-info-modal.css +219 -0
  29. package/src/asset_debugger/modals/mesh-info-modal/mesh-info-modal.html +20 -0
  30. package/src/asset_debugger/modals/mesh-info-modal/mesh-info-modal.js +548 -0
  31. package/src/asset_debugger/modals/settings-modal/settings-modal.css +103 -0
  32. package/src/asset_debugger/modals/settings-modal/settings-modal.html +158 -0
  33. package/src/asset_debugger/modals/settings-modal/settings-modal.js +475 -0
  34. package/src/asset_debugger/panels/asset-panel/asset-panel.css +263 -0
  35. package/src/asset_debugger/panels/asset-panel/asset-panel.html +123 -0
  36. package/src/asset_debugger/panels/asset-panel/asset-panel.js +136 -0
  37. package/src/asset_debugger/panels/asset-panel/atlas-heading/atlas-heading.css +94 -0
  38. package/src/asset_debugger/panels/asset-panel/atlas-heading/atlas-heading.js +312 -0
  39. package/src/asset_debugger/panels/asset-panel/mesh-heading/mesh-heading.css +129 -0
  40. package/src/asset_debugger/panels/asset-panel/mesh-heading/mesh-heading.js +486 -0
  41. package/src/asset_debugger/panels/asset-panel/rig-heading/rig-heading.css +545 -0
  42. package/src/asset_debugger/panels/asset-panel/rig-heading/rig-heading.js +538 -0
  43. package/src/asset_debugger/panels/asset-panel/uv-heading/uv-heading.css +70 -0
  44. package/src/asset_debugger/panels/asset-panel/uv-heading/uv-heading.js +586 -0
  45. package/src/asset_debugger/panels/world-panel/world-panel.css +364 -0
  46. package/src/asset_debugger/panels/world-panel/world-panel.html +173 -0
  47. package/src/asset_debugger/panels/world-panel/world-panel.js +1891 -0
  48. package/src/asset_debugger/router.js +190 -0
  49. package/src/asset_debugger/util/animation/playback/animation-playback-controller.js +150 -0
  50. package/src/asset_debugger/util/animation/playback/animation-preview-controller.js +316 -0
  51. package/src/asset_debugger/util/animation/playback/css3d-bounce-controller.js +400 -0
  52. package/src/asset_debugger/util/animation/playback/css3d-reversal-controller.js +821 -0
  53. package/src/asset_debugger/util/animation/render/css3d-prerender-controller.js +696 -0
  54. package/src/asset_debugger/util/animation/render/debug-texture-factory.js +0 -0
  55. package/src/asset_debugger/util/animation/render/iframe2texture-render-controller.js +199 -0
  56. package/src/asset_debugger/util/animation/render/image2texture-prerender-controller.js +461 -0
  57. package/src/asset_debugger/util/animation/render/pbr-material-factory.js +82 -0
  58. package/src/asset_debugger/util/common.css +280 -0
  59. package/src/asset_debugger/util/data/animation-classifier.js +323 -0
  60. package/src/asset_debugger/util/data/duplicate-handler.js +20 -0
  61. package/src/asset_debugger/util/data/glb-buffer-manager.js +407 -0
  62. package/src/asset_debugger/util/data/glb-classifier.js +290 -0
  63. package/src/asset_debugger/util/data/html-formatter.js +76 -0
  64. package/src/asset_debugger/util/data/html-linter.js +276 -0
  65. package/src/asset_debugger/util/data/localstorage-manager.js +265 -0
  66. package/src/asset_debugger/util/data/mesh-html-manager.js +295 -0
  67. package/src/asset_debugger/util/data/string-serder.js +303 -0
  68. package/src/asset_debugger/util/data/texture-classifier.js +663 -0
  69. package/src/asset_debugger/util/data/upload/background-file-handler.js +292 -0
  70. package/src/asset_debugger/util/data/upload/dropzone-preview-controller.js +396 -0
  71. package/src/asset_debugger/util/data/upload/file-upload-manager.js +495 -0
  72. package/src/asset_debugger/util/data/upload/glb-file-handler.js +36 -0
  73. package/src/asset_debugger/util/data/upload/glb-preview-controller.js +317 -0
  74. package/src/asset_debugger/util/data/upload/lighting-file-handler.js +194 -0
  75. package/src/asset_debugger/util/data/upload/model-file-manager.js +104 -0
  76. package/src/asset_debugger/util/data/upload/texture-file-handler.js +166 -0
  77. package/src/asset_debugger/util/data/upload/zip-handler.js +686 -0
  78. package/src/asset_debugger/util/loaders/html2canvas-loader.js +107 -0
  79. package/src/asset_debugger/util/rig/bone-kinematics.js +403 -0
  80. package/src/asset_debugger/util/rig/rig-constraint-manager.js +618 -0
  81. package/src/asset_debugger/util/rig/rig-controller.js +612 -0
  82. package/src/asset_debugger/util/rig/rig-factory.js +628 -0
  83. package/src/asset_debugger/util/rig/rig-handle-factory.js +46 -0
  84. package/src/asset_debugger/util/rig/rig-label-factory.js +441 -0
  85. package/src/asset_debugger/util/rig/rig-mouse-handler.js +377 -0
  86. package/src/asset_debugger/util/rig/rig-state-manager.js +175 -0
  87. package/src/asset_debugger/util/rig/rig-tooltip-manager.js +267 -0
  88. package/src/asset_debugger/util/rig/rig-ui-factory.js +700 -0
  89. package/src/asset_debugger/util/scene/background-manager.js +284 -0
  90. package/src/asset_debugger/util/scene/camera-controller.js +243 -0
  91. package/src/asset_debugger/util/scene/css3d-debug-controller.js +406 -0
  92. package/src/asset_debugger/util/scene/css3d-frame-factory.js +113 -0
  93. package/src/asset_debugger/util/scene/css3d-scene-manager.js +529 -0
  94. package/src/asset_debugger/util/scene/glb-controller.js +208 -0
  95. package/src/asset_debugger/util/scene/lighting-manager.js +690 -0
  96. package/src/asset_debugger/util/scene/threejs-model-manager.js +437 -0
  97. package/src/asset_debugger/util/scene/threejs-preview-manager.js +207 -0
  98. package/src/asset_debugger/util/scene/threejs-preview-setup.js +478 -0
  99. package/src/asset_debugger/util/scene/threejs-scene-controller.js +286 -0
  100. package/src/asset_debugger/util/scene/ui-manager.js +107 -0
  101. package/src/asset_debugger/util/state/animation-state.js +128 -0
  102. package/src/asset_debugger/util/state/css3d-state.js +83 -0
  103. package/src/asset_debugger/util/state/glb-preview-state.js +31 -0
  104. package/src/asset_debugger/util/state/log-util.js +197 -0
  105. package/src/asset_debugger/util/state/scene-state.js +452 -0
  106. package/src/asset_debugger/util/state/threejs-state.js +54 -0
  107. package/src/asset_debugger/util/workers/lighting-worker.js +61 -0
  108. package/src/asset_debugger/util/workers/model-worker.js +109 -0
  109. package/src/asset_debugger/util/workers/texture-worker.js +54 -0
  110. package/src/asset_debugger/util/workers/worker-manager.js +212 -0
  111. package/src/asset_debugger/widgets/mesh-info-widget.js +280 -0
  112. package/src/index.html +261 -0
  113. package/src/index.js +8 -0
  114. package/vite.config.js +66 -0
@@ -0,0 +1,292 @@
1
+ import { createClearButton } from '../../../landing-page/landing-page';
2
+ import { hidePreviewLoading, showPreviewLoading } from '../../../loading-splash/preview-loading-splash';
3
+ import { updateState } from '../../state/scene-state';
4
+ import { formatFileSize } from './file-upload-manager';
5
+
6
+ /**
7
+ * Handle background image file upload
8
+ * @param {File} file - The background image file
9
+ * @param {HTMLElement} infoElement - Element to display file info
10
+ * @param {HTMLElement} previewElement - Element to show preview (optional)
11
+ * @param {HTMLElement} dropzone - The dropzone element
12
+ */
13
+ export function handleBackgroundUpload(file, infoElement, previewElement, dropzone) {
14
+ if (!file) return;
15
+
16
+ // Store original h3 title
17
+ const originalTitle = dropzone.querySelector('h3').textContent;
18
+
19
+ // Mark dropzone as having a file
20
+ dropzone.classList.add('has-file');
21
+
22
+ // Clear the entire dropzone content
23
+ dropzone.innerHTML = '';
24
+
25
+ // Add back just the title as a header
26
+ const titleElement = document.createElement('h3');
27
+ titleElement.textContent = originalTitle;
28
+ dropzone.appendChild(titleElement);
29
+
30
+ // Add the clear button using the shared function
31
+ dropzone.appendChild(createClearButton(dropzone, 'background', originalTitle));
32
+
33
+ // Add file info
34
+ infoElement = document.createElement('p');
35
+ infoElement.className = 'file-info';
36
+ infoElement.id = 'background-info';
37
+ infoElement.textContent = `${file.name} (${formatFileSize(file.size)})`;
38
+ dropzone.appendChild(infoElement);
39
+
40
+ // Create a container for the preview that will hold both the canvas and the loading indicator
41
+ const previewDiv = document.createElement('div');
42
+ previewDiv.className = 'preview';
43
+ dropzone.appendChild(previewDiv);
44
+
45
+ const containerDiv = document.createElement('div');
46
+ containerDiv.className = 'hdr-preview-container';
47
+
48
+ // Add event listener to prevent click events from reaching the dropzone
49
+ containerDiv.addEventListener('click', (e) => {
50
+ e.stopPropagation();
51
+ });
52
+
53
+ // Add event listener to prevent mousedown events to avoid accidental drag interactions
54
+ containerDiv.addEventListener('mousedown', (e) => {
55
+ e.stopPropagation();
56
+ });
57
+
58
+ previewDiv.appendChild(containerDiv);
59
+
60
+ // Show loading state directly on the container
61
+ showPreviewLoading(containerDiv);
62
+
63
+ // Create canvas for the preview with appropriate size
64
+ const canvas = document.createElement('canvas');
65
+ canvas.className = 'hdr-preview-canvas';
66
+
67
+ // Make canvas dimensions equal for a square aspect ratio
68
+ const previewSize = 256;
69
+ canvas.width = previewSize;
70
+ canvas.height = previewSize;
71
+
72
+ canvas.classList.add('hidden'); // Initially hidden until loaded
73
+
74
+ // Create a message element for errors/status
75
+ const messageDiv = document.createElement('div');
76
+ messageDiv.className = 'no-image-message-container hidden';
77
+
78
+ // Add elements to the container
79
+ containerDiv.appendChild(canvas);
80
+ containerDiv.appendChild(messageDiv);
81
+
82
+ const fileExtension = file.name.split('.').pop().toLowerCase();
83
+
84
+ // Process the file based on its type
85
+ if (['exr'].includes(fileExtension)) {
86
+ // EXR needs special loader
87
+ import('three').then(THREE => {
88
+ import('three/addons/loaders/EXRLoader.js').then(({ EXRLoader }) => {
89
+ const loader = new EXRLoader();
90
+ loader.setDataType(THREE.FloatType);
91
+
92
+ // Create reader for the file
93
+ const reader = new FileReader();
94
+ reader.onload = function(e) {
95
+ const arrayBuffer = e.target.result;
96
+
97
+ // Create a Blob and URL from the array buffer
98
+ const blob = new Blob([arrayBuffer], { type: 'application/octet-stream' });
99
+ const url = URL.createObjectURL(blob);
100
+
101
+ loader.load(url, texture => {
102
+ // Show the canvas
103
+ canvas.classList.add('visible');
104
+ canvas.classList.remove('hidden');
105
+
106
+ // Create a sphere preview with proper controls
107
+ worldPanelModule.createSpherePreview(THREE, texture, canvas, messageDiv);
108
+
109
+ // Clean up URL after loading
110
+ URL.revokeObjectURL(url);
111
+
112
+ // Hide loading indicator
113
+ hidePreviewLoading(containerDiv);
114
+
115
+ // Store the background texture for use in previews
116
+ updateState('backgroundTexture', texture);
117
+ }, undefined, error => {
118
+ console.error('Error loading EXR background texture:', error);
119
+ canvas.classList.add('visible');
120
+ canvas.classList.remove('hidden');
121
+ hidePreviewLoading(containerDiv);
122
+
123
+ if (messageDiv) {
124
+ messageDiv.classList.remove('hidden');
125
+ messageDiv.classList.add('visible');
126
+ messageDiv.textContent = 'Error loading EXR file';
127
+ }
128
+ });
129
+ };
130
+
131
+ reader.onerror = function() {
132
+ console.error('Error reading file');
133
+ hidePreviewLoading(containerDiv);
134
+ if (messageDiv) {
135
+ messageDiv.classList.remove('hidden');
136
+ messageDiv.classList.add('visible');
137
+ messageDiv.textContent = 'Error reading file';
138
+ }
139
+ };
140
+
141
+ reader.readAsArrayBuffer(file);
142
+ }).catch(error => {
143
+ console.error('Error loading EXRLoader:', error);
144
+ hidePreviewLoading(containerDiv);
145
+ canvas.classList.add('visible');
146
+ canvas.classList.remove('hidden');
147
+ });
148
+ }).catch(error => {
149
+ console.error('Error loading Three.js:', error);
150
+ hidePreviewLoading(containerDiv);
151
+ if (messageDiv) {
152
+ messageDiv.classList.remove('hidden');
153
+ messageDiv.classList.add('visible');
154
+ messageDiv.textContent = 'Error loading Three.js';
155
+ }
156
+ });
157
+ } else if (['hdr'].includes(fileExtension)) {
158
+ // HDR needs special loader
159
+ import('three').then(THREE => {
160
+ import('three/addons/loaders/RGBELoader.js').then(({ RGBELoader }) => {
161
+ const loader = new RGBELoader();
162
+
163
+ // Create reader for the file
164
+ const reader = new FileReader();
165
+ reader.onload = function(e) {
166
+ const arrayBuffer = e.target.result;
167
+
168
+ // Create a Blob and URL from the array buffer
169
+ const blob = new Blob([arrayBuffer], { type: 'application/octet-stream' });
170
+ const url = URL.createObjectURL(blob);
171
+
172
+ loader.load(url, texture => {
173
+ // Show the canvas
174
+ canvas.classList.add('visible');
175
+ canvas.classList.remove('hidden');
176
+
177
+ // Create a sphere preview with proper controls
178
+ worldPanelModule.createSpherePreview(THREE, texture, canvas, messageDiv);
179
+
180
+ // Clean up URL after loading
181
+ URL.revokeObjectURL(url);
182
+
183
+ // Hide loading indicator
184
+ hidePreviewLoading(containerDiv);
185
+
186
+ // Store the background texture for use in previews
187
+ updateState('backgroundTexture', texture);
188
+ }, undefined, error => {
189
+ console.error('Error loading HDR background texture:', error);
190
+ canvas.classList.add('visible');
191
+ canvas.classList.remove('hidden');
192
+ hidePreviewLoading(containerDiv);
193
+
194
+ if (messageDiv) {
195
+ messageDiv.classList.remove('hidden');
196
+ messageDiv.classList.add('visible');
197
+ messageDiv.textContent = 'Error loading HDR file';
198
+ }
199
+ });
200
+ };
201
+
202
+ reader.onerror = function() {
203
+ console.error('Error reading file');
204
+ hidePreviewLoading(containerDiv);
205
+ if (messageDiv) {
206
+ messageDiv.classList.remove('hidden');
207
+ messageDiv.classList.add('visible');
208
+ messageDiv.textContent = 'Error reading file';
209
+ }
210
+ };
211
+
212
+ reader.readAsArrayBuffer(file);
213
+ }).catch(error => {
214
+ console.error('Error loading RGBELoader:', error);
215
+ hidePreviewLoading(containerDiv);
216
+ canvas.classList.add('visible');
217
+ canvas.classList.remove('hidden');
218
+ });
219
+ }).catch(error => {
220
+ console.error('Error loading Three.js:', error);
221
+ hidePreviewLoading(containerDiv);
222
+ if (messageDiv) {
223
+ messageDiv.classList.remove('hidden');
224
+ messageDiv.classList.add('visible');
225
+ messageDiv.textContent = 'Error loading Three.js';
226
+ }
227
+ });
228
+ } else if (['jpg', 'jpeg', 'png', 'webp', 'tiff', 'tif'].includes(fileExtension)) {
229
+ // Standard image formats - load with regular THREE.TextureLoader
230
+ import('three').then(THREE => {
231
+ // Create a reader to get the data URL
232
+ const reader = new FileReader();
233
+ reader.onload = function(e) {
234
+ // Create a texture from the data URL using THREE.TextureLoader
235
+ const textureLoader = new THREE.TextureLoader();
236
+ textureLoader.load(e.target.result, texture => {
237
+ // Show the canvas
238
+ canvas.classList.add('visible');
239
+ canvas.classList.remove('hidden');
240
+
241
+ // Make sure to set proper texture parameters
242
+ texture.mapping = THREE.EquirectangularReflectionMapping;
243
+
244
+ // Create a sphere preview with proper controls
245
+ worldPanelModule.createSpherePreview(THREE, texture, canvas, messageDiv);
246
+
247
+ // Hide loading indicator
248
+ hidePreviewLoading(containerDiv);
249
+
250
+ // Store the background texture for use in previews
251
+ updateState('backgroundTexture', texture);
252
+ }, undefined, error => {
253
+ console.error('Error loading image texture:', error);
254
+ canvas.classList.add('visible');
255
+ canvas.classList.remove('hidden');
256
+ hidePreviewLoading(containerDiv);
257
+
258
+ if (messageDiv) {
259
+ messageDiv.classList.remove('hidden');
260
+ messageDiv.classList.add('visible');
261
+ messageDiv.textContent = 'Error loading image file';
262
+ }
263
+ });
264
+ };
265
+
266
+ reader.onerror = function() {
267
+ console.error('Error reading image file');
268
+ hidePreviewLoading(containerDiv);
269
+ if (messageDiv) {
270
+ messageDiv.classList.remove('hidden');
271
+ messageDiv.classList.add('visible');
272
+ messageDiv.textContent = 'Error reading image file';
273
+ }
274
+ };
275
+
276
+ reader.readAsDataURL(file);
277
+ }).catch(error => {
278
+ console.error('Error loading Three.js:', error);
279
+ hidePreviewLoading(containerDiv);
280
+ if (messageDiv) {
281
+ messageDiv.classList.remove('hidden');
282
+ messageDiv.classList.add('visible');
283
+ messageDiv.textContent = 'Error loading Three.js';
284
+ }
285
+ });
286
+ }
287
+
288
+ // Update state with the background file
289
+ updateState({
290
+ backgroundFile: file
291
+ });
292
+ }
@@ -0,0 +1,396 @@
1
+ /**
2
+ * Model Preview - Handles GLB preview in the model dropzone
3
+ *
4
+ * This module provides functionality to:
5
+ * 1. Create a mini Three.js scene for GLB preview
6
+ * 2. Add orbit controls for interactive preview
7
+ * 3. Load and display GLB models in the dropzone
8
+ */
9
+
10
+ import * as THREE from 'three';
11
+ import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
12
+ import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
13
+
14
+ // Keep track of the preview resources for cleanup
15
+ let previewRenderer = null;
16
+ let previewScene = null;
17
+ let previewCamera = null;
18
+ let previewControls = null;
19
+ let previewAnimationFrame = null;
20
+
21
+ /**
22
+ * Creates a preview of a GLB model file in the model dropzone
23
+ * @param {File} modelFile - The GLB file to preview
24
+ */
25
+ export function createModelPreview(modelFile) {
26
+ if (!modelFile) return;
27
+
28
+ // Get the preview container
29
+ const previewContainer = document.getElementById('model-preview');
30
+ if (!previewContainer) return;
31
+
32
+ // Clean up any existing preview
33
+ cleanupPreview();
34
+
35
+ // Create the preview elements
36
+ previewContainer.innerHTML = '';
37
+ previewContainer.style.position = 'relative';
38
+
39
+ // Create loading indicator
40
+ const loadingIndicator = document.createElement('div');
41
+ loadingIndicator.className = 'preview-loading';
42
+ loadingIndicator.innerHTML = `
43
+ <div class="preview-loading-spinner"></div>
44
+ <div class="preview-loading-text">Loading model...</div>
45
+ `;
46
+ previewContainer.appendChild(loadingIndicator);
47
+
48
+ // Create renderer
49
+ previewRenderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
50
+ previewRenderer.setPixelRatio(window.devicePixelRatio);
51
+ previewRenderer.setClearColor(0x000000, 0);
52
+ previewRenderer.setSize(previewContainer.clientWidth, previewContainer.clientHeight);
53
+ previewRenderer.outputEncoding = THREE.sRGBEncoding;
54
+ previewContainer.appendChild(previewRenderer.domElement);
55
+
56
+ // Create scene
57
+ previewScene = new THREE.Scene();
58
+ previewScene.background = new THREE.Color(0x111111);
59
+
60
+ // Add lighting
61
+ const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
62
+ previewScene.add(ambientLight);
63
+
64
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
65
+ directionalLight.position.set(1, 2, 3);
66
+ previewScene.add(directionalLight);
67
+
68
+ // Create camera
69
+ previewCamera = new THREE.PerspectiveCamera(
70
+ 45, previewContainer.clientWidth / previewContainer.clientHeight, 0.1, 100
71
+ );
72
+ previewCamera.position.set(0, 0, 2);
73
+
74
+ // Create controls
75
+ previewControls = new OrbitControls(previewCamera, previewRenderer.domElement);
76
+ previewControls.enableDamping = true;
77
+ previewControls.dampingFactor = 0.1;
78
+ previewControls.rotateSpeed = 0.8;
79
+ previewControls.enableZoom = true;
80
+ previewControls.zoomSpeed = 0.5;
81
+ previewControls.enablePan = false;
82
+
83
+ // Create a URL for the model file
84
+ const modelUrl = URL.createObjectURL(modelFile);
85
+
86
+ // Load the model
87
+ const loader = new GLTFLoader();
88
+ loader.load(
89
+ modelUrl,
90
+ (gltf) => {
91
+ // Remove loading indicator
92
+ loadingIndicator.remove();
93
+
94
+ // Add the model to the scene
95
+ const model = gltf.scene;
96
+
97
+ // Calculate bounding box to properly scale and center the model
98
+ const box = new THREE.Box3().setFromObject(model);
99
+ const size = box.getSize(new THREE.Vector3());
100
+ const center = box.getCenter(new THREE.Vector3());
101
+
102
+ // Reset position to center
103
+ model.position.x = -center.x;
104
+ model.position.y = -center.y;
105
+ model.position.z = -center.z;
106
+
107
+ // Scale to fit
108
+ const maxDim = Math.max(size.x, size.y, size.z);
109
+ if (maxDim > 0) {
110
+ const scale = 1.5 / maxDim; // Scale to fit within a 1.5 unit sphere
111
+ model.scale.multiplyScalar(scale);
112
+
113
+ // Update bounding box after scaling
114
+ box.setFromObject(model);
115
+ box.getSize(size);
116
+ box.getCenter(center);
117
+ }
118
+
119
+ // Create center group to help with positioning
120
+ const modelGroup = new THREE.Group();
121
+ modelGroup.add(model);
122
+
123
+ // Position the model on the grid properly
124
+ // Find the lowest point of the model after centering
125
+ const minY = box.min.y;
126
+
127
+ // If the model's minY isn't at the grid level, adjust it
128
+ if (minY !== 0) {
129
+ // Move the model so its bottom is just above the grid
130
+ model.position.y -= minY - 0.01; // Slight offset to avoid z-fighting
131
+ }
132
+
133
+ // Update the bounding box after repositioning
134
+ box.setFromObject(model);
135
+ box.getSize(size);
136
+ box.getCenter(center);
137
+
138
+ // Add to scene
139
+ previewScene.add(modelGroup);
140
+
141
+ // Add a grid helper
142
+ const gridSize = Math.max(2, Math.ceil(Math.max(size.x, size.z) * 1.5));
143
+ const gridHelper = new THREE.GridHelper(gridSize, gridSize * 2, 0x888888, 0x444444);
144
+ // Position grid at y=0 (standard ground plane)
145
+ gridHelper.position.y = 0;
146
+ previewScene.add(gridHelper);
147
+
148
+ // Calculate optimal camera position to ensure the model is fully visible
149
+ const fov = previewCamera.fov * (Math.PI / 180); // convert to radians
150
+ const aspectRatio = previewContainer.clientWidth / previewContainer.clientHeight;
151
+
152
+ // Get model dimensions after scaling
153
+ const scaledSizeX = size.x;
154
+ const scaledSizeY = size.y;
155
+ const scaledSizeZ = size.z;
156
+
157
+ // Calculate the center point of the model for camera targeting
158
+ // This should be the geometric center, not just (0,0,0)
159
+ const modelCenter = new THREE.Vector3(0, box.getCenter(new THREE.Vector3()).y, 0);
160
+
161
+ // Calculate required distance for each dimension
162
+ // For Z dimension, we need to consider the model's depth and our angle of view
163
+ const distanceForHeight = scaledSizeY / (2 * Math.tan(fov / 2));
164
+ const distanceForWidth = scaledSizeX / (2 * Math.tan(fov / 2) * aspectRatio);
165
+ const distanceForDepth = scaledSizeZ * 1.2; // Add more space for depth
166
+
167
+ // Base distance calculation
168
+ let optimalDistance = Math.max(distanceForWidth, distanceForHeight, distanceForDepth);
169
+
170
+ // Detect extreme model shapes and adjust accordingly
171
+ const aspectRatioXY = scaledSizeX / scaledSizeY;
172
+ const aspectRatioXZ = scaledSizeX / scaledSizeZ;
173
+ const aspectRatioYZ = scaledSizeY / scaledSizeZ;
174
+
175
+ // Add extra buffer for camera distance
176
+ let bufferMultiplier = 1.6; // Base buffer multiplier
177
+
178
+ // Add more buffer for flat/wide models (high aspect ratios)
179
+ if (aspectRatioXY > 4 || aspectRatioXY < 0.25 ||
180
+ aspectRatioXZ > 4 || aspectRatioXZ < 0.25 ||
181
+ aspectRatioYZ > 4 || aspectRatioYZ < 0.25) {
182
+ bufferMultiplier = 2.0; // More space for extreme shapes
183
+ console.log('Extreme model shape detected - using larger buffer');
184
+ }
185
+
186
+ // Apply buffer to optimal distance
187
+ optimalDistance *= bufferMultiplier;
188
+
189
+ // Set minimum distance
190
+ const finalDistance = Math.max(optimalDistance, 2.5);
191
+
192
+ // Calculate X and Y offsets for perspective view
193
+ const xOffset = finalDistance * 0.4;
194
+ let yOffset = finalDistance * 0.3;
195
+
196
+ // Auto orient the model for better visibility for flat models
197
+ if (scaledSizeZ < scaledSizeX * 0.25 && scaledSizeZ < scaledSizeY * 0.25) {
198
+ // Model is very flat - orient it to face the camera better
199
+ modelGroup.rotation.x = -Math.PI / 12; // Slight tilt
200
+
201
+ // Adjust model position to remain on grid after tilting
202
+ const elevationOffset = scaledSizeX * Math.sin(Math.PI / 12) / 2;
203
+ model.position.y += elevationOffset;
204
+ }
205
+
206
+ // Calculate optimal camera target (vertical center of the model)
207
+ // This ensures the camera is looking at the middle of the model, not the bottom
208
+ const targetY = modelCenter.y;
209
+
210
+ // Special handling for very tall models
211
+ if (scaledSizeY > scaledSizeX * 3 && scaledSizeY > scaledSizeZ * 3) {
212
+ // For very tall models, position camera higher to see from middle
213
+ yOffset = Math.min(targetY + finalDistance * 0.2, finalDistance * 0.7);
214
+ } else if (scaledSizeY < 0.5) {
215
+ // For very flat horizontal models, don't position camera too low
216
+ yOffset = Math.max(targetY * 2, finalDistance * 0.2);
217
+ } else {
218
+ // For normal models, position camera to see the entire height
219
+ // We need to raise the camera enough to see the top of the model
220
+ yOffset = Math.max(targetY, finalDistance * 0.2);
221
+ }
222
+
223
+ // Final camera positioning - position relative to the model's center point
224
+ previewCamera.position.set(
225
+ xOffset,
226
+ yOffset,
227
+ finalDistance
228
+ );
229
+
230
+ // Ensure controls target is at the center of the model (y-center, not ground level)
231
+ previewControls.target.set(0, targetY, 0);
232
+ previewControls.update();
233
+
234
+ // Animation loop
235
+ const animate = () => {
236
+ previewAnimationFrame = requestAnimationFrame(animate);
237
+ previewControls.update();
238
+ previewRenderer.render(previewScene, previewCamera);
239
+ };
240
+ animate();
241
+
242
+ // Add clear button
243
+ addClearButton(previewContainer, modelFile);
244
+ },
245
+ (xhr) => {
246
+ // Update loading progress
247
+ const percent = Math.floor((xhr.loaded / xhr.total) * 100);
248
+ loadingIndicator.querySelector('.preview-loading-text').textContent =
249
+ `Loading model... ${percent}%`;
250
+ },
251
+ (error) => {
252
+ console.error('Error loading model:', error);
253
+ loadingIndicator.remove();
254
+
255
+ // Show error message
256
+ const errorMsg = document.createElement('div');
257
+ errorMsg.className = 'no-image-message-container visible';
258
+ errorMsg.textContent = 'Error loading model. Please try another file.';
259
+ previewContainer.appendChild(errorMsg);
260
+ }
261
+ );
262
+
263
+ // Clean up object URL when done
264
+ return () => {
265
+ URL.revokeObjectURL(modelUrl);
266
+ cleanupPreview();
267
+ };
268
+ }
269
+
270
+ /**
271
+ * Adds a clear button to the preview
272
+ * @param {HTMLElement} container - The preview container
273
+ * @param {File} file - The model file
274
+ */
275
+ function addClearButton(container, file) {
276
+ // Find the parent dropzone element
277
+ const dropzone = document.getElementById('model-dropzone');
278
+
279
+ if (dropzone) {
280
+ // Create clear button
281
+ const clearButton = document.createElement('button');
282
+ clearButton.className = 'clear-preview-button';
283
+ clearButton.innerHTML = '×';
284
+ clearButton.title = 'Clear preview';
285
+
286
+ // Add to the dropzone instead of the preview container to match other previews
287
+ dropzone.appendChild(clearButton);
288
+
289
+ // Add click event
290
+ clearButton.addEventListener('click', (event) => {
291
+ event.stopPropagation();
292
+
293
+ // Clean up preview
294
+ cleanupPreview();
295
+
296
+ // Store the original title before clearing
297
+ const originalTitle = dropzone.querySelector('h3')?.textContent || '3D Model';
298
+
299
+ // Clean up the dropzone
300
+ if (dropzone) {
301
+ dropzone.classList.remove('has-file');
302
+
303
+ // Clear the dropzone content
304
+ dropzone.innerHTML = '';
305
+
306
+ // Import the file handler module to properly restore the dropzone
307
+ import('./file-upload-manager.js').then(module => {
308
+ // Use the clearDropzone function to restore the original content
309
+ if (module.clearDropzone) {
310
+ module.clearDropzone(dropzone, 'model', originalTitle);
311
+ } else {
312
+ // Fallback if clearDropzone isn't directly accessible
313
+
314
+ // Recreate basic content
315
+ const titleElement = document.createElement('h3');
316
+ titleElement.textContent = originalTitle;
317
+ dropzone.appendChild(titleElement);
318
+
319
+ // Add instruction text
320
+ const instructionText = document.createElement('p');
321
+ instructionText.textContent = 'Drag & drop a GLB model file here';
322
+ dropzone.appendChild(instructionText);
323
+
324
+ // Add optional text
325
+ const optionalText = document.createElement('p');
326
+ optionalText.textContent = 'If not provided, a cube will be used';
327
+ dropzone.appendChild(optionalText);
328
+
329
+ // Add file info element
330
+ const infoElement = document.createElement('p');
331
+ infoElement.className = 'file-info';
332
+ infoElement.id = 'model-info';
333
+ dropzone.appendChild(infoElement);
334
+
335
+ // Re-setup the dropzone
336
+ if (module.setupDropzone) {
337
+ module.setupDropzone(dropzone, 'model', infoElement);
338
+ }
339
+ }
340
+ });
341
+
342
+ // Reset state model file
343
+ import('../state/scene-state.js').then(stateModule => {
344
+ const state = stateModule.getState();
345
+ state.modelFile = null;
346
+ state.useCustomModel = false;
347
+ });
348
+
349
+ // Clear preview container
350
+ container.innerHTML = '';
351
+ }
352
+ });
353
+ }
354
+ }
355
+
356
+ /**
357
+ * Cleans up all preview resources
358
+ */
359
+ export function cleanupPreview() {
360
+ if (previewAnimationFrame) {
361
+ cancelAnimationFrame(previewAnimationFrame);
362
+ previewAnimationFrame = null;
363
+ }
364
+
365
+ if (previewControls) {
366
+ previewControls.dispose();
367
+ previewControls = null;
368
+ }
369
+
370
+ if (previewRenderer) {
371
+ previewRenderer.dispose();
372
+ previewRenderer = null;
373
+ }
374
+
375
+ // Clear references
376
+ previewScene = null;
377
+ previewCamera = null;
378
+ }
379
+
380
+ /**
381
+ * Handles window resize for the preview
382
+ */
383
+ export function handlePreviewResize() {
384
+ const previewContainer = document.getElementById('model-preview');
385
+ if (!previewContainer || !previewRenderer || !previewCamera) return;
386
+
387
+ // Update camera aspect ratio
388
+ previewCamera.aspect = previewContainer.clientWidth / previewContainer.clientHeight;
389
+ previewCamera.updateProjectionMatrix();
390
+
391
+ // Update renderer size
392
+ previewRenderer.setSize(previewContainer.clientWidth, previewContainer.clientHeight);
393
+ }
394
+
395
+ // Add window resize listener
396
+ window.addEventListener('resize', handlePreviewResize);