@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,1891 @@
1
+ /**
2
+ * Asset Debugger - World Panel Module
3
+ *
4
+ * This module handles world properties, environment, and lighting visualization and controls.
5
+ */
6
+ import { getState } from '../../util/state/scene-state';
7
+ import { updateLighting, resetLighting, updateExposure } from '../../util/scene/lighting-manager';
8
+ import * as THREE from 'three';
9
+
10
+ // Track initialization state
11
+ let controlsInitialized = false;
12
+
13
+ // Store environment (HDR/EXR) metadata for display
14
+ let currentLightingMetadata = null;
15
+
16
+ // Store background image metadata for display
17
+ let currentBackgroundMetadata = null;
18
+
19
+ // Store the environment texture for preview
20
+ let environmentTexture = null;
21
+
22
+ // Store the background texture for preview
23
+ let backgroundTexture = null;
24
+
25
+ // Track the currently selected background option
26
+ let currentBackgroundOption = null; // Initialize to null for proper first-time selection logic
27
+
28
+ // Track if an EXR/HDR file is currently being loaded
29
+ let exrLoadInProgress = false;
30
+
31
+ // Add a single flag to track when an EXR/HDR file is being loaded
32
+ let isLoadingEnvironmentFile = false;
33
+
34
+ // NEW: Track event listeners for proper cleanup
35
+ let eventListeners = [];
36
+
37
+ /**
38
+ * NEW: Cleanup function to remove all event listeners
39
+ */
40
+ function cleanupEventListeners() {
41
+ console.log('Cleaning up world panel event listeners...');
42
+ eventListeners.forEach(({ element, event, handler }) => {
43
+ if (element && typeof element.removeEventListener === 'function') {
44
+ element.removeEventListener(event, handler);
45
+ }
46
+ });
47
+ eventListeners = [];
48
+ }
49
+
50
+ /**
51
+ * NEW: Helper to add tracked event listeners
52
+ */
53
+ function addTrackedEventListener(element, event, handler) {
54
+ if (element && typeof element.addEventListener === 'function') {
55
+ element.addEventListener(event, handler);
56
+ eventListeners.push({ element, event, handler });
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Set the scene background to null and save the current background if it exists
62
+ * @param {THREE.Scene} scene - The Three.js scene
63
+ */
64
+ function setNullBackground(scene) {
65
+ if (scene.background) {
66
+ if (!scene.userData) scene.userData = {};
67
+ scene.userData.savedBackground = scene.background;
68
+ scene.background = null;
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Determine the current background UI state based on available data
74
+ * This centralizes all the logic for determining what should be visible
75
+ * @returns {Object} UI state object with flags for visibility control
76
+ */
77
+ function determineBackgroundUIState() {
78
+ const state = getState();
79
+ return {
80
+ // Background image state
81
+ hasBackgroundImage: Boolean(state.backgroundFile || state.backgroundTexture),
82
+ // Environment (HDR/EXR) state
83
+ hasEnvironment: Boolean(state.scene && state.scene.environment),
84
+ // Currently selected option
85
+ currentSelection: currentBackgroundOption || 'none',
86
+ // Detailed state data for debugging
87
+ details: {
88
+ backgroundFile: state.backgroundFile ?
89
+ `${state.backgroundFile.name} (${state.backgroundFile.type})` : null,
90
+ backgroundTexture: state.backgroundTexture ? 'present' : null,
91
+ environmentTexture: (state.scene && state.scene.environment) ? 'present' : null,
92
+ backgroundMetadata: currentBackgroundMetadata ? 'present' : null,
93
+ lightingMetadata: currentLightingMetadata ? 'present' : null
94
+ }
95
+ };
96
+ }
97
+
98
+ /**
99
+ * Toggle visibility of a specific background option
100
+ * @param {string} optionId - The ID of the option element to toggle ('background-option' or 'hdr-option')
101
+ * @param {boolean} visible - Whether the option should be visible
102
+ */
103
+ export function toggleOptionVisibility(optionId, visible) {
104
+ const option = document.getElementById(optionId);
105
+ if (!option) {
106
+ console.warn(`Cannot toggle visibility: Option with ID '${optionId}' not found`);
107
+ return;
108
+ }
109
+
110
+ // Get input and label elements specifically (not all children)
111
+ const input = option.querySelector('input[type="radio"]');
112
+ const label = option.querySelector('label');
113
+
114
+ if (!input || !label) {
115
+ console.warn(`Radio input or label not found in option with ID '${optionId}'`);
116
+ return;
117
+ }
118
+
119
+ // Set display style for the elements
120
+ // Radio buttons use inline-block, labels use inline
121
+ input.style.display = visible ? 'inline-block' : 'none';
122
+ label.style.display = visible ? 'inline' : 'none';
123
+
124
+ // For debugging
125
+ console.log(`Background option '${optionId}' visibility set to ${visible ? 'visible' : 'hidden'}`);
126
+ }
127
+
128
+ /**
129
+ * Update all background UI elements based on the current state
130
+ * @param {Object} uiState - The UI state object from determineBackgroundUIState
131
+ */
132
+ function updateBackgroundUIVisibility(uiState) {
133
+ // Log the current state for debugging
134
+ console.debug('Updating background UI visibility with state:', uiState);
135
+
136
+ // 1. Update the preview canvas opacity
137
+ updateCanvasOpacity(currentBackgroundOption);
138
+
139
+ // 2. Update message visibility - show the "no data" message only if BOTH background types are missing
140
+ const hasAnyBackground = uiState.hasBackgroundImage || uiState.hasEnvironment;
141
+ toggleBackgroundMessages(hasAnyBackground);
142
+
143
+ // Helper function to update canvas opacity
144
+ function updateCanvasOpacity(selectedValue) {
145
+ const bgPreviewCanvas = document.getElementById('bg-preview-canvas');
146
+ const environmentPreviewCanvas = document.getElementById('hdr-preview-canvas');
147
+
148
+ if (bgPreviewCanvas) {
149
+ bgPreviewCanvas.style.opacity = (selectedValue === 'background') ? '1' : '0.3';
150
+ }
151
+
152
+ if (environmentPreviewCanvas) {
153
+ environmentPreviewCanvas.style.opacity = (selectedValue === 'hdr') ? '1' : '0.3';
154
+ }
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Refresh all background UI based on current state
160
+ * This is the main entry point to update all background UI elements
161
+ */
162
+ function refreshBackgroundUI() {
163
+ const uiState = determineBackgroundUIState();
164
+ updateBackgroundUIVisibility(uiState);
165
+ }
166
+
167
+ /**
168
+ * Initialize the World panel and cache DOM elements
169
+ * @param {boolean} [forceReset=false] - Force reset even if already initialized
170
+ */
171
+ export function initWorldPanel(forceReset = false) {
172
+ // Only initialize if not already done, and only if we're in the debug phase
173
+ // where the panel was explicitly requested to be initialized
174
+ if (controlsInitialized && !forceReset) {
175
+ console.log('World Panel already initialized, skipping');
176
+ return;
177
+ }
178
+
179
+ // ALWAYS cleanup existing event listeners first
180
+ cleanupEventListeners();
181
+
182
+ // If we're already initialized but forcing a reset, just reset the critical state
183
+ if (controlsInitialized && forceReset) {
184
+ console.log('World Panel already initialized, but forcing reset of critical state');
185
+ // Reset background related state on reinitialization
186
+ currentBackgroundOption = 'none';
187
+ currentBackgroundMetadata = null;
188
+ currentLightingMetadata = null;
189
+ backgroundTexture = null;
190
+ environmentTexture = null;
191
+
192
+ // Reset radio button visibility
193
+ toggleOptionVisibility('background-option', false);
194
+ toggleOptionVisibility('hdr-option', false);
195
+
196
+ console.log('Continuing initialization to check for loaded files');
197
+
198
+ // Get current state to check for loaded files
199
+ const state = getState();
200
+ const hasEnvironmentTexture = state.lightingFile !== null || (state.scene && state.scene.environment);
201
+ const hasBackgroundTexture = state.backgroundTexture !== null || state.backgroundFile !== null;
202
+
203
+ // Show options based on available files
204
+ toggleOptionVisibility('hdr-option', hasEnvironmentTexture);
205
+ toggleOptionVisibility('background-option', hasBackgroundTexture);
206
+ console.log(`Updated radio visibility after reset - HDR: ${hasEnvironmentTexture}, Background: ${hasBackgroundTexture}`);
207
+
208
+ // Set up event listeners again
209
+ setupBgToggleListener();
210
+
211
+ // Also update the background UI
212
+ refreshBackgroundUI();
213
+ return;
214
+ }
215
+
216
+ console.debug('Initializing World Panel...');
217
+
218
+ // Look for world-tab (from world-panel.html) or world-tab-container (from debugger-scene.html)
219
+ const worldPanel = document.getElementById('world-tab') || document.getElementById('world-tab-container');
220
+
221
+ if (!worldPanel) {
222
+ console.error('World panel elements not found. Panel may not be loaded in DOM yet.');
223
+ return;
224
+ }
225
+
226
+ console.debug('World panel found, initializing...');
227
+
228
+ // Reset background related state on initialization
229
+ currentBackgroundOption = 'none';
230
+ currentBackgroundMetadata = null;
231
+ currentLightingMetadata = null;
232
+ backgroundTexture = null;
233
+ environmentTexture = null;
234
+
235
+ // Initially hide background and HDR options until content is loaded
236
+ toggleOptionVisibility('background-option', false);
237
+ toggleOptionVisibility('hdr-option', false);
238
+
239
+ // Initialize value displays on sliders and set up event listeners
240
+ document.querySelectorAll('.control-group input[type="range"]').forEach(slider => {
241
+ const valueDisplay = slider.previousElementSibling.querySelector('.value-display');
242
+ if (valueDisplay) {
243
+ valueDisplay.textContent = slider.value;
244
+ }
245
+
246
+ // Add input event listener to update value display when dragging
247
+ const inputHandler = function() {
248
+ const valueDisplay = this.previousElementSibling.querySelector('.value-display');
249
+ if (valueDisplay) {
250
+ valueDisplay.textContent = this.value;
251
+ }
252
+ };
253
+
254
+ addTrackedEventListener(slider, 'input', inputHandler);
255
+ });
256
+
257
+ // Initialize collapsible functionality
258
+ const collapsibleHeaders = document.querySelectorAll('.collapsible-header');
259
+ if (collapsibleHeaders) {
260
+ collapsibleHeaders.forEach(header => {
261
+ const clickHandler = function() {
262
+ const content = this.nextElementSibling;
263
+ const indicator = this.querySelector('.collapse-indicator');
264
+
265
+ if (content.style.display === 'none') {
266
+ content.style.display = 'block';
267
+ indicator.textContent = '[-]';
268
+ } else {
269
+ content.style.display = 'none';
270
+ indicator.textContent = '[+]';
271
+ }
272
+ };
273
+
274
+ addTrackedEventListener(header, 'click', clickHandler);
275
+ });
276
+ }
277
+
278
+ // Set up event listeners for lighting controls
279
+ setupLightingControls();
280
+
281
+ // Set up Background toggle event listener
282
+ setupBgToggleListener();
283
+
284
+ // Add event listener for background updates from background-util.js
285
+ const backgroundUpdateHandler = function(e) {
286
+ const texture = e.detail.texture;
287
+ if (texture) {
288
+ console.log('Background texture updated event received');
289
+
290
+ // Extract metadata from the texture
291
+ let width = 0;
292
+ let height = 0;
293
+ let type = 'Unknown Texture';
294
+ let fileName = 'Background Texture';
295
+
296
+ // Get dimensions
297
+ if (texture.image) {
298
+ width = texture.image.width || 0;
299
+ height = texture.image.height || 0;
300
+ }
301
+
302
+ // Determine type
303
+ if (texture.isHDRTexture) {
304
+ type = 'HDR Texture';
305
+ } else if (texture.isCompressedTexture) {
306
+ type = 'Compressed Texture';
307
+ } else if (texture.isDataTexture) {
308
+ type = 'Data Texture';
309
+ } else if (texture.isTexture) {
310
+ type = 'Standard Texture';
311
+ }
312
+
313
+ // Try to get filename from userData if available
314
+ if (texture.userData && texture.userData.fileName) {
315
+ fileName = texture.userData.fileName;
316
+ }
317
+
318
+ // Create metadata
319
+ const metadata = {
320
+ fileName: fileName,
321
+ type: type,
322
+ dimensions: { width, height },
323
+ // Estimate file size based on dimensions and encoding
324
+ fileSizeBytes: (width * height * 4) // Rough estimate: pixels * 4 bytes per pixel
325
+ };
326
+
327
+ // Update the UI with the texture metadata
328
+ updateBackgroundInfo(metadata);
329
+
330
+ // Store the texture for preview rendering
331
+ backgroundTexture = texture;
332
+
333
+ // Render the preview using the texture
334
+ renderBackgroundPreview(texture);
335
+ }
336
+ };
337
+
338
+ addTrackedEventListener(document, 'background-updated', backgroundUpdateHandler);
339
+
340
+ // Mark as initialized
341
+ controlsInitialized = true;
342
+ }
343
+
344
+ /**
345
+ * Set up event listeners for lighting controls
346
+ */
347
+ function setupLightingControls() {
348
+ // Look for lighting control elements
349
+ const ambientIntensityControl = document.getElementById('ambient-light-intensity');
350
+ const directionalIntensityControl = document.getElementById('directional-light-intensity');
351
+ const exposureControl = document.getElementById('exposure-value');
352
+ const resetLightingButton = document.getElementById('reset-lighting');
353
+
354
+ // Log if elements are not found
355
+ if (!ambientIntensityControl) {
356
+ console.warn('Ambient light intensity control not found');
357
+ }
358
+ if (!directionalIntensityControl) {
359
+ console.warn('Directional light intensity control not found');
360
+ }
361
+ if (!exposureControl) {
362
+ console.warn('Exposure control not found');
363
+ }
364
+ if (!resetLightingButton) {
365
+ console.warn('Reset lighting button not found');
366
+ }
367
+
368
+ // Set up ambient light intensity control
369
+ if (ambientIntensityControl) {
370
+ const ambientHandler = (e) => {
371
+ const value = parseFloat(e.target.value);
372
+ updateLighting({
373
+ ambient: { intensity: value }
374
+ });
375
+ // Update value display
376
+ const valueDisplay = e.target.previousElementSibling.querySelector('.value-display');
377
+ if (valueDisplay) {
378
+ valueDisplay.textContent = value.toFixed(1);
379
+ }
380
+ };
381
+
382
+ addTrackedEventListener(ambientIntensityControl, 'input', ambientHandler);
383
+ }
384
+
385
+ // Set up directional light intensity control
386
+ if (directionalIntensityControl) {
387
+ const directionalHandler = (e) => {
388
+ const value = parseFloat(e.target.value);
389
+ updateLighting({
390
+ directional: { intensity: value }
391
+ });
392
+ // Update value display
393
+ const valueDisplay = e.target.previousElementSibling.querySelector('.value-display');
394
+ if (valueDisplay) {
395
+ valueDisplay.textContent = value.toFixed(1);
396
+ }
397
+ };
398
+
399
+ addTrackedEventListener(directionalIntensityControl, 'input', directionalHandler);
400
+ }
401
+
402
+ // Set up exposure control
403
+ if (exposureControl) {
404
+ const exposureHandler = (e) => {
405
+ const value = parseFloat(e.target.value);
406
+ updateExposure(value);
407
+ // Update value display
408
+ const valueDisplay = e.target.previousElementSibling.querySelector('.value-display');
409
+ if (valueDisplay) {
410
+ valueDisplay.textContent = value.toFixed(1);
411
+ }
412
+ };
413
+
414
+ addTrackedEventListener(exposureControl, 'input', exposureHandler);
415
+ }
416
+
417
+ // Set up reset lighting button
418
+ if (resetLightingButton) {
419
+ const resetHandler = () => {
420
+ resetLighting();
421
+
422
+ // Reset control values if they exist
423
+ if (ambientIntensityControl) {
424
+ ambientIntensityControl.value = 0.5;
425
+ const valueDisplay = ambientIntensityControl.previousElementSibling.querySelector('.value-display');
426
+ if (valueDisplay) {
427
+ valueDisplay.textContent = '0.5';
428
+ }
429
+ }
430
+
431
+ if (directionalIntensityControl) {
432
+ directionalIntensityControl.value = 1.0;
433
+ const valueDisplay = directionalIntensityControl.previousElementSibling.querySelector('.value-display');
434
+ if (valueDisplay) {
435
+ valueDisplay.textContent = '1.0';
436
+ }
437
+ }
438
+
439
+ if (exposureControl) {
440
+ exposureControl.value = 1.0;
441
+ const valueDisplay = exposureControl.previousElementSibling.querySelector('.value-display');
442
+ if (valueDisplay) {
443
+ valueDisplay.textContent = '1.0';
444
+ }
445
+ }
446
+
447
+ // Don't clear lighting info if we have an environment texture
448
+ const state = getState();
449
+ const hasEnvironmentTexture = state.scene && state.scene.environment !== null;
450
+
451
+ if (!hasEnvironmentTexture) {
452
+ // Only clear lighting info if no environment texture is loaded
453
+ clearLightingInfo();
454
+ }
455
+
456
+ // Update slider visibility based on whether environment is loaded
457
+ updateSliderVisibility(hasEnvironmentTexture);
458
+ };
459
+
460
+ addTrackedEventListener(resetLightingButton, 'click', resetHandler);
461
+ }
462
+
463
+ // Initialize message visibility
464
+ updateLightingMessage();
465
+ }
466
+
467
+ /**
468
+ * Set up listener for Background radio button selection
469
+ */
470
+ function setupBgToggleListener() {
471
+ // Find all background toggle radio buttons
472
+ const backgroundOptions = document.querySelectorAll('input[name="bg-option"]');
473
+ const state = getState();
474
+
475
+ // Initial selection based on loaded files
476
+ let initialOption = 'none'; // Default is none
477
+
478
+ // Always default to 'none' unless otherwise specified
479
+ const selectedRadio = document.querySelector(`input[name="bg-option"][value="${initialOption}"]`);
480
+ if (selectedRadio) {
481
+ selectedRadio.checked = true;
482
+ currentBackgroundOption = initialOption;
483
+ }
484
+
485
+ // Process UI state based on available resources
486
+ const uiState = determineBackgroundUIState();
487
+ updateBackgroundUIVisibility(uiState);
488
+
489
+ // CRITICAL FIX: Remove any existing change listeners before adding new ones
490
+ backgroundOptions.forEach(option => {
491
+ // Clone the node to remove all existing event listeners
492
+ const newOption = option.cloneNode(true);
493
+ option.parentNode.replaceChild(newOption, option);
494
+ });
495
+
496
+ // Now get the fresh nodes and add new listeners
497
+ const freshBackgroundOptions = document.querySelectorAll('input[name="bg-option"]');
498
+
499
+ // Add event listeners to each radio button
500
+ freshBackgroundOptions.forEach(option => {
501
+ const changeHandler = function() {
502
+ if (this.checked) {
503
+ const selectedValue = this.value;
504
+ currentBackgroundOption = selectedValue;
505
+
506
+ console.log(`Background option changed to: ${selectedValue}`);
507
+
508
+ // Get scene from state
509
+ const state = getState();
510
+ if (!state.scene) {
511
+ console.error('Cannot update background - scene not found in state');
512
+ return;
513
+ }
514
+
515
+ // Apply the appropriate background based on selection
516
+ switch (selectedValue) {
517
+ case 'none':
518
+ // Set background to null or a dark color
519
+ setNullBackground(state.scene);
520
+ console.log('Background set to none');
521
+ break;
522
+
523
+ case 'hdr':
524
+ // Only apply HDR/EXR background if we have one
525
+ if (state.scene.environment) {
526
+ console.log('Setting background to HDR/EXR environment map');
527
+ state.scene.background = state.scene.environment;
528
+ console.log('HDR/EXR background applied successfully:', state.scene.environment);
529
+ } else if (environmentTexture) {
530
+ // Try using the cached environment texture if the scene doesn't have one
531
+ console.log('Using cached environment texture');
532
+ state.scene.background = environmentTexture;
533
+ // Also set it as the environment if not already set
534
+ if (!state.scene.environment) {
535
+ state.scene.environment = environmentTexture;
536
+ }
537
+ } else if (state.lightingFile) {
538
+ // If we have a lighting file but no texture, try to reload it
539
+ console.log('Attempting to reload environment texture from lighting file');
540
+
541
+ // Import lighting utility and load the texture again
542
+ import('../../util/scene/lighting-manager.js').then(lightingModule => {
543
+ if (lightingModule.setupEnvironmentLighting && state.lightingFile) {
544
+ lightingModule.setupEnvironmentLighting(state.lightingFile)
545
+ .then(newTexture => {
546
+ if (newTexture) {
547
+ console.log('Successfully reloaded environment texture');
548
+ state.scene.background = newTexture;
549
+ state.scene.environment = newTexture;
550
+ environmentTexture = newTexture;
551
+ }
552
+ })
553
+ .catch(error => {
554
+ console.error('Error reloading environment texture:', error);
555
+ });
556
+ }
557
+ });
558
+ } else {
559
+ console.warn('No HDR/EXR environment map available');
560
+ // Fall back to null background but KEEP the radio selection
561
+ setNullBackground(state.scene);
562
+ }
563
+ break;
564
+
565
+ case 'background':
566
+ // Only apply background texture if we have one
567
+ if (state.backgroundTexture) {
568
+ console.log('Setting background to loaded background texture');
569
+ state.scene.background = state.backgroundTexture;
570
+ } else if (backgroundTexture) {
571
+ // Try using the cached background texture if the state doesn't have one
572
+ console.log('Using cached background texture');
573
+ state.scene.background = backgroundTexture;
574
+ } else {
575
+ console.warn('No background texture available');
576
+ // Fall back to null background but KEEP the radio selection
577
+ setNullBackground(state.scene);
578
+ }
579
+ break;
580
+ }
581
+
582
+ // Update canvas opacity based on selection
583
+ updateCanvasOpacity(selectedValue);
584
+ }
585
+ };
586
+
587
+ // Add the event listener and track it for cleanup
588
+ addTrackedEventListener(option, 'change', changeHandler);
589
+ });
590
+
591
+ // Helper function to update canvas opacity based on selection
592
+ function updateCanvasOpacity(selectedValue) {
593
+ const bgPreviewCanvas = document.getElementById('bg-preview-canvas');
594
+ const environmentPreviewCanvas = document.getElementById('hdr-preview-canvas');
595
+
596
+ if (bgPreviewCanvas) {
597
+ bgPreviewCanvas.style.opacity = (selectedValue === 'background') ? '1' : '0.3';
598
+ }
599
+
600
+ if (environmentPreviewCanvas) {
601
+ environmentPreviewCanvas.style.opacity = (selectedValue === 'hdr') ? '1' : '0.3';
602
+ }
603
+ }
604
+ }
605
+
606
+ /**
607
+ * Toggle visibility of background messages based on whether environment texture or background is loaded
608
+ * @param {boolean} hasContent - Whether environment texture or background is loaded
609
+ * @param {boolean} [forceShow=false] - Force show/hide the data info regardless of content status
610
+ */
611
+ export function toggleBackgroundMessages(hasContent, forceShow = false) {
612
+ const noBackgroundMessage = document.querySelector('.no-background-message');
613
+ const backgroundDataInfo = document.querySelector('.background-data-info');
614
+
615
+ if (!noBackgroundMessage || !backgroundDataInfo) {
616
+ console.warn('Background message elements not found');
617
+ return;
618
+ }
619
+
620
+ if (hasContent || forceShow) {
621
+ console.log("Revealing Background Data section in World Panel");
622
+ // We have content, so show the data info and hide the "no data" message
623
+ noBackgroundMessage.style.display = 'none';
624
+ backgroundDataInfo.style.display = 'block';
625
+ } else {
626
+ console.log("Hiding Background Data section in World Panel");
627
+ // No content, show the "no data" message and hide the data info
628
+ noBackgroundMessage.style.display = 'block';
629
+ backgroundDataInfo.style.display = 'none';
630
+ }
631
+ }
632
+
633
+ /**
634
+ * Toggle visibility of lighting messages based on whether environment lighting is loaded
635
+ * @param {boolean} hasContent - Whether environment lighting is loaded
636
+ * @param {boolean} [forceShow=false] - Force show/hide the data info regardless of content status
637
+ */
638
+ function toggleLightingMessages(hasContent, forceShow = false) {
639
+ const noDataMessage = document.querySelector('.no-data-message');
640
+ const lightingDataInfo = document.querySelector('.lighting-data-info');
641
+
642
+ if (!noDataMessage || !lightingDataInfo) {
643
+ console.warn('Lighting message elements not found');
644
+ return;
645
+ }
646
+
647
+ if (hasContent || forceShow) {
648
+ noDataMessage.style.display = 'none';
649
+ lightingDataInfo.style.display = 'block';
650
+ } else {
651
+ noDataMessage.style.display = 'block';
652
+ lightingDataInfo.style.display = 'none';
653
+ }
654
+ }
655
+
656
+ /**
657
+ * Update lighting message visibility based on whether environment lighting is loaded
658
+ */
659
+ function updateLightingMessage() {
660
+ const state = getState();
661
+
662
+ const hasEnvironment = state.scene && state.scene.environment;
663
+
664
+ // Use the standardized toggle function
665
+ toggleLightingMessages(hasEnvironment);
666
+
667
+ // Update slider visibility based on environment presence
668
+ updateSliderVisibility(hasEnvironment);
669
+ }
670
+
671
+ /**
672
+ * Update slider visibility based on whether HDR/EXR is loaded
673
+ * @param {boolean} hasEnvironment - Whether environment lighting is loaded
674
+ */
675
+ function updateSliderVisibility(hasEnvironment) {
676
+ const ambientControl = document.querySelector('.ambient-control');
677
+ const directionalControl = document.querySelector('.directional-control');
678
+ const exposureControl = document.querySelector('.exposure-control');
679
+
680
+ if (!ambientControl || !directionalControl || !exposureControl) {
681
+ console.warn('Could not find slider controls for visibility update');
682
+ return;
683
+ }
684
+
685
+ if (hasEnvironment) {
686
+ // When HDR/EXR is loaded: hide ambient/directional, show exposure
687
+ ambientControl.style.display = 'none';
688
+ directionalControl.style.display = 'none';
689
+ exposureControl.style.display = 'flex';
690
+ } else {
691
+ // When no HDR/EXR is loaded: show ambient/directional, hide exposure
692
+ ambientControl.style.display = 'flex';
693
+ directionalControl.style.display = 'flex';
694
+ exposureControl.style.display = 'none';
695
+ }
696
+ }
697
+
698
+ /**
699
+ * Update the lighting info display with metadata
700
+ * @param {Object} metadata - The HDR/EXR metadata
701
+ */
702
+ export function updateLightingInfo(metadata) {
703
+ console.log('Updating lighting info with metadata:', metadata.fileName, metadata.type);
704
+
705
+ // Store the metadata for later use if the panel isn't ready yet
706
+ currentLightingMetadata = metadata;
707
+
708
+ // Try to initialize the panel if not already done
709
+ if (!controlsInitialized) {
710
+ console.warn('Panel not initialized yet, storing metadata for later');
711
+ return;
712
+ }
713
+
714
+ // Find the UI elements
715
+ const filenameEl = document.getElementById('lighting-filename');
716
+ const typeEl = document.getElementById('lighting-type');
717
+ const resolutionEl = document.getElementById('lighting-resolution');
718
+ const sizeEl = document.getElementById('lighting-size');
719
+ const rangeEl = document.getElementById('lighting-range');
720
+ const luminanceEl = document.getElementById('lighting-luminance');
721
+ const softwareEl = document.getElementById('lighting-software');
722
+
723
+ // Make sure all elements exist
724
+ if (!filenameEl || !typeEl || !resolutionEl || !sizeEl || !rangeEl || !luminanceEl || !softwareEl) {
725
+ console.error('Cannot update lighting info: UI elements not found');
726
+ return;
727
+ }
728
+
729
+ // Update the UI with metadata
730
+ filenameEl.textContent = metadata.fileName || '-';
731
+ typeEl.textContent = metadata.type || '-';
732
+
733
+ const width = metadata.dimensions?.width || 0;
734
+ const height = metadata.dimensions?.height || 0;
735
+ resolutionEl.textContent = (width && height) ? `${width} × ${height}` : '-';
736
+
737
+ const fileSizeMB = metadata.fileSizeBytes ? (metadata.fileSizeBytes / 1024 / 1024).toFixed(2) + ' MB' : '-';
738
+ sizeEl.textContent = fileSizeMB;
739
+
740
+ rangeEl.textContent = metadata.dynamicRange ? metadata.dynamicRange.toFixed(2) + ' stops' : '-';
741
+ luminanceEl.textContent = metadata.maxLuminance ? metadata.maxLuminance.toFixed(2) : '-';
742
+ softwareEl.textContent = metadata.creationSoftware || '-';
743
+
744
+ // Show the lighting info section and hide the no data message
745
+ console.log('Showing lighting data info and hiding no data message');
746
+ toggleLightingMessages(true);
747
+
748
+ // Make the HDR option visible since we have environment lighting
749
+ toggleOptionVisibility('hdr-option', true);
750
+ console.log('Making HDR/EXR radio option visible');
751
+
752
+ // Update slider visibility - we have HDR/EXR data
753
+ updateSliderVisibility(true);
754
+
755
+ // IMPORTANT: We no longer auto-select the HDR/EXR option
756
+ // The "None" option will remain selected by default
757
+ // Only update the radio option if it doesn't exist yet
758
+ console.log('HDR/EXR radio option is available but not auto-selected');
759
+
760
+ // Make sure any collapsible content is still properly collapsed
761
+ const metadataContents = document.querySelectorAll('.metadata-content');
762
+ if (metadataContents && metadataContents.length > 0) {
763
+ console.log('Ensuring collapsible content is collapsed initially');
764
+ metadataContents.forEach(content => {
765
+ content.style.display = 'none';
766
+ });
767
+
768
+ // Make sure any indicators show the right symbol
769
+ const indicators = document.querySelectorAll('.collapse-indicator');
770
+ if (indicators && indicators.length > 0) {
771
+ indicators.forEach(indicator => {
772
+ // Always set to '+' to indicate collapsed state
773
+ indicator.textContent = '[+]';
774
+ });
775
+ }
776
+ }
777
+
778
+ // Try to get environment texture and render it
779
+ const state = getState();
780
+ if (state.scene && state.scene.environment) {
781
+ console.log('Found environment texture in scene, rendering preview');
782
+
783
+ // Store the environment texture for later use
784
+ environmentTexture = state.scene.environment;
785
+
786
+ // Render the preview
787
+ renderEnvironmentPreview(environmentTexture);
788
+ }
789
+ }
790
+
791
+ /**
792
+ * Render the HDR/EXR environment texture preview on canvas
793
+ * @param {THREE.Texture} texture - The environment texture to render
794
+ * @param {HTMLCanvasElement} [externalCanvas] - Optional external canvas to render on
795
+ * @param {HTMLElement} [externalNoImageMessage] - Optional external message element
796
+ * @returns {boolean} - Whether preview was rendered successfully
797
+ */
798
+ export function renderEnvironmentPreview(texture, externalCanvas, externalNoImageMessage) {
799
+ // Look for the canvas element - either use provided external one or find in DOM
800
+ const canvas = externalCanvas || document.getElementById('hdr-preview-canvas');
801
+ const noImageMessage = externalNoImageMessage || document.getElementById('no-image-message');
802
+
803
+ // If canvas not found, panel may not be initialized yet
804
+ if (!canvas) {
805
+ console.error('HDR preview canvas not found, cannot render preview');
806
+ return false;
807
+ }
808
+
809
+ // If texture doesn't have image data, show error message
810
+ if (!texture || !texture.image) {
811
+ console.warn('No texture or image data found:', texture);
812
+ showNoImageMessage(canvas, noImageMessage, 'No image data available.');
813
+ return false;
814
+ }
815
+
816
+ console.log('Rendering environment texture preview as 3D sphere');
817
+
818
+ // Hide the no image message
819
+ if (noImageMessage) noImageMessage.style.display = 'none';
820
+
821
+ // Make canvas visible
822
+ canvas.style.display = 'block';
823
+
824
+ // Show background data info and hide no background message
825
+ toggleBackgroundMessages(true);
826
+
827
+ // Set canvas size (always square for the sphere preview)
828
+ const previewSize = externalCanvas ? Math.max(canvas.width, canvas.height) : 260;
829
+
830
+ // Always enforce a square aspect ratio regardless of container dimensions
831
+ canvas.width = previewSize;
832
+ canvas.height = previewSize;
833
+
834
+ // Apply class to ensure proper scaling with CSS
835
+ canvas.classList.add('square-preview');
836
+
837
+ try {
838
+ // Create a mini Three.js scene for the sphere preview
839
+ // Use the imported THREE directly
840
+ createSpherePreview(THREE, texture, canvas, noImageMessage);
841
+
842
+ return true;
843
+ } catch (error) {
844
+ console.error('Error rendering HDR preview as sphere:', error);
845
+ }
846
+
847
+ return true;
848
+ }
849
+
850
+ /**
851
+ * Create a 3D sphere preview with the environment texture
852
+ * @param {Object} THREE - The Three.js library
853
+ * @param {THREE.Texture} texture - The environment texture
854
+ * @param {HTMLCanvasElement} canvas - The canvas element
855
+ * @param {HTMLElement} noImageMessage - The no image message element
856
+ */
857
+ export function createSpherePreview(THREE, texture, canvas, noImageMessage) {
858
+ try {
859
+ // Create a mini renderer
860
+ const renderer = new THREE.WebGLRenderer({
861
+ canvas: canvas,
862
+ alpha: true,
863
+ antialias: true
864
+ });
865
+ renderer.setSize(canvas.width, canvas.height);
866
+ renderer.setPixelRatio(window.devicePixelRatio);
867
+
868
+ // Critical for HDR/EXR: set proper encoding and tone mapping
869
+ // Update to use modern THREE.js properties
870
+ renderer.outputColorSpace = THREE.SRGBColorSpace;
871
+ renderer.toneMapping = THREE.ACESFilmicToneMapping;
872
+ renderer.toneMappingExposure = 1.0;
873
+
874
+ // Create a mini scene
875
+ const scene = new THREE.Scene();
876
+ scene.background = new THREE.Color(0x111111); // Dark background
877
+
878
+ // Set the environment texture for the scene - this affects reflective materials
879
+ texture.mapping = THREE.EquirectangularReflectionMapping;
880
+ scene.environment = texture;
881
+
882
+ // Create a mini camera - move it back a bit more to make the sphere appear smaller
883
+ const camera = new THREE.PerspectiveCamera(40, 1, 0.1, 100);
884
+ camera.position.z = 3.2; // Increased camera distance to make sphere smaller
885
+
886
+ // Create a 3D sphere with high polygon count for smooth reflections
887
+ // Make the sphere slightly smaller
888
+ const sphereGeometry = new THREE.SphereGeometry(0.8, 64, 64);
889
+
890
+ // Create a metallic material
891
+ const metallicMaterial = new THREE.MeshStandardMaterial({
892
+ color: 0xffffff,
893
+ metalness: 1.0,
894
+ roughness: 0.05,
895
+ envMapIntensity: 1.0
896
+ });
897
+
898
+ // Create and add the metallic sphere
899
+ const sphere = new THREE.Mesh(sphereGeometry, metallicMaterial);
900
+ scene.add(sphere);
901
+
902
+ // Add some lighting - even with environment lighting, we need some direct light
903
+ // for better highlights
904
+ const ambientLight = new THREE.AmbientLight(0xffffff, 0.2);
905
+ scene.add(ambientLight);
906
+
907
+ // Add a directional light for highlights
908
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 1.0);
909
+ directionalLight.position.set(1, 1, 1);
910
+ scene.add(directionalLight);
911
+
912
+ // Add a point light for additional dimension
913
+ const pointLight = new THREE.PointLight(0xffffff, 0.5);
914
+ pointLight.position.set(-1, 1, 0.5);
915
+ scene.add(pointLight);
916
+
917
+ let animationFrameId;
918
+ const clock = new THREE.Clock();
919
+
920
+ // Add a simple message to indicate the sphere is interactive
921
+ const interactiveHint = document.createElement('div');
922
+ interactiveHint.style.position = 'absolute';
923
+ interactiveHint.style.bottom = '5px';
924
+ interactiveHint.style.left = '50%';
925
+ interactiveHint.style.transform = 'translateX(-50%)';
926
+ interactiveHint.style.fontSize = '10px';
927
+ interactiveHint.style.color = 'rgba(255,255,255,0.7)';
928
+ interactiveHint.style.pointerEvents = 'none';
929
+ canvas.parentElement.style.position = 'relative';
930
+ canvas.parentElement.appendChild(interactiveHint);
931
+
932
+ // Initial slight rotation to show it's 3D
933
+ sphere.rotation.y = Math.PI / 6;
934
+ sphere.rotation.x = Math.PI / 12;
935
+
936
+ // Create dedicated OrbitControls for the preview sphere
937
+ // We need to dynamically import it for this specific use case
938
+ let previewControls = null;
939
+ let previewControlsReady = false;
940
+
941
+ // Function to dynamically import and create OrbitControls
942
+ const initPreviewControls = async () => {
943
+ try {
944
+ // Use dynamic import to get OrbitControls
945
+ const { OrbitControls } = await import('three/addons/controls/OrbitControls.js');
946
+
947
+ // Create dedicated controls for this preview only
948
+ previewControls = new OrbitControls(camera, canvas);
949
+
950
+ // Configure the preview controls
951
+ previewControls.enableDamping = true;
952
+ previewControls.dampingFactor = 0.05;
953
+ previewControls.rotateSpeed = 1.0;
954
+ previewControls.enableZoom = false;
955
+ previewControls.enablePan = false;
956
+
957
+ // Mark as ready
958
+ previewControlsReady = true;
959
+
960
+ console.log('Preview controls initialized successfully');
961
+ } catch (error) {
962
+ console.error('Failed to initialize preview controls:', error);
963
+ }
964
+ };
965
+
966
+ // Initialize the controls
967
+ initPreviewControls();
968
+
969
+ function renderSphere() {
970
+ animationFrameId = requestAnimationFrame(renderSphere);
971
+
972
+ const delta = clock.getDelta();
973
+
974
+ // Update preview controls if available
975
+ if (previewControlsReady && previewControls) {
976
+ previewControls.update();
977
+ } else {
978
+ // If no controls yet, add a very slow rotation to show it's 3D
979
+ sphere.rotation.y += delta * 0.1;
980
+ }
981
+
982
+ renderer.render(scene, camera);
983
+ }
984
+
985
+ // Start the animation
986
+ renderSphere();
987
+
988
+ // Store the animation frame ID for cleanup
989
+ canvas.setAttribute('data-animation-id', animationFrameId);
990
+
991
+ // Add cleanup function when tab changes or element is removed
992
+ const cleanup = () => {
993
+ if (animationFrameId) {
994
+ cancelAnimationFrame(animationFrameId);
995
+ animationFrameId = null;
996
+ }
997
+
998
+ // Remove the interactive hint
999
+ if (interactiveHint && interactiveHint.parentElement) {
1000
+ interactiveHint.parentElement.removeChild(interactiveHint);
1001
+ }
1002
+
1003
+ // Dispose of the preview controls if they exist
1004
+ if (previewControls) {
1005
+ previewControls.dispose();
1006
+ previewControls = null;
1007
+ }
1008
+
1009
+ // Proper disposal of Three.js resources
1010
+ renderer.dispose();
1011
+ sphereGeometry.dispose();
1012
+ metallicMaterial.dispose();
1013
+
1014
+ // Remove references
1015
+ sphere.geometry = null;
1016
+ sphere.material = null;
1017
+ scene.remove(sphere);
1018
+ };
1019
+
1020
+ // Store cleanup function for later use
1021
+ canvas.cleanup = cleanup;
1022
+ console.log('Successfully rendered environment map as interactive 3D sphere');
1023
+ } catch (error) {
1024
+ console.error('Error in createSpherePreview:', error);
1025
+ }
1026
+ }
1027
+
1028
+ /**
1029
+ * Show "No image data" message
1030
+ * @param {HTMLCanvasElement} canvas - The canvas element
1031
+ * @param {HTMLElement} messageEl - The message element to show
1032
+ * @param {string} message - The error message to display
1033
+ */
1034
+ export function showNoImageMessage(canvas, messageEl, message = 'No image data available.') {
1035
+ // Hide canvas
1036
+ if (canvas) {
1037
+ canvas.style.display = 'none';
1038
+ }
1039
+
1040
+ // Set error message if provided
1041
+ if (messageEl) {
1042
+ messageEl.textContent = message;
1043
+ }
1044
+
1045
+ // Use the standardized toggle function to show/hide appropriate messages
1046
+ toggleLightingMessages(false);
1047
+ }
1048
+
1049
+ /**
1050
+ * Clear the lighting info display
1051
+ */
1052
+ function clearLightingInfo() {
1053
+ // Clear the stored metadata
1054
+ currentLightingMetadata = null;
1055
+ environmentTexture = null;
1056
+
1057
+ // Find the UI elements
1058
+ const filenameEl = document.getElementById('lighting-filename');
1059
+ const typeEl = document.getElementById('lighting-type');
1060
+ const resolutionEl = document.getElementById('lighting-resolution');
1061
+ const sizeEl = document.getElementById('lighting-size');
1062
+ const rangeEl = document.getElementById('lighting-range');
1063
+ const luminanceEl = document.getElementById('lighting-luminance');
1064
+ const softwareEl = document.getElementById('lighting-software');
1065
+
1066
+ // Reset all values
1067
+ if (filenameEl) filenameEl.textContent = '-';
1068
+ if (typeEl) typeEl.textContent = '-';
1069
+ if (resolutionEl) resolutionEl.textContent = '-';
1070
+ if (sizeEl) sizeEl.textContent = '-';
1071
+ if (rangeEl) rangeEl.textContent = '-';
1072
+ if (luminanceEl) luminanceEl.textContent = '-';
1073
+ if (softwareEl) softwareEl.textContent = '-';
1074
+
1075
+ // Hide lighting info and show no data message
1076
+ toggleLightingMessages(false);
1077
+
1078
+ // Hide the HDR/EXR radio option since we no longer have environment lighting
1079
+ toggleOptionVisibility('hdr-option', false);
1080
+ console.log('Hiding HDR/EXR radio option');
1081
+
1082
+ // Hide background info and show no background message
1083
+ toggleBackgroundMessages(false);
1084
+
1085
+ // Reset collapse state
1086
+ const metadataContents = document.querySelectorAll('.metadata-content');
1087
+ if (metadataContents) {
1088
+ metadataContents.forEach(content => {
1089
+ content.style.display = 'none';
1090
+ });
1091
+ }
1092
+
1093
+ const indicators = document.querySelectorAll('.collapse-indicator');
1094
+ if (indicators) {
1095
+ indicators.forEach(indicator => {
1096
+ indicator.textContent = '[+]';
1097
+ });
1098
+ }
1099
+
1100
+ // Update slider visibility - we have no HDR/EXR data
1101
+ updateSliderVisibility(false);
1102
+
1103
+ // Clean up any ThreeJS resources
1104
+ const canvas = document.getElementById('hdr-preview-canvas');
1105
+ if (canvas) {
1106
+ // Execute cleanup function if it exists
1107
+ if (typeof canvas.cleanup === 'function') {
1108
+ canvas.cleanup();
1109
+ canvas.cleanup = null;
1110
+ }
1111
+
1112
+ // Cancel any animation frame
1113
+ const animationId = canvas.getAttribute('data-animation-id');
1114
+ if (animationId) {
1115
+ cancelAnimationFrame(parseInt(animationId, 10));
1116
+ canvas.removeAttribute('data-animation-id');
1117
+ }
1118
+
1119
+ // Clear the canvas
1120
+ const ctx = canvas.getContext('2d');
1121
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
1122
+ canvas.style.display = 'none';
1123
+ }
1124
+ }
1125
+
1126
+ /**
1127
+ * Update the background info display with metadata
1128
+ * @param {Object} metadata - The background image metadata
1129
+ * @param {boolean} [skipRendering=false] - Whether to skip rendering the preview
1130
+ */
1131
+ export function updateBackgroundInfo(metadata, skipRendering = false) {
1132
+ console.log('Updating background info with metadata:', metadata.fileName, metadata.type);
1133
+
1134
+ // Store the metadata for later use if the panel isn't ready yet
1135
+ currentBackgroundMetadata = metadata;
1136
+
1137
+ // Try to initialize the panel if not already done
1138
+ if (!controlsInitialized) {
1139
+ console.warn('Panel not initialized yet, storing metadata for later');
1140
+ return;
1141
+ }
1142
+
1143
+ // Find the UI elements
1144
+ const filenameEl = document.getElementById('bg-filename');
1145
+ const typeEl = document.getElementById('bg-type');
1146
+ const resolutionEl = document.getElementById('bg-resolution');
1147
+ const sizeEl = document.getElementById('bg-size');
1148
+
1149
+ // Make sure all elements exist
1150
+ if (!filenameEl || !typeEl || !resolutionEl || !sizeEl) {
1151
+ console.error('Cannot update background info: UI elements not found');
1152
+ return;
1153
+ }
1154
+
1155
+ // Update the UI with metadata
1156
+ filenameEl.textContent = metadata.fileName || '-';
1157
+ typeEl.textContent = metadata.type || '-';
1158
+
1159
+ const width = metadata.dimensions?.width || 0;
1160
+ const height = metadata.dimensions?.height || 0;
1161
+ resolutionEl.textContent = (width && height) ? `${width} × ${height}` : '-';
1162
+
1163
+ const fileSizeMB = metadata.fileSizeBytes ? (metadata.fileSizeBytes / 1024 / 1024).toFixed(2) + ' MB' : '-';
1164
+ sizeEl.textContent = fileSizeMB;
1165
+
1166
+ // Make the background option visible since we have a background image
1167
+ toggleOptionVisibility('background-option', true);
1168
+ console.log('Making Background Image radio option visible');
1169
+
1170
+ // Respect the current radio selection without changing it on automatic background updates
1171
+ // Only auto-select the option if this is a fresh load (no option selected yet)
1172
+ if (currentBackgroundOption === null || currentBackgroundOption === undefined) {
1173
+ // First-time initialization - default to 'none' but don't change scene background
1174
+ const noneRadio = document.querySelector('input[name="bg-option"][value="none"]');
1175
+ if (noneRadio) {
1176
+ noneRadio.checked = true;
1177
+ currentBackgroundOption = 'none';
1178
+ }
1179
+ } else {
1180
+ // Make sure the correct radio button matches our stored selection
1181
+ const selectedRadio = document.querySelector(`input[name="bg-option"][value="${currentBackgroundOption}"]`);
1182
+ if (selectedRadio) {
1183
+ selectedRadio.checked = true;
1184
+ }
1185
+ }
1186
+
1187
+ // Make sure any collapsible content is still properly collapsed
1188
+ const metadataContents = document.querySelectorAll('.metadata-content');
1189
+ if (metadataContents && metadataContents.length > 0) {
1190
+ console.log('Ensuring collapsible content is collapsed initially');
1191
+ metadataContents.forEach(content => {
1192
+ if (!content.classList.contains('active')) {
1193
+ content.style.display = 'none';
1194
+ }
1195
+ });
1196
+
1197
+ // Make sure any indicators show the right symbol
1198
+ const indicators = document.querySelectorAll('.collapse-indicator');
1199
+ if (indicators && indicators.length > 0) {
1200
+ indicators.forEach(indicator => {
1201
+ // Always set to '+' to indicate collapsed state
1202
+ indicator.textContent = '[+]';
1203
+ });
1204
+ }
1205
+ }
1206
+
1207
+ // Only render preview if not skipped and we have a file in state
1208
+ if (!skipRendering) {
1209
+ const state = getState();
1210
+ if (state.backgroundFile && !backgroundTexture) {
1211
+ console.log('Found background file in state, rendering preview');
1212
+ // Create a preview of the background image
1213
+ renderBackgroundPreview(state.backgroundFile);
1214
+ }
1215
+ }
1216
+ }
1217
+
1218
+ /**
1219
+ * Render the background image preview on canvas
1220
+ * @param {File|Blob|THREE.Texture} fileOrTexture - The background file or texture to render
1221
+ * @returns {boolean} - Whether preview was rendered successfully
1222
+ */
1223
+ export function renderBackgroundPreview(fileOrTexture) {
1224
+ // Simple prevention of recursive calls
1225
+ if (isLoadingEnvironmentFile) {
1226
+ console.log('Already loading an HDR/EXR file, preventing recursive calls');
1227
+ return true;
1228
+ }
1229
+
1230
+ // Look for the canvas element
1231
+ const canvas = document.getElementById('bg-preview-canvas');
1232
+ const noImageMessage = document.getElementById('no-bg-image-message');
1233
+
1234
+ // If canvas not found, panel may not be initialized yet
1235
+ if (!canvas) {
1236
+ console.error('Background preview canvas not found, cannot render preview');
1237
+ return false;
1238
+ }
1239
+
1240
+ // Store the texture for later use
1241
+ backgroundTexture = fileOrTexture;
1242
+
1243
+ // If there's no file or texture, show error message
1244
+ if (!fileOrTexture) {
1245
+ console.warn('No background file or texture provided');
1246
+ showNoBackgroundImageMessage(canvas, noImageMessage, 'No image data available.');
1247
+ return false;
1248
+ }
1249
+
1250
+ console.log('Rendering background image preview for:',
1251
+ typeof fileOrTexture === 'object' && fileOrTexture.isTexture
1252
+ ? 'Texture object'
1253
+ : (fileOrTexture.name || 'Unknown file'));
1254
+
1255
+ // Hide the no image message
1256
+ if (noImageMessage) noImageMessage.style.display = 'none';
1257
+
1258
+ // Make canvas visible
1259
+ canvas.style.display = 'block';
1260
+
1261
+ // Show background data info and hide no background message
1262
+ toggleBackgroundMessages(true);
1263
+
1264
+ try {
1265
+ // Different handling based on what type of data we have
1266
+ if (fileOrTexture instanceof File || fileOrTexture instanceof Blob) {
1267
+ const fileName = fileOrTexture.name ? fileOrTexture.name.toLowerCase() : '';
1268
+ const isEXR = fileName.endsWith('.exr');
1269
+ const isHDR = fileName.endsWith('.hdr');
1270
+
1271
+ // Special handling for EXR/HDR files
1272
+ if (isEXR || isHDR) {
1273
+ console.log(`Handling ${isEXR ? 'EXR' : 'HDR'} file with special loader`);
1274
+
1275
+ // Set loading flag to prevent recursion
1276
+ isLoadingEnvironmentFile = true;
1277
+
1278
+ // Create metadata early for better UX
1279
+ if (!currentBackgroundMetadata) {
1280
+ currentBackgroundMetadata = {
1281
+ fileName: fileOrTexture.name,
1282
+ type: isEXR ? 'EXR' : 'HDR',
1283
+ dimensions: { width: 0, height: 0 }, // Will be updated when loaded
1284
+ fileSizeBytes: fileOrTexture.size
1285
+ };
1286
+ updateBackgroundInfo(currentBackgroundMetadata);
1287
+ }
1288
+
1289
+ // Set canvas to square for sphere preview
1290
+ const previewSize = 260;
1291
+ canvas.width = previewSize;
1292
+ canvas.height = previewSize;
1293
+
1294
+ // Load the file with the appropriate loader
1295
+ const loadHDRorEXR = async () => {
1296
+ try {
1297
+ // Use the statically imported THREE
1298
+ // Choose the right loader based on file type
1299
+ let loader;
1300
+ if (isEXR) {
1301
+ const { EXRLoader } = await import('three/addons/loaders/EXRLoader.js');
1302
+ loader = new EXRLoader();
1303
+ } else {
1304
+ const { RGBELoader } = await import('three/addons/loaders/RGBELoader.js');
1305
+ loader = new RGBELoader();
1306
+ }
1307
+
1308
+ // Create a file URL
1309
+ const url = URL.createObjectURL(fileOrTexture);
1310
+
1311
+ // Load the texture
1312
+ loader.load(url,
1313
+ // onLoad callback
1314
+ (texture) => {
1315
+ // Update metadata with actual dimensions
1316
+ if (currentBackgroundMetadata) {
1317
+ currentBackgroundMetadata.dimensions = {
1318
+ width: texture.image.width,
1319
+ height: texture.image.height
1320
+ };
1321
+ // Important: skip updateBackgroundInfo's render step
1322
+ // to prevent recursive loading
1323
+ // Just update UI fields directly
1324
+ const widthHeight = `${texture.image.width} × ${texture.image.height}`;
1325
+ const resolutionEl = document.getElementById('bg-resolution');
1326
+ if (resolutionEl) {
1327
+ resolutionEl.textContent = widthHeight;
1328
+ }
1329
+ }
1330
+
1331
+ // Create sphere preview with the texture
1332
+ createSpherePreview(THREE, texture, canvas, noImageMessage);
1333
+
1334
+ // Clean up URL object
1335
+ URL.revokeObjectURL(url);
1336
+
1337
+ // Clear the loading flag
1338
+ isLoadingEnvironmentFile = false;
1339
+ },
1340
+ // onProgress callback
1341
+ (xhr) => {
1342
+ const percentComplete = xhr.loaded / xhr.total * 100;
1343
+ console.log(`${isEXR ? 'EXR' : 'HDR'} loading: ${Math.round(percentComplete)}%`);
1344
+ },
1345
+ // onError callback
1346
+ (error) => {
1347
+ console.error(`Error loading ${isEXR ? 'EXR' : 'HDR'} file:`, error);
1348
+ showNoBackgroundImageMessage(canvas, noImageMessage, `Error loading ${isEXR ? 'EXR' : 'HDR'} file`);
1349
+ URL.revokeObjectURL(url);
1350
+
1351
+ // Clear the loading flag on error
1352
+ isLoadingEnvironmentFile = false;
1353
+ }
1354
+ );
1355
+ } catch (error) {
1356
+ console.error(`Error in ${isEXR ? 'EXR' : 'HDR'} loading:`, error);
1357
+ showNoBackgroundImageMessage(canvas, noImageMessage, `Error: Could not load ${isEXR ? 'EXR' : 'HDR'} file`);
1358
+
1359
+ // Clear the loading flag on error
1360
+ isLoadingEnvironmentFile = false;
1361
+ }
1362
+ };
1363
+
1364
+ // Start loading
1365
+ loadHDRorEXR();
1366
+ return true;
1367
+ }
1368
+
1369
+ // For standard image files, create a texture and use 3D sphere preview
1370
+ const url = URL.createObjectURL(fileOrTexture);
1371
+ const img = new Image();
1372
+
1373
+ img.onload = function() {
1374
+ // Get file dimensions
1375
+ const width = img.width;
1376
+ const height = img.height;
1377
+
1378
+ // Update metadata if needed
1379
+ if (!currentBackgroundMetadata) {
1380
+ currentBackgroundMetadata = {
1381
+ fileName: fileOrTexture.name,
1382
+ type: fileOrTexture.type || 'Image',
1383
+ dimensions: { width, height },
1384
+ fileSizeBytes: fileOrTexture.size
1385
+ };
1386
+ updateBackgroundInfo(currentBackgroundMetadata);
1387
+ }
1388
+
1389
+ // Set canvas size (square for the sphere preview)
1390
+ const previewSize = 260;
1391
+ canvas.width = previewSize;
1392
+ canvas.height = previewSize;
1393
+
1394
+ // Create texture from loaded image and create sphere preview
1395
+ try {
1396
+ // Use the statically imported THREE
1397
+ const textureLoader = new THREE.TextureLoader();
1398
+ const texture = textureLoader.load(url);
1399
+
1400
+ // Use the same sphere preview as for environment maps
1401
+ createSpherePreview(THREE, texture, canvas, noImageMessage);
1402
+
1403
+ // Clean up URL object after texture is loaded
1404
+ URL.revokeObjectURL(url);
1405
+ } catch (error) {
1406
+ console.error('Error creating sphere preview:', error);
1407
+
1408
+ // Fall back to 2D canvas if sphere preview fails
1409
+ const ctx = canvas.getContext('2d');
1410
+ canvas.width = width;
1411
+ canvas.height = height;
1412
+ ctx.drawImage(img, 0, 0, width, height);
1413
+ URL.revokeObjectURL(url);
1414
+ }
1415
+ };
1416
+
1417
+ img.onerror = function() {
1418
+ console.error('Error loading background image');
1419
+ showNoBackgroundImageMessage(canvas, noImageMessage, 'Error loading image');
1420
+ URL.revokeObjectURL(url);
1421
+ };
1422
+
1423
+ img.src = url;
1424
+ return true;
1425
+ } else if (typeof fileOrTexture === 'object' && fileOrTexture.isTexture) {
1426
+ // For THREE.Texture objects, use sphere preview directly
1427
+
1428
+ // Create metadata if not present
1429
+ if (!currentBackgroundMetadata) {
1430
+ currentBackgroundMetadata = {
1431
+ fileName: fileOrTexture.userData?.fileName || 'Background Texture',
1432
+ type: fileOrTexture.isHDRTexture ? 'HDR Texture' : 'Standard Texture',
1433
+ dimensions: {
1434
+ width: fileOrTexture.image ? fileOrTexture.image.width : 0,
1435
+ height: fileOrTexture.image ? fileOrTexture.image.height : 0
1436
+ },
1437
+ fileSizeBytes: 0 // Unknown size
1438
+ };
1439
+ updateBackgroundInfo(currentBackgroundMetadata);
1440
+ }
1441
+
1442
+ // Set canvas size (square for the sphere preview)
1443
+ const previewSize = 260;
1444
+ canvas.width = previewSize;
1445
+ canvas.height = previewSize;
1446
+
1447
+ try {
1448
+ // Use the statically imported THREE
1449
+ createSpherePreview(THREE, fileOrTexture, canvas, noImageMessage);
1450
+ return true;
1451
+ } catch (error) {
1452
+ console.error('Error creating sphere preview for texture:', error);
1453
+ }
1454
+ return true;
1455
+ } else {
1456
+ console.error('Unknown fileOrTexture type:', fileOrTexture);
1457
+ showNoBackgroundImageMessage(canvas, noImageMessage, 'Unsupported background format.');
1458
+ return false;
1459
+ }
1460
+
1461
+ } catch (error) {
1462
+ console.error('Error in renderBackgroundPreview:', error);
1463
+ showNoBackgroundImageMessage(canvas, noImageMessage, `Error: ${error.message}`);
1464
+ return false;
1465
+ }
1466
+ }
1467
+
1468
+ /**
1469
+ * Show "No background image data" message
1470
+ * @param {HTMLCanvasElement} canvas - The canvas element
1471
+ * @param {HTMLElement} messageEl - The message element to show
1472
+ * @param {string} message - The error message to display
1473
+ */
1474
+ export function showNoBackgroundImageMessage(canvas, messageEl, message = 'No image data available.') {
1475
+ // Hide canvas
1476
+ if (canvas) {
1477
+ canvas.style.display = 'none';
1478
+ }
1479
+
1480
+ // Set error message if provided
1481
+ if (messageEl) {
1482
+ messageEl.textContent = message;
1483
+ }
1484
+
1485
+ // Use the standardized toggle function to show/hide appropriate messages
1486
+ toggleBackgroundMessages(false);
1487
+ }
1488
+
1489
+ /**
1490
+ * Update the World panel with current state
1491
+ */
1492
+ export function updateWorldPanel() {
1493
+ // If still not initialized, log error and return
1494
+ if (!controlsInitialized) {
1495
+ console.error('Cannot update World panel: not initialized');
1496
+ return;
1497
+ }
1498
+
1499
+ const state = getState();
1500
+
1501
+ // Update lighting controls with current values
1502
+ const ambientIntensityControl = document.getElementById('ambient-light-intensity');
1503
+ const directionalIntensityControl = document.getElementById('directional-light-intensity');
1504
+ const exposureControl = document.getElementById('exposure-value');
1505
+
1506
+ if (state.ambientLight && ambientIntensityControl) {
1507
+ ambientIntensityControl.value = state.ambientLight.intensity;
1508
+ const valueDisplay = ambientIntensityControl.previousElementSibling.querySelector('.value-display');
1509
+ if (valueDisplay) {
1510
+ valueDisplay.textContent = state.ambientLight.intensity.toFixed(1);
1511
+ }
1512
+ }
1513
+
1514
+ if (state.directionalLight && directionalIntensityControl) {
1515
+ directionalIntensityControl.value = state.directionalLight.intensity;
1516
+ const valueDisplay = directionalIntensityControl.previousElementSibling.querySelector('.value-display');
1517
+ if (valueDisplay) {
1518
+ valueDisplay.textContent = state.directionalLight.intensity.toFixed(1);
1519
+ }
1520
+ }
1521
+
1522
+ if (state.renderer && exposureControl) {
1523
+ exposureControl.value = state.renderer.toneMappingExposure || 1.0;
1524
+ const valueDisplay = exposureControl.previousElementSibling.querySelector('.value-display');
1525
+ if (valueDisplay) {
1526
+ valueDisplay.textContent = (state.renderer.toneMappingExposure || 1.0).toFixed(1);
1527
+ }
1528
+ }
1529
+
1530
+ // Update lighting message visibility
1531
+ updateLightingMessage();
1532
+
1533
+ // Get the current state for file existence - be more thorough with checks
1534
+ const hasEnvironmentTexture = state.lightingFile !== null ||
1535
+ (state.scene && state.scene.environment) ||
1536
+ currentLightingMetadata !== null;
1537
+
1538
+ const hasBackgroundTexture = state.backgroundTexture !== null ||
1539
+ state.backgroundFile !== null ||
1540
+ currentBackgroundMetadata !== null;
1541
+
1542
+ // Log the current state for debugging
1543
+ console.log('Updating world panel with state:', {
1544
+ hasEnvironmentTexture,
1545
+ hasBackgroundTexture,
1546
+ lightingFile: state.lightingFile ? state.lightingFile.name : null,
1547
+ backgroundFile: state.backgroundFile ? state.backgroundFile.name : null,
1548
+ environment: state.scene && state.scene.environment ? 'present' : null,
1549
+ backgroundTexture: state.backgroundTexture ? 'present' : null,
1550
+ metadata: {
1551
+ lighting: currentLightingMetadata ? 'present' : null,
1552
+ background: currentBackgroundMetadata ? 'present' : null
1553
+ }
1554
+ });
1555
+
1556
+ // Always update radio button visibility based on current state
1557
+ toggleOptionVisibility('hdr-option', hasEnvironmentTexture);
1558
+ toggleOptionVisibility('background-option', hasBackgroundTexture);
1559
+
1560
+ console.log(`Radio button visibility updated - HDR: ${hasEnvironmentTexture}, Background: ${hasBackgroundTexture}`);
1561
+
1562
+ // First render any available previews
1563
+
1564
+ // If we have an environment texture, render its preview
1565
+ if (state.scene && state.scene.environment && !environmentTexture) {
1566
+ environmentTexture = state.scene.environment;
1567
+ renderEnvironmentPreview(environmentTexture);
1568
+ }
1569
+
1570
+ // If we have a background file, render its preview
1571
+ if (state.backgroundFile && !backgroundTexture) {
1572
+ backgroundTexture = state.backgroundFile;
1573
+ renderBackgroundPreview(backgroundTexture);
1574
+ }
1575
+
1576
+ // Update background UI with our centralized system
1577
+ refreshBackgroundUI();
1578
+ }
1579
+
1580
+ /**
1581
+ * Export a debug function to test EXR rendering directly
1582
+ * This can be called from the console for testing
1583
+ */
1584
+ export function testRenderExr(file) {
1585
+ console.log('Manual EXR rendering test with file:', file);
1586
+
1587
+ if (!file) {
1588
+ console.error('No file provided');
1589
+ return;
1590
+ }
1591
+
1592
+ // Import the EXRLoader
1593
+ import('three/addons/loaders/EXRLoader.js').then(({ EXRLoader }) => {
1594
+ console.log('EXRLoader imported successfully');
1595
+
1596
+ const loader = new EXRLoader();
1597
+ loader.setDataType(THREE.FloatType);
1598
+
1599
+ // Create URL
1600
+ const url = URL.createObjectURL(file);
1601
+ console.log('Created URL for manual test:', url);
1602
+
1603
+ // Load the texture
1604
+ loader.load(url, (texture) => {
1605
+ console.log('EXR loaded for manual test:', texture);
1606
+ console.log('EXR image data:', texture.image);
1607
+
1608
+ // Render it
1609
+ renderEnvironmentPreview(texture);
1610
+
1611
+ // Clean up
1612
+ URL.revokeObjectURL(url);
1613
+ },
1614
+ // Progress
1615
+ (xhr) => {
1616
+ if (xhr.lengthComputable) {
1617
+ const percentComplete = xhr.loaded / xhr.total * 100;
1618
+ console.log(`Manual test loading: ${Math.round(percentComplete)}%`);
1619
+ }
1620
+ },
1621
+ // Error
1622
+ (error) => {
1623
+ console.error('Error in manual test:', error);
1624
+ });
1625
+ }).catch(err => {
1626
+ console.error('Error importing EXRLoader for manual test:', err);
1627
+ });
1628
+ }
1629
+
1630
+ // Make the test function available globally for debugging
1631
+ window.testRenderExr = testRenderExr;
1632
+
1633
+ /**
1634
+ * Generate a preview for HDR/EXR files without affecting the world panel
1635
+ * This is specifically for the drop zone preview
1636
+ * @param {File} file - The HDR/EXR file
1637
+ * @param {HTMLElement} previewElement - The element to show the preview in
1638
+ */
1639
+ export function generatePreviewOnly(file, previewElement) {
1640
+ if (!file || !previewElement) return;
1641
+
1642
+ // Check if we've already generated a texture for this file
1643
+ const cachedTexture = backgroundTextureCache.get(file.name);
1644
+ if (cachedTexture) {
1645
+ console.log('Using cached texture for preview:', file.name);
1646
+ displayCachedPreview(cachedTexture, previewElement);
1647
+ return;
1648
+ }
1649
+
1650
+ console.log('Generating standalone preview for:', file.name);
1651
+
1652
+ // Create a canvas for the preview
1653
+ const canvas = document.createElement('canvas');
1654
+ canvas.width = 100; // Small size for the dropzone preview
1655
+ canvas.height = 100;
1656
+
1657
+ // Use appropriate loader based on file extension
1658
+ const extension = file.name.split('.').pop().toLowerCase();
1659
+ const isEXR = extension === 'exr';
1660
+ const isHDR = extension === 'hdr';
1661
+
1662
+ // Load and display the preview
1663
+ Promise.resolve().then(async () => {
1664
+ try {
1665
+ // Create a URL for the file
1666
+ const url = URL.createObjectURL(file);
1667
+
1668
+ // Select the appropriate loader
1669
+ let loader;
1670
+ if (isEXR) {
1671
+ const { EXRLoader } = await import('three/addons/loaders/EXRLoader.js');
1672
+ loader = new EXRLoader();
1673
+ } else {
1674
+ const { RGBELoader } = await import('three/addons/loaders/RGBELoader.js');
1675
+ loader = new RGBELoader();
1676
+ }
1677
+
1678
+ // Load the texture
1679
+ loader.load(
1680
+ url,
1681
+ (texture) => {
1682
+ // Store texture in the cache for future use
1683
+ texture.userData = { fileName: file.name };
1684
+ backgroundTextureCache.set(file.name, texture);
1685
+
1686
+ // Also store in state for use with debugging
1687
+ import('../../util/state/scene-state.js').then(stateModule => {
1688
+ stateModule.updateState({
1689
+ backgroundTexture: texture
1690
+ });
1691
+ });
1692
+
1693
+ // Clear the preview element and display the preview
1694
+ while (previewElement.firstChild) {
1695
+ previewElement.removeChild(previewElement.firstChild);
1696
+ }
1697
+ previewElement.textContent = '';
1698
+ previewElement.appendChild(canvas);
1699
+
1700
+ // Create a mini 3D preview
1701
+ createSimplePreview(THREE, texture, canvas);
1702
+
1703
+ // Clean up
1704
+ URL.revokeObjectURL(url);
1705
+ },
1706
+ (xhr) => {
1707
+ // Update progress
1708
+ if (xhr.lengthComputable) {
1709
+ const percentComplete = xhr.loaded / xhr.total * 100;
1710
+ previewElement.textContent = `Loading: ${Math.round(percentComplete)}%`;
1711
+ }
1712
+ },
1713
+ (error) => {
1714
+ console.error('Error loading preview:', error);
1715
+ previewElement.textContent = 'Preview unavailable';
1716
+ URL.revokeObjectURL(url);
1717
+ }
1718
+ );
1719
+ } catch (error) {
1720
+ console.error('Error generating preview:', error);
1721
+ previewElement.textContent = 'Preview unavailable';
1722
+ }
1723
+ });
1724
+ }
1725
+
1726
+ /**
1727
+ * Display a cached texture in the preview element
1728
+ * @param {THREE.Texture} texture - The cached texture
1729
+ * @param {HTMLElement} previewElement - The element to display in
1730
+ */
1731
+ function displayCachedPreview(texture, previewElement) {
1732
+ // Clear the preview element
1733
+ while (previewElement.firstChild) {
1734
+ previewElement.removeChild(previewElement.firstChild);
1735
+ }
1736
+ previewElement.textContent = '';
1737
+
1738
+ // Create a canvas for the preview
1739
+ const canvas = document.createElement('canvas');
1740
+ canvas.width = 100; // Small size for the dropzone preview
1741
+ canvas.height = 100;
1742
+ previewElement.appendChild(canvas);
1743
+
1744
+ // Create a mini 3D preview
1745
+ createSimplePreview(THREE, texture, canvas);
1746
+ }
1747
+
1748
+ /**
1749
+ * Create a simple 3D preview using Three.js
1750
+ * @param {Object} THREE - The Three.js library
1751
+ * @param {THREE.Texture} texture - The texture to display
1752
+ * @param {HTMLCanvasElement} canvas - The canvas to render on
1753
+ */
1754
+ export function createSimplePreview(THREE, texture, canvas) {
1755
+ // Create renderer
1756
+ const renderer = new THREE.WebGLRenderer({
1757
+ canvas: canvas,
1758
+ alpha: true,
1759
+ antialias: true
1760
+ });
1761
+ renderer.setSize(canvas.width, canvas.height);
1762
+ renderer.outputColorSpace = THREE.SRGBColorSpace;
1763
+ renderer.toneMapping = THREE.ACESFilmicToneMapping;
1764
+ renderer.toneMappingExposure = 1.0;
1765
+
1766
+ // Create scene
1767
+ const scene = new THREE.Scene();
1768
+ scene.background = new THREE.Color(0x111111);
1769
+
1770
+ // Create camera
1771
+ const camera = new THREE.PerspectiveCamera(40, 1, 0.1, 100);
1772
+ camera.position.z = 3;
1773
+
1774
+ // Create sphere with material
1775
+ const geometry = new THREE.SphereGeometry(0.8, 32, 32);
1776
+ const material = new THREE.MeshStandardMaterial({
1777
+ color: 0xffffff,
1778
+ metalness: 1.0,
1779
+ roughness: 0.05,
1780
+ envMapIntensity: 1.0
1781
+ });
1782
+
1783
+ // Set the texture as environment map
1784
+ texture.mapping = THREE.EquirectangularReflectionMapping;
1785
+ scene.environment = texture;
1786
+
1787
+ // Create mesh
1788
+ const sphere = new THREE.Mesh(geometry, material);
1789
+ scene.add(sphere);
1790
+
1791
+ // Add lighting
1792
+ const ambientLight = new THREE.AmbientLight(0xffffff, 0.2);
1793
+ scene.add(ambientLight);
1794
+
1795
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 1.0);
1796
+ directionalLight.position.set(1, 1, 1);
1797
+ scene.add(directionalLight);
1798
+
1799
+ // Auto rotate the sphere
1800
+ let animationId;
1801
+ function animate() {
1802
+ animationId = requestAnimationFrame(animate);
1803
+ sphere.rotation.y += 0.01;
1804
+ renderer.render(scene, camera);
1805
+ }
1806
+
1807
+ // Start animation
1808
+ animate();
1809
+
1810
+ // Add cleanup on canvas removal
1811
+ const observer = new MutationObserver((mutations) => {
1812
+ for (const mutation of mutations) {
1813
+ if (mutation.type === 'childList' && !document.contains(canvas)) {
1814
+ cancelAnimationFrame(animationId);
1815
+ renderer.dispose();
1816
+ geometry.dispose();
1817
+ material.dispose();
1818
+ observer.disconnect();
1819
+ break;
1820
+ }
1821
+ }
1822
+ });
1823
+
1824
+ observer.observe(document.body, { childList: true, subtree: true });
1825
+ }
1826
+
1827
+ // Add event listener for lighting-updated custom event
1828
+ document.addEventListener('lighting-updated', function(e) {
1829
+ console.log('Received lighting-updated event:', e.detail);
1830
+
1831
+ // Show the lighting info section and hide the no data message
1832
+ console.log('Updating UI for example lighting');
1833
+ toggleLightingMessages(true);
1834
+
1835
+ // Update the lighting info with example data
1836
+ const metadata = {
1837
+ fileName: e.detail.description || 'Example Lighting',
1838
+ type: e.detail.type || 'default',
1839
+ description: e.detail.description || 'Default Example Lighting',
1840
+ dimensions: { width: 0, height: 0 },
1841
+ fileSizeBytes: 0
1842
+ };
1843
+
1844
+ // Update UI with simplified metadata
1845
+ const filenameEl = document.getElementById('lighting-filename');
1846
+ const typeEl = document.getElementById('lighting-type');
1847
+
1848
+ if (filenameEl) filenameEl.textContent = metadata.fileName;
1849
+ if (typeEl) typeEl.textContent = metadata.type;
1850
+
1851
+ // Update lighting controls visibility
1852
+ updateSliderVisibility(true);
1853
+
1854
+ // Update lighting message to show information
1855
+ updateLightingMessage();
1856
+
1857
+ console.log('Updated lighting info UI with example lighting data');
1858
+ });
1859
+
1860
+ /**
1861
+ * Set the current background option without triggering UI updates
1862
+ * @param {string} option - The option to set ('none', 'background', or 'hdr')
1863
+ */
1864
+ export function setCurrentBackgroundOption(option) {
1865
+ if (['none', 'background', 'hdr'].includes(option)) {
1866
+ console.log('Setting current background option to:', option);
1867
+ currentBackgroundOption = option;
1868
+ } else {
1869
+ console.warn('Invalid background option:', option);
1870
+ }
1871
+ }
1872
+
1873
+ /**
1874
+ * NEW: Add cleanup function for the module
1875
+ */
1876
+ export function cleanupWorldPanel() {
1877
+ console.log('Cleaning up World Panel...');
1878
+ cleanupEventListeners();
1879
+
1880
+ // Reset state variables
1881
+ controlsInitialized = false;
1882
+ currentLightingMetadata = null;
1883
+ currentBackgroundMetadata = null;
1884
+ environmentTexture = null;
1885
+ backgroundTexture = null;
1886
+ currentBackgroundOption = null;
1887
+ exrLoadInProgress = false;
1888
+ isLoadingEnvironmentFile = false;
1889
+
1890
+ console.log('World Panel cleanup complete');
1891
+ }