@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.
- package/bin/cli.js +69 -0
- package/package.json +13 -7
- package/src/asset_debugger/axis-indicator/axis-indicator.css +6 -0
- package/src/asset_debugger/axis-indicator/axis-indicator.html +20 -0
- package/src/asset_debugger/axis-indicator/axis-indicator.js +822 -0
- package/src/asset_debugger/debugger-scene/debugger-scene.css +142 -0
- package/src/asset_debugger/debugger-scene/debugger-scene.html +80 -0
- package/src/asset_debugger/debugger-scene/debugger-scene.js +791 -0
- package/src/asset_debugger/header/header.css +73 -0
- package/src/asset_debugger/header/header.html +24 -0
- package/src/asset_debugger/header/header.js +224 -0
- package/src/asset_debugger/index.html +76 -0
- package/src/asset_debugger/landing-page/landing-page.css +396 -0
- package/src/asset_debugger/landing-page/landing-page.html +81 -0
- package/src/asset_debugger/landing-page/landing-page.js +611 -0
- package/src/asset_debugger/loading-splash/loading-splash.css +195 -0
- package/src/asset_debugger/loading-splash/loading-splash.html +22 -0
- package/src/asset_debugger/loading-splash/loading-splash.js +59 -0
- package/src/asset_debugger/loading-splash/preview-loading-splash.js +66 -0
- package/src/asset_debugger/main.css +14 -0
- package/src/asset_debugger/modals/examples-modal/examples-modal.css +41 -0
- package/src/asset_debugger/modals/examples-modal/examples-modal.html +18 -0
- package/src/asset_debugger/modals/examples-modal/examples-modal.js +111 -0
- package/src/asset_debugger/modals/examples-modal/examples.js +125 -0
- package/src/asset_debugger/modals/html-editor-modal/html-editor-modal.css +452 -0
- package/src/asset_debugger/modals/html-editor-modal/html-editor-modal.html +87 -0
- package/src/asset_debugger/modals/html-editor-modal/html-editor-modal.js +675 -0
- package/src/asset_debugger/modals/mesh-info-modal/mesh-info-modal.css +219 -0
- package/src/asset_debugger/modals/mesh-info-modal/mesh-info-modal.html +20 -0
- package/src/asset_debugger/modals/mesh-info-modal/mesh-info-modal.js +548 -0
- package/src/asset_debugger/modals/settings-modal/settings-modal.css +103 -0
- package/src/asset_debugger/modals/settings-modal/settings-modal.html +158 -0
- package/src/asset_debugger/modals/settings-modal/settings-modal.js +475 -0
- package/src/asset_debugger/panels/asset-panel/asset-panel.css +263 -0
- package/src/asset_debugger/panels/asset-panel/asset-panel.html +123 -0
- package/src/asset_debugger/panels/asset-panel/asset-panel.js +136 -0
- package/src/asset_debugger/panels/asset-panel/atlas-heading/atlas-heading.css +94 -0
- package/src/asset_debugger/panels/asset-panel/atlas-heading/atlas-heading.js +312 -0
- package/src/asset_debugger/panels/asset-panel/mesh-heading/mesh-heading.css +129 -0
- package/src/asset_debugger/panels/asset-panel/mesh-heading/mesh-heading.js +486 -0
- package/src/asset_debugger/panels/asset-panel/rig-heading/rig-heading.css +545 -0
- package/src/asset_debugger/panels/asset-panel/rig-heading/rig-heading.js +538 -0
- package/src/asset_debugger/panels/asset-panel/uv-heading/uv-heading.css +70 -0
- package/src/asset_debugger/panels/asset-panel/uv-heading/uv-heading.js +586 -0
- package/src/asset_debugger/panels/world-panel/world-panel.css +364 -0
- package/src/asset_debugger/panels/world-panel/world-panel.html +173 -0
- package/src/asset_debugger/panels/world-panel/world-panel.js +1891 -0
- package/src/asset_debugger/router.js +190 -0
- package/src/asset_debugger/util/animation/playback/animation-playback-controller.js +150 -0
- package/src/asset_debugger/util/animation/playback/animation-preview-controller.js +316 -0
- package/src/asset_debugger/util/animation/playback/css3d-bounce-controller.js +400 -0
- package/src/asset_debugger/util/animation/playback/css3d-reversal-controller.js +821 -0
- package/src/asset_debugger/util/animation/render/css3d-prerender-controller.js +696 -0
- package/src/asset_debugger/util/animation/render/debug-texture-factory.js +0 -0
- package/src/asset_debugger/util/animation/render/iframe2texture-render-controller.js +199 -0
- package/src/asset_debugger/util/animation/render/image2texture-prerender-controller.js +461 -0
- package/src/asset_debugger/util/animation/render/pbr-material-factory.js +82 -0
- package/src/asset_debugger/util/common.css +280 -0
- package/src/asset_debugger/util/data/animation-classifier.js +323 -0
- package/src/asset_debugger/util/data/duplicate-handler.js +20 -0
- package/src/asset_debugger/util/data/glb-buffer-manager.js +407 -0
- package/src/asset_debugger/util/data/glb-classifier.js +290 -0
- package/src/asset_debugger/util/data/html-formatter.js +76 -0
- package/src/asset_debugger/util/data/html-linter.js +276 -0
- package/src/asset_debugger/util/data/localstorage-manager.js +265 -0
- package/src/asset_debugger/util/data/mesh-html-manager.js +295 -0
- package/src/asset_debugger/util/data/string-serder.js +303 -0
- package/src/asset_debugger/util/data/texture-classifier.js +663 -0
- package/src/asset_debugger/util/data/upload/background-file-handler.js +292 -0
- package/src/asset_debugger/util/data/upload/dropzone-preview-controller.js +396 -0
- package/src/asset_debugger/util/data/upload/file-upload-manager.js +495 -0
- package/src/asset_debugger/util/data/upload/glb-file-handler.js +36 -0
- package/src/asset_debugger/util/data/upload/glb-preview-controller.js +317 -0
- package/src/asset_debugger/util/data/upload/lighting-file-handler.js +194 -0
- package/src/asset_debugger/util/data/upload/model-file-manager.js +104 -0
- package/src/asset_debugger/util/data/upload/texture-file-handler.js +166 -0
- package/src/asset_debugger/util/data/upload/zip-handler.js +686 -0
- package/src/asset_debugger/util/loaders/html2canvas-loader.js +107 -0
- package/src/asset_debugger/util/rig/bone-kinematics.js +403 -0
- package/src/asset_debugger/util/rig/rig-constraint-manager.js +618 -0
- package/src/asset_debugger/util/rig/rig-controller.js +612 -0
- package/src/asset_debugger/util/rig/rig-factory.js +628 -0
- package/src/asset_debugger/util/rig/rig-handle-factory.js +46 -0
- package/src/asset_debugger/util/rig/rig-label-factory.js +441 -0
- package/src/asset_debugger/util/rig/rig-mouse-handler.js +377 -0
- package/src/asset_debugger/util/rig/rig-state-manager.js +175 -0
- package/src/asset_debugger/util/rig/rig-tooltip-manager.js +267 -0
- package/src/asset_debugger/util/rig/rig-ui-factory.js +700 -0
- package/src/asset_debugger/util/scene/background-manager.js +284 -0
- package/src/asset_debugger/util/scene/camera-controller.js +243 -0
- package/src/asset_debugger/util/scene/css3d-debug-controller.js +406 -0
- package/src/asset_debugger/util/scene/css3d-frame-factory.js +113 -0
- package/src/asset_debugger/util/scene/css3d-scene-manager.js +529 -0
- package/src/asset_debugger/util/scene/glb-controller.js +208 -0
- package/src/asset_debugger/util/scene/lighting-manager.js +690 -0
- package/src/asset_debugger/util/scene/threejs-model-manager.js +437 -0
- package/src/asset_debugger/util/scene/threejs-preview-manager.js +207 -0
- package/src/asset_debugger/util/scene/threejs-preview-setup.js +478 -0
- package/src/asset_debugger/util/scene/threejs-scene-controller.js +286 -0
- package/src/asset_debugger/util/scene/ui-manager.js +107 -0
- package/src/asset_debugger/util/state/animation-state.js +128 -0
- package/src/asset_debugger/util/state/css3d-state.js +83 -0
- package/src/asset_debugger/util/state/glb-preview-state.js +31 -0
- package/src/asset_debugger/util/state/log-util.js +197 -0
- package/src/asset_debugger/util/state/scene-state.js +452 -0
- package/src/asset_debugger/util/state/threejs-state.js +54 -0
- package/src/asset_debugger/util/workers/lighting-worker.js +61 -0
- package/src/asset_debugger/util/workers/model-worker.js +109 -0
- package/src/asset_debugger/util/workers/texture-worker.js +54 -0
- package/src/asset_debugger/util/workers/worker-manager.js +212 -0
- package/src/asset_debugger/widgets/mesh-info-widget.js +280 -0
- package/src/index.html +261 -0
- package/src/index.js +8 -0
- 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);
|