@nice2dev/ui-3d 1.0.0 → 1.0.2

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