@nice2dev/ui-3d 1.3.15 → 1.3.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/cjs/collaborative/collaborativeScene.js +160 -206
  3. package/dist/cjs/core/editorAsk.js +51 -41
  4. package/dist/cjs/core/i18n.js +13 -14
  5. package/dist/cjs/core/threeCompat.js +29 -42
  6. package/dist/cjs/dance/DanceBridge.js +148 -177
  7. package/dist/cjs/dance/DanceScoreEngine.js +209 -221
  8. package/dist/cjs/dance/PoseDetector.js +302 -275
  9. package/dist/cjs/index.js +110 -113
  10. package/dist/cjs/material/MaterialEditor.module.css.js +242 -5
  11. package/dist/cjs/material/NiceMaterialEditor.js +1213 -757
  12. package/dist/cjs/material/materialEditorTypes.js +35 -49
  13. package/dist/cjs/material/materialEditorUtils.js +684 -747
  14. package/dist/cjs/material/materialNodeDefinitions.js +1411 -1459
  15. package/dist/cjs/model/ModelEditor.js +90 -40
  16. package/dist/cjs/model/ModelEditor.module.css.js +257 -5
  17. package/dist/cjs/model/ModelEditorLeftPanel.js +126 -47
  18. package/dist/cjs/model/ModelEditorMenuBar.js +359 -32
  19. package/dist/cjs/model/ModelEditorRightPanel.js +647 -131
  20. package/dist/cjs/model/ModelEditorSubComponents.js +277 -112
  21. package/dist/cjs/model/ModelEditorTimeline.js +232 -34
  22. package/dist/cjs/model/ModelEditorToolbar.js +170 -14
  23. package/dist/cjs/model/ModelEditorViewport.js +231 -39
  24. package/dist/cjs/model/ModelViewer.js +388 -63
  25. package/dist/cjs/model/ModelViewer.module.css.js +77 -5
  26. package/dist/cjs/model/NiceArmatureEditor.js +473 -274
  27. package/dist/cjs/model/NiceMorphTargetEditor.js +333 -227
  28. package/dist/cjs/model/NiceOctree.js +372 -320
  29. package/dist/cjs/model/NicePhysicsSimulation.js +528 -298
  30. package/dist/cjs/model/NiceProceduralGeometry.js +673 -300
  31. package/dist/cjs/model/NiceTerrainEditor.js +595 -385
  32. package/dist/cjs/model/NiceWeightPainter.js +384 -261
  33. package/dist/cjs/model/NiceXRPreview.js +385 -308
  34. package/dist/cjs/model/cadModeUtils.js +103 -106
  35. package/dist/cjs/model/editorShortcuts.js +171 -194
  36. package/dist/cjs/model/modelEditorTypes.js +100 -118
  37. package/dist/cjs/model/modelEditorUtils.js +1061 -1146
  38. package/dist/cjs/model/simsModeUtils.js +1023 -1053
  39. package/dist/cjs/model/useModelEditor.js +1977 -1933
  40. package/dist/cjs/model/useModelViewer.js +633 -665
  41. package/dist/cjs/nice2dev-ui-3d.css +1 -1
  42. package/dist/cjs/particle/NiceParticleEditor.js +1186 -571
  43. package/dist/cjs/particle/ParticleEditor.module.css.js +122 -5
  44. package/dist/cjs/particle/particleEditorTypes.js +62 -70
  45. package/dist/cjs/particle/particleEditorUtils.js +998 -1013
  46. package/dist/cjs/rendering/NiceCascadedShadows.js +381 -259
  47. package/dist/cjs/rendering/NiceRenderExport.js +424 -337
  48. package/dist/cjs/rendering/NiceSSAO.js +454 -337
  49. package/dist/cjs/rendering/NiceSSR.js +387 -261
  50. package/dist/cjs/rendering/NiceWebGPURenderer.js +359 -230
  51. package/dist/cjs/uv/NiceUVEditor.js +787 -550
  52. package/dist/cjs/uv/UVEditor.module.css.js +53 -5
  53. package/dist/cjs/uv/uvEditorTypes.js +59 -76
  54. package/dist/cjs/uv/uvEditorUtils.js +501 -575
  55. package/dist/esm/collaborative/collaborativeScene.js +163 -205
  56. package/dist/esm/core/editorAsk.js +53 -40
  57. package/dist/esm/core/i18n.js +15 -13
  58. package/dist/esm/core/threeCompat.js +18 -26
  59. package/dist/esm/dance/DanceBridge.js +151 -176
  60. package/dist/esm/dance/DanceScoreEngine.js +211 -220
  61. package/dist/esm/dance/PoseDetector.js +304 -274
  62. package/dist/esm/index.js +258 -36
  63. package/dist/esm/material/MaterialEditor.module.css.js +242 -3
  64. package/dist/esm/material/NiceMaterialEditor.js +1215 -756
  65. package/dist/esm/material/materialEditorTypes.js +45 -48
  66. package/dist/esm/material/materialEditorUtils.js +713 -746
  67. package/dist/esm/material/materialNodeDefinitions.js +1415 -1458
  68. package/dist/esm/model/ModelEditor.js +91 -39
  69. package/dist/esm/model/ModelEditor.module.css.js +257 -3
  70. package/dist/esm/model/ModelEditorLeftPanel.js +126 -45
  71. package/dist/esm/model/ModelEditorMenuBar.js +359 -30
  72. package/dist/esm/model/ModelEditorRightPanel.js +634 -114
  73. package/dist/esm/model/ModelEditorSubComponents.js +265 -94
  74. package/dist/esm/model/ModelEditorTimeline.js +232 -32
  75. package/dist/esm/model/ModelEditorToolbar.js +170 -12
  76. package/dist/esm/model/ModelEditorViewport.js +231 -37
  77. package/dist/esm/model/ModelViewer.js +389 -62
  78. package/dist/esm/model/ModelViewer.module.css.js +77 -3
  79. package/dist/esm/model/NiceArmatureEditor.js +462 -258
  80. package/dist/esm/model/NiceMorphTargetEditor.js +322 -211
  81. package/dist/esm/model/NiceOctree.js +361 -304
  82. package/dist/esm/model/NicePhysicsSimulation.js +517 -282
  83. package/dist/esm/model/NiceProceduralGeometry.js +667 -284
  84. package/dist/esm/model/NiceTerrainEditor.js +584 -369
  85. package/dist/esm/model/NiceWeightPainter.js +373 -245
  86. package/dist/esm/model/NiceXRPreview.js +374 -292
  87. package/dist/esm/model/cadModeUtils.js +95 -88
  88. package/dist/esm/model/editorShortcuts.js +172 -193
  89. package/dist/esm/model/modelEditorTypes.js +90 -100
  90. package/dist/esm/model/modelEditorUtils.js +1056 -1128
  91. package/dist/esm/model/simsModeUtils.js +1021 -1035
  92. package/dist/esm/model/useModelEditor.js +1946 -1920
  93. package/dist/esm/model/useModelViewer.js +619 -647
  94. package/dist/esm/nice2dev-ui-3d.css +1 -1
  95. package/dist/esm/particle/NiceParticleEditor.js +1188 -570
  96. package/dist/esm/particle/ParticleEditor.module.css.js +122 -3
  97. package/dist/esm/particle/particleEditorTypes.js +69 -69
  98. package/dist/esm/particle/particleEditorUtils.js +1027 -1012
  99. package/dist/esm/rendering/NiceCascadedShadows.js +370 -243
  100. package/dist/esm/rendering/NiceRenderExport.js +413 -321
  101. package/dist/esm/rendering/NiceSSAO.js +450 -328
  102. package/dist/esm/rendering/NiceSSR.js +383 -252
  103. package/dist/esm/rendering/NiceWebGPURenderer.js +326 -214
  104. package/dist/esm/uv/NiceUVEditor.js +788 -549
  105. package/dist/esm/uv/UVEditor.module.css.js +53 -3
  106. package/dist/esm/uv/uvEditorTypes.js +68 -75
  107. package/dist/esm/uv/uvEditorUtils.js +515 -557
  108. package/package.json +7 -12
