@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.
- package/CHANGELOG.md +13 -0
- package/dist/cjs/collaborative/collaborativeScene.js +160 -206
- package/dist/cjs/core/editorAsk.js +51 -41
- package/dist/cjs/core/i18n.js +13 -14
- package/dist/cjs/core/threeCompat.js +29 -42
- package/dist/cjs/dance/DanceBridge.js +148 -177
- package/dist/cjs/dance/DanceScoreEngine.js +209 -221
- package/dist/cjs/dance/PoseDetector.js +302 -275
- package/dist/cjs/index.js +110 -113
- package/dist/cjs/material/MaterialEditor.module.css.js +242 -5
- package/dist/cjs/material/NiceMaterialEditor.js +1213 -757
- package/dist/cjs/material/materialEditorTypes.js +35 -49
- package/dist/cjs/material/materialEditorUtils.js +684 -747
- package/dist/cjs/material/materialNodeDefinitions.js +1411 -1459
- package/dist/cjs/model/ModelEditor.js +90 -40
- package/dist/cjs/model/ModelEditor.module.css.js +257 -5
- package/dist/cjs/model/ModelEditorLeftPanel.js +126 -47
- package/dist/cjs/model/ModelEditorMenuBar.js +359 -32
- package/dist/cjs/model/ModelEditorRightPanel.js +647 -131
- package/dist/cjs/model/ModelEditorSubComponents.js +277 -112
- package/dist/cjs/model/ModelEditorTimeline.js +232 -34
- package/dist/cjs/model/ModelEditorToolbar.js +170 -14
- package/dist/cjs/model/ModelEditorViewport.js +231 -39
- package/dist/cjs/model/ModelViewer.js +388 -63
- package/dist/cjs/model/ModelViewer.module.css.js +77 -5
- package/dist/cjs/model/NiceArmatureEditor.js +473 -274
- package/dist/cjs/model/NiceMorphTargetEditor.js +333 -227
- package/dist/cjs/model/NiceOctree.js +372 -320
- package/dist/cjs/model/NicePhysicsSimulation.js +528 -298
- package/dist/cjs/model/NiceProceduralGeometry.js +673 -300
- package/dist/cjs/model/NiceTerrainEditor.js +595 -385
- package/dist/cjs/model/NiceWeightPainter.js +384 -261
- package/dist/cjs/model/NiceXRPreview.js +385 -308
- package/dist/cjs/model/cadModeUtils.js +103 -106
- package/dist/cjs/model/editorShortcuts.js +171 -194
- package/dist/cjs/model/modelEditorTypes.js +100 -118
- package/dist/cjs/model/modelEditorUtils.js +1061 -1146
- package/dist/cjs/model/simsModeUtils.js +1023 -1053
- package/dist/cjs/model/useModelEditor.js +1977 -1933
- package/dist/cjs/model/useModelViewer.js +633 -665
- package/dist/cjs/nice2dev-ui-3d.css +1 -1
- package/dist/cjs/particle/NiceParticleEditor.js +1186 -571
- package/dist/cjs/particle/ParticleEditor.module.css.js +122 -5
- package/dist/cjs/particle/particleEditorTypes.js +62 -70
- package/dist/cjs/particle/particleEditorUtils.js +998 -1013
- package/dist/cjs/rendering/NiceCascadedShadows.js +381 -259
- package/dist/cjs/rendering/NiceRenderExport.js +424 -337
- package/dist/cjs/rendering/NiceSSAO.js +454 -337
- package/dist/cjs/rendering/NiceSSR.js +387 -261
- package/dist/cjs/rendering/NiceWebGPURenderer.js +359 -230
- package/dist/cjs/uv/NiceUVEditor.js +787 -550
- package/dist/cjs/uv/UVEditor.module.css.js +53 -5
- package/dist/cjs/uv/uvEditorTypes.js +59 -76
- package/dist/cjs/uv/uvEditorUtils.js +501 -575
- package/dist/esm/collaborative/collaborativeScene.js +163 -205
- package/dist/esm/core/editorAsk.js +53 -40
- package/dist/esm/core/i18n.js +15 -13
- package/dist/esm/core/threeCompat.js +18 -26
- package/dist/esm/dance/DanceBridge.js +151 -176
- package/dist/esm/dance/DanceScoreEngine.js +211 -220
- package/dist/esm/dance/PoseDetector.js +304 -274
- package/dist/esm/index.js +258 -36
- package/dist/esm/material/MaterialEditor.module.css.js +242 -3
- package/dist/esm/material/NiceMaterialEditor.js +1215 -756
- package/dist/esm/material/materialEditorTypes.js +45 -48
- package/dist/esm/material/materialEditorUtils.js +713 -746
- package/dist/esm/material/materialNodeDefinitions.js +1415 -1458
- package/dist/esm/model/ModelEditor.js +91 -39
- package/dist/esm/model/ModelEditor.module.css.js +257 -3
- package/dist/esm/model/ModelEditorLeftPanel.js +126 -45
- package/dist/esm/model/ModelEditorMenuBar.js +359 -30
- package/dist/esm/model/ModelEditorRightPanel.js +634 -114
- package/dist/esm/model/ModelEditorSubComponents.js +265 -94
- package/dist/esm/model/ModelEditorTimeline.js +232 -32
- package/dist/esm/model/ModelEditorToolbar.js +170 -12
- package/dist/esm/model/ModelEditorViewport.js +231 -37
- package/dist/esm/model/ModelViewer.js +389 -62
- package/dist/esm/model/ModelViewer.module.css.js +77 -3
- package/dist/esm/model/NiceArmatureEditor.js +462 -258
- package/dist/esm/model/NiceMorphTargetEditor.js +322 -211
- package/dist/esm/model/NiceOctree.js +361 -304
- package/dist/esm/model/NicePhysicsSimulation.js +517 -282
- package/dist/esm/model/NiceProceduralGeometry.js +667 -284
- package/dist/esm/model/NiceTerrainEditor.js +584 -369
- package/dist/esm/model/NiceWeightPainter.js +373 -245
- package/dist/esm/model/NiceXRPreview.js +374 -292
- package/dist/esm/model/cadModeUtils.js +95 -88
- package/dist/esm/model/editorShortcuts.js +172 -193
- package/dist/esm/model/modelEditorTypes.js +90 -100
- package/dist/esm/model/modelEditorUtils.js +1056 -1128
- package/dist/esm/model/simsModeUtils.js +1021 -1035
- package/dist/esm/model/useModelEditor.js +1946 -1920
- package/dist/esm/model/useModelViewer.js +619 -647
- package/dist/esm/nice2dev-ui-3d.css +1 -1
- package/dist/esm/particle/NiceParticleEditor.js +1188 -570
- package/dist/esm/particle/ParticleEditor.module.css.js +122 -3
- package/dist/esm/particle/particleEditorTypes.js +69 -69
- package/dist/esm/particle/particleEditorUtils.js +1027 -1012
- package/dist/esm/rendering/NiceCascadedShadows.js +370 -243
- package/dist/esm/rendering/NiceRenderExport.js +413 -321
- package/dist/esm/rendering/NiceSSAO.js +450 -328
- package/dist/esm/rendering/NiceSSR.js +383 -252
- package/dist/esm/rendering/NiceWebGPURenderer.js +326 -214
- package/dist/esm/uv/NiceUVEditor.js +788 -549
- package/dist/esm/uv/UVEditor.module.css.js +53 -3
- package/dist/esm/uv/uvEditorTypes.js +68 -75
- package/dist/esm/uv/uvEditorUtils.js +515 -557
- package/package.json +7 -12
|
@@ -1,348 +1,405 @@
|
|
|
1
|
-
import { jsxs, jsx } from
|
|
2
|
-
import { NiceIcon } from
|
|
3
|
-
import { useEffect, useMemo, useState, useRef, useCallback } from
|
|
4
|
-
import * as THREE from
|
|
5
|
-
import { useNiceTranslation } from
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
maxDepth: 8,
|
|
8
|
+
maxObjectsPerLeaf: 8,
|
|
9
|
+
looseness: 1
|
|
12
10
|
};
|
|
13
11
|
function createNode(bounds, depth) {
|
|
14
|
-
|
|
12
|
+
return { bounds, children: null, objects: [], depth };
|
|
15
13
|
}
|
|
16
14
|
function splitNode(node, config) {
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
38
|
-
|
|
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
|
-
|
|
56
|
+
}
|
|
57
|
+
node.objects = orphans;
|
|
54
58
|
}
|
|
55
59
|
function insertObject(node, object, config) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
73
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
83
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
119
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}
|
|
161
|
+
let count = 1;
|
|
162
|
+
if (node.children) {
|
|
163
|
+
for (const child of node.children) {
|
|
164
|
+
count += countNodes(child);
|
|
167
165
|
}
|
|
168
|
-
|
|
166
|
+
}
|
|
167
|
+
return count;
|
|
169
168
|
}
|
|
170
169
|
function countObjects(node) {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
175
|
+
}
|
|
176
|
+
return count;
|
|
178
177
|
}
|
|
179
178
|
function buildDebugVisualization(node, group, maxDepth) {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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 };
|