@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,586 @@
1
+ /**
2
+ * Texture Debugger - UV Panel Module
3
+ *
4
+ * This module handles UV mapping visualization and controls.
5
+ */
6
+ import { getState, updateState } from '../../../util/state/scene-state.js';
7
+ import { updateUvRegion } from '../atlas-heading/atlas-heading.js';
8
+
9
+ // DOM elements
10
+ let uvInfoContainer = null;
11
+ let uvManualControls = null;
12
+ let uvOffsetX = null;
13
+ let uvOffsetY = null;
14
+ let uvScaleW = null;
15
+ let uvScaleH = null;
16
+ let uvPredefinedSegments = null;
17
+ let controlsInitialized = false;
18
+ let uvChannelSelectContainer = null;
19
+
20
+ /**
21
+ * Initialize the UV panel and cache DOM elements
22
+ */
23
+ export function initUvPanel() {
24
+ // Cache DOM elements
25
+ uvInfoContainer = document.getElementById('uv-info-container');
26
+ uvManualControls = document.getElementById('uv-manual-controls');
27
+ uvOffsetX = document.getElementById('uv-offset-x');
28
+ uvOffsetY = document.getElementById('uv-offset-y');
29
+ uvScaleW = document.getElementById('uv-scale-w');
30
+ uvScaleH = document.getElementById('uv-scale-h');
31
+ uvPredefinedSegments = document.getElementById('uv-predefined-segments');
32
+
33
+ // Initial setup
34
+ updateUvPanel();
35
+ }
36
+
37
+ /**
38
+ * Update the UV panel with current state
39
+ */
40
+ export function updateUvPanel() {
41
+ const state = getState();
42
+
43
+ if (!uvInfoContainer || !uvManualControls) {
44
+ // Cache DOM elements if not already done
45
+ uvInfoContainer = document.getElementById('uv-info-container');
46
+ uvManualControls = document.getElementById('uv-manual-controls');
47
+ uvOffsetX = document.getElementById('uv-offset-x');
48
+ uvOffsetY = document.getElementById('uv-offset-y');
49
+ uvScaleW = document.getElementById('uv-scale-w');
50
+ uvScaleH = document.getElementById('uv-scale-h');
51
+ uvPredefinedSegments = document.getElementById('uv-predefined-segments');
52
+ }
53
+
54
+ if (!state.model || state.meshes.length === 0) {
55
+ // Hide manual controls and show no data message
56
+ if (uvManualControls) uvManualControls.style.display = 'none';
57
+ if (uvInfoContainer) {
58
+ uvInfoContainer.innerHTML = `
59
+ <p>No model loaded or no UV data available.</p>
60
+ <p>Load a model to view UV information.</p>
61
+ `;
62
+ }
63
+ return;
64
+ }
65
+
66
+ // Analyze UV sets in the model
67
+ analyzeUvSets();
68
+
69
+ // Identify display/screen meshes
70
+ identifyScreenMeshes();
71
+
72
+ // If we have UV data, show controls and update info
73
+ if (state.availableUvSets.length > 0) {
74
+ if (uvManualControls) uvManualControls.style.display = 'block';
75
+ updateUvInfo();
76
+
77
+ // Setup UV manual control handlers if not already done
78
+ if (!controlsInitialized) {
79
+ setupUvControls();
80
+ controlsInitialized = true;
81
+ }
82
+ } else {
83
+ if (uvManualControls) uvManualControls.style.display = 'none';
84
+ if (uvInfoContainer) {
85
+ uvInfoContainer.innerHTML = `
86
+ <p>No UV data found in this model.</p>
87
+ <p>The model doesn't contain any UV mapping information.</p>
88
+ `;
89
+ }
90
+ }
91
+
92
+ // Update or create UV channel selector for display meshes if needed
93
+ updateUvChannelSelector();
94
+ }
95
+
96
+ /**
97
+ * Analyze UV sets available in the model
98
+ */
99
+ function analyzeUvSets() {
100
+ const state = getState();
101
+ const availableUvSets = [];
102
+ const uvSetNames = [];
103
+
104
+ // Return if no meshes
105
+ if (!state.meshes || state.meshes.length === 0) {
106
+ updateState('availableUvSets', availableUvSets);
107
+ updateState('uvSetNames', uvSetNames);
108
+ return;
109
+ }
110
+
111
+ // Collect all available UV sets from all meshes
112
+ const uvSets = new Set();
113
+ state.meshes.forEach(mesh => {
114
+ if (mesh.geometry && mesh.geometry.attributes) {
115
+ Object.keys(mesh.geometry.attributes).forEach(key => {
116
+ if (key === 'uv' || key.startsWith('uv')) {
117
+ uvSets.add(key);
118
+ }
119
+ });
120
+ }
121
+ });
122
+
123
+ // Convert to array and sort
124
+ const availableSets = Array.from(uvSets);
125
+ availableSets.sort(); // Sort for consistent order
126
+
127
+ // Create friendly names
128
+ const setNames = availableSets.map(name => {
129
+ if (name === 'uv') return 'UV Channel 0 (Default)';
130
+ if (name === 'uv2') return 'UV Channel 1 (Secondary)';
131
+ // Extract number for other UV channels
132
+ const match = name.match(/uv(\d+)/);
133
+ if (match) {
134
+ return `UV Channel ${match[1]} (Custom)`;
135
+ }
136
+ return name;
137
+ });
138
+
139
+ // Update state
140
+ updateState('availableUvSets', availableSets);
141
+ updateState('uvSetNames', setNames);
142
+
143
+ // If we don't have a current set but have available sets, select the first
144
+ if ((state.currentUvSet === undefined || state.currentUvSet >= availableSets.length) &&
145
+ availableSets.length > 0) {
146
+ updateState('currentUvSet', 0);
147
+ }
148
+
149
+ // Analyze and collect detailed UV mapping info for each channel
150
+ const uvMappingInfo = {};
151
+ availableSets.forEach(uvChannel => {
152
+ const info = {
153
+ mappingType: 'Standard',
154
+ textureUsage: 'Full Texture',
155
+ meshes: [],
156
+ minU: 1, maxU: 0,
157
+ minV: 1, maxV: 0,
158
+ sampleUVs: null,
159
+ sampleMesh: null
160
+ };
161
+
162
+ // Find all meshes using this UV channel
163
+ state.meshes.forEach(mesh => {
164
+ if (mesh.geometry && mesh.geometry.attributes && mesh.geometry.attributes[uvChannel]) {
165
+ info.meshes.push(mesh);
166
+
167
+ // Use this as a sample mesh if we don't have one yet
168
+ if (!info.sampleMesh) {
169
+ info.sampleMesh = mesh;
170
+
171
+ // Calculate UV min/max range
172
+ const uvAttr = mesh.geometry.attributes[uvChannel];
173
+ for (let i = 0; i < uvAttr.count; i++) {
174
+ const u = uvAttr.getX(i);
175
+ const v = uvAttr.getY(i);
176
+ info.minU = Math.min(info.minU, u);
177
+ info.minV = Math.min(info.minV, v);
178
+ info.maxU = Math.max(info.maxU, u);
179
+ info.maxV = Math.max(info.maxV, v);
180
+ }
181
+ }
182
+ }
183
+ });
184
+
185
+ // Determine mapping type based on UV range
186
+ if (info.minU < 0 || info.maxU > 1 || info.minV < 0 || info.maxV > 1) {
187
+ info.mappingType = 'Extended Range';
188
+ }
189
+
190
+ // If we have a good sample of UVs, calculate texture usage
191
+ if (info.maxU - info.minU < 0.5 || info.maxV - info.minV < 0.5) {
192
+ info.textureUsage = 'Partial (Atlas Segment)';
193
+ }
194
+
195
+ uvMappingInfo[uvChannel] = info;
196
+ });
197
+
198
+ updateState('uvMappingInfo', uvMappingInfo);
199
+ }
200
+
201
+ /**
202
+ * Identify screen/display meshes in the model
203
+ */
204
+ function identifyScreenMeshes() {
205
+ const state = getState();
206
+
207
+ // Return if no meshes
208
+ if (!state.meshes || state.meshes.length === 0) {
209
+ updateState('screenMeshes', []);
210
+ return;
211
+ }
212
+
213
+ // Find meshes with names that look like displays
214
+ const screenRegex = /display|screen|monitor|lcd|led|panel|tv/i;
215
+ const screenMeshes = state.meshes.filter(mesh =>
216
+ mesh.name && screenRegex.test(mesh.name)
217
+ );
218
+
219
+ // Update state
220
+ updateState('screenMeshes', screenMeshes);
221
+
222
+ console.log(`Found ${screenMeshes.length} screen/display meshes:`,
223
+ screenMeshes.map(m => m.name));
224
+ }
225
+
226
+ /**
227
+ * Update the UV channel selector for display meshes
228
+ */
229
+ function updateUvChannelSelector() {
230
+ const state = getState();
231
+
232
+ // Don't show selector if no display meshes or no UV channels
233
+ if (!state.screenMeshes || state.screenMeshes.length === 0 ||
234
+ !state.availableUvSets || state.availableUvSets.length === 0) {
235
+ if (uvChannelSelectContainer) {
236
+ uvChannelSelectContainer.style.display = 'none';
237
+ }
238
+ return;
239
+ }
240
+
241
+ // Create container if it doesn't exist
242
+ if (!uvChannelSelectContainer) {
243
+ uvChannelSelectContainer = document.createElement('div');
244
+ uvChannelSelectContainer.id = 'uv-channel-select-container';
245
+ uvChannelSelectContainer.className = 'uv-control-group';
246
+ uvChannelSelectContainer.style.marginBottom = '15px';
247
+ uvChannelSelectContainer.style.backgroundColor = 'rgba(0, 0, 0, 0.3)';
248
+ uvChannelSelectContainer.style.padding = '10px';
249
+ uvChannelSelectContainer.style.borderRadius = '5px';
250
+
251
+ const title = document.createElement('label');
252
+ title.textContent = 'Display Mesh UV Channel:';
253
+ title.style.display = 'block';
254
+ title.style.marginBottom = '5px';
255
+ title.style.fontWeight = 'bold';
256
+ title.style.color = '#f1c40f';
257
+ uvChannelSelectContainer.appendChild(title);
258
+
259
+ const description = document.createElement('div');
260
+ description.textContent = 'Select UV channel for display meshes';
261
+ description.style.fontSize = '11px';
262
+ description.style.color = '#bbb';
263
+ description.style.marginBottom = '10px';
264
+ uvChannelSelectContainer.appendChild(description);
265
+
266
+ // Create the dropdown
267
+ const select = document.createElement('select');
268
+ select.id = 'display-uv-channel-select';
269
+ select.style.width = '100%';
270
+ select.style.padding = '5px';
271
+ select.style.backgroundColor = '#333';
272
+ select.style.border = '1px solid #555';
273
+ select.style.borderRadius = '3px';
274
+ select.style.color = 'white';
275
+
276
+ // Add event listener
277
+ select.addEventListener('change', function() {
278
+ switchUvChannelForDisplays(this.value);
279
+ });
280
+
281
+ uvChannelSelectContainer.appendChild(select);
282
+
283
+ // Show count of display meshes
284
+ const screenCount = document.createElement('div');
285
+ screenCount.id = 'screen-mesh-count';
286
+ screenCount.style.marginTop = '5px';
287
+ screenCount.style.fontSize = '11px';
288
+ screenCount.style.color = '#3498db';
289
+ uvChannelSelectContainer.appendChild(screenCount);
290
+
291
+ // Add to UV panel container
292
+ if (uvManualControls && uvManualControls.parentNode) {
293
+ uvManualControls.parentNode.insertBefore(uvChannelSelectContainer, uvManualControls);
294
+ }
295
+ }
296
+
297
+ // Update dropdown options
298
+ const select = document.getElementById('display-uv-channel-select');
299
+ if (select) {
300
+ // Clear existing options
301
+ select.innerHTML = '';
302
+
303
+ // Add options for each UV set
304
+ state.availableUvSets.forEach((uvSet, index) => {
305
+ const option = document.createElement('option');
306
+ option.value = uvSet;
307
+ option.textContent = state.uvSetNames[index];
308
+ select.appendChild(option);
309
+ });
310
+
311
+ // Set default selection to match current UV set
312
+ if (state.currentUvSet !== undefined && state.availableUvSets[state.currentUvSet]) {
313
+ select.value = state.availableUvSets[state.currentUvSet];
314
+ }
315
+ }
316
+
317
+ // Update screen mesh count
318
+ const screenCount = document.getElementById('screen-mesh-count');
319
+ if (screenCount) {
320
+ screenCount.textContent = `Found ${state.screenMeshes.length} display/screen meshes`;
321
+ }
322
+
323
+ // Show the container
324
+ uvChannelSelectContainer.style.display = 'block';
325
+ }
326
+
327
+ /**
328
+ * Switch UV channel for all display meshes
329
+ * @param {string} uvChannel - The UV channel to switch to
330
+ */
331
+ function switchUvChannelForDisplays(uvChannel) {
332
+ const state = getState();
333
+
334
+ if (!state.screenMeshes || state.screenMeshes.length === 0) {
335
+ console.warn('No display meshes to update');
336
+ return;
337
+ }
338
+
339
+ console.log(`Switching ${state.screenMeshes.length} display meshes to UV channel: ${uvChannel}`);
340
+
341
+ state.screenMeshes.forEach(mesh => {
342
+ // Skip if mesh doesn't have this UV channel
343
+ if (!mesh.geometry || !mesh.geometry.attributes || !mesh.geometry.attributes[uvChannel]) {
344
+ console.warn(`Mesh ${mesh.name} doesn't have UV channel ${uvChannel}`);
345
+ return;
346
+ }
347
+
348
+ // Get the UV attribute
349
+ const uvAttribute = mesh.geometry.attributes[uvChannel];
350
+
351
+ // Skip if mesh doesn't have a material
352
+ if (!mesh.material) {
353
+ console.warn(`Mesh ${mesh.name} doesn't have a material`);
354
+ return;
355
+ }
356
+
357
+ // If mesh has material array, update all materials
358
+ if (Array.isArray(mesh.material)) {
359
+ mesh.material.forEach(mat => {
360
+ if (mat.map) {
361
+ mat.map.needsUpdate = true;
362
+ }
363
+ mat.needsUpdate = true;
364
+ });
365
+ } else {
366
+ // Update textures
367
+ if (mesh.material.map) {
368
+ mesh.material.map.needsUpdate = true;
369
+ }
370
+ mesh.material.needsUpdate = true;
371
+ }
372
+
373
+ // Update mesh with new UV channel
374
+ mesh.geometry.setAttribute('uv', uvAttribute);
375
+ mesh.geometry.attributes.uv.needsUpdate = true;
376
+ });
377
+
378
+ // Register the function for switching UV channels
379
+ if (!state.switchUvChannel) {
380
+ updateState('switchUvChannel', switchUvChannelForDisplays);
381
+ }
382
+
383
+ // Update UV info
384
+ updateUvInfo();
385
+ }
386
+
387
+ /**
388
+ * Update UV info display
389
+ */
390
+ function updateUvInfo() {
391
+ const state = getState();
392
+
393
+ // Exit if no UV sets or no container
394
+ if (state.availableUvSets.length === 0 || !uvInfoContainer) return;
395
+
396
+ // Get current UV set
397
+ const currentSetName = state.availableUvSets[state.currentUvSet] || 'uv';
398
+
399
+ // Build HTML content
400
+ let content = '<div style="color: #f1c40f; font-weight: bold;">UV Channel Info:</div>';
401
+ content += `<div>Channel Name: <span style="color: #3498db">${currentSetName}</span></div>`;
402
+
403
+ // Add display mesh info if available
404
+ const displayMeshesWithUv = state.screenMeshes.filter(mesh =>
405
+ mesh.geometry && mesh.geometry.attributes &&
406
+ mesh.geometry.attributes[currentSetName]);
407
+
408
+ if (displayMeshesWithUv.length > 0) {
409
+ content += `<div style="margin-top: 5px; color: #e74c3c;">Display Meshes: <span style="color: white">${displayMeshesWithUv.length}</span></div>`;
410
+ }
411
+
412
+ // Get a sample mesh that has this UV set
413
+ const sampleMesh = state.meshes.find(mesh =>
414
+ mesh.geometry && mesh.geometry.attributes &&
415
+ mesh.geometry.attributes[currentSetName]);
416
+
417
+ if (sampleMesh) {
418
+ // Get UV attribute
419
+ const uvAttr = sampleMesh.geometry.attributes[currentSetName];
420
+
421
+ // Add sample UV coordinates
422
+ content += '<div style="margin-top: 5px; color: #f1c40f;">Sample UV Coordinates:</div>';
423
+ content += `<div>From: <span style="color: #3498db">${sampleMesh.name || 'Unnamed mesh'}</span></div>`;
424
+
425
+ // Get a few sample vertices
426
+ const sampleCount = Math.min(5, uvAttr.count);
427
+ let minU = 1, minV = 1, maxU = 0, maxV = 0;
428
+
429
+ for (let i = 0; i < sampleCount; i++) {
430
+ const u = uvAttr.getX(i);
431
+ const v = uvAttr.getY(i);
432
+ minU = Math.min(minU, u);
433
+ minV = Math.min(minV, v);
434
+ maxU = Math.max(maxU, u);
435
+ maxV = Math.max(maxV, v);
436
+ content += `<div>Vertex ${i}: <span style="color: #3498db">(${u.toFixed(4)}, ${v.toFixed(4)})</span></div>`;
437
+ }
438
+
439
+ if (uvAttr.count > sampleCount) {
440
+ content += `<div>... and ${uvAttr.count - sampleCount} more vertices</div>`;
441
+ }
442
+
443
+ // Add UV range
444
+ content += '<div style="margin-top: 5px; color: #f1c40f;">UV Range:</div>';
445
+ content += `<div>U: <span style="color: #3498db">${minU.toFixed(4)} to ${maxU.toFixed(4)}</span></div>`;
446
+ content += `<div>V: <span style="color: #3498db">${minV.toFixed(4)} to ${maxV.toFixed(4)}</span></div>`;
447
+
448
+ // Add mesh statistics
449
+ const meshesWithUv = state.meshes.filter(mesh =>
450
+ mesh.geometry && mesh.geometry.attributes &&
451
+ mesh.geometry.attributes[currentSetName]);
452
+
453
+ content += '<div style="margin-top: 5px; color: #f1c40f;">Mesh Statistics:</div>';
454
+ content += `<div>Meshes with this UV: <span style="color: #3498db">${meshesWithUv.length} of ${state.meshes.length}</span></div>`;
455
+ } else {
456
+ content += '<div style="color: #e74c3c;">No meshes use this UV channel</div>';
457
+ }
458
+
459
+ uvInfoContainer.innerHTML = content;
460
+ }
461
+
462
+ /**
463
+ * Setup UV manual controls
464
+ */
465
+ function setupUvControls() {
466
+ if (!uvOffsetX || !uvOffsetY || !uvScaleW || !uvScaleH || !uvPredefinedSegments) return;
467
+
468
+ // Define common segment options
469
+ const segments = [
470
+ { name: 'Full texture (1×1)', u: 0, v: 0, w: 1, h: 1 },
471
+ { name: 'Top-left quarter (1/2×1/2)', u: 0, v: 0, w: 0.5, h: 0.5 },
472
+ { name: 'Top-right quarter (1/2×1/2)', u: 0.5, v: 0, w: 0.5, h: 0.5 },
473
+ { name: 'Bottom-left quarter (1/2×1/2)', u: 0, v: 0.5, w: 0.5, h: 0.5 },
474
+ { name: 'Bottom-right quarter (1/2×1/2)', u: 0.5, v: 0.5, w: 0.5, h: 0.5 },
475
+ { name: 'Top-left ninth (1/3×1/3)', u: 0, v: 0, w: 0.33, h: 0.33 },
476
+ { name: 'Top-center ninth (1/3×1/3)', u: 0.33, v: 0, w: 0.33, h: 0.33 },
477
+ { name: 'Top-right ninth (1/3×1/3)', u: 0.66, v: 0, w: 0.33, h: 0.33 },
478
+ { name: 'Middle-left ninth (1/3×1/3)', u: 0, v: 0.33, w: 0.33, h: 0.33 }
479
+ ];
480
+
481
+ // Clear and populate predefined segments dropdown
482
+ uvPredefinedSegments.innerHTML = '';
483
+ segments.forEach((segment, index) => {
484
+ const option = document.createElement('option');
485
+ option.value = index;
486
+ option.textContent = segment.name;
487
+ uvPredefinedSegments.appendChild(option);
488
+ });
489
+
490
+ // Function to apply mapping changes
491
+ const applyMapping = () => {
492
+ // Get values
493
+ const offsetX = parseFloat(uvOffsetX.value) || 0;
494
+ const offsetY = parseFloat(uvOffsetY.value) || 0;
495
+ const scaleW = parseFloat(uvScaleW.value) || 1;
496
+ const scaleH = parseFloat(uvScaleH.value) || 1;
497
+
498
+ // Check if all values are valid
499
+ if (offsetX < 0 || offsetX > 1 ||
500
+ offsetY < 0 || offsetY > 1 ||
501
+ scaleW <= 0 || scaleW > 1 ||
502
+ scaleH <= 0 || scaleH > 1) {
503
+ return;
504
+ }
505
+
506
+ const state = getState();
507
+
508
+ // Apply mapping to all meshes (or just display meshes if available)
509
+ const targetMeshes = state.screenMeshes.length > 0 ? state.screenMeshes : state.meshes;
510
+
511
+ targetMeshes.forEach(mesh => {
512
+ if (mesh.material) {
513
+ // Apply to all texture maps
514
+ const textureMaps = ['map', 'normalMap', 'aoMap', 'roughnessMap', 'metalnessMap', 'alphaMap'];
515
+
516
+ textureMaps.forEach(mapName => {
517
+ if (mesh.material[mapName]) {
518
+ mesh.material[mapName].offset.set(offsetX, offsetY);
519
+ mesh.material[mapName].repeat.set(scaleW, scaleH);
520
+ mesh.material[mapName].needsUpdate = true;
521
+ }
522
+ });
523
+
524
+ mesh.material.needsUpdate = true;
525
+ }
526
+ });
527
+
528
+ // Update the current UV region for visualization
529
+ updateUvRegion([offsetX, offsetY], [offsetX + scaleW, offsetY + scaleH]);
530
+
531
+ // Update our state function
532
+ if (!state.setCurrentUvRegion) {
533
+ updateState('setCurrentUvRegion', (min, max) => {
534
+ updateUvRegion(min, max);
535
+ });
536
+ }
537
+ };
538
+
539
+ // Set up event listeners
540
+ uvOffsetX.addEventListener('input', applyMapping);
541
+ uvOffsetY.addEventListener('input', applyMapping);
542
+ uvScaleW.addEventListener('input', applyMapping);
543
+ uvScaleH.addEventListener('input', applyMapping);
544
+
545
+ // Set up predefined segments
546
+ uvPredefinedSegments.addEventListener('change', function() {
547
+ const selectedSegment = segments[this.value];
548
+
549
+ // Update input fields
550
+ uvOffsetX.value = selectedSegment.u;
551
+ uvOffsetY.value = selectedSegment.v;
552
+ uvScaleW.value = selectedSegment.w;
553
+ uvScaleH.value = selectedSegment.h;
554
+
555
+ // Apply the mapping immediately
556
+ applyMapping();
557
+ });
558
+
559
+ // Initialize values
560
+ uvOffsetX.value = 0;
561
+ uvOffsetY.value = 0;
562
+ uvScaleW.value = 1;
563
+ uvScaleH.value = 1;
564
+
565
+ // Create atlas segment cycle function
566
+ const state = getState();
567
+ const cycleAtlasSegments = () => {
568
+ // Get the current segment index
569
+ let currentIndex = parseInt(uvPredefinedSegments.value);
570
+ // Move to the next segment
571
+ currentIndex = (currentIndex + 1) % segments.length;
572
+ // Update the dropdown
573
+ uvPredefinedSegments.value = currentIndex;
574
+ // Trigger the change
575
+ const event = new Event('change');
576
+ uvPredefinedSegments.dispatchEvent(event);
577
+ };
578
+
579
+ // Register the function in the state
580
+ updateState('cycleAtlasSegments', cycleAtlasSegments);
581
+ }
582
+
583
+ export default {
584
+ initUvPanel,
585
+ updateUvPanel
586
+ };