@nice2dev/ui-3d 1.3.15 → 1.3.17

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 +13 -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,348 +1,405 @@
1
- import { jsxs, jsx } from 'react/jsx-runtime';
2
- import { NiceIcon } from '@nice2dev/ui';
3
- import { useEffect, useMemo, useState, useRef, useCallback } from 'react';
4
- import * as THREE from 'three';
5
- import { useNiceTranslation } from '../core/i18n.js';
6
-
7
- /* ── Octree implementation ── */
1
+ import { jsxs, jsx } from "react/jsx-runtime";
2
+ import { NiceIcon } from "@nice2dev/ui";
3
+ import { useEffect, useMemo, useState, useRef, useCallback } from "react";
4
+ import * as THREE from "three";
5
+ import { useNiceTranslation } from "../core/i18n.js";
8
6
  const DEFAULT_CONFIG = {
9
- maxDepth: 8,
10
- maxObjectsPerLeaf: 8,
11
- looseness: 1.0,
7
+ maxDepth: 8,
8
+ maxObjectsPerLeaf: 8,
9
+ looseness: 1
12
10
  };
13
11
  function createNode(bounds, depth) {
14
- return { bounds, children: null, objects: [], depth };
12
+ return { bounds, children: null, objects: [], depth };
15
13
  }