@@ -1,302 +1,537 @@
1
- import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
2
- import { NiceIcon } from '@nice2dev/ui';
3
- import { useState, useRef, useEffect, useCallback } from 'react';
4
- import * as THREE from 'three';
5
- import { useNiceTranslation } from '../core/i18n.js';
6
-
1
+ import { jsxs, jsx, Fragment } from "react/jsx-runtime";
2
+ import { NiceIcon } from "@nice2dev/ui";
3
+ import { useState, useRef, useEffect, useCallback } from "react";
4
+ import * as THREE from "three";
5
+ import { useNiceTranslation } from "../core/i18n.js";
7
6
  function inferColliderShape(object) {
8
- if (object instanceof THREE.Mesh) {
9
- const geo = object.geometry;
10
- if (geo instanceof THREE.SphereGeometry) {
11
- return 'sphere';
12
- }
13
- if (geo instanceof THREE.CapsuleGeometry) {
14
- return 'capsule';
15
- }
16
- if (geo instanceof THREE.BoxGeometry) {
17
- return 'box';
18
- }
19
- // Complex meshes default to convex hull
20
- if (geo.attributes.position && geo.attributes.position.count > 24) {
21
- return 'convexHull';
22
- }
7
+ if (object instanceof THREE.Mesh) {
8
+ const geo = object.geometry;
9
+ if (geo instanceof THREE.SphereGeometry) {
10
+ return "sphere";
23
11
  }
24
- return 'box';
12
+ if (geo instanceof THREE.CapsuleGeometry) {
13
+ return "capsule";
14
+ }
15
+ if (geo instanceof THREE.BoxGeometry) {
16
+ return "box";
17
+ }
18
+ if (geo.attributes.position && geo.attributes.position.count > 24) {
19
+ return "convexHull";
20
+ }
21
+ }
22
+ return "box";
25
23
  }
26
24
  function computeAABB(object) {
27
- return new THREE.Box3().setFromObject(object);
25
+ return new THREE.Box3().setFromObject(object);
28
26
  }
29
- /* ── Hook ── */
30
27
  let bodyIdCounter = 0;
31
28
  let jointIdCounter = 0;
32
29
  function usePhysicsSimulation(scene, onSimulationStep) {
33
- const [worldConfig, setWorldConfigState] = useState({
34
- gravity: new THREE.Vector3(0, -9.81, 0),
35
- timestep: 1 / 60,
36
- maxSubsteps: 4,
37
- });
38
- const [bodies, setBodies] = useState([]);
39
- const [joints, setJoints] = useState([]);
40
- const [isRunning, setIsRunning] = useState(false);
41
- const [debugDraw, setDebugDraw] = useState(false);
42
- const internalBodies = useRef(new Map());
43
- const debugGroup = useRef(new THREE.Group());
44
- const animFrameId = useRef(0);
45
- const lastTime = useRef(0);
46
- // Debug wireframe colliders
47
- useEffect(() => {
48
- if (!scene) {
49
- return;
30
+ const [worldConfig, setWorldConfigState] = useState({
31
+ gravity: new THREE.Vector3(0, -9.81, 0),
32
+ timestep: 1 / 60,
33
+ maxSubsteps: 4
34
+ });
35
+ const [bodies, setBodies] = useState([]);
36
+ const [joints, setJoints] = useState([]);
37
+ const [isRunning, setIsRunning] = useState(false);
38
+ const [debugDraw, setDebugDraw] = useState(false);
39
+ const internalBodies = useRef(/* @__PURE__ */ new Map());
40
+ const debugGroup = useRef(new THREE.Group());
41
+ const animFrameId = useRef(0);
42
+ const lastTime = useRef(0);
43
+ useEffect(() => {
44
+ if (!scene) {
45
+ return;
46
+ }
47
+ const group = debugGroup.current;
48
+ group.name = "__physics_debug__";
49
+ if (debugDraw) {
50
+ scene.add(group);
51
+ }
52
+ return () => {
53
+ scene.remove(group);
54
+ };
55
+ }, [scene, debugDraw]);
56
+ const setWorldConfig = useCallback((partial) => {
57
+ setWorldConfigState((prev) => ({ ...prev, ...partial }));
58
+ }, []);
59
+ const addBody = useCallback(
60
+ (object) => {
61
+ const id = `phys_${++bodyIdCounter}`;
62
+ const desc = {
63
+ id,
64
+ objectName: object.name || object.uuid,
65
+ bodyType: "dynamic",
66
+ colliderShape: inferColliderShape(object),
67
+ mass: 1,
68
+ restitution: 0.3,
69
+ friction: 0.5,
70
+ linearDamping: 0.01,
71
+ angularDamping: 0.05,
72
+ gravityScale: 1,
73
+ isSensor: false
74
+ };
75
+ const internal = {
76
+ desc,
77
+ object,
78
+ velocity: new THREE.Vector3(),
79
+ angularVelocity: new THREE.Vector3(),
80
+ initialPosition: object.position.clone(),
81
+ initialRotation: object.quaternion.clone(),
82
+ initialScale: object.scale.clone()
83
+ };
84
+ internalBodies.current.set(id, internal);
85
+ setBodies((prev) => [...prev, desc]);
86
+ if (debugDraw) {
87
+ addDebugCollider(object, desc.colliderShape, id);
88
+ }
89
+ return desc;
90
+ },
91
+ [debugDraw]
92
+ );
93
+ const removeBody = useCallback((id) => {
94
+ internalBodies.current.delete(id);
95
+ setBodies((prev) => prev.filter((b) => b.id !== id));
96
+ const debugObj = debugGroup.current.getObjectByName(`debug_${id}`);
97
+ if (debugObj) {
98
+ debugGroup.current.remove(debugObj);
99
+ }
100
+ }, []);
101
+ const updateBody = useCallback((id, partial) => {
102
+ const internal = internalBodies.current.get(id);
103
+ if (internal) {
104
+ Object.assign(internal.desc, partial);
105
+ }
106
+ setBodies((prev) => prev.map((b) => b.id === id ? { ...b, ...partial } : b));
107
+ }, []);
108
+ const addJoint = useCallback((joint) => {
109
+ const id = `joint_${++jointIdCounter}`;
110
+ setJoints((prev) => [...prev, { ...joint, id }]);
111
+ }, []);
112
+ const removeJoint = useCallback((id) => {
113
+ setJoints((prev) => prev.filter((j) => j.id !== id));
114
+ }, []);
115
+ const getBodyForObject = useCallback(
116
+ (objectName) => bodies.find((b) => b.objectName === objectName),
117
+ [bodies]
118
+ );
119
+ const simulationStep = useCallback(
120
+ (dt) => {
121
+ const gravity = worldConfig.gravity;
122
+ internalBodies.current.forEach((body) => {
123
+ if (body.desc.bodyType !== "dynamic") {
124
+ return;
50
125
  }
51
- const group = debugGroup.current;
52
- group.name = '__physics_debug__';
53
- if (debugDraw) {
54
- scene.add(group);
126
+ body.velocity.addScaledVector(gravity, dt * body.desc.gravityScale);
127
+ body.velocity.multiplyScalar(1 - body.desc.linearDamping);
128
+ body.angularVelocity.multiplyScalar(1 - body.desc.angularDamping);
129
+ body.object.position.addScaledVector(body.velocity, dt);
130
+ if (body.angularVelocity.lengthSq() > 1e-4) {
131
+ const q = new THREE.Quaternion().setFromAxisAngle(
132
+ body.angularVelocity.clone().normalize(),
133
+ body.angularVelocity.length() * dt
134
+ );
135
+ body.object.quaternion.premultiply(q);
55
136
  }
56
- return () => {
57
- scene.remove(group);
58
- };
59
- }, [scene, debugDraw]);
60
- const setWorldConfig = useCallback((partial) => {
61
- setWorldConfigState((prev) => ({ ...prev, ...partial }));
62
- }, []);
63
- const addBody = useCallback((object) => {
64
- const id = `phys_${++bodyIdCounter}`;
65
- const desc = {
66
- id,
67
- objectName: object.name || object.uuid,
68
- bodyType: 'dynamic',
69
- colliderShape: inferColliderShape(object),
70
- mass: 1,
71
- restitution: 0.3,
72
- friction: 0.5,
73
- linearDamping: 0.01,
74
- angularDamping: 0.05,
75
- gravityScale: 1,
76
- isSensor: false,
77
- };
78
- const internal = {
79
- desc,
80
- object,
81
- velocity: new THREE.Vector3(),
82
- angularVelocity: new THREE.Vector3(),
83
- initialPosition: object.position.clone(),
84
- initialRotation: object.quaternion.clone(),
85
- initialScale: object.scale.clone(),
86
- };
87
- internalBodies.current.set(id, internal);
88
- setBodies((prev) => [...prev, desc]);
89
- // Add debug wireframe
90
- if (debugDraw) {
91
- addDebugCollider(object, desc.colliderShape, id);
137
+ const aabb = computeAABB(body.object);
138
+ if (aabb.min.y < 0) {
139
+ body.object.position.y -= aabb.min.y;
140
+ body.velocity.y = -body.velocity.y * body.desc.restitution;
141
+ body.velocity.x *= 1 - body.desc.friction * dt;
142
+ body.velocity.z *= 1 - body.desc.friction * dt;
92
143
  }
93
- return desc;
94
- }, [debugDraw]);
95
- const removeBody = useCallback((id) => {
96
- internalBodies.current.delete(id);
97
- setBodies((prev) => prev.filter((b) => b.id !== id));
98
- // Remove debug wireframe
99
- const debugObj = debugGroup.current.getObjectByName(`debug_${id}`);
144
+ const debugObj = debugGroup.current.getObjectByName(`debug_${body.desc.id}`);
100
145
  if (debugObj) {
101
- debugGroup.current.remove(debugObj);
102
- }
103
- }, []);
104
- const updateBody = useCallback((id, partial) => {
105
- const internal = internalBodies.current.get(id);
106
- if (internal) {
107
- Object.assign(internal.desc, partial);
146
+ debugObj.position.copy(body.object.position);
147
+ debugObj.quaternion.copy(body.object.quaternion);
108
148
  }
109
- setBodies((prev) => prev.map((b) => (b.id === id ? { ...b, ...partial } : b)));
110
- }, []);
111
- const addJoint = useCallback((joint) => {
112
- const id = `joint_${++jointIdCounter}`;
113
- setJoints((prev) => [...prev, { ...joint, id }]);
114
- }, []);
115
- const removeJoint = useCallback((id) => {
116
- setJoints((prev) => prev.filter((j) => j.id !== id));
117
- }, []);
118
- const getBodyForObject = useCallback((objectName) => bodies.find((b) => b.objectName === objectName), [bodies]);
119
- // Simplified physics step (Euler integration with collision detection)
120
- const simulationStep = useCallback((dt) => {
121
- const gravity = worldConfig.gravity;
122
- internalBodies.current.forEach((body) => {
123
- if (body.desc.bodyType !== 'dynamic') {
124
- return;
149
+ });
150
+ const dynamicBodies = [...internalBodies.current.values()].filter(
151
+ (b) => b.desc.bodyType === "dynamic"
152
+ );
153
+ for (let i = 0; i < dynamicBodies.length; i++) {
154
+ for (let j = i + 1; j < dynamicBodies.length; j++) {
155
+ const a = dynamicBodies[i];
156
+ const b = dynamicBodies[j];
157
+ const aabbA = computeAABB(a.object);
158
+ const aabbB = computeAABB(b.object);
159
+ if (aabbA.intersectsBox(aabbB)) {
160
+ const normal = new THREE.Vector3().subVectors(b.object.position, a.object.position).normalize();
161
+ const relVel = new THREE.Vector3().subVectors(a.velocity, b.velocity);
162
+ const velAlongNormal = relVel.dot(normal);
163
+ if (velAlongNormal > 0) {
164
+ continue;
125
165
  }
126
- // Apply gravity
127
- body.velocity.addScaledVector(gravity, dt * body.desc.gravityScale);
128
- // Apply damping
129
- body.velocity.multiplyScalar(1 - body.desc.linearDamping);
130
- body.angularVelocity.multiplyScalar(1 - body.desc.angularDamping);
131
- // Integrate position
132
- body.object.position.addScaledVector(body.velocity, dt);
133
- // Integrate rotation (simplified)
134
- if (body.angularVelocity.lengthSq() > 0.0001) {
135
- const q = new THREE.Quaternion().setFromAxisAngle(body.angularVelocity.clone().normalize(), body.angularVelocity.length() * dt);
136
- body.object.quaternion.premultiply(q);
166
+ const restitution = Math.min(a.desc.restitution, b.desc.restitution);
167
+ const impulseMag = -(1 + restitution) * velAlongNormal / (1 / a.desc.mass + 1 / b.desc.mass);
168
+ const impulse = normal.clone().multiplyScalar(impulseMag);
169
+ a.velocity.addScaledVector(impulse, 1 / a.desc.mass);
170
+ b.velocity.addScaledVector(impulse, -1 / b.desc.mass);
171
+ const overlap = new THREE.Vector3();
172
+ aabbA.intersect(aabbB).getSize(overlap);
173
+ const sep = Math.min(overlap.x, overlap.y, overlap.z) * 0.5;
174
+ a.object.position.addScaledVector(normal, -sep);
175
+ b.object.position.addScaledVector(normal, sep);
176
+ }
177
+ }
178
+ }
179
+ onSimulationStep?.(dt);
180
+ },
181
+ [worldConfig.gravity, onSimulationStep]
182
+ );
183
+ useEffect(() => {
184
+ if (!isRunning) {
185
+ return;
186
+ }
187
+ lastTime.current = performance.now();
188
+ const loop = (now) => {
189
+ const dt = Math.min(
190
+ (now - lastTime.current) / 1e3,
191
+ worldConfig.timestep * worldConfig.maxSubsteps
192
+ );
193
+ lastTime.current = now;
194
+ let accum = dt;
195
+ while (accum >= worldConfig.timestep) {
196
+ simulationStep(worldConfig.timestep);
197
+ accum -= worldConfig.timestep;
198
+ }
199
+ animFrameId.current = requestAnimationFrame(loop);
200
+ };
201
+ animFrameId.current = requestAnimationFrame(loop);
202
+ return () => cancelAnimationFrame(animFrameId.current);
203
+ }, [isRunning, simulationStep, worldConfig.timestep, worldConfig.maxSubsteps]);
204
+ const play = useCallback(() => setIsRunning(true), []);
205
+ const pause = useCallback(() => setIsRunning(false), []);
206
+ const step = useCallback(() => {
207
+ simulationStep(worldConfig.timestep);
208
+ }, [simulationStep, worldConfig.timestep]);
209
+ const reset = useCallback(() => {
210
+ setIsRunning(false);
211
+ internalBodies.current.forEach((body) => {
212
+ body.object.position.copy(body.initialPosition);
213
+ body.object.quaternion.copy(body.initialRotation);
214
+ body.object.scale.copy(body.initialScale);
215
+ body.velocity.set(0, 0, 0);
216
+ body.angularVelocity.set(0, 0, 0);
217
+ });
218
+ }, []);
219
+ function addDebugCollider(object, shape, id) {
220
+ let geo;
221
+ const aabb = computeAABB(object);
222
+ const size = new THREE.Vector3();
223
+ aabb.getSize(size);
224
+ switch (shape) {
225
+ case "sphere":
226
+ geo = new THREE.SphereGeometry(Math.max(size.x, size.y, size.z) / 2, 12, 8);
227
+ break;
228
+ case "capsule":
229
+ geo = new THREE.CapsuleGeometry(size.x / 2, size.y - size.x, 8, 4);
230
+ break;
231
+ default:
232
+ geo = new THREE.BoxGeometry(size.x, size.y, size.z);
233
+ }
234
+ const wireframe = new THREE.LineSegments(
235
+ new THREE.WireframeGeometry(geo),
236
+ new THREE.LineBasicMaterial({ color: 65280, opacity: 0.5, transparent: true })
237
+ );
238
+ wireframe.name = `debug_${id}`;
239
+ wireframe.position.copy(object.position);
240
+ wireframe.quaternion.copy(object.quaternion);
241
+ debugGroup.current.add(wireframe);
242
+ }
243
+ return {
244
+ worldConfig,
245
+ setWorldConfig,
246
+ bodies,
247
+ joints,
248
+ addBody,
249
+ removeBody,
250
+ updateBody,
251
+ addJoint,
252
+ removeJoint,
253
+ getBodyForObject,
254
+ isRunning,
255
+ play,
256
+ pause,
257
+ step,
258
+ reset,
259
+ debugDraw,
260
+ setDebugDraw
261
+ };
262
+ }
263
+ const NicePhysicsSimulation = ({
264
+ scene,
265
+ selectedObject,
266
+ onSimulationStep,
267
+ className = ""
268
+ }) => {
269
+ const { t } = useNiceTranslation();
270
+ const {
271
+ worldConfig,
272
+ setWorldConfig,
273
+ bodies,
274
+ addBody,
275
+ removeBody,
276
+ updateBody,
277
+ getBodyForObject,
278
+ isRunning,
279
+ play,
280
+ pause,
281
+ step,
282
+ reset,
283
+ debugDraw,
284
+ setDebugDraw
285
+ } = usePhysicsSimulation(scene, onSimulationStep);
286
+ const selectedBody = selectedObject ? getBodyForObject(selectedObject.name || selectedObject.uuid) : void 0;
287
+ return /* @__PURE__ */ jsxs(
288
+ "div",
289
+ {
290
+ className: `nice-physics-simulation ${className}`,
291
+ style: { display: "grid", gap: 8, fontSize: 13 },
292
+ children: [
293
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center" }, children: [
294
+ /* @__PURE__ */ jsxs("strong", { children: [
295
+ /* @__PURE__ */ jsx(NiceIcon, { name: "zap", size: 14 }),
296
+ " ",
297
+ t("physicsSimulation.physics", "Physics")
298
+ ] }),
299
+ /* @__PURE__ */ jsxs("label", { style: { display: "flex", alignItems: "center", gap: 4, fontSize: 11 }, children: [
300
+ /* @__PURE__ */ jsx(
301
+ "input",
302
+ {
303
+ type: "checkbox",
304
+ checked: debugDraw,
305
+ onChange: (e) => setDebugDraw(e.target.checked)
306
+ }
307
+ ),
308
+ t("physicsSimulation.debug", "Debug")
309
+ ] })
310
+ ] }),
311
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 4 }, children: [
312
+ /* @__PURE__ */ jsx(
313
+ "button",
314
+ {
315
+ type: "button",
316
+ onClick: isRunning ? pause : play,
317
+ style: {
318
+ flex: 1,
319
+ background: isRunning ? "var(--nice-danger, #ef4444)" : "var(--nice-success, #22c55e)",
320
+ color: "var(--nice-color-white, #fff)",
321
+ border: "none",
322
+ borderRadius: 4,
323
+ padding: "5px 8px",
324
+ cursor: "pointer"
325
+ },
326
+ children: isRunning ? /* @__PURE__ */ jsxs(Fragment, { children: [
327
+ /* @__PURE__ */ jsx(NiceIcon, { name: "pause", size: 14 }),
328
+ " ",
329
+ t("physicsSimulation.pause", "Pause")
330
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
331
+ /* @__PURE__ */ jsx(NiceIcon, { name: "play", size: 14 }),
332
+ " ",
333
+ t("physicsSimulation.play", "Play")
334
+ ] })
137
335
  }
138
- // Ground plane collision (y = 0)
139
- const aabb = computeAABB(body.object);
140
- if (aabb.min.y < 0) {
141
- body.object.position.y -= aabb.min.y;
142
- body.velocity.y = -body.velocity.y * body.desc.restitution;
143
- // Friction on ground
144
- body.velocity.x *= 1 - body.desc.friction * dt;
145
- body.velocity.z *= 1 - body.desc.friction * dt;
336
+ ),
337
+ /* @__PURE__ */ jsxs(
338
+ "button",
339
+ {
340
+ type: "button",
341
+ onClick: step,
342
+ disabled: isRunning,
343
+ style: { padding: "5px 8px", cursor: "pointer" },
344
+ children: [
345
+ /* @__PURE__ */ jsx(NiceIcon, { name: "skip-forward", size: 14 }),
346
+ " ",
347
+ t("physicsSimulation.step", "Step")
348
+ ]
146
349
  }
147
- // Update debug wireframe position
148
- const debugObj = debugGroup.current.getObjectByName(`debug_${body.desc.id}`);
149
- if (debugObj) {
150
- debugObj.position.copy(body.object.position);
151
- debugObj.quaternion.copy(body.object.quaternion);
350
+ ),
351
+ /* @__PURE__ */ jsxs("button", { type: "button", onClick: reset, style: { padding: "5px 8px", cursor: "pointer" }, children: [
352
+ /* @__PURE__ */ jsx(NiceIcon, { name: "refresh", size: 14 }),
353
+ " ",
354
+ t("physicsSimulation.reset", "Reset")
355
+ ] })
356
+ ] }),
357
+ /* @__PURE__ */ jsxs("label", { children: [
358
+ t("physicsSimulation.gravityY", "Gravity Y: {y}").replace(
359
+ "{y}",
360
+ String(worldConfig.gravity.y.toFixed(1))
361
+ ),
362
+ /* @__PURE__ */ jsx(
363
+ "input",
364
+ {
365
+ type: "range",
366
+ min: -20,
367
+ max: 0,
368
+ step: 0.1,
369
+ value: worldConfig.gravity.y,
370
+ onChange: (e) => setWorldConfig({ gravity: new THREE.Vector3(0, parseFloat(e.target.value), 0) }),
371
+ style: { width: "100%" }
152
372
  }
153
- });
154
- // AABB body-body collision (simplified)
155
- const dynamicBodies = [...internalBodies.current.values()].filter((b) => b.desc.bodyType === 'dynamic');
156
- for (let i = 0; i < dynamicBodies.length; i++) {
157
- for (let j = i + 1; j < dynamicBodies.length; j++) {
158
- const a = dynamicBodies[i];
159
- const b = dynamicBodies[j];
160
- const aabbA = computeAABB(a.object);
161
- const aabbB = computeAABB(b.object);
162
- if (aabbA.intersectsBox(aabbB)) {
163
- // Simple elastic collision response
164
- const normal = new THREE.Vector3()
165
- .subVectors(b.object.position, a.object.position)
166
- .normalize();
167
- const relVel = new THREE.Vector3().subVectors(a.velocity, b.velocity);
168
- const velAlongNormal = relVel.dot(normal);
169
- if (velAlongNormal > 0) {
170
- continue;
171
- } // separating
172
- const restitution = Math.min(a.desc.restitution, b.desc.restitution);
173
- const impulseMag = (-(1 + restitution) * velAlongNormal) / (1 / a.desc.mass + 1 / b.desc.mass);
174
- const impulse = normal.clone().multiplyScalar(impulseMag);
175
- a.velocity.addScaledVector(impulse, 1 / a.desc.mass);
176
- b.velocity.addScaledVector(impulse, -1 / b.desc.mass);
177
- // Separate overlapping bodies
178
- const overlap = new THREE.Vector3();
179
- aabbA.intersect(aabbB).getSize(overlap);
180
- const sep = Math.min(overlap.x, overlap.y, overlap.z) * 0.5;
181
- a.object.position.addScaledVector(normal, -sep);
182
- b.object.position.addScaledVector(normal, sep);
373
+ )
374
+ ] }),
375
+ selectedObject && /* @__PURE__ */ jsxs(
376
+ "fieldset",
377
+ {
378
+ style: {
379
+ border: "1px solid var(--nice-border, #e2e8f0)",
380
+ borderRadius: 6,
381
+ padding: 6,
382
+ display: "grid",
383
+ gap: 4
384
+ },
385
+ children: [
386
+ /* @__PURE__ */ jsx("legend", { style: { fontSize: 11 }, children: t("physicsSimulation.objectPhysics", "{name} — Physics").replace(
387
+ "{name}",
388
+ selectedObject.name || t("physicsSimulation.object", "Object")
389
+ ) }),
390
+ !selectedBody ? /* @__PURE__ */ jsx(
391
+ "button",
392
+ {
393
+ type: "button",
394
+ onClick: () => addBody(selectedObject),
395
+ style: {
396
+ background: "var(--nice-color-accent, #6366f1)",
397
+ color: "var(--nice-text-on-primary, #fff)",
398
+ border: "none",
399
+ borderRadius: 4,
400
+ padding: "5px 8px",
401
+ cursor: "pointer"
402
+ },
403
+ children: t("physicsSimulation.addRigidBody", "+ Add Rigid Body")
183
404
  }
184
- }
185
- }
186
- onSimulationStep === null || onSimulationStep === void 0 ? void 0 : onSimulationStep(dt);
187
- }, [worldConfig.gravity, onSimulationStep]);
188
- // Animation loop
189
- useEffect(() => {
190
- if (!isRunning) {
191
- return;
192
- }
193
- lastTime.current = performance.now();
194
- const loop = (now) => {
195
- const dt = Math.min((now - lastTime.current) / 1000, worldConfig.timestep * worldConfig.maxSubsteps);
196
- lastTime.current = now;
197
- let accum = dt;
198
- while (accum >= worldConfig.timestep) {
199
- simulationStep(worldConfig.timestep);
200
- accum -= worldConfig.timestep;
201
- }
202
- animFrameId.current = requestAnimationFrame(loop);
203
- };
204
- animFrameId.current = requestAnimationFrame(loop);
205
- return () => cancelAnimationFrame(animFrameId.current);
206
- }, [isRunning, simulationStep, worldConfig.timestep, worldConfig.maxSubsteps]);
207
- const play = useCallback(() => setIsRunning(true), []);
208
- const pause = useCallback(() => setIsRunning(false), []);
209
- const step = useCallback(() => {
210
- simulationStep(worldConfig.timestep);
211
- }, [simulationStep, worldConfig.timestep]);
212
- const reset = useCallback(() => {
213
- setIsRunning(false);
214
- internalBodies.current.forEach((body) => {
215
- body.object.position.copy(body.initialPosition);
216
- body.object.quaternion.copy(body.initialRotation);
217
- body.object.scale.copy(body.initialScale);
218
- body.velocity.set(0, 0, 0);
219
- body.angularVelocity.set(0, 0, 0);
220
- });
221
- }, []);
222
- function addDebugCollider(object, shape, id) {
223
- let geo;
224
- const aabb = computeAABB(object);
225
- const size = new THREE.Vector3();
226
- aabb.getSize(size);
227
- switch (shape) {
228
- case 'sphere':
229
- geo = new THREE.SphereGeometry(Math.max(size.x, size.y, size.z) / 2, 12, 8);
230
- break;
231
- case 'capsule':
232
- geo = new THREE.CapsuleGeometry(size.x / 2, size.y - size.x, 8, 4);
233
- break;
234
- default:
235
- geo = new THREE.BoxGeometry(size.x, size.y, size.z);
236
- }
237
- const wireframe = new THREE.LineSegments(new THREE.WireframeGeometry(geo), new THREE.LineBasicMaterial({ color: 0x00ff00, opacity: 0.5, transparent: true }));
238
- wireframe.name = `debug_${id}`;
239
- wireframe.position.copy(object.position);
240
- wireframe.quaternion.copy(object.quaternion);
241
- debugGroup.current.add(wireframe);
405
+ ) : /* @__PURE__ */ jsxs(Fragment, { children: [
406
+ /* @__PURE__ */ jsxs("label", { children: [
407
+ t("physicsSimulation.type", "Type:"),
408
+ /* @__PURE__ */ jsxs(
409
+ "select",
410
+ {
411
+ value: selectedBody.bodyType,
412
+ onChange: (e) => updateBody(selectedBody.id, { bodyType: e.target.value }),
413
+ style: { marginLeft: 8, width: "100%" },
414
+ children: [
415
+ /* @__PURE__ */ jsx("option", { value: "dynamic", children: t("physicsSimulation.dynamic", "Dynamic") }),
416
+ /* @__PURE__ */ jsx("option", { value: "static", children: t("physicsSimulation.static", "Static") }),
417
+ /* @__PURE__ */ jsx("option", { value: "kinematic", children: t("physicsSimulation.kinematic", "Kinematic") })
418
+ ]
419
+ }
420
+ )
421
+ ] }),
422
+ /* @__PURE__ */ jsxs("label", { children: [
423
+ t("physicsSimulation.shape", "Shape:"),
424
+ /* @__PURE__ */ jsxs(
425
+ "select",
426
+ {
427
+ value: selectedBody.colliderShape,
428
+ onChange: (e) => updateBody(selectedBody.id, { colliderShape: e.target.value }),
429
+ style: { marginLeft: 8, width: "100%" },
430
+ children: [
431
+ /* @__PURE__ */ jsx("option", { value: "box", children: t("physicsSimulation.box", "Box") }),
432
+ /* @__PURE__ */ jsx("option", { value: "sphere", children: t("physicsSimulation.sphere", "Sphere") }),
433
+ /* @__PURE__ */ jsx("option", { value: "capsule", children: t("physicsSimulation.capsule", "Capsule") }),
434
+ /* @__PURE__ */ jsx("option", { value: "convexHull", children: t("physicsSimulation.convexHull", "Convex Hull") }),
435
+ /* @__PURE__ */ jsx("option", { value: "trimesh", children: t("physicsSimulation.trimesh", "Trimesh") })
436
+ ]
437
+ }
438
+ )
439
+ ] }),
440
+ /* @__PURE__ */ jsxs("label", { children: [
441
+ t("physicsSimulation.mass", "Mass: {mass}").replace(
442
+ "{mass}",
443
+ String(selectedBody.mass.toFixed(1))
444
+ ),
445
+ /* @__PURE__ */ jsx(
446
+ "input",
447
+ {
448
+ type: "range",
449
+ min: 0.1,
450
+ max: 100,
451
+ step: 0.1,
452
+ value: selectedBody.mass,
453
+ onChange: (e) => updateBody(selectedBody.id, { mass: parseFloat(e.target.value) }),
454
+ style: { width: "100%" }
455
+ }
456
+ )
457
+ ] }),
458
+ /* @__PURE__ */ jsxs("label", { children: [
459
+ t("physicsSimulation.restitution", "Restitution: {restitution}").replace(
460
+ "{restitution}",
461
+ String(selectedBody.restitution.toFixed(2))
462
+ ),
463
+ /* @__PURE__ */ jsx(
464
+ "input",
465
+ {
466
+ type: "range",
467
+ min: 0,
468
+ max: 1,
469
+ step: 0.01,
470
+ value: selectedBody.restitution,
471
+ onChange: (e) => updateBody(selectedBody.id, { restitution: parseFloat(e.target.value) }),
472
+ style: { width: "100%" }
473
+ }
474
+ )
475
+ ] }),
476
+ /* @__PURE__ */ jsxs("label", { children: [
477
+ t("physicsSimulation.friction", "Friction: {friction}").replace(
478
+ "{friction}",
479
+ String(selectedBody.friction.toFixed(2))
480
+ ),
481
+ /* @__PURE__ */ jsx(
482
+ "input",
483
+ {
484
+ type: "range",
485
+ min: 0,
486
+ max: 1,
487
+ step: 0.01,
488
+ value: selectedBody.friction,
489
+ onChange: (e) => updateBody(selectedBody.id, { friction: parseFloat(e.target.value) }),
490
+ style: { width: "100%" }
491
+ }
492
+ )
493
+ ] }),
494
+ /* @__PURE__ */ jsxs("label", { style: { display: "flex", alignItems: "center", gap: 6 }, children: [
495
+ /* @__PURE__ */ jsx(
496
+ "input",
497
+ {
498
+ type: "checkbox",
499
+ checked: selectedBody.isSensor,
500
+ onChange: (e) => updateBody(selectedBody.id, { isSensor: e.target.checked })
501
+ }
502
+ ),
503
+ t("physicsSimulation.sensorTriggerOnly", "Sensor (trigger only)")
504
+ ] }),
505
+ /* @__PURE__ */ jsx(
506
+ "button",
507
+ {
508
+ type: "button",
509
+ onClick: () => removeBody(selectedBody.id),
510
+ style: {
511
+ background: "var(--nice-danger, #ef4444)",
512
+ color: "var(--nice-text-on-primary, #fff)",
513
+ border: "none",
514
+ borderRadius: 4,
515
+ padding: "4px 8px",
516
+ cursor: "pointer",
517
+ fontSize: 11
518
+ },
519
+ children: t("physicsSimulation.removeRigidBody", "Remove Rigid Body")
520
+ }
521
+ )
522
+ ] })
523
+ ]
524
+ }
525
+ ),
526
+ /* @__PURE__ */ jsx("div", { style: { fontSize: 11, opacity: 0.7 }, children: t("physicsSimulation.totalBodies", "Total bodies: {bodies}").replace(
527
+ "{bodies}",
528
+ String(bodies.length)
529
+ ) })
530
+ ]
242
531
  }
243
- return {
244
- worldConfig,
245
- setWorldConfig,
246
- bodies,
247
- joints,
248
- addBody,
249
- removeBody,
250
- updateBody,
251
- addJoint,
252
- removeJoint,
253
- getBodyForObject,
254
- isRunning,
255
- play,
256
- pause,
257
- step,
258
- reset,
259
- debugDraw,
260
- setDebugDraw,
261
- };
262
- }
263
- /* ── Component ── */
264
- const NicePhysicsSimulation = ({ scene, selectedObject, onSimulationStep, className = '', }) => {
265
- const { t } = useNiceTranslation();
266
- const { worldConfig, setWorldConfig, bodies, addBody, removeBody, updateBody, getBodyForObject, isRunning, play, pause, step, reset, debugDraw, setDebugDraw, } = usePhysicsSimulation(scene, onSimulationStep);
267
- const selectedBody = selectedObject
268
- ? getBodyForObject(selectedObject.name || selectedObject.uuid)
269
- : undefined;
270
- return (jsxs("div", { className: `nice-physics-simulation ${className}`, style: { display: 'grid', gap: 8, fontSize: 13 }, children: [jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' }, children: [jsxs("strong", { children: [jsx(NiceIcon, { name: "zap", size: 14 }), " ", t('physicsSimulation.physics', 'Physics')] }), jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 4, fontSize: 11 }, children: [jsx("input", { type: "checkbox", checked: debugDraw, onChange: (e) => setDebugDraw(e.target.checked) }), t('physicsSimulation.debug', 'Debug')] })] }), jsxs("div", { style: { display: 'flex', gap: 4 }, children: [jsx("button", { type: "button", onClick: isRunning ? pause : play, style: {
271
- flex: 1,
272
- background: isRunning ? 'var(--nice-danger, #ef4444)' : 'var(--nice-success, #22c55e)',
273
- color: 'var(--nice-color-white, #fff)',
274
- border: 'none',
275
- borderRadius: 4,
276
- padding: '5px 8px',
277
- cursor: 'pointer',
278
- }, children: isRunning ? (jsxs(Fragment, { children: [jsx(NiceIcon, { name: "pause", size: 14 }), " ", t('physicsSimulation.pause', 'Pause')] })) : (jsxs(Fragment, { children: [jsx(NiceIcon, { name: "play", size: 14 }), " ", t('physicsSimulation.play', 'Play')] })) }), jsxs("button", { type: "button", onClick: step, disabled: isRunning, style: { padding: '5px 8px', cursor: 'pointer' }, children: [jsx(NiceIcon, { name: "skip-forward", size: 14 }), " ", t('physicsSimulation.step', 'Step')] }), jsxs("button", { type: "button", onClick: reset, style: { padding: '5px 8px', cursor: 'pointer' }, children: [jsx(NiceIcon, { name: "refresh", size: 14 }), " ", t('physicsSimulation.reset', 'Reset')] })] }), jsxs("label", { children: [t('physicsSimulation.gravityY', 'Gravity Y: {y}').replace('{y}', String(worldConfig.gravity.y.toFixed(1))), jsx("input", { type: "range", min: -20, max: 0, step: 0.1, value: worldConfig.gravity.y, onChange: (e) => setWorldConfig({ gravity: new THREE.Vector3(0, parseFloat(e.target.value), 0) }), style: { width: '100%' } })] }), selectedObject && (jsxs("fieldset", { style: {
279
- border: '1px solid var(--nice-border, #e2e8f0)',
280
- borderRadius: 6,
281
- padding: 6,
282
- display: 'grid',
283
- gap: 4,
284
- }, children: [jsx("legend", { style: { fontSize: 11 }, children: t('physicsSimulation.objectPhysics', '{name} — Physics').replace('{name}', selectedObject.name || t('physicsSimulation.object', 'Object')) }), !selectedBody ? (jsx("button", { type: "button", onClick: () => addBody(selectedObject), style: {
285
- background: 'var(--nice-color-accent, #6366f1)',
286
- color: 'var(--nice-text-on-primary, #fff)',
287
- border: 'none',
288
- borderRadius: 4,
289
- padding: '5px 8px',
290
- cursor: 'pointer',
291
- }, children: t('physicsSimulation.addRigidBody', '+ Add Rigid Body') })) : (jsxs(Fragment, { children: [jsxs("label", { children: [t('physicsSimulation.type', 'Type:'), jsxs("select", { value: selectedBody.bodyType, onChange: (e) => updateBody(selectedBody.id, { bodyType: e.target.value }), style: { marginLeft: 8, width: '100%' }, children: [jsx("option", { value: "dynamic", children: t('physicsSimulation.dynamic', 'Dynamic') }), jsx("option", { value: "static", children: t('physicsSimulation.static', 'Static') }), jsx("option", { value: "kinematic", children: t('physicsSimulation.kinematic', 'Kinematic') })] })] }), jsxs("label", { children: [t('physicsSimulation.shape', 'Shape:'), jsxs("select", { value: selectedBody.colliderShape, onChange: (e) => updateBody(selectedBody.id, { colliderShape: e.target.value }), style: { marginLeft: 8, width: '100%' }, children: [jsx("option", { value: "box", children: t('physicsSimulation.box', 'Box') }), jsx("option", { value: "sphere", children: t('physicsSimulation.sphere', 'Sphere') }), jsx("option", { value: "capsule", children: t('physicsSimulation.capsule', 'Capsule') }), jsx("option", { value: "convexHull", children: t('physicsSimulation.convexHull', 'Convex Hull') }), jsx("option", { value: "trimesh", children: t('physicsSimulation.trimesh', 'Trimesh') })] })] }), jsxs("label", { children: [t('physicsSimulation.mass', 'Mass: {mass}').replace('{mass}', String(selectedBody.mass.toFixed(1))), jsx("input", { type: "range", min: 0.1, max: 100, step: 0.1, value: selectedBody.mass, onChange: (e) => updateBody(selectedBody.id, { mass: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsxs("label", { children: [t('physicsSimulation.restitution', 'Restitution: {restitution}').replace('{restitution}', String(selectedBody.restitution.toFixed(2))), jsx("input", { type: "range", min: 0, max: 1, step: 0.01, value: selectedBody.restitution, onChange: (e) => updateBody(selectedBody.id, { restitution: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsxs("label", { children: [t('physicsSimulation.friction', 'Friction: {friction}').replace('{friction}', String(selectedBody.friction.toFixed(2))), jsx("input", { type: "range", min: 0, max: 1, step: 0.01, value: selectedBody.friction, onChange: (e) => updateBody(selectedBody.id, { friction: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 6 }, children: [jsx("input", { type: "checkbox", checked: selectedBody.isSensor, onChange: (e) => updateBody(selectedBody.id, { isSensor: e.target.checked }) }), t('physicsSimulation.sensorTriggerOnly', 'Sensor (trigger only)')] }), jsx("button", { type: "button", onClick: () => removeBody(selectedBody.id), style: {
292
- background: 'var(--nice-danger, #ef4444)',
293
- color: 'var(--nice-text-on-primary, #fff)',
294
- border: 'none',
295
- borderRadius: 4,
296
- padding: '4px 8px',
297
- cursor: 'pointer',
298
- fontSize: 11,
299
- }, children: t('physicsSimulation.removeRigidBody', 'Remove Rigid Body') })] }))] })), jsx("div", { style: { fontSize: 11, opacity: 0.7 }, children: t('physicsSimulation.totalBodies', 'Total bodies: {bodies}').replace('{bodies}', String(bodies.length)) })] }));
532
+ );
533
+ };
534
+ export {
535
+ NicePhysicsSimulation,
536
+ usePhysicsSimulation
300
537
  };
301
-
302
- export { NicePhysicsSimulation, usePhysicsSimulation };