@nice2dev/ui-3d 1.3.15 → 1.3.16

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 (108) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/cjs/collaborative/collaborativeScene.js +160 -206
  3. package/dist/cjs/core/editorAsk.js +51 -41
  4. package/dist/cjs/core/i18n.js +13 -14
  5. package/dist/cjs/core/threeCompat.js +29 -42
  6. package/dist/cjs/dance/DanceBridge.js +148 -177
  7. package/dist/cjs/dance/DanceScoreEngine.js +209 -221
  8. package/dist/cjs/dance/PoseDetector.js +302 -275
  9. package/dist/cjs/index.js +110 -113
  10. package/dist/cjs/material/MaterialEditor.module.css.js +242 -5
  11. package/dist/cjs/material/NiceMaterialEditor.js +1213 -757
  12. package/dist/cjs/material/materialEditorTypes.js +35 -49
  13. package/dist/cjs/material/materialEditorUtils.js +684 -747
  14. package/dist/cjs/material/materialNodeDefinitions.js +1411 -1459
  15. package/dist/cjs/model/ModelEditor.js +90 -40
  16. package/dist/cjs/model/ModelEditor.module.css.js +257 -5
  17. package/dist/cjs/model/ModelEditorLeftPanel.js +126 -47
  18. package/dist/cjs/model/ModelEditorMenuBar.js +359 -32
  19. package/dist/cjs/model/ModelEditorRightPanel.js +647 -131
  20. package/dist/cjs/model/ModelEditorSubComponents.js +277 -112
  21. package/dist/cjs/model/ModelEditorTimeline.js +232 -34
  22. package/dist/cjs/model/ModelEditorToolbar.js +170 -14
  23. package/dist/cjs/model/ModelEditorViewport.js +231 -39
  24. package/dist/cjs/model/ModelViewer.js +388 -63
  25. package/dist/cjs/model/ModelViewer.module.css.js +77 -5
  26. package/dist/cjs/model/NiceArmatureEditor.js +473 -274
  27. package/dist/cjs/model/NiceMorphTargetEditor.js +333 -227
  28. package/dist/cjs/model/NiceOctree.js +372 -320
  29. package/dist/cjs/model/NicePhysicsSimulation.js +528 -298
  30. package/dist/cjs/model/NiceProceduralGeometry.js +673 -300
  31. package/dist/cjs/model/NiceTerrainEditor.js +595 -385
  32. package/dist/cjs/model/NiceWeightPainter.js +384 -261
  33. package/dist/cjs/model/NiceXRPreview.js +385 -308
  34. package/dist/cjs/model/cadModeUtils.js +103 -106
  35. package/dist/cjs/model/editorShortcuts.js +171 -194
  36. package/dist/cjs/model/modelEditorTypes.js +100 -118
  37. package/dist/cjs/model/modelEditorUtils.js +1061 -1146
  38. package/dist/cjs/model/simsModeUtils.js +1023 -1053
  39. package/dist/cjs/model/useModelEditor.js +1977 -1933
  40. package/dist/cjs/model/useModelViewer.js +633 -665
  41. package/dist/cjs/nice2dev-ui-3d.css +1 -1
  42. package/dist/cjs/particle/NiceParticleEditor.js +1186 -571
  43. package/dist/cjs/particle/ParticleEditor.module.css.js +122 -5
  44. package/dist/cjs/particle/particleEditorTypes.js +62 -70
  45. package/dist/cjs/particle/particleEditorUtils.js +998 -1013
  46. package/dist/cjs/rendering/NiceCascadedShadows.js +381 -259
  47. package/dist/cjs/rendering/NiceRenderExport.js +424 -337
  48. package/dist/cjs/rendering/NiceSSAO.js +454 -337
  49. package/dist/cjs/rendering/NiceSSR.js +387 -261
  50. package/dist/cjs/rendering/NiceWebGPURenderer.js +359 -230
  51. package/dist/cjs/uv/NiceUVEditor.js +787 -550
  52. package/dist/cjs/uv/UVEditor.module.css.js +53 -5
  53. package/dist/cjs/uv/uvEditorTypes.js +59 -76
  54. package/dist/cjs/uv/uvEditorUtils.js +501 -575
  55. package/dist/esm/collaborative/collaborativeScene.js +163 -205
  56. package/dist/esm/core/editorAsk.js +53 -40
  57. package/dist/esm/core/i18n.js +15 -13
  58. package/dist/esm/core/threeCompat.js +18 -26
  59. package/dist/esm/dance/DanceBridge.js +151 -176
  60. package/dist/esm/dance/DanceScoreEngine.js +211 -220
  61. package/dist/esm/dance/PoseDetector.js +304 -274
  62. package/dist/esm/index.js +258 -36
  63. package/dist/esm/material/MaterialEditor.module.css.js +242 -3
  64. package/dist/esm/material/NiceMaterialEditor.js +1215 -756
  65. package/dist/esm/material/materialEditorTypes.js +45 -48
  66. package/dist/esm/material/materialEditorUtils.js +713 -746
  67. package/dist/esm/material/materialNodeDefinitions.js +1415 -1458
  68. package/dist/esm/model/ModelEditor.js +91 -39
  69. package/dist/esm/model/ModelEditor.module.css.js +257 -3
  70. package/dist/esm/model/ModelEditorLeftPanel.js +126 -45
  71. package/dist/esm/model/ModelEditorMenuBar.js +359 -30
  72. package/dist/esm/model/ModelEditorRightPanel.js +634 -114
  73. package/dist/esm/model/ModelEditorSubComponents.js +265 -94
  74. package/dist/esm/model/ModelEditorTimeline.js +232 -32
  75. package/dist/esm/model/ModelEditorToolbar.js +170 -12
  76. package/dist/esm/model/ModelEditorViewport.js +231 -37
  77. package/dist/esm/model/ModelViewer.js +389 -62
  78. package/dist/esm/model/ModelViewer.module.css.js +77 -3
  79. package/dist/esm/model/NiceArmatureEditor.js +462 -258
  80. package/dist/esm/model/NiceMorphTargetEditor.js +322 -211
  81. package/dist/esm/model/NiceOctree.js +361 -304
  82. package/dist/esm/model/NicePhysicsSimulation.js +517 -282
  83. package/dist/esm/model/NiceProceduralGeometry.js +667 -284
  84. package/dist/esm/model/NiceTerrainEditor.js +584 -369
  85. package/dist/esm/model/NiceWeightPainter.js +373 -245
  86. package/dist/esm/model/NiceXRPreview.js +374 -292
  87. package/dist/esm/model/cadModeUtils.js +95 -88
  88. package/dist/esm/model/editorShortcuts.js +172 -193
  89. package/dist/esm/model/modelEditorTypes.js +90 -100
  90. package/dist/esm/model/modelEditorUtils.js +1056 -1128
  91. package/dist/esm/model/simsModeUtils.js +1021 -1035
  92. package/dist/esm/model/useModelEditor.js +1946 -1920
  93. package/dist/esm/model/useModelViewer.js +619 -647
  94. package/dist/esm/nice2dev-ui-3d.css +1 -1
  95. package/dist/esm/particle/NiceParticleEditor.js +1188 -570
  96. package/dist/esm/particle/ParticleEditor.module.css.js +122 -3
  97. package/dist/esm/particle/particleEditorTypes.js +69 -69
  98. package/dist/esm/particle/particleEditorUtils.js +1027 -1012
  99. package/dist/esm/rendering/NiceCascadedShadows.js +370 -243
  100. package/dist/esm/rendering/NiceRenderExport.js +413 -321
  101. package/dist/esm/rendering/NiceSSAO.js +450 -328
  102. package/dist/esm/rendering/NiceSSR.js +383 -252
  103. package/dist/esm/rendering/NiceWebGPURenderer.js +326 -214
  104. package/dist/esm/uv/NiceUVEditor.js +788 -549
  105. package/dist/esm/uv/UVEditor.module.css.js +53 -3
  106. package/dist/esm/uv/uvEditorTypes.js +68 -75
  107. package/dist/esm/uv/uvEditorUtils.js +515 -557
  108. package/package.json +7 -12
