@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,486 @@
1
+ /**
2
+ * Texture Debugger - Mesh Panel Module
3
+ *
4
+ * This module handles mesh visibility panel UI and interaction.
5
+ */
6
+ import { deserializeStringFromBinary, isValidHtml } from '../../../util/data/string-serder.js';
7
+ import { openMeshInfoModal } from '../../../modals/mesh-info-modal/mesh-info-modal.js';
8
+ import { getBinaryBufferForMesh } from '../../../util/data/glb-buffer-manager.js';
9
+ import { getCurrentGlbBuffer } from '../../../util/scene/glb-controller.js';
10
+ import { getState, updateState } from '../../../util/state/scene-state.js';
11
+ import { checkMeshHasHtmlContent } from '../../../util/data/mesh-html-manager.js';
12
+
13
+ // Load mesh panel CSS
14
+ const link = document.createElement('link');
15
+ link.rel = 'stylesheet';
16
+ link.href = '/asset_debugger/panels/asset-panel/mesh-heading/mesh-heading.css';
17
+ document.head.appendChild(link);
18
+
19
+ // Track meshes with binary content
20
+ export const meshesWithHtml = new Set();
21
+ // Icon color constants
22
+ const ICON_COLORS = {
23
+ HAS_HTML: '#f8d73e', // Yellow color for meshes with binary content (Fallout yellow)
24
+ VALID_HTML: '#4CAF50', // Green color for meshes with valid HTML content
25
+ NO_HTML: '#8a8a8a' // Default color for meshes without content
26
+ };
27
+
28
+ /**
29
+ * Toggle the binary content icon appearance for a specific mesh
30
+ * @param {number} meshIndex - The index of the mesh to toggle
31
+ * @param {boolean} hasHtml - Whether the mesh has binary content
32
+ * @param {boolean} forceUpdate - If true, forces the update without rechecking
33
+ * @returns {boolean} Whether the operation was successful
34
+ */
35
+ export function toggleMeshCodeIcon(meshIndex, hasHtml, forceUpdate = false) {
36
+ console.log(`Toggling mesh code icon for mesh ${meshIndex} to ${hasHtml ? 'has content' : 'no content'}`);
37
+
38
+ if (hasHtml) {
39
+ meshesWithHtml.add(meshIndex);
40
+ } else {
41
+ meshesWithHtml.delete(meshIndex);
42
+ }
43
+
44
+ if (forceUpdate) {
45
+ if (!window._forcedHtmlStates) {
46
+ window._forcedHtmlStates = {};
47
+ }
48
+ window._forcedHtmlStates[meshIndex] = hasHtml;
49
+
50
+ setTimeout(() => {
51
+ if (window._forcedHtmlStates) {
52
+ delete window._forcedHtmlStates[meshIndex];
53
+ }
54
+ }, 500);
55
+ }
56
+
57
+ const icons = document.querySelectorAll(`.mesh-html-editor-icon[data-mesh-index="${meshIndex}"]`);
58
+
59
+ if (icons.length === 0) {
60
+ console.log(`No icons found for mesh index ${meshIndex}, trying broader search`);
61
+ const allIcons = document.querySelectorAll('.mesh-html-editor-icon');
62
+
63
+ allIcons.forEach((icon) => {
64
+ const iconMeshIndex = parseInt(icon.dataset.meshIndex);
65
+ if (iconMeshIndex === meshIndex) {
66
+ updateIconAppearance(icon, hasHtml);
67
+ }
68
+ });
69
+ } else {
70
+ icons.forEach(icon => {
71
+ updateIconAppearance(icon, hasHtml);
72
+ });
73
+ }
74
+
75
+ return true;
76
+ }
77
+
78
+ /**
79
+ * Create the mesh visibility panel in the UI
80
+ */
81
+ export function createMeshVisibilityPanel() {
82
+ window.removeMeshHtmlFlag = removeMeshHtmlFlag;
83
+ window.updateHtmlIcons = updateHtmlIcons;
84
+ window.openMeshInfoModal = openMeshInfoModal;
85
+
86
+ const state = getState();
87
+ const hasMeshes = state.meshes && state.meshes.length > 0;
88
+
89
+ if (!hasMeshes) {
90
+ return;
91
+ }
92
+
93
+ groupMeshesByName();
94
+
95
+ const meshGroupsContainer = document.getElementById('mesh-groups');
96
+ if (!meshGroupsContainer) return;
97
+
98
+ meshGroupsContainer.innerHTML = '';
99
+
100
+ for (const groupName in state.meshGroups) {
101
+ const groupMeshes = state.meshGroups[groupName];
102
+
103
+ const groupDiv = document.createElement('div');
104
+ groupDiv.className = 'mesh-group';
105
+
106
+ const headerDiv = document.createElement('div');
107
+ headerDiv.className = 'mesh-group-header';
108
+ headerDiv.style.display = 'flex';
109
+ headerDiv.style.alignItems = 'center';
110
+ headerDiv.style.justifyContent = 'space-between';
111
+ headerDiv.style.width = '100%';
112
+ headerDiv.style.cursor = 'pointer';
113
+
114
+ const headerLeftDiv = document.createElement('div');
115
+ headerLeftDiv.style.display = 'flex';
116
+ headerLeftDiv.style.alignItems = 'center';
117
+
118
+ const groupToggle = document.createElement('input');
119
+ groupToggle.type = 'checkbox';
120
+ groupToggle.className = 'mesh-group-toggle';
121
+ groupToggle.checked = true;
122
+ groupToggle.dataset.group = groupName;
123
+
124
+ groupToggle.addEventListener('change', (e) => {
125
+ const isVisible = e.target.checked;
126
+ toggleMeshGroupVisibility(groupName, isVisible);
127
+
128
+ const meshToggles = groupDiv.querySelectorAll('.mesh-toggle');
129
+ meshToggles.forEach(toggle => {
130
+ toggle.checked = isVisible;
131
+ });
132
+ });
133
+
134
+ groupToggle.addEventListener('click', (e) => {
135
+ e.stopPropagation();
136
+ });
137
+
138
+ const groupNameSpan = document.createElement('span');
139
+ groupNameSpan.className = 'mesh-group-name';
140
+ groupNameSpan.textContent = groupName + ' ';
141
+
142
+ const groupCountSpan = document.createElement('span');
143
+ groupCountSpan.className = 'mesh-group-count';
144
+ groupCountSpan.textContent = `(${groupMeshes.length})`;
145
+
146
+ const collapseBtn = document.createElement('span');
147
+ collapseBtn.className = 'mesh-group-collapse';
148
+ collapseBtn.textContent = '+';
149
+ collapseBtn.style.cursor = 'pointer';
150
+ collapseBtn.style.marginLeft = 'auto';
151
+
152
+ const toggleCollapse = () => {
153
+ const meshItemsDiv = groupDiv.querySelector('.mesh-items');
154
+ const isCollapsed = meshItemsDiv.style.display === 'none';
155
+
156
+ meshItemsDiv.style.display = isCollapsed ? 'block' : 'none';
157
+ collapseBtn.textContent = isCollapsed ? '-' : '+';
158
+ };
159
+
160
+ headerDiv.addEventListener('click', toggleCollapse);
161
+
162
+ collapseBtn.addEventListener('click', (e) => {
163
+ e.stopPropagation();
164
+ toggleCollapse();
165
+ });
166
+
167
+ headerLeftDiv.appendChild(groupToggle);
168
+ headerLeftDiv.appendChild(groupNameSpan);
169
+ headerLeftDiv.appendChild(groupCountSpan);
170
+
171
+ headerDiv.appendChild(headerLeftDiv);
172
+ headerDiv.appendChild(collapseBtn);
173
+
174
+ const meshItemsDiv = document.createElement('div');
175
+ meshItemsDiv.className = 'mesh-items';
176
+ meshItemsDiv.style.display = 'none';
177
+
178
+ const meshPromises = groupMeshes.map(async (mesh, index) => {
179
+ const meshDiv = document.createElement('div');
180
+ meshDiv.className = 'mesh-item';
181
+ meshDiv.style.display = 'flex';
182
+ meshDiv.style.alignItems = 'center';
183
+
184
+ const meshToggle = document.createElement('input');
185
+ meshToggle.type = 'checkbox';
186
+ meshToggle.className = 'mesh-toggle';
187
+ meshToggle.checked = mesh.visible;
188
+ const meshIndex = state.meshes.indexOf(mesh);
189
+ meshToggle.dataset.meshIndex = meshIndex;
190
+
191
+ meshToggle.addEventListener('change', (e) => {
192
+ const isVisible = e.target.checked;
193
+ const meshIndex = parseInt(e.target.dataset.meshIndex);
194
+
195
+ if (!isNaN(meshIndex) && meshIndex >= 0 && meshIndex < state.meshes.length) {
196
+ state.meshes[meshIndex].visible = isVisible;
197
+ updateGroupToggleState(groupName);
198
+ }
199
+ });
200
+
201
+ const meshNameSpan = document.createElement('span');
202
+ meshNameSpan.className = 'mesh-name';
203
+ meshNameSpan.textContent = getMeshDisplayName(mesh);
204
+ meshNameSpan.title = mesh.name || "Unnamed mesh";
205
+ meshNameSpan.style.flexGrow = '1';
206
+
207
+ const htmlEditorIcon = document.createElement('span');
208
+ htmlEditorIcon.className = 'mesh-html-editor-icon';
209
+ htmlEditorIcon.title = 'Edit HTML';
210
+ htmlEditorIcon.dataset.meshIndex = meshIndex;
211
+ htmlEditorIcon.innerHTML = `
212
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512">
213
+ <path d="M234.8 511.7L196 500.4c-4.2-1.2-6.7-5.7-5.5-9.9L331.3 5.8c1.2-4.2 5.7-6.7 9.9-5.5L380 11.6c4.2 1.2 6.7 5.7 5.5 9.9L244.7 506.2c-1.2 4.3-5.6 6.7-9.9 5.5zm-83.2-121.1l27.2-29c3.1-3.3 2.8-8.5-.5-11.5L72.2 256l106.1-94.1c3.4-3 3.6-8.2.5-11.5l-27.2-29c-3-3.2-8.1-3.4-11.3-.4L2.5 250.2c-3.4 3.2-3.4 8.5 0 11.7L140.3 391c3.2 3 8.2 2.8 11.3-.4zm284.1.4l137.7-129.1c3.4-3.2 3.4-8.5 0-11.7L435.7 121c-3.2-3-8.3-2.9-11.3.4l-27.2 29c-3.1 3.3-2.8 8.5.5 11.5L503.8 256l-106.1 94.1c-3.4 3-3.6 8.2-.5 11.5l27.2 29c3.1 3.2 8.1 3.4 11.3.4z"/>
214
+ </svg>
215
+ `;
216
+
217
+ const hasHtml = await checkMeshHasHtmlContent(meshIndex);
218
+ htmlEditorIcon.dataset.hasHtml = hasHtml;
219
+ updateIconAppearance(htmlEditorIcon, hasHtml);
220
+
221
+ htmlEditorIcon.addEventListener('click', (e) => {
222
+ e.stopPropagation();
223
+ console.log('HTML editor icon clicked');
224
+
225
+ try {
226
+ const meshIndex = parseInt(meshToggle.dataset.meshIndex);
227
+ const meshName = state.meshes[meshIndex].name || 'Unnamed mesh';
228
+
229
+ console.log(`Opening HTML editor for mesh: ${meshName} (index: ${meshIndex})`);
230
+
231
+ if (typeof window.openEmbeddedHtmlEditor === 'function') {
232
+ window.openEmbeddedHtmlEditor(meshName, meshIndex);
233
+ } else {
234
+ console.error('HTML Editor function not found. Modal may not be initialized yet.');
235
+ alert('HTML Editor not ready. Please try again in a moment.');
236
+ }
237
+ } catch (error) {
238
+ console.error('Error opening HTML editor modal:', error);
239
+ alert('Error opening HTML editor. See console for details.');
240
+ }
241
+ });
242
+
243
+ const meshInfoIcon = document.createElement('span');
244
+ meshInfoIcon.className = 'mesh-info-icon';
245
+ meshInfoIcon.title = 'View mesh details';
246
+ meshInfoIcon.dataset.meshIndex = meshIndex;
247
+ meshInfoIcon.innerHTML = `
248
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="14" height="14">
249
+ <circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"/>
250
+ <line x1="12" y1="16" x2="12" y2="12" stroke="currentColor" stroke-width="2"/>
251
+ <circle cx="12" cy="8" r="1" fill="currentColor"/>
252
+ </svg>
253
+ `;
254
+ meshInfoIcon.style.color = '#6c757d';
255
+ meshInfoIcon.style.cursor = 'pointer';
256
+ meshInfoIcon.style.marginLeft = '8px';
257
+
258
+ meshInfoIcon.addEventListener('click', (e) => {
259
+ e.stopPropagation();
260
+ console.log('Mesh info icon clicked for mesh index:', meshIndex);
261
+ try {
262
+ const meshName = state.meshes[meshIndex].name || 'Unnamed mesh';
263
+ console.log(`Opening mesh info modal for mesh: ${meshName} (index: ${meshIndex})`);
264
+ if (typeof window.openMeshInfoModal === 'function') {
265
+ window.openMeshInfoModal(meshName, meshIndex);
266
+ } else {
267
+ console.error('Mesh Info Modal function not found. Modal may not be initialized yet.');
268
+ alert('Mesh Info Modal not ready. Please try again in a moment.');
269
+ }
270
+ } catch (error) {
271
+ console.error('Error opening mesh info modal:', error);
272
+ alert('Error opening mesh info modal. See console for details.');
273
+ }
274
+ });
275
+
276
+ const iconsContainer = document.createElement('div');
277
+ iconsContainer.className = 'mesh-item-icons';
278
+
279
+ if (mesh.name && mesh.name.toLowerCase().includes('display')) {
280
+ iconsContainer.appendChild(htmlEditorIcon);
281
+ }
282
+
283
+ iconsContainer.appendChild(meshInfoIcon);
284
+
285
+ meshDiv.appendChild(meshToggle);
286
+ meshDiv.appendChild(meshNameSpan);
287
+ meshDiv.appendChild(iconsContainer);
288
+
289
+ return meshDiv;
290
+ });
291
+
292
+ Promise.all(meshPromises).then(meshDivs => {
293
+ meshDivs.forEach(div => {
294
+ meshItemsDiv.appendChild(div);
295
+ });
296
+
297
+ groupDiv.appendChild(headerDiv);
298
+ groupDiv.appendChild(meshItemsDiv);
299
+
300
+ meshGroupsContainer.appendChild(groupDiv);
301
+ });
302
+ }
303
+ }
304
+
305
+ /**
306
+ * Update binary content icons to reflect current content
307
+ * This should be called after saving binary data
308
+ */
309
+ export function updateHtmlIcons() {
310
+ document.querySelectorAll('.mesh-html-editor-icon').forEach(async (icon) => {
311
+ const meshIndex = parseInt(icon.dataset.meshIndex);
312
+ if (!isNaN(meshIndex)) {
313
+ const hasHtml = await checkMeshHasHtmlContent(meshIndex);
314
+ toggleMeshCodeIcon(meshIndex, hasHtml);
315
+ }
316
+ });
317
+ }
318
+
319
+ /**
320
+ * Group meshes by name prefix
321
+ */
322
+ function groupMeshesByName() {
323
+ const state = getState();
324
+ const meshGroups = {};
325
+
326
+ state.meshes.forEach(mesh => {
327
+ const groupName = getGroupName(mesh);
328
+
329
+ if (!meshGroups[groupName]) {
330
+ meshGroups[groupName] = [];
331
+ }
332
+
333
+ meshGroups[groupName].push(mesh);
334
+ });
335
+
336
+ updateState('meshGroups', meshGroups);
337
+ }
338
+
339
+ /**
340
+ * Get group name from mesh based on naming pattern
341
+ * @param {THREE.Mesh} mesh - The mesh to get group name for
342
+ * @returns {string} Group name for the mesh
343
+ */
344
+ function getGroupName(mesh) {
345
+ const name = mesh.name || 'Unnamed';
346
+
347
+ const patterns = [
348
+ /^([^_]+)_.*$/, // Anything before first underscore
349
+ /^([^.]+)\..*$/, // Anything before first period
350
+ /^([^0-9]+).*$/ // Anything before first number
351
+ ];
352
+
353
+ for (const pattern of patterns) {
354
+ const match = name.match(pattern);
355
+ if (match && match[1]) {
356
+ return match[1];
357
+ }
358
+ }
359
+
360
+ if (name === 'Unnamed' || name === 'Cube') {
361
+ return 'Default';
362
+ }
363
+
364
+ return name.substring(0, 4);
365
+ }
366
+
367
+ /**
368
+ * Get display name for a mesh
369
+ * @param {THREE.Mesh} mesh - The mesh to get display name for
370
+ * @returns {string} Display name for the mesh
371
+ */
372
+ function getMeshDisplayName(mesh) {
373
+ return mesh.name || "Unnamed mesh";
374
+ }
375
+
376
+ /**
377
+ * Toggle visibility of all meshes in a group
378
+ * @param {string} groupName - The name of the group to toggle
379
+ * @param {boolean} isVisible - Whether the meshes should be visible
380
+ */
381
+ export function toggleMeshGroupVisibility(groupName, isVisible) {
382
+ const state = getState();
383
+ if (state.meshGroups[groupName]) {
384
+ state.meshGroups[groupName].forEach(mesh => {
385
+ mesh.visible = isVisible;
386
+ });
387
+ }
388
+ }
389
+
390
+ /**
391
+ * Update group toggle state based on individual mesh visibility
392
+ * @param {string} groupName - The name of the group to update
393
+ */
394
+ export function updateGroupToggleState(groupName) {
395
+ const groupToggle = document.querySelector(`.mesh-group-toggle[data-group="${groupName}"]`);
396
+ const state = getState();
397
+
398
+ if (!groupToggle || !state.meshGroups[groupName]) return;
399
+
400
+ const allVisible = state.meshGroups[groupName].every(mesh => mesh.visible);
401
+ const anyVisible = state.meshGroups[groupName].some(mesh => mesh.visible);
402
+
403
+ if (anyVisible && !allVisible) {
404
+ groupToggle.indeterminate = true;
405
+ } else {
406
+ groupToggle.indeterminate = false;
407
+ groupToggle.checked = allVisible;
408
+ }
409
+ }
410
+
411
+ /**
412
+ * Remove binary content flag for a specific mesh
413
+ * @param {number} meshIndex - The index of the mesh to update
414
+ */
415
+ export function removeMeshHtmlFlag(meshIndex) {
416
+ console.log(`Removing binary content flag for mesh index ${meshIndex}`);
417
+
418
+ toggleMeshCodeIcon(meshIndex, false, true);
419
+
420
+ checkMeshHasHtmlContent(meshIndex).then(() => {
421
+ console.log(`Re-checked mesh binary content status for mesh ${meshIndex}`);
422
+ });
423
+ }
424
+
425
+ /**
426
+ * Update an icon's appearance based on content status
427
+ * @param {HTMLElement} icon - The icon element to update
428
+ * @param {boolean} hasHtml - Whether the mesh has content
429
+ */
430
+ function updateIconAppearance(icon, hasHtml) {
431
+ icon.dataset.hasHtml = hasHtml ? 'true' : 'false';
432
+
433
+ if (hasHtml) {
434
+ icon.classList.add('has-html');
435
+
436
+ const meshIndex = parseInt(icon.dataset.meshIndex);
437
+
438
+ icon.style.color = ICON_COLORS.HAS_HTML;
439
+ icon.title = 'Edit content (has content)';
440
+
441
+ if (!isNaN(meshIndex)) {
442
+ (async () => {
443
+ try {
444
+ const glbBuffer = getCurrentGlbBuffer();
445
+ if (glbBuffer) {
446
+ const binaryBuffer = await getBinaryBufferForMesh(glbBuffer, meshIndex);
447
+ if (binaryBuffer) {
448
+ const result = deserializeStringFromBinary(binaryBuffer);
449
+ const content = result.content;
450
+ if (isValidHtml(content)) {
451
+ icon.style.color = ICON_COLORS.VALID_HTML;
452
+ icon.title = 'Edit content (valid HTML)';
453
+ }
454
+ }
455
+ }
456
+ } catch (error) {
457
+ console.error('Error checking if content is valid HTML:', error);
458
+ }
459
+ })();
460
+ }
461
+ } else {
462
+ icon.classList.remove('has-html');
463
+ icon.style.color = ICON_COLORS.NO_HTML;
464
+ icon.title = 'Edit content';
465
+ }
466
+
467
+ console.log(`Updated icon appearance, hasContent=${hasHtml}, color=${icon.style.color}`);
468
+ }
469
+
470
+ /**
471
+ * Force update mesh binary icon state - to be called from html-editor-modal.js
472
+ * This avoids race conditions when saving/removing binary content
473
+ * @param {number} meshIndex - The index of the mesh to update
474
+ * @param {boolean} hasHtml - Whether the mesh has binary content
475
+ */
476
+ export function forceUpdateMeshHtmlState(meshIndex, hasHtml) {
477
+ return toggleMeshCodeIcon(meshIndex, hasHtml, true);
478
+ }
479
+
480
+ export default {
481
+ createMeshVisibilityPanel,
482
+ toggleMeshGroupVisibility,
483
+ updateGroupToggleState,
484
+ toggleMeshCodeIcon,
485
+ forceUpdateMeshHtmlState
486
+ };