16
14
  function splitNode(node, config) {
17
- if (node.depth >= config.maxDepth) {
18
- return;
15
+ if (node.depth >= config.maxDepth) {
16
+ return;
17
+ }
18
+ const center = new THREE.Vector3();
19
+ node.bounds.getCenter(center);
20
+ const min = node.bounds.min;
21
+ const max = node.bounds.max;
22
+ node.children = [];
23
+ for (let i = 0; i < 8; i++) {
24
+ const childMin = new THREE.Vector3(
25
+ i & 1 ? center.x : min.x,
26
+ i & 2 ? center.y : min.y,
27
+ i & 4 ? center.z : min.z
28
+ );
29
+ const childMax = new THREE.Vector3(
30
+ i & 1 ? max.x : center.x,
31
+ i & 2 ? max.y : center.y,
32
+ i & 4 ? max.z : center.z
33
+ );
34
+ if (config.looseness > 1) {
35
+ const size = new THREE.Vector3().subVectors(childMax, childMin);
36
+ const expansion = size.clone().multiplyScalar((config.looseness - 1) * 0.5);
37
+ childMin.sub(expansion);
38
+ childMax.add(expansion);
19
39
  }
20
- const center = new THREE.Vector3();
21
- node.bounds.getCenter(center);
22
- const min = node.bounds.min;
23
- const max = node.bounds.max;
24
- node.children = [];
25
- for (let i = 0; i < 8; i++) {
26
- const childMin = new THREE.Vector3(i & 1 ? center.x : min.x, i & 2 ? center.y : min.y, i & 4 ? center.z : min.z);
27
- const childMax = new THREE.Vector3(i & 1 ? max.x : center.x, i & 2 ? max.y : center.y, i & 4 ? max.z : center.z);
28
- // Apply looseness
29
- if (config.looseness > 1) {
30
- const size = new THREE.Vector3().subVectors(childMax, childMin);
31
- const expansion = size.clone().multiplyScalar((config.looseness - 1) * 0.5);
32
- childMin.sub(expansion);
33
- childMax.add(expansion);
34
- }
35
- node.children.push(createNode(new THREE.Box3(childMin, childMax), node.depth + 1));
40
+ node.children.push(createNode(new THREE.Box3(childMin, childMax), node.depth + 1));
41
+ }
42
+ const orphans = [];
43
+ for (const obj of node.objects) {
44
+ const objBounds = new THREE.Box3().setFromObject(obj);
45
+ let placed = false;
46
+ for (const child of node.children) {
47
+ if (child.bounds.containsBox(objBounds)) {
48
+ child.objects.push(obj);
49
+ placed = true;
50
+ break;
51
+ }
36
52
  }
37
- // Redistribute objects to children
38
- const orphans = [];
39
- for (const obj of node.objects) {
40
- const objBounds = new THREE.Box3().setFromObject(obj);
41
- let placed = false;
42
- for (const child of node.children) {
43
- if (child.bounds.containsBox(objBounds)) {
44
- child.objects.push(obj);
45
- placed = true;
46
- break;
47
- }
48
- }
49
- if (!placed) {
50
- orphans.push(obj);
51
- }
53
+ if (!placed) {
54
+ orphans.push(obj);
52
55
  }
53
- node.objects = orphans;
56
+ }
57
+ node.objects = orphans;
54
58
  }
55
59
  function insertObject(node, object, config) {
56
- const objBounds = new THREE.Box3().setFromObject(object);
57
- if (!node.bounds.intersectsBox(objBounds)) {
58
- return;
59
- }
60
- if (node.children) {
61
- for (const child of node.children) {
62
- if (child.bounds.containsBox(objBounds)) {
63
- insertObject(child, object, config);
64
- return;
65
- }
66
- }
67
- // Doesn't fit in any child — store in this node
68
- node.objects.push(object);
60
+ const objBounds = new THREE.Box3().setFromObject(object);
61
+ if (!node.bounds.intersectsBox(objBounds)) {
62
+ return;
63
+ }
64
+ if (node.children) {
65
+ for (const child of node.children) {
66
+ if (child.bounds.containsBox(objBounds)) {
67
+ insertObject(child, object, config);
69
68
  return;
69
+ }
70
70
  }
71
71
  node.objects.push(object);
72
- if (node.objects.length > config.maxObjectsPerLeaf && node.depth < config.maxDepth) {
73
- splitNode(node, config);
74
- }
72
+ return;
73
+ }
74
+ node.objects.push(object);
75
+ if (node.objects.length > config.maxObjectsPerLeaf && node.depth < config.maxDepth) {
76
+ splitNode(node, config);
77
+ }
75
78
  }
76
79
  function removeObject(node, object) {
77
- const idx = node.objects.indexOf(object);
78
- if (idx !== -1) {
79
- node.objects.splice(idx, 1);
80
+ const idx = node.objects.indexOf(object);
81
+ if (idx !== -1) {
82
+ node.objects.splice(idx, 1);
83
+ return true;
84
+ }
85
+ if (node.children) {
86
+ for (const child of node.children) {
87
+ if (removeObject(child, object)) {
80
88
  return true;
89
+ }
81
90
  }
82
- if (node.children) {
83
- for (const child of node.children) {
84
- if (removeObject(child, object)) {
85
- return true;
86
- }
87
- }
88
- }
89
- return false;
91
+ }
92
+ return false;
90
93
  }
91
94
  function raycastNode(node, raycaster, results) {
92
- var _a;
93
- if (!raycaster.ray.intersectsBox(node.bounds)) {
94
- return;
95
+ if (!raycaster.ray.intersectsBox(node.bounds)) {
96
+ return;
97
+ }
98
+ for (const obj of node.objects) {
99
+ if (obj instanceof THREE.Mesh) {
100
+ const intersects = raycaster.intersectObject(obj, false);
101
+ for (const hit of intersects) {
102
+ results.push({
103
+ object: obj,
104
+ point: hit.point,
105
+ distance: hit.distance,
106
+ face: hit.face ?? null
107
+ });
108
+ }
95
109
  }
96
- // Test objects in this node
97
- for (const obj of node.objects) {
98
- if (obj instanceof THREE.Mesh) {
99
- const intersects = raycaster.intersectObject(obj, false);
100
- for (const hit of intersects) {
101
- results.push({
102
- object: obj,
103
- point: hit.point,
104
- distance: hit.distance,
105
- face: (_a = hit.face) !== null && _a !== void 0 ? _a : null,
106
- });
107
- }
108
- }
109
- }
110
- // Recurse into children
111
- if (node.children) {
112
- for (const child of node.children) {
113
- raycastNode(child, raycaster, results);
114
- }
110
+ }
111
+ if (node.children) {
112
+ for (const child of node.children) {
113
+ raycastNode(child, raycaster, results);
115
114
  }
115
+ }
116
116
  }
117
117
  function frustumCullNode(node, frustum, results) {
118
- if (!frustum.intersectsBox(node.bounds)) {
119
- return;
118
+ if (!frustum.intersectsBox(node.bounds)) {
119
+ return;
120
+ }
121
+ for (const obj of node.objects) {
122
+ const objBounds = new THREE.Box3().setFromObject(obj);
123
+ if (frustum.intersectsBox(objBounds)) {
124
+ results.push(obj);
120
125
  }
121
- for (const obj of node.objects) {
122
- const objBounds = new THREE.Box3().setFromObject(obj);
123
- if (frustum.intersectsBox(objBounds)) {
124
- results.push(obj);
125
- }
126
- }
127
- if (node.children) {
128
- for (const child of node.children) {
129
- frustumCullNode(child, frustum, results);
130
- }
126
+ }
127
+ if (node.children) {
128
+ for (const child of node.children) {
129
+ frustumCullNode(child, frustum, results);
131
130
  }
131
+ }
132
132
  }
133
133
  function nearestInNode(node, point, maxDist, best) {
134
- if (!node.bounds.distanceToPoint(point)) {
135
- return;
136
- }
137
- if (node.bounds.distanceToPoint(point) > best.dist) {
138
- return;
134
+ if (!node.bounds.distanceToPoint(point)) {
135
+ return;
136
+ }
137
+ if (node.bounds.distanceToPoint(point) > best.dist) {
138
+ return;
139
+ }
140
+ for (const obj of node.objects) {
141
+ const pos = new THREE.Vector3();
142
+ obj.getWorldPosition(pos);
143
+ const dist = pos.distanceTo(point);
144
+ if (dist < best.dist && dist <= maxDist) {
145
+ best.dist = dist;
146
+ best.object = obj;
139
147
  }
140
- for (const obj of node.objects) {
141
- const pos = new THREE.Vector3();
142
- obj.getWorldPosition(pos);
143
- const dist = pos.distanceTo(point);
144
- if (dist < best.dist && dist <= maxDist) {
145
- best.dist = dist;
146
- best.object = obj;
147
- }
148
- }
149
- if (node.children) {
150
- // Sort children by distance to point for early termination
151
- const sorted = [...node.children].sort((a, b) => {
152
- const da = a.bounds.distanceToPoint(point);
153
- const db = b.bounds.distanceToPoint(point);
154
- return da - db;
155
- });
156
- for (const child of sorted) {
157
- nearestInNode(child, point, maxDist, best);
158
- }
148
+ }
149
+ if (node.children) {
150
+ const sorted = [...node.children].sort((a, b) => {
151
+ const da = a.bounds.distanceToPoint(point);
152
+ const db = b.bounds.distanceToPoint(point);
153
+ return da - db;
154
+ });
155
+ for (const child of sorted) {
156
+ nearestInNode(child, point, maxDist, best);
159
157
  }
158
+ }
160
159
  }
161
160
  function countNodes(node) {
162
- let count = 1;
163
- if (node.children) {
164
- for (const child of node.children) {
165
- count += countNodes(child);
166
- }
161
+ let count = 1;
162
+ if (node.children) {
163
+ for (const child of node.children) {
164
+ count += countNodes(child);
167
165
  }
168
- return count;
166
+ }
167
+ return count;
169
168
  }
170
169
  function countObjects(node) {
171
- let count = node.objects.length;
172
- if (node.children) {
173
- for (const child of node.children) {
174
- count += countObjects(child);
175
- }
170
+ let count = node.objects.length;
171
+ if (node.children) {
172
+ for (const child of node.children) {
173
+ count += countObjects(child);
176
174
  }
177
- return count;
175
+ }
176
+ return count;
178
177
  }
179
178
  function buildDebugVisualization(node, group, maxDepth) {
180
- const size = new THREE.Vector3();
181
- node.bounds.getSize(size);
182
- const center = new THREE.Vector3();
183
- node.bounds.getCenter(center);
184
- const hue = node.depth / maxDepth;
185
- const color = new THREE.Color().setHSL(hue, 0.8, 0.5);
186
- const geo = new THREE.BoxGeometry(size.x, size.y, size.z);
187
- const wireframe = new THREE.LineSegments(new THREE.WireframeGeometry(geo), new THREE.LineBasicMaterial({ color, opacity: 0.3, transparent: true }));
188
- wireframe.position.copy(center);
189
- group.add(wireframe);
190
- if (node.children) {
191
- for (const child of node.children) {
192
- if (child.objects.length > 0 || child.children) {
193
- buildDebugVisualization(child, group, maxDepth);
194
- }
195
- }
179
+ const size = new THREE.Vector3();
180
+ node.bounds.getSize(size);
181
+ const center = new THREE.Vector3();
182
+ node.bounds.getCenter(center);
183
+ const hue = node.depth / maxDepth;
184
+ const color = new THREE.Color().setHSL(hue, 0.8, 0.5);
185
+ const geo = new THREE.BoxGeometry(size.x, size.y, size.z);
186
+ const wireframe = new THREE.LineSegments(
187
+ new THREE.WireframeGeometry(geo),
188
+ new THREE.LineBasicMaterial({ color, opacity: 0.3, transparent: true })
189
+ );
190
+ wireframe.position.copy(center);
191
+ group.add(wireframe);
192
+ if (node.children) {
193
+ for (const child of node.children) {
194
+ if (child.objects.length > 0 || child.children) {
195
+ buildDebugVisualization(child, group, maxDepth);
196
+ }
196
197
  }
198
+ }
197
199
  }
198
- /* ── Hook ── */
199
200
  function useOctree(scene, configOverride) {
200
- const config = useMemo(() => ({ ...DEFAULT_CONFIG, ...configOverride }), [configOverride]);
201
- const [root, setRoot] = useState(null);
202
- const [showDebug, setShowDebug] = useState(false);
203
- const [stats, setStats] = useState({ nodeCount: 0, objectCount: 0 });
204
- const debugGroup = useRef(new THREE.Group());
205
- useEffect(() => {
206
- if (!scene) {
207
- return;
208
- }
209
- const group = debugGroup.current;
210
- group.name = '__octree_debug__';
211
- if (showDebug) {
212
- scene.add(group);
213
- }
214
- return () => {
215
- scene.remove(group);
216
- };
217
- }, [scene, showDebug]);
218
- const refreshDebug = useCallback((rootNode) => {
219
- // Clear existing debug
220
- while (debugGroup.current.children.length) {
221
- const child = debugGroup.current.children[0];
222
- debugGroup.current.remove(child);
223
- }
224
- if (showDebug) {
225
- buildDebugVisualization(rootNode, debugGroup.current, config.maxDepth);
226
- }
227
- }, [showDebug, config.maxDepth]);
228
- const build = useCallback(() => {
229
- if (!scene) {
230
- return;
231
- }
232
- // Compute scene bounds
233
- const sceneBounds = new THREE.Box3();
234
- scene.traverse((child) => {
235
- if (child instanceof THREE.Mesh && !child.name.startsWith('__')) {
236
- sceneBounds.expandByObject(child);
237
- }
238
- });
239
- if (sceneBounds.isEmpty()) {
240
- return;
241
- }
242
- // Make bounds cubic
243
- const size = new THREE.Vector3();
244
- sceneBounds.getSize(size);
245
- const maxDim = Math.max(size.x, size.y, size.z) * 1.1;
246
- const center = new THREE.Vector3();
247
- sceneBounds.getCenter(center);
248
- const halfSize = maxDim / 2;
249
- const cubicBounds = new THREE.Box3(new THREE.Vector3(center.x - halfSize, center.y - halfSize, center.z - halfSize), new THREE.Vector3(center.x + halfSize, center.y + halfSize, center.z + halfSize));
250
- const rootNode = createNode(cubicBounds, 0);
251
- scene.traverse((child) => {
252
- if (child instanceof THREE.Mesh && !child.name.startsWith('__')) {
253
- insertObject(rootNode, child, config);
254
- }
255
- });
256
- setRoot(rootNode);
257
- setStats({ nodeCount: countNodes(rootNode), objectCount: countObjects(rootNode) });
258
- refreshDebug(rootNode);
259
- }, [scene, config, refreshDebug]);
260
- const insert = useCallback((object) => {
261
- if (!root) {
262
- return;
263
- }
264
- insertObject(root, object, config);
265
- setStats({ nodeCount: countNodes(root), objectCount: countObjects(root) });
266
- refreshDebug(root);
267
- }, [root, config, refreshDebug]);
268
- const remove = useCallback((object) => {
269
- if (!root) {
270
- return;
271
- }
272
- removeObject(root, object);
273
- setStats({ nodeCount: countNodes(root), objectCount: countObjects(root) });
274
- }, [root]);
275
- const update = useCallback((object) => {
276
- if (!root) {
277
- return;
278
- }
279
- removeObject(root, object);
280
- insertObject(root, object, config);
281
- }, [root, config]);
282
- const raycast = useCallback((raycaster) => {
283
- if (!root) {
284
- return [];
285
- }
286
- const results = [];
287
- raycastNode(root, raycaster, results);
288
- results.sort((a, b) => a.distance - b.distance);
289
- return results;
290
- }, [root]);
291
- const frustumCull = useCallback((camera) => {
292
- if (!root) {
293
- return [];
294
- }
295
- const frustum = new THREE.Frustum();
296
- const projScreenMatrix = new THREE.Matrix4();
297
- projScreenMatrix.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
298
- frustum.setFromProjectionMatrix(projScreenMatrix);
299
- const results = [];
300
- frustumCullNode(root, frustum, results);
301
- return results;
302
- }, [root]);
303
- const nearestNeighbor = useCallback((point, maxDistance = Infinity) => {
304
- if (!root) {
305
- return null;
306
- }
307
- const best = { object: null, dist: maxDistance };
308
- nearestInNode(root, point, maxDistance, best);
309
- return best.object;
310
- }, [root]);
311
- return {
312
- build,
313
- insert,
314
- remove,
315
- update,
316
- raycast,
317
- frustumCull,
318
- nearestNeighbor,
319
- nodeCount: stats.nodeCount,
320
- objectCount: stats.objectCount,
321
- showDebug,
322
- setShowDebug,
323
- root,
201
+ const config = useMemo(() => ({ ...DEFAULT_CONFIG, ...configOverride }), [configOverride]);
202
+ const [root, setRoot] = useState(null);
203
+ const [showDebug, setShowDebug] = useState(false);
204
+ const [stats, setStats] = useState({ nodeCount: 0, objectCount: 0 });
205
+ const debugGroup = useRef(new THREE.Group());
206
+ useEffect(() => {
207
+ if (!scene) {
208
+ return;
209
+ }
210
+ const group = debugGroup.current;
211
+ group.name = "__octree_debug__";
212
+ if (showDebug) {
213
+ scene.add(group);
214
+ }
215
+ return () => {
216
+ scene.remove(group);
324
217
  };
218
+ }, [scene, showDebug]);
219
+ const refreshDebug = useCallback(
220
+ (rootNode) => {
221
+ while (debugGroup.current.children.length) {
222
+ const child = debugGroup.current.children[0];
223
+ debugGroup.current.remove(child);
224
+ }
225
+ if (showDebug) {
226
+ buildDebugVisualization(rootNode, debugGroup.current, config.maxDepth);
227
+ }
228
+ },
229
+ [showDebug, config.maxDepth]
230
+ );
231
+ const build = useCallback(() => {
232
+ if (!scene) {
233
+ return;
234
+ }
235
+ const sceneBounds = new THREE.Box3();
236
+ scene.traverse((child) => {
237
+ if (child instanceof THREE.Mesh && !child.name.startsWith("__")) {
238
+ sceneBounds.expandByObject(child);
239
+ }
240
+ });
241
+ if (sceneBounds.isEmpty()) {
242
+ return;
243
+ }
244
+ const size = new THREE.Vector3();
245
+ sceneBounds.getSize(size);
246
+ const maxDim = Math.max(size.x, size.y, size.z) * 1.1;
247
+ const center = new THREE.Vector3();
248
+ sceneBounds.getCenter(center);
249
+ const halfSize = maxDim / 2;
250
+ const cubicBounds = new THREE.Box3(
251
+ new THREE.Vector3(center.x - halfSize, center.y - halfSize, center.z - halfSize),
252
+ new THREE.Vector3(center.x + halfSize, center.y + halfSize, center.z + halfSize)
253
+ );
254
+ const rootNode = createNode(cubicBounds, 0);
255
+ scene.traverse((child) => {
256
+ if (child instanceof THREE.Mesh && !child.name.startsWith("__")) {
257
+ insertObject(rootNode, child, config);
258
+ }
259
+ });
260
+ setRoot(rootNode);
261
+ setStats({ nodeCount: countNodes(rootNode), objectCount: countObjects(rootNode) });
262
+ refreshDebug(rootNode);
263
+ }, [scene, config, refreshDebug]);
264
+ const insert = useCallback(
265
+ (object) => {
266
+ if (!root) {
267
+ return;
268
+ }
269
+ insertObject(root, object, config);
270
+ setStats({ nodeCount: countNodes(root), objectCount: countObjects(root) });
271
+ refreshDebug(root);
272
+ },
273
+ [root, config, refreshDebug]
274
+ );
275
+ const remove = useCallback(
276
+ (object) => {
277
+ if (!root) {
278
+ return;
279
+ }
280
+ removeObject(root, object);
281
+ setStats({ nodeCount: countNodes(root), objectCount: countObjects(root) });
282
+ },
283
+ [root]
284
+ );
285
+ const update = useCallback(
286
+ (object) => {
287
+ if (!root) {
288
+ return;
289
+ }
290
+ removeObject(root, object);
291
+ insertObject(root, object, config);
292
+ },
293
+ [root, config]
294
+ );
295
+ const raycast = useCallback(
296
+ (raycaster) => {
297
+ if (!root) {
298
+ return [];
299
+ }
300
+ const results = [];
301
+ raycastNode(root, raycaster, results);
302
+ results.sort((a, b) => a.distance - b.distance);
303
+ return results;
304
+ },
305
+ [root]
306
+ );
307
+ const frustumCull = useCallback(
308
+ (camera) => {
309
+ if (!root) {
310
+ return [];
311
+ }
312
+ const frustum = new THREE.Frustum();
313
+ const projScreenMatrix = new THREE.Matrix4();
314
+ projScreenMatrix.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
315
+ frustum.setFromProjectionMatrix(projScreenMatrix);
316
+ const results = [];
317
+ frustumCullNode(root, frustum, results);
318
+ return results;
319
+ },
320
+ [root]
321
+ );
322
+ const nearestNeighbor = useCallback(
323
+ (point, maxDistance = Infinity) => {
324
+ if (!root) {
325
+ return null;
326
+ }
327
+ const best = { object: null, dist: maxDistance };
328
+ nearestInNode(root, point, maxDistance, best);
329
+ return best.object;
330
+ },
331
+ [root]
332
+ );
333
+ return {
334
+ build,
335
+ insert,
336
+ remove,
337
+ update,
338
+ raycast,
339
+ frustumCull,
340
+ nearestNeighbor,
341
+ nodeCount: stats.nodeCount,
342
+ objectCount: stats.objectCount,
343
+ showDebug,
344
+ setShowDebug,
345
+ root
346
+ };
325
347
  }
326
- /* ── Component ── */
327
- const NiceOctree = ({ scene, config, showDebug: initialShowDebug, className = '', }) => {
328
- const { t } = useNiceTranslation();
329
- const octree = useOctree(scene, config);
330
- const { setShowDebug } = octree;
331
- useEffect(() => {
332
- if (initialShowDebug !== undefined) {
333
- setShowDebug(initialShowDebug);
348
+ const NiceOctree = ({
349
+ scene,
350
+ config,
351
+ showDebug: initialShowDebug,
352
+ className = ""
353
+ }) => {
354
+ const { t } = useNiceTranslation();
355
+ const octree = useOctree(scene, config);
356
+ const { setShowDebug } = octree;
357
+ useEffect(() => {
358
+ if (initialShowDebug !== void 0) {
359
+ setShowDebug(initialShowDebug);
360
+ }
361
+ }, [initialShowDebug, setShowDebug]);
362
+ return /* @__PURE__ */ jsxs("div", { className: `nice-octree ${className}`, style: { display: "grid", gap: 8, fontSize: 13 }, children: [
363
+ /* @__PURE__ */ jsxs("strong", { children: [
364
+ /* @__PURE__ */ jsx(NiceIcon, { name: "tree", size: 14 }),
365
+ " ",
366
+ t("octree.spatialIndexOctree", "Spatial Index (Octree)")
367
+ ] }),
368
+ /* @__PURE__ */ jsxs(
369
+ "button",
370
+ {
371
+ type: "button",
372
+ onClick: octree.build,
373
+ style: {
374
+ background: "var(--nice-color-accent, #6366f1)",
375
+ color: "var(--nice-text-on-primary, #fff)",
376
+ border: "none",
377
+ borderRadius: 4,
378
+ padding: "6px 12px",
379
+ cursor: "pointer"
380
+ },
381
+ children: [
382
+ /* @__PURE__ */ jsx(NiceIcon, { name: "zap", size: 14 }),
383
+ " ",
384
+ t("octree.buildOctree", "Build Octree")
385
+ ]
386
+ }
387
+ ),
388
+ /* @__PURE__ */ jsx("div", { style: { fontSize: 11, opacity: 0.7 }, children: t("octree.nodesObjects", "Nodes: {nodeCount} · Objects: {objectCount}").replace("{nodeCount}", String(octree.nodeCount)).replace("{objectCount}", String(octree.objectCount)) }),
389
+ /* @__PURE__ */ jsxs("label", { style: { display: "flex", alignItems: "center", gap: 6 }, children: [
390
+ /* @__PURE__ */ jsx(
391
+ "input",
392
+ {
393
+ type: "checkbox",
394
+ checked: octree.showDebug,
395
+ onChange: (e) => octree.setShowDebug(e.target.checked)
334
396
  }
335
- }, [initialShowDebug, setShowDebug]);
336
- return (jsxs("div", { className: `nice-octree ${className}`, style: { display: 'grid', gap: 8, fontSize: 13 }, children: [jsxs("strong", { children: [jsx(NiceIcon, { name: "tree", size: 14 }), ' ', t('octree.spatialIndexOctree', 'Spatial Index (Octree)')] }), jsxs("button", { type: "button", onClick: octree.build, style: {
337
- background: 'var(--nice-color-accent, #6366f1)',
338
- color: 'var(--nice-text-on-primary, #fff)',
339
- border: 'none',
340
- borderRadius: 4,
341
- padding: '6px 12px',
342
- cursor: 'pointer',
343
- }, children: [jsx(NiceIcon, { name: "zap", size: 14 }), " ", t('octree.buildOctree', 'Build Octree')] }), jsx("div", { style: { fontSize: 11, opacity: 0.7 }, children: t('octree.nodesObjects', 'Nodes: {nodeCount} · Objects: {objectCount}')
344
- .replace('{nodeCount}', String(octree.nodeCount))
345
- .replace('{objectCount}', String(octree.objectCount)) }), jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 6 }, children: [jsx("input", { type: "checkbox", checked: octree.showDebug, onChange: (e) => octree.setShowDebug(e.target.checked) }), t('octree.showDebugWireframes', 'Show debug wireframes')] })] }));
397
+ ),
398
+ t("octree.showDebugWireframes", "Show debug wireframes")
399
+ ] })
400
+ ] });
401
+ };
402
+ export {
403
+ NiceOctree,
404
+ useOctree
346
405
  };
347
-
348
- export { NiceOctree, useOctree };