@@ -1,1221 +1,1149 @@
1
- import * as THREE from 'three';
2
-
3
- /**
4
- * modelEditorUtils.ts — Utility functions for the ModelEditor.
5
- *
6
- * Features:
7
- * - Undo/Redo stack
8
- * - LOD auto-generation
9
- * - PBR material presets
10
- * - Batch import
11
- * - Instancing helpers
12
- */
1
+ import * as THREE from "three";
13
2
  class UndoRedoStack {
14
- constructor(maxSize = 50) {
15
- this.undoStack = [];
16
- this.redoStack = [];
17
- this.maxSize = maxSize;
3
+ constructor(maxSize = 50) {
4
+ this.undoStack = [];
5
+ this.redoStack = [];
6
+ this.maxSize = maxSize;
7
+ }
8
+ push(action) {
9
+ this.undoStack.push(action);
10
+ if (this.undoStack.length > this.maxSize) {
11
+ this.undoStack.shift();
18
12
  }
19
- push(action) {
20
- this.undoStack.push(action);
21
- if (this.undoStack.length > this.maxSize) {
22
- this.undoStack.shift();
23
- }
24
- this.redoStack.length = 0;
13
+ this.redoStack.length = 0;
14
+ }
15
+ undo() {
16
+ const action = this.undoStack.pop();
17
+ if (!action) {
18
+ return null;
25
19
  }
26
- undo() {
27
- const action = this.undoStack.pop();
28
- if (!action) {
29
- return null;
30
- }
31
- action.undo();
32
- this.redoStack.push(action);
33
- return action.label;
20
+ action.undo();
21
+ this.redoStack.push(action);
22
+ return action.label;
23
+ }
24
+ redo() {
25
+ const action = this.redoStack.pop();
26
+ if (!action) {
27
+ return null;
34
28
  }
35
- redo() {
36
- const action = this.redoStack.pop();
37
- if (!action) {
38
- return null;
39
- }
40
- action.redo();
41
- this.undoStack.push(action);
42
- return action.label;
29
+ action.redo();
30
+ this.undoStack.push(action);
31
+ return action.label;
32
+ }
33
+ canUndo() {
34
+ return this.undoStack.length > 0;
35
+ }
36
+ canRedo() {
37
+ return this.redoStack.length > 0;
38
+ }
39
+ clear() {
40
+ this.undoStack.length = 0;
41
+ this.redoStack.length = 0;
42
+ }
43
+ get undoLabel() {
44
+ return this.undoStack.length > 0 ? this.undoStack[this.undoStack.length - 1].label : null;
45
+ }
46
+ get redoLabel() {
47
+ return this.redoStack.length > 0 ? this.redoStack[this.redoStack.length - 1].label : null;
48
+ }
49
+ }
50
+ const DEFAULT_LOD_LEVELS = [
51
+ { distance: 0, ratio: 1 },
52
+ { distance: 10, ratio: 0.5 },
53
+ { distance: 25, ratio: 0.25 },
54
+ { distance: 50, ratio: 0.1 }
55
+ ];
56
+ function decimateGeometry(geometry, ratio) {
57
+ if (ratio >= 1) {
58
+ return geometry.clone();
59
+ }
60
+ const posAttr = geometry.getAttribute("position");
61
+ if (!posAttr) {
62
+ return geometry.clone();
63
+ }
64
+ const positions = Array.from(posAttr.array);
65
+ const vertexCount = posAttr.count;
66
+ const targetCount = Math.max(3, Math.floor(vertexCount * ratio));
67
+ const gridSize = Math.cbrt(vertexCount / targetCount) * 0.5;
68
+ const merged = /* @__PURE__ */ new Map();
69
+ const vertexMap = new Int32Array(vertexCount);
70
+ let newIndex = 0;
71
+ for (let i = 0; i < vertexCount; i++) {
72
+ const x = positions[i * 3];
73
+ const y = positions[i * 3 + 1];
74
+ const z = positions[i * 3 + 2];
75
+ const key = `${Math.round(x / gridSize)},${Math.round(y / gridSize)},${Math.round(z / gridSize)}`;
76
+ const existing = merged.get(key);
77
+ if (existing) {
78
+ existing.x += x;
79
+ existing.y += y;
80
+ existing.z += z;
81
+ existing.count++;
82
+ vertexMap[i] = existing.index;
83
+ } else {
84
+ merged.set(key, { x, y, z, count: 1, index: newIndex });
85
+ vertexMap[i] = newIndex;
86
+ newIndex++;
43
87
  }
44
- canUndo() {
45
- return this.undoStack.length > 0;
88
+ }
89
+ const newPositions = new Float32Array(merged.size * 3);
90
+ for (const cell of merged.values()) {
91
+ newPositions[cell.index * 3] = cell.x / cell.count;
92
+ newPositions[cell.index * 3 + 1] = cell.y / cell.count;
93
+ newPositions[cell.index * 3 + 2] = cell.z / cell.count;
94
+ }
95
+ const oldIndex = geometry.index;
96
+ const newIndices = [];
97
+ if (oldIndex) {
98
+ for (let i = 0; i < oldIndex.count; i += 3) {
99
+ const a = vertexMap[oldIndex.array[i]];
100
+ const b = vertexMap[oldIndex.array[i + 1]];
101
+ const c = vertexMap[oldIndex.array[i + 2]];
102
+ if (a !== b && b !== c && a !== c) {
103
+ newIndices.push(a, b, c);
104
+ }
46
105
  }
47
- canRedo() {
48
- return this.redoStack.length > 0;
106
+ } else {
107
+ for (let i = 0; i < vertexCount; i += 3) {
108
+ const a = vertexMap[i];
109
+ const b = vertexMap[i + 1];
110
+ const c = vertexMap[i + 2];
111
+ if (a !== b && b !== c && a !== c) {
112
+ newIndices.push(a, b, c);
113
+ }
49
114
  }
50
- clear() {
51
- this.undoStack.length = 0;
52
- this.redoStack.length = 0;
115
+ }
116
+ const result = new THREE.BufferGeometry();
117
+ result.setAttribute("position", new THREE.BufferAttribute(newPositions, 3));
118
+ result.setIndex(newIndices);
119
+ result.computeVertexNormals();
120
+ return result;
121
+ }
122
+ function generateLOD(mesh, levels = DEFAULT_LOD_LEVELS) {
123
+ const lod = new THREE.LOD();
124
+ lod.name = mesh.name + "_LOD";
125
+ lod.position.copy(mesh.position);
126
+ lod.rotation.copy(mesh.rotation);
127
+ lod.scale.copy(mesh.scale);
128
+ for (const level of levels) {
129
+ const decimated = decimateGeometry(mesh.geometry, level.ratio);
130
+ const mat = Array.isArray(mesh.material) ? mesh.material.map((m) => m.clone()) : mesh.material.clone();
131
+ const lodMesh = new THREE.Mesh(decimated, mat);
132
+ lodMesh.castShadow = mesh.castShadow;
133
+ lodMesh.receiveShadow = mesh.receiveShadow;
134
+ lod.addLevel(lodMesh, level.distance);
135
+ }
136
+ return lod;
137
+ }
138
+ const PBR_PRESETS = [
139
+ // Metals
140
+ { name: "Gold", category: "Metal", params: { color: 16766720, metalness: 1, roughness: 0.15 } },
141
+ {
142
+ name: "Silver",
143
+ category: "Metal",
144
+ params: { color: 12632256, metalness: 1, roughness: 0.1 }
145
+ },
146
+ {
147
+ name: "Copper",
148
+ category: "Metal",
149
+ params: { color: 12088115, metalness: 1, roughness: 0.25 }
150
+ },
151
+ { name: "Iron", category: "Metal", params: { color: 8421504, metalness: 0.95, roughness: 0.6 } },
152
+ {
153
+ name: "Brushed Steel",
154
+ category: "Metal",
155
+ params: { color: 10526880, metalness: 0.95, roughness: 0.4 }
156
+ },
157
+ {
158
+ name: "Chrome",
159
+ category: "Metal",
160
+ params: { color: 14540253, metalness: 1, roughness: 0.02 }
161
+ },
162
+ {
163
+ name: "Bronze",
164
+ category: "Metal",
165
+ params: { color: 13467442, metalness: 0.9, roughness: 0.35 }
166
+ },
167
+ {
168
+ name: "Aluminum",
169
+ category: "Metal",
170
+ params: { color: 14079702, metalness: 0.85, roughness: 0.2 }
171
+ },
172
+ // Non-metals
173
+ {
174
+ name: "Plastic (White)",
175
+ category: "Plastic",
176
+ params: { color: 15790320, metalness: 0, roughness: 0.3 }
177
+ },
178
+ {
179
+ name: "Plastic (Red)",
180
+ category: "Plastic",
181
+ params: { color: 13378082, metalness: 0, roughness: 0.35 }
182
+ },
183
+ {
184
+ name: "Rubber",
185
+ category: "Plastic",
186
+ params: { color: 3355443, metalness: 0, roughness: 0.9 }
187
+ },
188
+ // Glass
189
+ {
190
+ name: "Clear Glass",
191
+ category: "Glass",
192
+ params: {
193
+ color: 16777215,
194
+ metalness: 0,
195
+ roughness: 0,
196
+ opacity: 0.15,
197
+ transparent: true,
198
+ ior: 1.5,
199
+ transmission: 0.95
53
200
  }
54
- get undoLabel() {
55
- return this.undoStack.length > 0 ? this.undoStack[this.undoStack.length - 1].label : null;
201
+ },
202
+ {
203
+ name: "Frosted Glass",
204
+ category: "Glass",
205
+ params: {
206
+ color: 15658751,
207
+ metalness: 0,
208
+ roughness: 0.5,
209
+ opacity: 0.3,
210
+ transparent: true,
211
+ ior: 1.5,
212
+ transmission: 0.7
56
213
  }
57
- get redoLabel() {
58
- return this.redoStack.length > 0 ? this.redoStack[this.redoStack.length - 1].label : null;
214
+ },
215
+ {
216
+ name: "Tinted Glass",
217
+ category: "Glass",
218
+ params: {
219
+ color: 2245768,
220
+ metalness: 0,
221
+ roughness: 0.05,
222
+ opacity: 0.3,
223
+ transparent: true,
224
+ ior: 1.5,
225
+ transmission: 0.8
59
226
  }
60
- }
61
- const DEFAULT_LOD_LEVELS = [
62
- { distance: 0, ratio: 1.0 },
63
- { distance: 10, ratio: 0.5 },
64
- { distance: 25, ratio: 0.25 },
65
- { distance: 50, ratio: 0.1 },
66
- ];
67
- /**
68
- * Simple vertex decimation by merging nearby vertices.
69
- * Returns a new geometry with reduced vertex count.
70
- */
71
- function decimateGeometry(geometry, ratio) {
72
- if (ratio >= 1.0) {
73
- return geometry.clone();
227
+ },
228
+ // Natural
229
+ {
230
+ name: "Wood (Light)",
231
+ category: "Natural",
232
+ params: { color: 12687979, metalness: 0, roughness: 0.7 }
233
+ },
234
+ {
235
+ name: "Wood (Dark)",
236
+ category: "Natural",
237
+ params: { color: 6042391, metalness: 0, roughness: 0.65 }
238
+ },
239
+ {
240
+ name: "Stone",
241
+ category: "Natural",
242
+ params: { color: 8947848, metalness: 0, roughness: 0.85 }
243
+ },
244
+ {
245
+ name: "Marble",
246
+ category: "Natural",
247
+ params: { color: 15789280, metalness: 0, roughness: 0.15 }
248
+ },
249
+ {
250
+ name: "Clay",
251
+ category: "Natural",
252
+ params: { color: 11887901, metalness: 0, roughness: 0.8 }
253
+ },
254
+ {
255
+ name: "Concrete",
256
+ category: "Natural",
257
+ params: { color: 10066329, metalness: 0, roughness: 0.95 }
258
+ },
259
+ // Fabric
260
+ {
261
+ name: "Cotton (White)",
262
+ category: "Fabric",
263
+ params: {
264
+ color: 16119260,
265
+ metalness: 0,
266
+ roughness: 0.9,
267
+ sheen: 0.5,
268
+ sheenRoughness: 0.8,
269
+ sheenColor: 16777215
74
270
  }
75
- const posAttr = geometry.getAttribute('position');
76
- if (!posAttr) {
77
- return geometry.clone();
271
+ },
272
+ {
273
+ name: "Silk",
274
+ category: "Fabric",
275
+ params: {
276
+ color: 14932681,
277
+ metalness: 0,
278
+ roughness: 0.3,
279
+ sheen: 1,
280
+ sheenRoughness: 0.3,
281
+ sheenColor: 16772829
78
282
  }
79
- const positions = Array.from(posAttr.array);
80
- const vertexCount = posAttr.count;
81
- const targetCount = Math.max(3, Math.floor(vertexCount * ratio));
82
- // Grid-based vertex merging
83
- const gridSize = Math.cbrt(vertexCount / targetCount) * 0.5;
84
- const merged = new Map();
85
- const vertexMap = new Int32Array(vertexCount);
86
- let newIndex = 0;
87
- for (let i = 0; i < vertexCount; i++) {
88
- const x = positions[i * 3];
89
- const y = positions[i * 3 + 1];
90
- const z = positions[i * 3 + 2];
91
- const key = `${Math.round(x / gridSize)},${Math.round(y / gridSize)},${Math.round(z / gridSize)}`;
92
- const existing = merged.get(key);
93
- if (existing) {
94
- existing.x += x;
95
- existing.y += y;
96
- existing.z += z;
97
- existing.count++;
98
- vertexMap[i] = existing.index;
99
- }
100
- else {
101
- merged.set(key, { x, y, z, count: 1, index: newIndex });
102
- vertexMap[i] = newIndex;
103
- newIndex++;
104
- }
283
+ },
284
+ {
285
+ name: "Velvet",
286
+ category: "Fabric",
287
+ params: {
288
+ color: 8388640,
289
+ metalness: 0,
290
+ roughness: 0.95,
291
+ sheen: 0.9,
292
+ sheenRoughness: 0.9,
293
+ sheenColor: 16728160
105
294
  }
106
- // Build new positions
107
- const newPositions = new Float32Array(merged.size * 3);
108
- for (const cell of merged.values()) {
109
- newPositions[cell.index * 3] = cell.x / cell.count;
110
- newPositions[cell.index * 3 + 1] = cell.y / cell.count;
111
- newPositions[cell.index * 3 + 2] = cell.z / cell.count;
295
+ },
296
+ {
297
+ name: "Leather",
298
+ category: "Fabric",
299
+ params: { color: 5583650, metalness: 0, roughness: 0.6 }
300
+ },
301
+ {
302
+ name: "Denim",
303
+ category: "Fabric",
304
+ params: { color: 3889560, metalness: 0, roughness: 0.85 }
305
+ },
306
+ // Skin
307
+ {
308
+ name: "Skin (Light)",
309
+ category: "Skin",
310
+ params: { color: 16767916, metalness: 0, roughness: 0.55 }
311
+ },
312
+ {
313
+ name: "Skin (Medium)",
314
+ category: "Skin",
315
+ params: { color: 13010498, metalness: 0, roughness: 0.5 }
316
+ },
317
+ {
318
+ name: "Skin (Dark)",
319
+ category: "Skin",
320
+ params: { color: 9262372, metalness: 0, roughness: 0.5 }
321
+ },
322
+ // Special
323
+ {
324
+ name: "Car Paint",
325
+ category: "Special",
326
+ params: {
327
+ color: 13369344,
328
+ metalness: 0.5,
329
+ roughness: 0.15,
330
+ clearcoat: 1,
331
+ clearcoatRoughness: 0.05
112
332
  }
113
- // Build new indices
114
- const oldIndex = geometry.index;
115
- const newIndices = [];
116
- if (oldIndex) {
117
- for (let i = 0; i < oldIndex.count; i += 3) {
118
- const a = vertexMap[oldIndex.array[i]];
119
- const b = vertexMap[oldIndex.array[i + 1]];
120
- const c = vertexMap[oldIndex.array[i + 2]];
121
- if (a !== b && b !== c && a !== c) {
122
- newIndices.push(a, b, c);
123
- }
124
- }
333
+ },
334
+ {
335
+ name: "Emissive (Blue)",
336
+ category: "Special",
337
+ params: {
338
+ color: 1118481,
339
+ metalness: 0,
340
+ roughness: 0.5,
341
+ emissive: 35071,
342
+ emissiveIntensity: 2
125
343
  }
126
- else {
127
- for (let i = 0; i < vertexCount; i += 3) {
128
- const a = vertexMap[i];
129
- const b = vertexMap[i + 1];
130
- const c = vertexMap[i + 2];
131
- if (a !== b && b !== c && a !== c) {
132
- newIndices.push(a, b, c);
133
- }
134
- }
344
+ },
345
+ {
346
+ name: "Emissive (Orange)",
347
+ category: "Special",
348
+ params: {
349
+ color: 1118481,
350
+ metalness: 0,
351
+ roughness: 0.5,
352
+ emissive: 16737792,
353
+ emissiveIntensity: 2
135
354
  }
136
- const result = new THREE.BufferGeometry();
137
- result.setAttribute('position', new THREE.BufferAttribute(newPositions, 3));
138
- result.setIndex(newIndices);
139
- result.computeVertexNormals();
140
- return result;
141
- }
142
- /**
143
- * Creates a THREE.LOD object from a mesh with automatic level generation.
144
- */
145
- function generateLOD(mesh, levels = DEFAULT_LOD_LEVELS) {
146
- const lod = new THREE.LOD();
147
- lod.name = mesh.name + '_LOD';
148
- lod.position.copy(mesh.position);
149
- lod.rotation.copy(mesh.rotation);
150
- lod.scale.copy(mesh.scale);
151
- for (const level of levels) {
152
- const decimated = decimateGeometry(mesh.geometry, level.ratio);
153
- const mat = Array.isArray(mesh.material)
154
- ? mesh.material.map((m) => m.clone())
155
- : mesh.material.clone();
156
- const lodMesh = new THREE.Mesh(decimated, mat);
157
- lodMesh.castShadow = mesh.castShadow;
158
- lodMesh.receiveShadow = mesh.receiveShadow;
159
- lod.addLevel(lodMesh, level.distance);
355
+ },
356
+ {
357
+ name: "Neon",
358
+ category: "Special",
359
+ params: {
360
+ color: 0,
361
+ metalness: 0,
362
+ roughness: 0.1,
363
+ emissive: 65416,
364
+ emissiveIntensity: 5
160
365
  }
161
- return lod;
162
- }
163
- const PBR_PRESETS = [
164
- // Metals
165
- { name: 'Gold', category: 'Metal', params: { color: 0xffd700, metalness: 1.0, roughness: 0.15 } },
166
- {
167
- name: 'Silver',
168
- category: 'Metal',
169
- params: { color: 0xc0c0c0, metalness: 1.0, roughness: 0.1 },
170
- },
171
- {
172
- name: 'Copper',
173
- category: 'Metal',
174
- params: { color: 0xb87333, metalness: 1.0, roughness: 0.25 },
175
- },
176
- { name: 'Iron', category: 'Metal', params: { color: 0x808080, metalness: 0.95, roughness: 0.6 } },
177
- {
178
- name: 'Brushed Steel',
179
- category: 'Metal',
180
- params: { color: 0xa0a0a0, metalness: 0.95, roughness: 0.4 },
181
- },
182
- {
183
- name: 'Chrome',
184
- category: 'Metal',
185
- params: { color: 0xdddddd, metalness: 1.0, roughness: 0.02 },
186
- },
187
- {
188
- name: 'Bronze',
189
- category: 'Metal',
190
- params: { color: 0xcd7f32, metalness: 0.9, roughness: 0.35 },
191
- },
192
- {
193
- name: 'Aluminum',
194
- category: 'Metal',
195
- params: { color: 0xd6d6d6, metalness: 0.85, roughness: 0.2 },
196
- },
197
- // Non-metals
198
- {
199
- name: 'Plastic (White)',
200
- category: 'Plastic',
201
- params: { color: 0xf0f0f0, metalness: 0.0, roughness: 0.3 },
202
- },
203
- {
204
- name: 'Plastic (Red)',
205
- category: 'Plastic',
206
- params: { color: 0xcc2222, metalness: 0.0, roughness: 0.35 },
207
- },
208
- {
209
- name: 'Rubber',
210
- category: 'Plastic',
211
- params: { color: 0x333333, metalness: 0.0, roughness: 0.9 },
212
- },
213
- // Glass
214
- {
215
- name: 'Clear Glass',
216
- category: 'Glass',
217
- params: {
218
- color: 0xffffff,
219
- metalness: 0.0,
220
- roughness: 0.0,
221
- opacity: 0.15,
222
- transparent: true,
223
- ior: 1.5,
224
- transmission: 0.95,
225
- },
226
- },
227
- {
228
- name: 'Frosted Glass',
229
- category: 'Glass',
230
- params: {
231
- color: 0xeeeeff,
232
- metalness: 0.0,
233
- roughness: 0.5,
234
- opacity: 0.3,
235
- transparent: true,
236
- ior: 1.5,
237
- transmission: 0.7,
238
- },
239
- },
240
- {
241
- name: 'Tinted Glass',
242
- category: 'Glass',
243
- params: {
244
- color: 0x224488,
245
- metalness: 0.0,
246
- roughness: 0.05,
247
- opacity: 0.3,
248
- transparent: true,
249
- ior: 1.5,
250
- transmission: 0.8,
251
- },
252
- },
253
- // Natural
254
- {
255
- name: 'Wood (Light)',
256
- category: 'Natural',
257
- params: { color: 0xc19a6b, metalness: 0.0, roughness: 0.7 },
258
- },
259
- {
260
- name: 'Wood (Dark)',
261
- category: 'Natural',
262
- params: { color: 0x5c3317, metalness: 0.0, roughness: 0.65 },
263
- },
264
- {
265
- name: 'Stone',
266
- category: 'Natural',
267
- params: { color: 0x888888, metalness: 0.0, roughness: 0.85 },
268
- },
269
- {
270
- name: 'Marble',
271
- category: 'Natural',
272
- params: { color: 0xf0ece0, metalness: 0.0, roughness: 0.15 },
273
- },
274
- {
275
- name: 'Clay',
276
- category: 'Natural',
277
- params: { color: 0xb5651d, metalness: 0.0, roughness: 0.8 },
278
- },
279
- {
280
- name: 'Concrete',
281
- category: 'Natural',
282
- params: { color: 0x999999, metalness: 0.0, roughness: 0.95 },
283
- },
284
- // Fabric
285
- {
286
- name: 'Cotton (White)',
287
- category: 'Fabric',
288
- params: {
289
- color: 0xf5f5dc,
290
- metalness: 0.0,
291
- roughness: 0.9,
292
- sheen: 0.5,
293
- sheenRoughness: 0.8,
294
- sheenColor: 0xffffff,
295
- },
296
- },
297
- {
298
- name: 'Silk',
299
- category: 'Fabric',
300
- params: {
301
- color: 0xe3dac9,
302
- metalness: 0.0,
303
- roughness: 0.3,
304
- sheen: 1.0,
305
- sheenRoughness: 0.3,
306
- sheenColor: 0xffeedd,
307
- },
308
- },
309
- {
310
- name: 'Velvet',
311
- category: 'Fabric',
312
- params: {
313
- color: 0x800020,
314
- metalness: 0.0,
315
- roughness: 0.95,
316
- sheen: 0.9,
317
- sheenRoughness: 0.9,
318
- sheenColor: 0xff4060,
319
- },
320
- },
321
- {
322
- name: 'Leather',
323
- category: 'Fabric',
324
- params: { color: 0x553322, metalness: 0.0, roughness: 0.6 },
325
- },
326
- {
327
- name: 'Denim',
328
- category: 'Fabric',
329
- params: { color: 0x3b5998, metalness: 0.0, roughness: 0.85 },
330
- },
331
- // Skin
332
- {
333
- name: 'Skin (Light)',
334
- category: 'Skin',
335
- params: { color: 0xffdbac, metalness: 0.0, roughness: 0.55 },
336
- },
337
- {
338
- name: 'Skin (Medium)',
339
- category: 'Skin',
340
- params: { color: 0xc68642, metalness: 0.0, roughness: 0.5 },
341
- },
342
- {
343
- name: 'Skin (Dark)',
344
- category: 'Skin',
345
- params: { color: 0x8d5524, metalness: 0.0, roughness: 0.5 },
346
- },
347
- // Special
348
- {
349
- name: 'Car Paint',
350
- category: 'Special',
351
- params: {
352
- color: 0xcc0000,
353
- metalness: 0.5,
354
- roughness: 0.15,
355
- clearcoat: 1.0,
356
- clearcoatRoughness: 0.05,
357
- },
358
- },
359
- {
360
- name: 'Emissive (Blue)',
361
- category: 'Special',
362
- params: {
363
- color: 0x111111,
364
- metalness: 0.0,
365
- roughness: 0.5,
366
- emissive: 0x0088ff,
367
- emissiveIntensity: 2.0,
368
- },
369
- },
370
- {
371
- name: 'Emissive (Orange)',
372
- category: 'Special',
373
- params: {
374
- color: 0x111111,
375
- metalness: 0.0,
376
- roughness: 0.5,
377
- emissive: 0xff6600,
378
- emissiveIntensity: 2.0,
379
- },
380
- },
381
- {
382
- name: 'Neon',
383
- category: 'Special',
384
- params: {
385
- color: 0x000000,
386
- metalness: 0.0,
387
- roughness: 0.1,
388
- emissive: 0x00ff88,
389
- emissiveIntensity: 5.0,
390
- },
391
- },
366
+ }
392
367
  ];
393
- /**
394
- * Apply a PBR preset to a MeshStandardMaterial or MeshPhysicalMaterial.
395
- */
396
368
  function applyPBRPreset(material, preset) {
397
- var _a, _b;
398
- const p = preset.params;
399
- // Upgrade to MeshPhysicalMaterial if needed for advanced features
400
- const needsPhysical = p.clearcoat != null || p.transmission != null || p.sheen != null || p.ior != null;
401
- if (material instanceof THREE.MeshPhysicalMaterial) {
402
- material.color.setHex(p.color);
403
- material.metalness = p.metalness;
404
- material.roughness = p.roughness;
405
- if (p.emissive != null) {
406
- material.emissive.setHex(p.emissive);
407
- }
408
- if (p.emissiveIntensity != null) {
409
- material.emissiveIntensity = p.emissiveIntensity;
410
- }
411
- if (p.opacity != null) {
412
- material.opacity = p.opacity;
413
- }
414
- material.transparent = (_a = p.transparent) !== null && _a !== void 0 ? _a : false;
415
- if (p.clearcoat != null) {
416
- material.clearcoat = p.clearcoat;
417
- }
418
- if (p.clearcoatRoughness != null) {
419
- material.clearcoatRoughness = p.clearcoatRoughness;
420
- }
421
- if (p.ior != null) {
422
- material.ior = p.ior;
423
- }
424
- if (p.transmission != null) {
425
- material.transmission = p.transmission;
426
- }
427
- if (p.sheen != null) {
428
- material.sheen = p.sheen;
429
- }
430
- if (p.sheenRoughness != null) {
431
- material.sheenRoughness = p.sheenRoughness;
432
- }
433
- if (p.sheenColor != null) {
434
- material.sheenColor.setHex(p.sheenColor);
435
- }
436
- material.needsUpdate = true;
369
+ const p = preset.params;
370
+ const needsPhysical = p.clearcoat != null || p.transmission != null || p.sheen != null || p.ior != null;
371
+ if (material instanceof THREE.MeshPhysicalMaterial) {
372
+ material.color.setHex(p.color);
373
+ material.metalness = p.metalness;
374
+ material.roughness = p.roughness;
375
+ if (p.emissive != null) {
376
+ material.emissive.setHex(p.emissive);
437
377
  }
438
- else if (material instanceof THREE.MeshStandardMaterial && !needsPhysical) {
439
- material.color.setHex(p.color);
440
- material.metalness = p.metalness;
441
- material.roughness = p.roughness;
442
- if (p.emissive != null) {
443
- material.emissive.setHex(p.emissive);
444
- }
445
- if (p.emissiveIntensity != null) {
446
- material.emissiveIntensity = p.emissiveIntensity;
447
- }
448
- if (p.opacity != null) {
449
- material.opacity = p.opacity;
450
- }
451
- material.transparent = (_b = p.transparent) !== null && _b !== void 0 ? _b : false;
452
- material.needsUpdate = true;
378
+ if (p.emissiveIntensity != null) {
379
+ material.emissiveIntensity = p.emissiveIntensity;
380
+ }
381
+ if (p.opacity != null) {
382
+ material.opacity = p.opacity;
383
+ }
384
+ material.transparent = p.transparent ?? false;
385
+ if (p.clearcoat != null) {
386
+ material.clearcoat = p.clearcoat;
387
+ }
388
+ if (p.clearcoatRoughness != null) {
389
+ material.clearcoatRoughness = p.clearcoatRoughness;
390
+ }
391
+ if (p.ior != null) {
392
+ material.ior = p.ior;
453
393
  }
394
+ if (p.transmission != null) {
395
+ material.transmission = p.transmission;
396
+ }
397
+ if (p.sheen != null) {
398
+ material.sheen = p.sheen;
399
+ }
400
+ if (p.sheenRoughness != null) {
401
+ material.sheenRoughness = p.sheenRoughness;
402
+ }
403
+ if (p.sheenColor != null) {
404
+ material.sheenColor.setHex(p.sheenColor);
405
+ }
406
+ material.needsUpdate = true;
407
+ } else if (material instanceof THREE.MeshStandardMaterial && !needsPhysical) {
408
+ material.color.setHex(p.color);
409
+ material.metalness = p.metalness;
410
+ material.roughness = p.roughness;
411
+ if (p.emissive != null) {
412
+ material.emissive.setHex(p.emissive);
413
+ }
414
+ if (p.emissiveIntensity != null) {
415
+ material.emissiveIntensity = p.emissiveIntensity;
416
+ }
417
+ if (p.opacity != null) {
418
+ material.opacity = p.opacity;
419
+ }
420
+ material.transparent = p.transparent ?? false;
421
+ material.needsUpdate = true;
422
+ }
454
423
  }
455
- /** Create a new MeshPhysicalMaterial from a preset. */
456
424
  function createMaterialFromPreset(preset) {
457
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
458
- const p = preset.params;
459
- return new THREE.MeshPhysicalMaterial({
460
- color: p.color,
461
- metalness: p.metalness,
462
- roughness: p.roughness,
463
- emissive: (_a = p.emissive) !== null && _a !== void 0 ? _a : 0x000000,
464
- emissiveIntensity: (_b = p.emissiveIntensity) !== null && _b !== void 0 ? _b : 0,
465
- opacity: (_c = p.opacity) !== null && _c !== void 0 ? _c : 1,
466
- transparent: (_d = p.transparent) !== null && _d !== void 0 ? _d : false,
467
- clearcoat: (_e = p.clearcoat) !== null && _e !== void 0 ? _e : 0,
468
- clearcoatRoughness: (_f = p.clearcoatRoughness) !== null && _f !== void 0 ? _f : 0,
469
- ior: (_g = p.ior) !== null && _g !== void 0 ? _g : 1.5,
470
- transmission: (_h = p.transmission) !== null && _h !== void 0 ? _h : 0,
471
- sheen: (_j = p.sheen) !== null && _j !== void 0 ? _j : 0,
472
- sheenRoughness: (_k = p.sheenRoughness) !== null && _k !== void 0 ? _k : 0,
473
- sheenColor: new THREE.Color((_l = p.sheenColor) !== null && _l !== void 0 ? _l : 0x000000),
474
- });
425
+ const p = preset.params;
426
+ return new THREE.MeshPhysicalMaterial({
427
+ color: p.color,
428
+ metalness: p.metalness,
429
+ roughness: p.roughness,
430
+ emissive: p.emissive ?? 0,
431
+ emissiveIntensity: p.emissiveIntensity ?? 0,
432
+ opacity: p.opacity ?? 1,
433
+ transparent: p.transparent ?? false,
434
+ clearcoat: p.clearcoat ?? 0,
435
+ clearcoatRoughness: p.clearcoatRoughness ?? 0,
436
+ ior: p.ior ?? 1.5,
437
+ transmission: p.transmission ?? 0,
438
+ sheen: p.sheen ?? 0,
439
+ sheenRoughness: p.sheenRoughness ?? 0,
440
+ sheenColor: new THREE.Color(p.sheenColor ?? 0)
441
+ });
475
442
  }
476
- /* ═══════════════════════════════════════════
477
- 4. INSTANCING
478
- ═══════════════════════════════════════════ */
479
- /**
480
- * Convert duplicate meshes to InstancedMesh for better performance.
481
- * Groups meshes by geometry UUID + material UUID.
482
- */
483
443
  function convertToInstanced(parent) {
484
- var _a;
485
- const groups = new Map();
486
- parent.traverse((child) => {
487
- if (child instanceof THREE.Mesh && !child.name.startsWith('__')) {
488
- const geoId = child.geometry.uuid;
489
- const matId = Array.isArray(child.material)
490
- ? child.material.map((m) => m.uuid).join(',')
491
- : child.material.uuid;
492
- const key = `${geoId}|${matId}`;
493
- let list = groups.get(key);
494
- if (!list) {
495
- list = [];
496
- groups.set(key, list);
497
- }
498
- list.push(child);
499
- }
500
- });
501
- let created = 0;
502
- let removed = 0;
503
- for (const [, meshes] of groups) {
504
- if (meshes.length < 2) {
505
- continue;
506
- }
507
- const source = meshes[0];
508
- const instancedMesh = new THREE.InstancedMesh(source.geometry, source.material, meshes.length);
509
- instancedMesh.name = source.name + '_Instanced';
510
- instancedMesh.castShadow = source.castShadow;
511
- instancedMesh.receiveShadow = source.receiveShadow;
512
- const matrix = new THREE.Matrix4();
513
- for (let i = 0; i < meshes.length; i++) {
514
- meshes[i].updateWorldMatrix(true, false);
515
- matrix.copy(meshes[i].matrixWorld);
516
- instancedMesh.setMatrixAt(i, matrix);
517
- }
518
- instancedMesh.instanceMatrix.needsUpdate = true;
519
- parent.add(instancedMesh);
520
- created++;
521
- for (const mesh of meshes) {
522
- (_a = mesh.parent) === null || _a === void 0 ? void 0 : _a.remove(mesh);
523
- removed++;
524
- }
444
+ const groups = /* @__PURE__ */ new Map();
445
+ parent.traverse((child) => {
446
+ if (child instanceof THREE.Mesh && !child.name.startsWith("__")) {
447
+ const geoId = child.geometry.uuid;
448
+ const matId = Array.isArray(child.material) ? child.material.map((m) => m.uuid).join(",") : child.material.uuid;
449
+ const key = `${geoId}|${matId}`;
450
+ let list = groups.get(key);
451
+ if (!list) {
452
+ list = [];
453
+ groups.set(key, list);
454
+ }
455
+ list.push(child);
456
+ }
457
+ });
458
+ let created = 0;
459
+ let removed = 0;
460
+ for (const [, meshes] of groups) {
461
+ if (meshes.length < 2) {
462
+ continue;
463
+ }
464
+ const source = meshes[0];
465
+ const instancedMesh = new THREE.InstancedMesh(source.geometry, source.material, meshes.length);
466
+ instancedMesh.name = source.name + "_Instanced";
467
+ instancedMesh.castShadow = source.castShadow;
468
+ instancedMesh.receiveShadow = source.receiveShadow;
469
+ const matrix = new THREE.Matrix4();
470
+ for (let i = 0; i < meshes.length; i++) {
471
+ meshes[i].updateWorldMatrix(true, false);
472
+ matrix.copy(meshes[i].matrixWorld);
473
+ instancedMesh.setMatrixAt(i, matrix);
525
474
  }
526
- return { created, removed };
475
+ instancedMesh.instanceMatrix.needsUpdate = true;
476
+ parent.add(instancedMesh);
477
+ created++;
478
+ for (const mesh of meshes) {
479
+ mesh.parent?.remove(mesh);
480
+ removed++;
481
+ }
482
+ }
483
+ return { created, removed };
527
484
  }
528
- /* ═══════════════════════════════════════════
529
- 5. GEOMETRY HELPERS
530
- ═══════════════════════════════════════════ */
531
- /** Count total triangles in a scene. */
532
485
  function countTriangles(object) {
533
- let count = 0;
534
- object.traverse((child) => {
535
- if (child instanceof THREE.Mesh) {
536
- const geo = child.geometry;
537
- if (geo.index) {
538
- count += geo.index.count / 3;
539
- }
540
- else {
541
- const pos = geo.getAttribute('position');
542
- if (pos) {
543
- count += pos.count / 3;
544
- }
545
- }
486
+ let count = 0;
487
+ object.traverse((child) => {
488
+ if (child instanceof THREE.Mesh) {
489
+ const geo = child.geometry;
490
+ if (geo.index) {
491
+ count += geo.index.count / 3;
492
+ } else {
493
+ const pos = geo.getAttribute("position");
494
+ if (pos) {
495
+ count += pos.count / 3;
546
496
  }
547
- });
548
- return Math.round(count);
497
+ }
498
+ }
499
+ });
500
+ return Math.round(count);
549
501
  }
550
- /** Compute bounding box for an object and all its children. */
551
502
  function computeSceneBounds(object) {
552
- const box = new THREE.Box3();
553
- object.traverse((child) => {
554
- if (child instanceof THREE.Mesh) {
555
- child.geometry.computeBoundingBox();
556
- if (child.geometry.boundingBox) {
557
- const worldBox = child.geometry.boundingBox.clone();
558
- worldBox.applyMatrix4(child.matrixWorld);
559
- box.union(worldBox);
560
- }
561
- }
562
- });
563
- return box;
503
+ const box = new THREE.Box3();
504
+ object.traverse((child) => {
505
+ if (child instanceof THREE.Mesh) {
506
+ child.geometry.computeBoundingBox();
507
+ if (child.geometry.boundingBox) {
508
+ const worldBox = child.geometry.boundingBox.clone();
509
+ worldBox.applyMatrix4(child.matrixWorld);
510
+ box.union(worldBox);
511
+ }
512
+ }
513
+ });
514
+ return box;
564
515
  }
565
- /** Measure distance between two world-space points. */
566
516
  function measureDistance(a, b) {
567
- return a.distanceTo(b);
517
+ return a.distanceTo(b);
568
518
  }
569
- /** Measure angle (in degrees) between three world-space points (vertex at B). */
570
519
  function measureAngle(a, b, c) {
571
- const ba = new THREE.Vector3().subVectors(a, b).normalize();
572
- const bc = new THREE.Vector3().subVectors(c, b).normalize();
573
- return THREE.MathUtils.radToDeg(Math.acos(THREE.MathUtils.clamp(ba.dot(bc), -1, 1)));
520
+ const ba = new THREE.Vector3().subVectors(a, b).normalize();
521
+ const bc = new THREE.Vector3().subVectors(c, b).normalize();
522
+ return THREE.MathUtils.radToDeg(Math.acos(THREE.MathUtils.clamp(ba.dot(bc), -1, 1)));
574
523
  }
575
524
  const HDRI_PRESETS = [
576
- // Studio
577
- {
578
- name: 'Studio Soft',
579
- category: 'Studio',
580
- bgColor: 0x1a1a2e,
581
- envIntensity: 1.0,
582
- ambientColor: 0xffffff,
583
- ambientIntensity: 0.6,
584
- dirColor: 0xffffff,
585
- dirIntensity: 1.2,
586
- dirPosition: [5, 10, 7],
587
- exposure: 1.0,
588
- },
589
- {
590
- name: 'Studio High Key',
591
- category: 'Studio',
592
- bgColor: 0xf0f0f0,
593
- envIntensity: 1.2,
594
- ambientColor: 0xffffff,
595
- ambientIntensity: 1.0,
596
- dirColor: 0xffffff,
597
- dirIntensity: 1.5,
598
- dirPosition: [2, 8, 3],
599
- exposure: 1.2,
600
- },
601
- {
602
- name: 'Studio Low Key',
603
- category: 'Studio',
604
- bgColor: 0x0a0a0a,
605
- envIntensity: 0.5,
606
- ambientColor: 0x222244,
607
- ambientIntensity: 0.2,
608
- dirColor: 0xffeedd,
609
- dirIntensity: 2.0,
610
- dirPosition: [3, 5, 2],
611
- exposure: 0.8,
612
- },
613
- {
614
- name: 'Studio Rim',
615
- category: 'Studio',
616
- bgColor: 0x111111,
617
- envIntensity: 0.3,
618
- ambientColor: 0x111122,
619
- ambientIntensity: 0.15,
620
- dirColor: 0xffffff,
621
- dirIntensity: 2.5,
622
- dirPosition: [-5, 3, -2],
623
- exposure: 0.9,
624
- },
625
- // Outdoor
626
- {
627
- name: 'Sunny Day',
628
- category: 'Outdoor',
629
- bgColor: 0x87ceeb,
630
- envIntensity: 1.0,
631
- ambientColor: 0x8ec8f0,
632
- ambientIntensity: 0.4,
633
- dirColor: 0xfff4e0,
634
- dirIntensity: 2.0,
635
- dirPosition: [5, 10, 5],
636
- exposure: 1.0,
637
- },
638
- {
639
- name: 'Overcast',
640
- category: 'Outdoor',
641
- bgColor: 0xb0b0b8,
642
- envIntensity: 0.8,
643
- ambientColor: 0xccccdd,
644
- ambientIntensity: 0.7,
645
- dirColor: 0xddddee,
646
- dirIntensity: 0.5,
647
- dirPosition: [0, 10, 0],
648
- fog: { color: 0xccccdd, near: 20, far: 200 },
649
- exposure: 1.0,
650
- },
651
- {
652
- name: 'Golden Hour',
653
- category: 'Outdoor',
654
- bgColor: 0xff8844,
655
- envIntensity: 1.0,
656
- ambientColor: 0xffaa55,
657
- ambientIntensity: 0.3,
658
- dirColor: 0xff9933,
659
- dirIntensity: 2.5,
660
- dirPosition: [10, 2, 5],
661
- exposure: 1.1,
662
- },
663
- {
664
- name: 'Blue Hour',
665
- category: 'Outdoor',
666
- bgColor: 0x1a2a4a,
667
- envIntensity: 0.6,
668
- ambientColor: 0x334466,
669
- ambientIntensity: 0.3,
670
- dirColor: 0x6688cc,
671
- dirIntensity: 0.8,
672
- dirPosition: [-5, 3, 5],
673
- exposure: 0.8,
674
- },
675
- {
676
- name: 'Night',
677
- category: 'Outdoor',
678
- bgColor: 0x050510,
679
- envIntensity: 0.1,
680
- ambientColor: 0x111133,
681
- ambientIntensity: 0.05,
682
- dirColor: 0xaabbdd,
683
- dirIntensity: 0.3,
684
- dirPosition: [-3, 8, 2],
685
- exposure: 0.5,
686
- },
687
- // Interior
688
- {
689
- name: 'Warm Interior',
690
- category: 'Interior',
691
- bgColor: 0x2a1a10,
692
- envIntensity: 0.7,
693
- ambientColor: 0xffddaa,
694
- ambientIntensity: 0.4,
695
- dirColor: 0xffeebb,
696
- dirIntensity: 1.0,
697
- dirPosition: [2, 5, 3],
698
- exposure: 1.0,
699
- },
700
- {
701
- name: 'Cool Office',
702
- category: 'Interior',
703
- bgColor: 0x1a2230,
704
- envIntensity: 0.8,
705
- ambientColor: 0xddeeff,
706
- ambientIntensity: 0.6,
707
- dirColor: 0xffffff,
708
- dirIntensity: 0.8,
709
- dirPosition: [0, 8, 0],
710
- exposure: 1.0,
711
- },
712
- {
713
- name: 'Gallery',
714
- category: 'Interior',
715
- bgColor: 0xfafafa,
716
- envIntensity: 1.0,
717
- ambientColor: 0xffffff,
718
- ambientIntensity: 0.8,
719
- dirColor: 0xffffff,
720
- dirIntensity: 0.6,
721
- dirPosition: [0, 10, 2],
722
- exposure: 1.1,
723
- },
724
- // Dramatic
725
- {
726
- name: 'Neon City',
727
- category: 'Dramatic',
728
- bgColor: 0x0a0015,
729
- envIntensity: 0.4,
730
- ambientColor: 0xff00ff,
731
- ambientIntensity: 0.2,
732
- dirColor: 0x00ffff,
733
- dirIntensity: 1.5,
734
- dirPosition: [-3, 5, 2],
735
- exposure: 1.0,
736
- },
737
- {
738
- name: 'Cinematic',
739
- category: 'Dramatic',
740
- bgColor: 0x0f0f1a,
741
- envIntensity: 0.5,
742
- ambientColor: 0x223344,
743
- ambientIntensity: 0.15,
744
- dirColor: 0xffeedd,
745
- dirIntensity: 2.0,
746
- dirPosition: [8, 4, 2],
747
- exposure: 0.9,
748
- },
749
- {
750
- name: 'Sunrise',
751
- category: 'Dramatic',
752
- bgColor: 0xff6633,
753
- envIntensity: 0.8,
754
- ambientColor: 0xffaa66,
755
- ambientIntensity: 0.3,
756
- dirColor: 0xff7744,
757
- dirIntensity: 2.0,
758
- dirPosition: [15, 1, 0],
759
- exposure: 1.2,
760
- },
761
- {
762
- name: 'Desert',
763
- category: 'Dramatic',
764
- bgColor: 0xe8c888,
765
- envIntensity: 1.0,
766
- ambientColor: 0xddcc99,
767
- ambientIntensity: 0.5,
768
- dirColor: 0xfff0cc,
769
- dirIntensity: 2.5,
770
- dirPosition: [5, 12, 3],
771
- exposure: 1.3,
772
- },
525
+ // Studio
526
+ {
527
+ name: "Studio Soft",
528
+ category: "Studio",
529
+ bgColor: 1710638,
530
+ envIntensity: 1,
531
+ ambientColor: 16777215,
532
+ ambientIntensity: 0.6,
533
+ dirColor: 16777215,
534
+ dirIntensity: 1.2,
535
+ dirPosition: [5, 10, 7],
536
+ exposure: 1
537
+ },
538
+ {
539
+ name: "Studio High Key",
540
+ category: "Studio",
541
+ bgColor: 15790320,
542
+ envIntensity: 1.2,
543
+ ambientColor: 16777215,
544
+ ambientIntensity: 1,
545
+ dirColor: 16777215,
546
+ dirIntensity: 1.5,
547
+ dirPosition: [2, 8, 3],
548
+ exposure: 1.2
549
+ },
550
+ {
551
+ name: "Studio Low Key",
552
+ category: "Studio",
553
+ bgColor: 657930,
554
+ envIntensity: 0.5,
555
+ ambientColor: 2236996,
556
+ ambientIntensity: 0.2,
557
+ dirColor: 16772829,
558
+ dirIntensity: 2,
559
+ dirPosition: [3, 5, 2],
560
+ exposure: 0.8
561
+ },
562
+ {
563
+ name: "Studio Rim",
564
+ category: "Studio",
565
+ bgColor: 1118481,
566
+ envIntensity: 0.3,
567
+ ambientColor: 1118498,
568
+ ambientIntensity: 0.15,
569
+ dirColor: 16777215,
570
+ dirIntensity: 2.5,
571
+ dirPosition: [-5, 3, -2],
572
+ exposure: 0.9
573
+ },
574
+ // Outdoor
575
+ {
576
+ name: "Sunny Day",
577
+ category: "Outdoor",
578
+ bgColor: 8900331,
579
+ envIntensity: 1,
580
+ ambientColor: 9357552,
581
+ ambientIntensity: 0.4,
582
+ dirColor: 16774368,
583
+ dirIntensity: 2,
584
+ dirPosition: [5, 10, 5],
585
+ exposure: 1
586
+ },
587
+ {
588
+ name: "Overcast",
589
+ category: "Outdoor",
590
+ bgColor: 11579576,
591
+ envIntensity: 0.8,
592
+ ambientColor: 13421789,
593
+ ambientIntensity: 0.7,
594
+ dirColor: 14540270,
595
+ dirIntensity: 0.5,
596
+ dirPosition: [0, 10, 0],
597
+ fog: { color: 13421789, near: 20, far: 200 },
598
+ exposure: 1
599
+ },
600
+ {
601
+ name: "Golden Hour",
602
+ category: "Outdoor",
603
+ bgColor: 16746564,
604
+ envIntensity: 1,
605
+ ambientColor: 16755285,
606
+ ambientIntensity: 0.3,
607
+ dirColor: 16750899,
608
+ dirIntensity: 2.5,
609
+ dirPosition: [10, 2, 5],
610
+ exposure: 1.1
611
+ },
612
+ {
613
+ name: "Blue Hour",
614
+ category: "Outdoor",
615
+ bgColor: 1714762,
616
+ envIntensity: 0.6,
617
+ ambientColor: 3359846,
618
+ ambientIntensity: 0.3,
619
+ dirColor: 6719692,
620
+ dirIntensity: 0.8,
621
+ dirPosition: [-5, 3, 5],
622
+ exposure: 0.8
623
+ },
624
+ {
625
+ name: "Night",
626
+ category: "Outdoor",
627
+ bgColor: 328976,
628
+ envIntensity: 0.1,
629
+ ambientColor: 1118515,
630
+ ambientIntensity: 0.05,
631
+ dirColor: 11189213,
632
+ dirIntensity: 0.3,
633
+ dirPosition: [-3, 8, 2],
634
+ exposure: 0.5
635
+ },
636
+ // Interior
637
+ {
638
+ name: "Warm Interior",
639
+ category: "Interior",
640
+ bgColor: 2759184,
641
+ envIntensity: 0.7,
642
+ ambientColor: 16768426,
643
+ ambientIntensity: 0.4,
644
+ dirColor: 16772795,
645
+ dirIntensity: 1,
646
+ dirPosition: [2, 5, 3],
647
+ exposure: 1
648
+ },
649
+ {
650
+ name: "Cool Office",
651
+ category: "Interior",
652
+ bgColor: 1712688,
653
+ envIntensity: 0.8,
654
+ ambientColor: 14544639,
655
+ ambientIntensity: 0.6,
656
+ dirColor: 16777215,
657
+ dirIntensity: 0.8,
658
+ dirPosition: [0, 8, 0],
659
+ exposure: 1
660
+ },
661
+ {
662
+ name: "Gallery",
663
+ category: "Interior",
664
+ bgColor: 16448250,
665
+ envIntensity: 1,
666
+ ambientColor: 16777215,
667
+ ambientIntensity: 0.8,
668
+ dirColor: 16777215,
669
+ dirIntensity: 0.6,
670
+ dirPosition: [0, 10, 2],
671
+ exposure: 1.1
672
+ },
673
+ // Dramatic
674
+ {
675
+ name: "Neon City",
676
+ category: "Dramatic",
677
+ bgColor: 655381,
678
+ envIntensity: 0.4,
679
+ ambientColor: 16711935,
680
+ ambientIntensity: 0.2,
681
+ dirColor: 65535,
682
+ dirIntensity: 1.5,
683
+ dirPosition: [-3, 5, 2],
684
+ exposure: 1
685
+ },
686
+ {
687
+ name: "Cinematic",
688
+ category: "Dramatic",
689
+ bgColor: 986906,
690
+ envIntensity: 0.5,
691
+ ambientColor: 2241348,
692
+ ambientIntensity: 0.15,
693
+ dirColor: 16772829,
694
+ dirIntensity: 2,
695
+ dirPosition: [8, 4, 2],
696
+ exposure: 0.9
697
+ },
698
+ {
699
+ name: "Sunrise",
700
+ category: "Dramatic",
701
+ bgColor: 16737843,
702
+ envIntensity: 0.8,
703
+ ambientColor: 16755302,
704
+ ambientIntensity: 0.3,
705
+ dirColor: 16742212,
706
+ dirIntensity: 2,
707
+ dirPosition: [15, 1, 0],
708
+ exposure: 1.2
709
+ },
710
+ {
711
+ name: "Desert",
712
+ category: "Dramatic",
713
+ bgColor: 15255688,
714
+ envIntensity: 1,
715
+ ambientColor: 14535833,
716
+ ambientIntensity: 0.5,
717
+ dirColor: 16773324,
718
+ dirIntensity: 2.5,
719
+ dirPosition: [5, 12, 3],
720
+ exposure: 1.3
721
+ }
773
722
  ];
774
- /**
775
- * Apply an HDRI preset to a Three.js scene and renderer.
776
- */
777
723
  function applyHDRIPreset(scene, renderer, preset) {
778
- // Background
779
- scene.background = new THREE.Color(preset.bgColor);
780
- // Tone mapping exposure
781
- renderer.toneMappingExposure = preset.exposure;
782
- // Update ambient light (find existing or create)
783
- let ambient;
784
- let directional;
785
- scene.traverse((child) => {
786
- if (child instanceof THREE.AmbientLight && !ambient) {
787
- ambient = child;
788
- }
789
- if (child instanceof THREE.DirectionalLight && !directional) {
790
- directional = child;
791
- }
792
- });
793
- if (ambient != null) {
794
- ambient.color.setHex(preset.ambientColor);
795
- ambient.intensity = preset.ambientIntensity;
796
- }
797
- if (directional != null) {
798
- directional.color.setHex(preset.dirColor);
799
- directional.intensity = preset.dirIntensity;
800
- directional.position.set(...preset.dirPosition);
724
+ scene.background = new THREE.Color(preset.bgColor);
725
+ renderer.toneMappingExposure = preset.exposure;
726
+ let ambient;
727
+ let directional;
728
+ scene.traverse((child) => {
729
+ if (child instanceof THREE.AmbientLight && !ambient) {
730
+ ambient = child;
801
731
  }
802
- // Fog
803
- if (preset.fog) {
804
- scene.fog = new THREE.Fog(preset.fog.color, preset.fog.near, preset.fog.far);
805
- }
806
- else {
807
- scene.fog = null;
732
+ if (child instanceof THREE.DirectionalLight && !directional) {
733
+ directional = child;
808
734
  }
735
+ });
736
+ if (ambient != null) {
737
+ ambient.color.setHex(preset.ambientColor);
738
+ ambient.intensity = preset.ambientIntensity;
739
+ }
740
+ if (directional != null) {
741
+ directional.color.setHex(preset.dirColor);
742
+ directional.intensity = preset.dirIntensity;
743
+ directional.position.set(...preset.dirPosition);
744
+ }
745
+ if (preset.fog) {
746
+ scene.fog = new THREE.Fog(preset.fog.color, preset.fog.near, preset.fog.far);
747
+ } else {
748
+ scene.fog = null;
749
+ }
809
750
  }
810
- /** Snap a point to the nearest grid intersection. */
811
751
  function snapToGrid(point, gridSize) {
812
- return new THREE.Vector3(Math.round(point.x / gridSize) * gridSize, Math.round(point.y / gridSize) * gridSize, Math.round(point.z / gridSize) * gridSize);
752
+ return new THREE.Vector3(
753
+ Math.round(point.x / gridSize) * gridSize,
754
+ Math.round(point.y / gridSize) * gridSize,
755
+ Math.round(point.z / gridSize) * gridSize
756
+ );
813
757
  }
814
- /** Find the closest vertex in a mesh to a given world-space point. */
815
758
  function snapToVertex(point, scene, maxDistance = Infinity) {
816
- let closest = null;
817
- let minDist = maxDistance;
818
- const tmpV = new THREE.Vector3();
819
- scene.traverse((child) => {
820
- if (!(child instanceof THREE.Mesh)) {
821
- return;
822
- }
823
- const posAttr = child.geometry.getAttribute('position');
824
- if (!posAttr) {
825
- return;
826
- }
827
- child.updateWorldMatrix(true, false);
828
- for (let i = 0; i < posAttr.count; i++) {
829
- tmpV.fromBufferAttribute(posAttr, i);
830
- tmpV.applyMatrix4(child.matrixWorld);
831
- const d = point.distanceTo(tmpV);
832
- if (d < minDist) {
833
- minDist = d;
834
- closest = { point: tmpV.clone(), target: 'vertex', object: child };
835
- }
836
- }
837
- });
838
- return closest;
759
+ let closest = null;
760
+ let minDist = maxDistance;
761
+ const tmpV = new THREE.Vector3();
762
+ scene.traverse((child) => {
763
+ if (!(child instanceof THREE.Mesh)) {
764
+ return;
765
+ }
766
+ const posAttr = child.geometry.getAttribute("position");
767
+ if (!posAttr) {
768
+ return;
769
+ }
770
+ child.updateWorldMatrix(true, false);
771
+ for (let i = 0; i < posAttr.count; i++) {
772
+ tmpV.fromBufferAttribute(posAttr, i);
773
+ tmpV.applyMatrix4(child.matrixWorld);
774
+ const d = point.distanceTo(tmpV);
775
+ if (d < minDist) {
776
+ minDist = d;
777
+ closest = { point: tmpV.clone(), target: "vertex", object: child };
778
+ }
779
+ }
780
+ });
781
+ return closest;
839
782
  }
840
- /** Find the closest point on any edge in the scene to a given world-space point. */
841
783
  function snapToEdge(point, scene, maxDistance = Infinity) {
842
- let closest = null;
843
- let minDist = maxDistance;
844
- const a = new THREE.Vector3();
845
- const b = new THREE.Vector3();
846
- scene.traverse((child) => {
847
- if (!(child instanceof THREE.Mesh)) {
848
- return;
849
- }
850
- const posAttr = child.geometry.getAttribute('position');
851
- if (!posAttr) {
852
- return;
853
- }
854
- const index = child.geometry.index;
855
- child.updateWorldMatrix(true, false);
856
- const processEdge = (i0, i1) => {
857
- a.fromBufferAttribute(posAttr, i0).applyMatrix4(child.matrixWorld);
858
- b.fromBufferAttribute(posAttr, i1).applyMatrix4(child.matrixWorld);
859
- const closestOnEdge = closestPointOnSegment(point, a, b);
860
- const d = point.distanceTo(closestOnEdge);
861
- if (d < minDist) {
862
- minDist = d;
863
- closest = { point: closestOnEdge.clone(), target: 'edge', object: child };
864
- }
865
- };
866
- if (index) {
867
- for (let i = 0; i < index.count; i += 3) {
868
- const i0 = index.array[i], i1 = index.array[i + 1], i2 = index.array[i + 2];
869
- processEdge(i0, i1);
870
- processEdge(i1, i2);
871
- processEdge(i2, i0);
872
- }
873
- }
874
- else {
875
- for (let i = 0; i < posAttr.count; i += 3) {
876
- processEdge(i, i + 1);
877
- processEdge(i + 1, i + 2);
878
- processEdge(i + 2, i);
879
- }
880
- }
881
- });
882
- return closest;
784
+ let closest = null;
785
+ let minDist = maxDistance;
786
+ const a = new THREE.Vector3();
787
+ const b = new THREE.Vector3();
788
+ scene.traverse((child) => {
789
+ if (!(child instanceof THREE.Mesh)) {
790
+ return;
791
+ }
792
+ const posAttr = child.geometry.getAttribute("position");
793
+ if (!posAttr) {
794
+ return;
795
+ }
796
+ const index = child.geometry.index;
797
+ child.updateWorldMatrix(true, false);
798
+ const processEdge = (i0, i1) => {
799
+ a.fromBufferAttribute(posAttr, i0).applyMatrix4(child.matrixWorld);
800
+ b.fromBufferAttribute(posAttr, i1).applyMatrix4(child.matrixWorld);
801
+ const closestOnEdge = closestPointOnSegment(point, a, b);
802
+ const d = point.distanceTo(closestOnEdge);
803
+ if (d < minDist) {
804
+ minDist = d;
805
+ closest = { point: closestOnEdge.clone(), target: "edge", object: child };
806
+ }
807
+ };
808
+ if (index) {
809
+ for (let i = 0; i < index.count; i += 3) {
810
+ const i0 = index.array[i], i1 = index.array[i + 1], i2 = index.array[i + 2];
811
+ processEdge(i0, i1);
812
+ processEdge(i1, i2);
813
+ processEdge(i2, i0);
814
+ }
815
+ } else {
816
+ for (let i = 0; i < posAttr.count; i += 3) {
817
+ processEdge(i, i + 1);
818
+ processEdge(i + 1, i + 2);
819
+ processEdge(i + 2, i);
820
+ }
821
+ }
822
+ });
823
+ return closest;
883
824
  }
884
- /** Find closest point on a line segment. */
885
825
  function closestPointOnSegment(p, a, b) {
886
- const ab = new THREE.Vector3().subVectors(b, a);
887
- const ap = new THREE.Vector3().subVectors(p, a);
888
- const t = THREE.MathUtils.clamp(ap.dot(ab) / ab.dot(ab), 0, 1);
889
- return new THREE.Vector3().addVectors(a, ab.multiplyScalar(t));
826
+ const ab = new THREE.Vector3().subVectors(b, a);
827
+ const ap = new THREE.Vector3().subVectors(p, a);
828
+ const t = THREE.MathUtils.clamp(ap.dot(ab) / ab.dot(ab), 0, 1);
829
+ return new THREE.Vector3().addVectors(a, ab.multiplyScalar(t));
890
830
  }
891
- /**
892
- * Multi-mode snap: tries vertex/edge/grid in priority order.
893
- * Returns the best snap within maxDistance, or grid snap as fallback.
894
- */
895
- function snapMulti(point, scene, gridSize, modes = ['vertex', 'edge', 'grid'], maxDistance = 0.5) {
896
- for (const mode of modes) {
897
- if (mode === 'vertex') {
898
- const result = snapToVertex(point, scene, maxDistance);
899
- if (result) {
900
- return result;
901
- }
902
- }
903
- else if (mode === 'edge') {
904
- const result = snapToEdge(point, scene, maxDistance);
905
- if (result) {
906
- return result;
907
- }
908
- }
909
- else if (mode === 'grid') {
910
- return { point: snapToGrid(point, gridSize), target: 'grid' };
911
- }
831
+ function snapMulti(point, scene, gridSize, modes = ["vertex", "edge", "grid"], maxDistance = 0.5) {
832
+ for (const mode of modes) {
833
+ if (mode === "vertex") {
834
+ const result = snapToVertex(point, scene, maxDistance);
835
+ if (result) {
836
+ return result;
837
+ }
838
+ } else if (mode === "edge") {
839
+ const result = snapToEdge(point, scene, maxDistance);
840
+ if (result) {
841
+ return result;
842
+ }
843
+ } else if (mode === "grid") {
844
+ return { point: snapToGrid(point, gridSize), target: "grid" };
912
845
  }
913
- return { point: snapToGrid(point, gridSize), target: 'grid' };
846
+ }
847
+ return { point: snapToGrid(point, gridSize), target: "grid" };
914
848
  }
915
- /** Evaluate a cubic bezier segment at parameter t (0-1). */
916
849
  function cubicBezier(p0, p1, p2, p3, t) {
917
- const t2 = t * t;
918
- const t3 = t2 * t;
919
- const mt = 1 - t;
920
- const mt2 = mt * mt;
921
- const mt3 = mt2 * mt;
922
- return mt3 * p0 + 3 * mt2 * t * p1 + 3 * mt * t2 * p2 + t3 * p3;
850
+ const t2 = t * t;
851
+ const t3 = t2 * t;
852
+ const mt = 1 - t;
853
+ const mt2 = mt * mt;
854
+ const mt3 = mt2 * mt;
855
+ return mt3 * p0 + 3 * mt2 * t * p1 + 3 * mt * t2 * p2 + t3 * p3;
923
856
  }
924
- /** Evaluate a keyframe track at a given time. */
925
857
  function evaluateTrack(track, time) {
926
- var _a, _b;
927
- const { keyframes } = track;
928
- if (keyframes.length === 0) {
929
- return 0;
930
- }
931
- if (keyframes.length === 1) {
932
- return keyframes[0].value;
858
+ const { keyframes } = track;
859
+ if (keyframes.length === 0) {
860
+ return 0;
861
+ }
862
+ if (keyframes.length === 1) {
863
+ return keyframes[0].value;
864
+ }
865
+ if (time <= keyframes[0].time) {
866
+ return keyframes[0].value;
867
+ }
868
+ if (time >= keyframes[keyframes.length - 1].time) {
869
+ return keyframes[keyframes.length - 1].value;
870
+ }
871
+ let left = 0;
872
+ for (let i = 1; i < keyframes.length; i++) {
873
+ if (keyframes[i].time >= time) {
874
+ left = i - 1;
875
+ break;
933
876
  }
934
- // Clamp to range
935
- if (time <= keyframes[0].time) {
936
- return keyframes[0].value;
937
- }
938
- if (time >= keyframes[keyframes.length - 1].time) {
939
- return keyframes[keyframes.length - 1].value;
940
- }
941
- // Find surrounding keyframes
942
- let left = 0;
943
- for (let i = 1; i < keyframes.length; i++) {
944
- if (keyframes[i].time >= time) {
945
- left = i - 1;
946
- break;
947
- }
948
- }
949
- const right = left + 1;
950
- const kfL = keyframes[left];
951
- const kfR = keyframes[right];
952
- const dt = kfR.time - kfL.time;
953
- if (dt === 0) {
954
- return kfL.value;
955
- }
956
- const t = (time - kfL.time) / dt;
957
- switch (kfL.interpolation) {
958
- case 'constant':
959
- return kfL.value;
960
- case 'linear':
961
- return kfL.value + (kfR.value - kfL.value) * t;
962
- case 'ease-in':
963
- return kfL.value + (kfR.value - kfL.value) * (t * t);
964
- case 'ease-out':
965
- return kfL.value + (kfR.value - kfL.value) * (1 - (1 - t) * (1 - t));
966
- case 'ease-in-out':
967
- return (kfL.value + (kfR.value - kfL.value) * (t < 0.5 ? 2 * t * t : 1 - 2 * (1 - t) * (1 - t)));
968
- case 'bezier': {
969
- const handleR = (_a = kfL.handleRight) !== null && _a !== void 0 ? _a : { y: 0 };
970
- const handleL = (_b = kfR.handleLeft) !== null && _b !== void 0 ? _b : { y: 0 };
971
- return cubicBezier(kfL.value, kfL.value + handleR.y, kfR.value + handleL.y, kfR.value, t);
972
- }
973
- default:
974
- return kfL.value + (kfR.value - kfL.value) * t;
877
+ }
878
+ const right = left + 1;
879
+ const kfL = keyframes[left];
880
+ const kfR = keyframes[right];
881
+ const dt = kfR.time - kfL.time;
882
+ if (dt === 0) {
883
+ return kfL.value;
884
+ }
885
+ const t = (time - kfL.time) / dt;
886
+ switch (kfL.interpolation) {
887
+ case "constant":
888
+ return kfL.value;
889
+ case "linear":
890
+ return kfL.value + (kfR.value - kfL.value) * t;
891
+ case "ease-in":
892
+ return kfL.value + (kfR.value - kfL.value) * (t * t);
893
+ case "ease-out":
894
+ return kfL.value + (kfR.value - kfL.value) * (1 - (1 - t) * (1 - t));
895
+ case "ease-in-out":
896
+ return kfL.value + (kfR.value - kfL.value) * (t < 0.5 ? 2 * t * t : 1 - 2 * (1 - t) * (1 - t));
897
+ case "bezier": {
898
+ const handleR = kfL.handleRight ?? { y: 0 };
899
+ const handleL = kfR.handleLeft ?? { y: 0 };
900
+ return cubicBezier(kfL.value, kfL.value + handleR.y, kfR.value + handleL.y, kfR.value, t);
975
901
  }
902
+ default:
903
+ return kfL.value + (kfR.value - kfL.value) * t;
904
+ }
976
905
  }
977
- /** Insert or update a keyframe at a given time. */
978
- function setKeyframe(track, time, value, interpolation = 'bezier') {
979
- // Find existing keyframe at same time (within epsilon)
980
- const epsilon = 0.001;
981
- const existingIdx = track.keyframes.findIndex((kf) => Math.abs(kf.time - time) < epsilon);
982
- const kf = { time, value, interpolation };
983
- if (existingIdx >= 0) {
984
- track.keyframes[existingIdx] = kf;
985
- }
986
- else {
987
- track.keyframes.push(kf);
988
- track.keyframes.sort((a, b) => a.time - b.time);
989
- }
990
- return kf;
906
+ function setKeyframe(track, time, value, interpolation = "bezier") {
907
+ const epsilon = 1e-3;
908
+ const existingIdx = track.keyframes.findIndex((kf2) => Math.abs(kf2.time - time) < epsilon);
909
+ const kf = { time, value, interpolation };
910
+ if (existingIdx >= 0) {
911
+ track.keyframes[existingIdx] = kf;
912
+ } else {
913
+ track.keyframes.push(kf);
914
+ track.keyframes.sort((a, b) => a.time - b.time);
915
+ }
916
+ return kf;
991
917
  }
992
- /** Remove a keyframe at a given time. */
993
918
  function removeKeyframe(track, time) {
994
- const epsilon = 0.001;
995
- const idx = track.keyframes.findIndex((kf) => Math.abs(kf.time - time) < epsilon);
996
- if (idx >= 0) {
997
- track.keyframes.splice(idx, 1);
998
- return true;
999
- }
1000
- return false;
919
+ const epsilon = 1e-3;
920
+ const idx = track.keyframes.findIndex((kf) => Math.abs(kf.time - time) < epsilon);
921
+ if (idx >= 0) {
922
+ track.keyframes.splice(idx, 1);
923
+ return true;
924
+ }
925
+ return false;
1001
926
  }
1002
- /** Convert KeyframeTrack to THREE.KeyframeTrack for use in AnimationClip. */
1003
927
  function toThreeKeyframeTrack(track, sampleRate = 30) {
1004
- if (track.keyframes.length === 0) {
1005
- return new THREE.NumberKeyframeTrack(track.property, [0], [0]);
1006
- }
1007
- const startTime = track.keyframes[0].time;
1008
- const endTime = track.keyframes[track.keyframes.length - 1].time;
1009
- const duration = endTime - startTime;
1010
- const numSamples = Math.max(2, Math.ceil(duration * sampleRate));
1011
- const times = [];
1012
- const values = [];
1013
- for (let i = 0; i < numSamples; i++) {
1014
- const t = startTime + (duration * i) / (numSamples - 1);
1015
- times.push(t);
1016
- values.push(evaluateTrack(track, t));
1017
- }
1018
- return new THREE.NumberKeyframeTrack(track.property, times, values);
928
+ if (track.keyframes.length === 0) {
929
+ return new THREE.NumberKeyframeTrack(track.property, [0], [0]);
930
+ }
931
+ const startTime = track.keyframes[0].time;
932
+ const endTime = track.keyframes[track.keyframes.length - 1].time;
933
+ const duration = endTime - startTime;
934
+ const numSamples = Math.max(2, Math.ceil(duration * sampleRate));
935
+ const times = [];
936
+ const values = [];
937
+ for (let i = 0; i < numSamples; i++) {
938
+ const t = startTime + duration * i / (numSamples - 1);
939
+ times.push(t);
940
+ values.push(evaluateTrack(track, t));
941
+ }
942
+ return new THREE.NumberKeyframeTrack(track.property, times, values);
1019
943
  }
1020
- /** Create an AnimationClip from multiple KeyframeTracks. */
1021
944
  function createAnimationClip(name, tracks, sampleRate = 30) {
1022
- const threeTracks = tracks.map((t) => toThreeKeyframeTrack(t, sampleRate));
1023
- return new THREE.AnimationClip(name, -1, threeTracks);
945
+ const threeTracks = tracks.map((t) => toThreeKeyframeTrack(t, sampleRate));
946
+ return new THREE.AnimationClip(name, -1, threeTracks);
1024
947
  }
1025
- /** Trigger a file download in the browser. */
1026
948
  function downloadBlob(blob, filename) {
1027
- const url = URL.createObjectURL(blob);
1028
- const a = document.createElement('a');
1029
- a.href = url;
1030
- a.download = filename;
1031
- document.body.appendChild(a);
1032
- a.click();
1033
- document.body.removeChild(a);
1034
- URL.revokeObjectURL(url);
949
+ const url = URL.createObjectURL(blob);
950
+ const a = document.createElement("a");
951
+ a.href = url;
952
+ a.download = filename;
953
+ document.body.appendChild(a);
954
+ a.click();
955
+ document.body.removeChild(a);
956
+ URL.revokeObjectURL(url);
1035
957
  }
1036
- /** Export scene to ArrayBuffer as GLTF/GLB with optional Draco. */
1037
958
  async function exportSceneGLTF(scene, options = {}) {
1038
- const { GLTFExporter } = await import('three/examples/jsm/exporters/GLTFExporter.js');
1039
- const exporter = new GLTFExporter();
1040
- return new Promise((resolve, reject) => {
1041
- var _a, _b;
1042
- exporter.parse(scene, (result) => resolve(result), (error) => reject(error), {
1043
- binary: (_a = options.binary) !== null && _a !== void 0 ? _a : true,
1044
- animations: (_b = options.animations) !== null && _b !== void 0 ? _b : [],
1045
- });
1046
- });
959
+ const { GLTFExporter } = await import("three/examples/jsm/exporters/GLTFExporter.js");
960
+ const exporter = new GLTFExporter();
961
+ return new Promise((resolve, reject) => {
962
+ exporter.parse(
963
+ scene,
964
+ (result) => resolve(result),
965
+ (error) => reject(error),
966
+ {
967
+ binary: options.binary ?? true,
968
+ animations: options.animations ?? []
969
+ }
970
+ );
971
+ });
1047
972
  }
1048
- /** Export scene to USDZ format (Apple AR). */
1049
973
  async function exportSceneUSDZ(scene) {
1050
- const { USDZExporter } = await import('three/examples/jsm/exporters/USDZExporter.js');
1051
- const exporter = new USDZExporter();
1052
- const arraybuffer = await new Promise((resolve, reject) => {
1053
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- USDZExporter.parse callback signature varies between three.js minor versions
1054
- exporter.parse(scene, resolve, reject);
1055
- });
1056
- return new Blob([arraybuffer], { type: 'model/vnd.usdz+zip' });
974
+ const { USDZExporter } = await import("three/examples/jsm/exporters/USDZExporter.js");
975
+ const exporter = new USDZExporter();
976
+ const arraybuffer = await new Promise((resolve, reject) => {
977
+ exporter.parse(scene, resolve, reject);
978
+ });
979
+ return new Blob([arraybuffer], { type: "model/vnd.usdz+zip" });
1057
980
  }
1058
- /**
1059
- * Setup crossfade between two animation actions.
1060
- * Smoothly transitions from current to target over duration.
1061
- */
1062
981
  function crossfadeAnimations(mixer, fromAction, toAction, duration = 0.5) {
1063
- toAction.reset();
1064
- toAction.setEffectiveTimeScale(1);
1065
- toAction.setEffectiveWeight(1);
1066
- toAction.play();
1067
- fromAction.crossFadeTo(toAction, duration, true);
982
+ toAction.reset();
983
+ toAction.setEffectiveTimeScale(1);
984
+ toAction.setEffectiveWeight(1);
985
+ toAction.play();
986
+ fromAction.crossFadeTo(toAction, duration, true);
1068
987
  }
1069
- /**
1070
- * Setup additive blending for an animation action.
1071
- * Additive animations can layer on top of other animations.
1072
- */
1073
988
  function makeAdditiveAction(mixer, clip, referenceClip) {
1074
- const action = mixer.clipAction(clip);
1075
- if (referenceClip) {
1076
- THREE.AnimationUtils.makeClipAdditive(clip, 0, referenceClip);
1077
- }
1078
- action.blendMode = THREE.AdditiveAnimationBlendMode;
1079
- return action;
989
+ const action = mixer.clipAction(clip);
990
+ if (referenceClip) {
991
+ THREE.AnimationUtils.makeClipAdditive(clip, 0, referenceClip);
992
+ }
993
+ action.blendMode = THREE.AdditiveAnimationBlendMode;
994
+ return action;
1080
995
  }
1081
- /**
1082
- * Apply weighted blend to multiple animation actions.
1083
- * Normalizes weights so they sum to 1.0.
1084
- */
1085
996
  function blendAnimations(actions) {
1086
- const totalWeight = actions.reduce((sum, a) => sum + a.weight, 0);
1087
- if (totalWeight === 0) {
1088
- return;
1089
- }
1090
- for (const { action, weight } of actions) {
1091
- const normalizedWeight = weight / totalWeight;
1092
- action.setEffectiveWeight(normalizedWeight);
1093
- if (!action.isRunning()) {
1094
- action.play();
1095
- }
997
+ const totalWeight = actions.reduce((sum, a) => sum + a.weight, 0);
998
+ if (totalWeight === 0) {
999
+ return;
1000
+ }
1001
+ for (const { action, weight } of actions) {
1002
+ const normalizedWeight = weight / totalWeight;
1003
+ action.setEffectiveWeight(normalizedWeight);
1004
+ if (!action.isRunning()) {
1005
+ action.play();
1096
1006
  }
1007
+ }
1097
1008
  }
1098
- /**
1099
- * Create a sub-clip from an existing animation clip (trim).
1100
- */
1101
1009
  function subclip(clip, name, startFrame, endFrame, fps = 30) {
1102
- return THREE.AnimationUtils.subclip(clip, name, startFrame, endFrame, fps);
1010
+ return THREE.AnimationUtils.subclip(clip, name, startFrame, endFrame, fps);
1103
1011
  }
1104
- /**
1105
- * Simple CCD (Cyclic Coordinate Descent) IK solver.
1106
- * Iteratively rotates each bone in the chain to reach the target.
1107
- */
1108
1012
  function solveIKChain(skeleton, chain) {
1109
- const { bones: boneNames, target, iterations = 10, tolerance = 0.001 } = chain;
1110
- const bones = [];
1111
- for (const name of boneNames) {
1112
- const bone = skeleton.bones.find((b) => b.name === name);
1113
- if (!bone) {
1114
- return false;
1115
- }
1116
- bones.push(bone);
1117
- }
1118
- if (bones.length < 2) {
1119
- return false;
1120
- }
1121
- const endEffector = bones[bones.length - 1];
1122
- const endPos = new THREE.Vector3();
1123
- const toEnd = new THREE.Vector3();
1124
- const toTarget = new THREE.Vector3();
1125
- const axis = new THREE.Vector3();
1126
- const quat = new THREE.Quaternion();
1127
- for (let iter = 0; iter < iterations; iter++) {
1128
- // Get end-effector world position
1129
- endEffector.updateWorldMatrix(true, false);
1130
- endPos.setFromMatrixPosition(endEffector.matrixWorld);
1131
- if (endPos.distanceTo(target) < tolerance) {
1132
- return true;
1133
- }
1134
- // Iterate bones from end to root (skip end-effector)
1135
- for (let i = bones.length - 2; i >= 0; i--) {
1136
- const bone = bones[i];
1137
- bone.updateWorldMatrix(true, false);
1138
- const boneWorldPos = new THREE.Vector3().setFromMatrixPosition(bone.matrixWorld);
1139
- // Direction from bone to end-effector
1140
- endEffector.updateWorldMatrix(true, false);
1141
- endPos.setFromMatrixPosition(endEffector.matrixWorld);
1142
- toEnd.subVectors(endPos, boneWorldPos).normalize();
1143
- // Direction from bone to target
1144
- toTarget.subVectors(target, boneWorldPos).normalize();
1145
- // Calculate rotation axis and angle
1146
- const dot = THREE.MathUtils.clamp(toEnd.dot(toTarget), -1, 1);
1147
- const angle = Math.acos(dot);
1148
- if (angle > 0.0001) {
1149
- axis.crossVectors(toEnd, toTarget).normalize();
1150
- // Convert to bone local space
1151
- const boneWorldQuat = new THREE.Quaternion();
1152
- bone.getWorldQuaternion(boneWorldQuat);
1153
- const invWorldQuat = boneWorldQuat.clone().invert();
1154
- const localAxis = axis.clone().applyQuaternion(invWorldQuat);
1155
- // Apply limited rotation
1156
- const maxAngle = Math.PI / 6; // 30° per iteration
1157
- const clampedAngle = Math.min(angle, maxAngle);
1158
- quat.setFromAxisAngle(localAxis, clampedAngle);
1159
- bone.quaternion.premultiply(quat);
1160
- bone.updateWorldMatrix(true, true);
1161
- }
1162
- }
1013
+ const { bones: boneNames, target, iterations = 10, tolerance = 1e-3 } = chain;
1014
+ const bones = [];
1015
+ for (const name of boneNames) {
1016
+ const bone = skeleton.bones.find((b) => b.name === name);
1017
+ if (!bone) {
1018
+ return false;
1163
1019
  }
1020
+ bones.push(bone);
1021
+ }
1022
+ if (bones.length < 2) {
1023
+ return false;
1024
+ }
1025
+ const endEffector = bones[bones.length - 1];
1026
+ const endPos = new THREE.Vector3();
1027
+ const toEnd = new THREE.Vector3();
1028
+ const toTarget = new THREE.Vector3();
1029
+ const axis = new THREE.Vector3();
1030
+ const quat = new THREE.Quaternion();
1031
+ for (let iter = 0; iter < iterations; iter++) {
1164
1032
  endEffector.updateWorldMatrix(true, false);
1165
1033
  endPos.setFromMatrixPosition(endEffector.matrixWorld);
1166
- return endPos.distanceTo(target) < tolerance;
1034
+ if (endPos.distanceTo(target) < tolerance) {
1035
+ return true;
1036
+ }
1037
+ for (let i = bones.length - 2; i >= 0; i--) {
1038
+ const bone = bones[i];
1039
+ bone.updateWorldMatrix(true, false);
1040
+ const boneWorldPos = new THREE.Vector3().setFromMatrixPosition(bone.matrixWorld);
1041
+ endEffector.updateWorldMatrix(true, false);
1042
+ endPos.setFromMatrixPosition(endEffector.matrixWorld);
1043
+ toEnd.subVectors(endPos, boneWorldPos).normalize();
1044
+ toTarget.subVectors(target, boneWorldPos).normalize();
1045
+ const dot = THREE.MathUtils.clamp(toEnd.dot(toTarget), -1, 1);
1046
+ const angle = Math.acos(dot);
1047
+ if (angle > 1e-4) {
1048
+ axis.crossVectors(toEnd, toTarget).normalize();
1049
+ const boneWorldQuat = new THREE.Quaternion();
1050
+ bone.getWorldQuaternion(boneWorldQuat);
1051
+ const invWorldQuat = boneWorldQuat.clone().invert();
1052
+ const localAxis = axis.clone().applyQuaternion(invWorldQuat);
1053
+ const maxAngle = Math.PI / 6;
1054
+ const clampedAngle = Math.min(angle, maxAngle);
1055
+ quat.setFromAxisAngle(localAxis, clampedAngle);
1056
+ bone.quaternion.premultiply(quat);
1057
+ bone.updateWorldMatrix(true, true);
1058
+ }
1059
+ }
1060
+ }
1061
+ endEffector.updateWorldMatrix(true, false);
1062
+ endPos.setFromMatrixPosition(endEffector.matrixWorld);
1063
+ return endPos.distanceTo(target) < tolerance;
1167
1064
  }
1168
- /** Standard quad viewport layout. */
1169
1065
  const QUAD_VIEWPORT = [
1170
- { preset: 'perspective', x: 0.5, y: 0.5, width: 0.5, height: 0.5 },
1171
- { preset: 'top', x: 0, y: 0.5, width: 0.5, height: 0.5 },
1172
- { preset: 'front', x: 0, y: 0, width: 0.5, height: 0.5 },
1173
- { preset: 'right', x: 0.5, y: 0, width: 0.5, height: 0.5 },
1066
+ { preset: "perspective", x: 0.5, y: 0.5, width: 0.5, height: 0.5 },
1067
+ { preset: "top", x: 0, y: 0.5, width: 0.5, height: 0.5 },
1068
+ { preset: "front", x: 0, y: 0, width: 0.5, height: 0.5 },
1069
+ { preset: "right", x: 0.5, y: 0, width: 0.5, height: 0.5 }
1174
1070
  ];
1175
- /** Camera position/rotation for orthographic viewport presets. */
1176
1071
  const VIEWPORT_CAMERAS = {
1177
- perspective: { position: [5, 5, 5], up: [0, 1, 0], ortho: false },
1178
- top: { position: [0, 10, 0], up: [0, 0, -1], ortho: true },
1179
- front: { position: [0, 0, 10], up: [0, 1, 0], ortho: true },
1180
- right: { position: [10, 0, 0], up: [0, 1, 0], ortho: true },
1181
- left: { position: [-10, 0, 0], up: [0, 1, 0], ortho: true },
1182
- back: { position: [0, 0, -10], up: [0, 1, 0], ortho: true },
1183
- bottom: { position: [0, -10, 0], up: [0, 0, 1], ortho: true },
1072
+ perspective: { position: [5, 5, 5], up: [0, 1, 0], ortho: false },
1073
+ top: { position: [0, 10, 0], up: [0, 0, -1], ortho: true },
1074
+ front: { position: [0, 0, 10], up: [0, 1, 0], ortho: true },
1075
+ right: { position: [10, 0, 0], up: [0, 1, 0], ortho: true },
1076
+ left: { position: [-10, 0, 0], up: [0, 1, 0], ortho: true },
1077
+ back: { position: [0, 0, -10], up: [0, 1, 0], ortho: true },
1078
+ bottom: { position: [0, -10, 0], up: [0, 0, 1], ortho: true }
1184
1079
  };
1185
- /**
1186
- * Create a camera for a viewport preset.
1187
- */
1188
1080
  function createViewportCamera(preset, aspect = 1, frustumSize = 10) {
1189
- const config = VIEWPORT_CAMERAS[preset];
1190
- if (config.ortho) {
1191
- const camera = new THREE.OrthographicCamera((-frustumSize * aspect) / 2, (frustumSize * aspect) / 2, frustumSize / 2, -frustumSize / 2, 0.01, 10000);
1192
- camera.position.set(...config.position);
1193
- camera.up.set(...config.up);
1194
- camera.lookAt(0, 0, 0);
1195
- return camera;
1196
- }
1197
- const camera = new THREE.PerspectiveCamera(50, aspect, 0.01, 10000);
1198
- camera.position.set(...config.position);
1199
- camera.up.set(...config.up);
1200
- camera.lookAt(0, 0, 0);
1201
- return camera;
1081
+ const config = VIEWPORT_CAMERAS[preset];
1082
+ if (config.ortho) {
1083
+ const camera2 = new THREE.OrthographicCamera(
1084
+ -frustumSize * aspect / 2,
1085
+ frustumSize * aspect / 2,
1086
+ frustumSize / 2,
1087
+ -frustumSize / 2,
1088
+ 0.01,
1089
+ 1e4
1090
+ );
1091
+ camera2.position.set(...config.position);
1092
+ camera2.up.set(...config.up);
1093
+ camera2.lookAt(0, 0, 0);
1094
+ return camera2;
1095
+ }
1096
+ const camera = new THREE.PerspectiveCamera(50, aspect, 0.01, 1e4);
1097
+ camera.position.set(...config.position);
1098
+ camera.up.set(...config.up);
1099
+ camera.lookAt(0, 0, 0);
1100
+ return camera;
1202
1101
  }
1203
- /**
1204
- * Render a multi-viewport layout to a single renderer.
1205
- */
1206
1102
  function renderMultiViewport(renderer, scene, viewports) {
1207
- const { width, height } = renderer.getSize(new THREE.Vector2());
1208
- renderer.setScissorTest(true);
1209
- for (const { camera, config } of viewports) {
1210
- const left = Math.floor(config.x * width);
1211
- const bottom = Math.floor(config.y * height);
1212
- const vpWidth = Math.floor(config.width * width);
1213
- const vpHeight = Math.floor(config.height * height);
1214
- renderer.setViewport(left, bottom, vpWidth, vpHeight);
1215
- renderer.setScissor(left, bottom, vpWidth, vpHeight);
1216
- renderer.render(scene, camera);
1217
- }
1218
- renderer.setScissorTest(false);
1103
+ const { width, height } = renderer.getSize(new THREE.Vector2());
1104
+ renderer.setScissorTest(true);
1105
+ for (const { camera, config } of viewports) {
1106
+ const left = Math.floor(config.x * width);
1107
+ const bottom = Math.floor(config.y * height);
1108
+ const vpWidth = Math.floor(config.width * width);
1109
+ const vpHeight = Math.floor(config.height * height);
1110
+ renderer.setViewport(left, bottom, vpWidth, vpHeight);
1111
+ renderer.setScissor(left, bottom, vpWidth, vpHeight);
1112
+ renderer.render(scene, camera);
1113
+ }
1114
+ renderer.setScissorTest(false);
1219
1115
  }
1220
-
1221
- export { HDRI_PRESETS, PBR_PRESETS, QUAD_VIEWPORT, UndoRedoStack, applyHDRIPreset, applyPBRPreset, blendAnimations, computeSceneBounds, convertToInstanced, countTriangles, createAnimationClip, createMaterialFromPreset, createViewportCamera, crossfadeAnimations, downloadBlob, evaluateTrack, exportSceneGLTF, exportSceneUSDZ, generateLOD, makeAdditiveAction, measureAngle, measureDistance, removeKeyframe, renderMultiViewport, setKeyframe, snapMulti, snapToEdge, snapToGrid, snapToVertex, solveIKChain, subclip, toThreeKeyframeTrack };
1116
+ export {
1117
+ HDRI_PRESETS,
1118
+ PBR_PRESETS,
1119
+ QUAD_VIEWPORT,
1120
+ UndoRedoStack,
1121
+ applyHDRIPreset,
1122
+ applyPBRPreset,
1123
+ blendAnimations,
1124
+ computeSceneBounds,
1125
+ convertToInstanced,
1126
+ countTriangles,
1127
+ createAnimationClip,
1128
+ createMaterialFromPreset,
1129
+ createViewportCamera,
1130
+ crossfadeAnimations,
1131
+ downloadBlob,
1132
+ evaluateTrack,
1133
+ exportSceneGLTF,
1134
+ exportSceneUSDZ,
1135
+ generateLOD,
1136
+ makeAdditiveAction,
1137
+ measureAngle,
1138
+ measureDistance,
1139
+ removeKeyframe,
1140
+ renderMultiViewport,
1141
+ setKeyframe,
1142
+ snapMulti,
1143
+ snapToEdge,
1144
+ snapToGrid,
1145
+ snapToVertex,
1146
+ solveIKChain,
1147
+ subclip,
1148
+ toThreeKeyframeTrack
1149
+ };