@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,269 +1,473 @@
1
- import { jsxs, jsx } from 'react/jsx-runtime';
2
- import { NiceTutorialButton, resolveTutorial, NiceIcon, createId } from '@nice2dev/ui';
3
- import { useState, useMemo, useRef, useEffect, useCallback } from 'react';
4
- import * as THREE from 'three';
5
- import { useNiceTranslation } from '../core/i18n.js';
6
-
7
- /** Built-in guided tour (EN inline + i18n keys → PL and other locales). */
1
+ import { jsxs, jsx } from "react/jsx-runtime";
2
+ import { NiceTutorialButton, resolveTutorial, NiceIcon, createId } from "@nice2dev/ui";
3
+ import { useState, useMemo, useRef, useEffect, useCallback } from "react";
4
+ import * as THREE from "three";
5
+ import { useNiceTranslation } from "../core/i18n.js";
8
6
  const ARMATURE_EDITOR_TOUR = [
9
- {
10
- target: '.nice-armature-editor',
11
- titleKey: 'tutorial.armatureEditor.intro.title',
12
- title: 'Armature editor',
13
- contentKey: 'tutorial.armatureEditor.intro.content',
14
- content: 'Select bones to edit their transforms, add or reparent bones, configure IK chains, and save or load poses.',
15
- placement: 'auto',
16
- },
7
+ {
8
+ target: ".nice-armature-editor",
9
+ titleKey: "tutorial.armatureEditor.intro.title",
10
+ title: "Armature editor",
11
+ contentKey: "tutorial.armatureEditor.intro.content",
12
+ content: "Select bones to edit their transforms, add or reparent bones, configure IK chains, and save or load poses.",
13
+ placement: "auto"
14
+ }
17
15
  ];
18
16
  function useArmatureEditor(skeleton, scene, onBoneSelect) {
19
- const [bones, setBones] = useState([]);
20
- const [selectedBoneId, setSelectedBoneId] = useState(null);
21
- const [savedPoses, setSavedPoses] = useState([]);
22
- const helperRef = useRef(null);
23
- // Build bone info tree from skeleton
24
- useEffect(() => {
25
- if (!skeleton) {
26
- setBones([]);
27
- return;
28
- }
29
- const boneInfos = skeleton.bones.map((bone) => ({
30
- id: bone.uuid,
31
- name: bone.name || `Bone_${bone.uuid.slice(0, 6)}`,
32
- bone,
33
- parentId: bone.parent instanceof THREE.Bone ? bone.parent.uuid : null,
34
- children: bone.children
35
- .filter((c) => c instanceof THREE.Bone)
36
- .map((c) => c.uuid),
37
- localPosition: bone.position.clone(),
38
- localRotation: bone.rotation.clone(),
39
- localScale: bone.scale.clone(),
40
- length: bone.children.length > 0 ? bone.children[0].position.length() : 0.1,
41
- }));
42
- setBones(boneInfos);
43
- }, [skeleton]);
44
- // Create skeleton helper for visualization
45
- useEffect(() => {
46
- var _a;
47
- if (!skeleton || !scene) {
48
- return;
49
- }
50
- const rootBone = ((_a = skeleton.bones[0]) === null || _a === void 0 ? void 0 : _a.parent) || skeleton.bones[0];
51
- if (!rootBone) {
52
- return;
53
- }
54
- const helper = new THREE.SkeletonHelper(rootBone);
55
- helper.material.linewidth = 2;
56
- scene.add(helper);
57
- helperRef.current = helper;
58
- return () => {
59
- scene.remove(helper);
60
- helper.dispose();
61
- helperRef.current = null;
62
- };
63
- }, [skeleton, scene]);
64
- const selectBone = useCallback((id) => {
65
- setSelectedBoneId(id);
66
- const bone = id ? (skeleton === null || skeleton === void 0 ? void 0 : skeleton.bones.find((b) => b.uuid === id)) || null : null;
67
- onBoneSelect === null || onBoneSelect === void 0 ? void 0 : onBoneSelect(bone);
68
- }, [skeleton, onBoneSelect]);
69
- const setBonePosition = useCallback((id, pos) => {
70
- const bone = skeleton === null || skeleton === void 0 ? void 0 : skeleton.bones.find((b) => b.uuid === id);
71
- if (bone) {
72
- bone.position.copy(pos);
73
- bone.updateMatrixWorld(true);
74
- setBones((prev) => prev.map((b) => (b.id === id ? { ...b, localPosition: pos.clone() } : b)));
75
- }
76
- }, [skeleton]);
77
- const setBoneRotation = useCallback((id, rot) => {
78
- const bone = skeleton === null || skeleton === void 0 ? void 0 : skeleton.bones.find((b) => b.uuid === id);
79
- if (bone) {
80
- bone.rotation.copy(rot);
81
- bone.updateMatrixWorld(true);
82
- setBones((prev) => prev.map((b) => (b.id === id ? { ...b, localRotation: rot.clone() } : b)));
17
+ const [bones, setBones] = useState([]);
18
+ const [selectedBoneId, setSelectedBoneId] = useState(null);
19
+ const [savedPoses, setSavedPoses] = useState([]);
20
+ const helperRef = useRef(null);
21
+ useEffect(() => {
22
+ if (!skeleton) {
23
+ setBones([]);
24
+ return;
25
+ }
26
+ const boneInfos = skeleton.bones.map((bone) => ({
27
+ id: bone.uuid,
28
+ name: bone.name || `Bone_${bone.uuid.slice(0, 6)}`,
29
+ bone,
30
+ parentId: bone.parent instanceof THREE.Bone ? bone.parent.uuid : null,
31
+ children: bone.children.filter((c) => c instanceof THREE.Bone).map((c) => c.uuid),
32
+ localPosition: bone.position.clone(),
33
+ localRotation: bone.rotation.clone(),
34
+ localScale: bone.scale.clone(),
35
+ length: bone.children.length > 0 ? bone.children[0].position.length() : 0.1
36
+ }));
37
+ setBones(boneInfos);
38
+ }, [skeleton]);
39
+ useEffect(() => {
40
+ if (!skeleton || !scene) {
41
+ return;
42
+ }
43
+ const rootBone = skeleton.bones[0]?.parent || skeleton.bones[0];
44
+ if (!rootBone) {
45
+ return;
46
+ }
47
+ const helper = new THREE.SkeletonHelper(rootBone);
48
+ helper.material.linewidth = 2;
49
+ scene.add(helper);
50
+ helperRef.current = helper;
51
+ return () => {
52
+ scene.remove(helper);
53
+ helper.dispose();
54
+ helperRef.current = null;
55
+ };
56
+ }, [skeleton, scene]);
57
+ const selectBone = useCallback(
58
+ (id) => {
59
+ setSelectedBoneId(id);
60
+ const bone = id ? skeleton?.bones.find((b) => b.uuid === id) || null : null;
61
+ onBoneSelect?.(bone);
62
+ },
63
+ [skeleton, onBoneSelect]
64
+ );
65
+ const setBonePosition = useCallback(
66
+ (id, pos) => {
67
+ const bone = skeleton?.bones.find((b) => b.uuid === id);
68
+ if (bone) {
69
+ bone.position.copy(pos);
70
+ bone.updateMatrixWorld(true);
71
+ setBones(
72
+ (prev) => prev.map((b) => b.id === id ? { ...b, localPosition: pos.clone() } : b)
73
+ );
74
+ }
75
+ },
76
+ [skeleton]
77
+ );
78
+ const setBoneRotation = useCallback(
79
+ (id, rot) => {
80
+ const bone = skeleton?.bones.find((b) => b.uuid === id);
81
+ if (bone) {
82
+ bone.rotation.copy(rot);
83
+ bone.updateMatrixWorld(true);
84
+ setBones(
85
+ (prev) => prev.map((b) => b.id === id ? { ...b, localRotation: rot.clone() } : b)
86
+ );
87
+ }
88
+ },
89
+ [skeleton]
90
+ );
91
+ const addBone = useCallback(
92
+ (parentId, name) => {
93
+ const parent = skeleton?.bones.find((b) => b.uuid === parentId);
94
+ const newBone = new THREE.Bone();
95
+ newBone.name = name;
96
+ newBone.position.set(0, 0.5, 0);
97
+ if (parent) {
98
+ parent.add(newBone);
99
+ }
100
+ if (skeleton) {
101
+ skeleton.bones.push(newBone);
102
+ skeleton.update();
103
+ }
104
+ const boneInfo = {
105
+ id: newBone.uuid,
106
+ name,
107
+ bone: newBone,
108
+ parentId,
109
+ children: [],
110
+ localPosition: newBone.position.clone(),
111
+ localRotation: newBone.rotation.clone(),
112
+ localScale: newBone.scale.clone(),
113
+ length: 0.5
114
+ };
115
+ setBones((prev) => [
116
+ ...prev.map(
117
+ (b) => b.id === parentId ? { ...b, children: [...b.children, newBone.uuid] } : b
118
+ ),
119
+ boneInfo
120
+ ]);
121
+ return newBone;
122
+ },
123
+ [skeleton]
124
+ );
125
+ const removeBone = useCallback(
126
+ (id) => {
127
+ const bone = skeleton?.bones.find((b) => b.uuid === id);
128
+ if (!bone) {
129
+ return;
130
+ }
131
+ bone.removeFromParent();
132
+ if (skeleton) {
133
+ const idx = skeleton.bones.indexOf(bone);
134
+ if (idx >= 0) {
135
+ skeleton.bones.splice(idx, 1);
83
136
  }
84
- }, [skeleton]);
85
- const addBone = useCallback((parentId, name) => {
86
- const parent = skeleton === null || skeleton === void 0 ? void 0 : skeleton.bones.find((b) => b.uuid === parentId);
87
- const newBone = new THREE.Bone();
88
- newBone.name = name;
89
- newBone.position.set(0, 0.5, 0);
90
- if (parent) {
91
- parent.add(newBone);
137
+ skeleton.update();
138
+ }
139
+ setBones(
140
+ (prev) => prev.filter((b) => b.id !== id).map((b) => ({
141
+ ...b,
142
+ children: b.children.filter((c) => c !== id)
143
+ }))
144
+ );
145
+ if (selectedBoneId === id) {
146
+ setSelectedBoneId(null);
147
+ }
148
+ },
149
+ [skeleton, selectedBoneId]
150
+ );
151
+ const savePose = useCallback(
152
+ (name) => {
153
+ const transforms = /* @__PURE__ */ new Map();
154
+ if (skeleton) {
155
+ for (const bone of skeleton.bones) {
156
+ transforms.set(bone.uuid, {
157
+ position: bone.position.toArray(),
158
+ rotation: [bone.rotation.x, bone.rotation.y, bone.rotation.z],
159
+ scale: bone.scale.toArray()
160
+ });
92
161
  }
93
- if (skeleton) {
94
- skeleton.bones.push(newBone);
95
- skeleton.update();
162
+ }
163
+ const pose = {
164
+ id: createId(),
165
+ name,
166
+ timestamp: Date.now(),
167
+ boneTransforms: transforms
168
+ };
169
+ setSavedPoses((prev) => [...prev, pose]);
170
+ return pose;
171
+ },
172
+ [skeleton]
173
+ );
174
+ const loadPose = useCallback(
175
+ (pose) => {
176
+ if (!skeleton) {
177
+ return;
178
+ }
179
+ for (const bone of skeleton.bones) {
180
+ const transform = pose.boneTransforms.get(bone.uuid);
181
+ if (transform) {
182
+ bone.position.fromArray(transform.position);
183
+ bone.rotation.set(transform.rotation[0], transform.rotation[1], transform.rotation[2]);
184
+ bone.scale.fromArray(transform.scale);
96
185
  }
97
- const boneInfo = {
98
- id: newBone.uuid,
99
- name,
100
- bone: newBone,
101
- parentId,
102
- children: [],
103
- localPosition: newBone.position.clone(),
104
- localRotation: newBone.rotation.clone(),
105
- localScale: newBone.scale.clone(),
106
- length: 0.5,
107
- };
108
- setBones((prev) => [
109
- ...prev.map((b) => b.id === parentId ? { ...b, children: [...b.children, newBone.uuid] } : b),
110
- boneInfo,
111
- ]);
112
- return newBone;
113
- }, [skeleton]);
114
- const removeBone = useCallback((id) => {
115
- const bone = skeleton === null || skeleton === void 0 ? void 0 : skeleton.bones.find((b) => b.uuid === id);
116
- if (!bone) {
117
- return;
186
+ }
187
+ skeleton.update();
188
+ setBones(
189
+ (prev) => prev.map((b) => ({
190
+ ...b,
191
+ localPosition: b.bone.position.clone(),
192
+ localRotation: b.bone.rotation.clone(),
193
+ localScale: b.bone.scale.clone()
194
+ }))
195
+ );
196
+ },
197
+ [skeleton]
198
+ );
199
+ const resetPose = useCallback(() => {
200
+ if (!skeleton) {
201
+ return;
202
+ }
203
+ for (const bone of skeleton.bones) {
204
+ bone.position.set(0, 0, 0);
205
+ bone.rotation.set(0, 0, 0);
206
+ bone.scale.set(1, 1, 1);
207
+ }
208
+ if (skeleton.boneInverses.length > 0) {
209
+ skeleton.pose();
210
+ }
211
+ skeleton.update();
212
+ setBones(
213
+ (prev) => prev.map((b) => ({
214
+ ...b,
215
+ localPosition: b.bone.position.clone(),
216
+ localRotation: b.bone.rotation.clone(),
217
+ localScale: b.bone.scale.clone()
218
+ }))
219
+ );
220
+ }, [skeleton]);
221
+ return {
222
+ bones,
223
+ selectedBoneId,
224
+ selectBone,
225
+ setBonePosition,
226
+ setBoneRotation,
227
+ addBone,
228
+ removeBone,
229
+ savePose,
230
+ loadPose,
231
+ resetPose,
232
+ savedPoses,
233
+ skeletonHelper: helperRef.current
234
+ };
235
+ }
236
+ const NiceArmatureEditor = ({
237
+ skeleton,
238
+ skinnedMesh: _skinnedMesh,
239
+ scene,
240
+ camera: _camera,
241
+ onBoneSelect,
242
+ onPoseChange: _onPoseChange,
243
+ className = "",
244
+ tutorial
245
+ }) => {
246
+ const { t } = useNiceTranslation();
247
+ const {
248
+ bones,
249
+ selectedBoneId,
250
+ selectBone,
251
+ setBonePosition: _setBonePosition,
252
+ setBoneRotation,
253
+ addBone,
254
+ removeBone,
255
+ savePose,
256
+ loadPose,
257
+ resetPose,
258
+ savedPoses,
259
+ skeletonHelper: _skeletonHelper
260
+ } = useArmatureEditor(skeleton, scene, onBoneSelect);
261
+ const [newBoneName, setNewBoneName] = useState("");
262
+ const selectedBone = useMemo(
263
+ () => bones.find((b) => b.id === selectedBoneId),
264
+ [bones, selectedBoneId]
265
+ );
266
+ const rootBones = useMemo(() => bones.filter((b) => !b.parentId), [bones]);
267
+ const renderBoneTree = (boneInfo, depth = 0) => {
268
+ const isSelected = boneInfo.id === selectedBoneId;
269
+ const childBones = bones.filter((b) => b.parentId === boneInfo.id);
270
+ return /* @__PURE__ */ jsxs("div", { children: [
271
+ /* @__PURE__ */ jsxs(
272
+ "div",
273
+ {
274
+ onClick: () => selectBone(boneInfo.id),
275
+ role: "button",
276
+ tabIndex: 0,
277
+ onKeyDown: (e) => e.key === "Enter" && selectBone(boneInfo.id),
278
+ style: {
279
+ padding: "4px 8px",
280
+ paddingLeft: depth * 16 + 8,
281
+ background: isSelected ? "var(--nice-color-accent, #6366f1)" : "transparent",
282
+ color: isSelected ? "var(--nice-color-white, #fff)" : "inherit",
283
+ cursor: "pointer",
284
+ fontSize: 13,
285
+ borderRadius: 4
286
+ },
287
+ children: [
288
+ /* @__PURE__ */ jsx(NiceIcon, { name: "target", size: 14 }),
289
+ " ",
290
+ boneInfo.name
291
+ ]
118
292
  }
119
- bone.removeFromParent();
120
- if (skeleton) {
121
- const idx = skeleton.bones.indexOf(bone);
122
- if (idx >= 0) {
123
- skeleton.bones.splice(idx, 1);
293
+ ),
294
+ childBones.map((child) => renderBoneTree(child, depth + 1))
295
+ ] }, boneInfo.id);
296
+ };
297
+ return /* @__PURE__ */ jsxs(
298
+ "div",
299
+ {
300
+ className: `nice-armature-editor ${className}`,
301
+ style: { position: "relative", display: "flex", flexDirection: "column", gap: 8 },
302
+ children: [
303
+ /* @__PURE__ */ jsx("span", { style: { position: "absolute", top: 0, right: 0, zIndex: 20 }, children: /* @__PURE__ */ jsx(NiceTutorialButton, { steps: resolveTutorial(tutorial, ARMATURE_EDITOR_TOUR) }) }),
304
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 4 }, children: [
305
+ /* @__PURE__ */ jsxs(
306
+ "button",
307
+ {
308
+ type: "button",
309
+ onClick: resetPose,
310
+ title: t("armatureEditor.resetPose", "Reset Pose"),
311
+ children: [
312
+ /* @__PURE__ */ jsx(NiceIcon, { name: "refresh", size: 14 }),
313
+ " ",
314
+ t("armatureEditor.reset", "Reset")
315
+ ]
124
316
  }
125
- skeleton.update();
126
- }
127
- setBones((prev) => prev
128
- .filter((b) => b.id !== id)
129
- .map((b) => ({
130
- ...b,
131
- children: b.children.filter((c) => c !== id),
132
- })));
133
- if (selectedBoneId === id) {
134
- setSelectedBoneId(null);
135
- }
136
- }, [skeleton, selectedBoneId]);
137
- const savePose = useCallback((name) => {
138
- const transforms = new Map();
139
- if (skeleton) {
140
- for (const bone of skeleton.bones) {
141
- transforms.set(bone.uuid, {
142
- position: bone.position.toArray(),
143
- rotation: [bone.rotation.x, bone.rotation.y, bone.rotation.z],
144
- scale: bone.scale.toArray(),
145
- });
317
+ ),
318
+ /* @__PURE__ */ jsxs(
319
+ "button",
320
+ {
321
+ type: "button",
322
+ onClick: () => savePose(`Pose ${savedPoses.length + 1}`),
323
+ title: t("armatureEditor.savePose", "Save Pose"),
324
+ children: [
325
+ /* @__PURE__ */ jsx(NiceIcon, { name: "save", size: 14 }),
326
+ " ",
327
+ t("armatureEditor.savePose", "Save Pose")
328
+ ]
146
329
  }
147
- }
148
- const pose = {
149
- id: createId(),
150
- name,
151
- timestamp: Date.now(),
152
- boneTransforms: transforms,
153
- };
154
- setSavedPoses((prev) => [...prev, pose]);
155
- return pose;
156
- }, [skeleton]);
157
- const loadPose = useCallback((pose) => {
158
- if (!skeleton) {
159
- return;
160
- }
161
- for (const bone of skeleton.bones) {
162
- const transform = pose.boneTransforms.get(bone.uuid);
163
- if (transform) {
164
- bone.position.fromArray(transform.position);
165
- bone.rotation.set(transform.rotation[0], transform.rotation[1], transform.rotation[2]);
166
- bone.scale.fromArray(transform.scale);
330
+ )
331
+ ] }),
332
+ /* @__PURE__ */ jsx(
333
+ "div",
334
+ {
335
+ style: {
336
+ maxHeight: 200,
337
+ overflowY: "auto",
338
+ border: "1px solid var(--nice-border, #e2e8f0)",
339
+ borderRadius: 6,
340
+ padding: 4
341
+ },
342
+ children: rootBones.length === 0 ? /* @__PURE__ */ jsx("div", { style: { padding: 16, textAlign: "center", color: "var(--nice-text-muted, #999)" }, children: t("armatureEditor.noSkeletonFound", "No skeleton found") }) : rootBones.map((b) => renderBoneTree(b))
343
+ }
344
+ ),
345
+ selectedBoneId && /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 4 }, children: [
346
+ /* @__PURE__ */ jsx(
347
+ "input",
348
+ {
349
+ type: "text",
350
+ value: newBoneName,
351
+ onChange: (e) => setNewBoneName(e.target.value),
352
+ placeholder: t("armatureEditor.newBoneName", "New bone name"),
353
+ style: {
354
+ flex: 1,
355
+ padding: "4px 8px",
356
+ borderRadius: 4,
357
+ border: "1px solid var(--nice-border, #e2e8f0)"
358
+ }
167
359
  }
168
- }
169
- skeleton.update();
170
- setBones((prev) => prev.map((b) => ({
171
- ...b,
172
- localPosition: b.bone.position.clone(),
173
- localRotation: b.bone.rotation.clone(),
174
- localScale: b.bone.scale.clone(),
175
- })));
176
- }, [skeleton]);
177
- const resetPose = useCallback(() => {
178
- if (!skeleton) {
179
- return;
180
- }
181
- for (const bone of skeleton.bones) {
182
- bone.position.set(0, 0, 0);
183
- bone.rotation.set(0, 0, 0);
184
- bone.scale.set(1, 1, 1);
185
- }
186
- // Restore bind pose
187
- if (skeleton.boneInverses.length > 0) {
188
- skeleton.pose();
189
- }
190
- skeleton.update();
191
- setBones((prev) => prev.map((b) => ({
192
- ...b,
193
- localPosition: b.bone.position.clone(),
194
- localRotation: b.bone.rotation.clone(),
195
- localScale: b.bone.scale.clone(),
196
- })));
197
- }, [skeleton]);
198
- return {
199
- bones,
200
- selectedBoneId,
201
- selectBone,
202
- setBonePosition,
203
- setBoneRotation,
204
- addBone,
205
- removeBone,
206
- savePose,
207
- loadPose,
208
- resetPose,
209
- savedPoses,
210
- skeletonHelper: helperRef.current,
211
- };
212
- }
213
- /* ── Component ── */
214
- const NiceArmatureEditor = ({ skeleton, skinnedMesh: _skinnedMesh, scene, camera: _camera, onBoneSelect, onPoseChange: _onPoseChange, className = '', tutorial, }) => {
215
- const { t } = useNiceTranslation();
216
- const { bones, selectedBoneId, selectBone, setBonePosition: _setBonePosition, setBoneRotation, addBone, removeBone, savePose, loadPose, resetPose, savedPoses, skeletonHelper: _skeletonHelper, } = useArmatureEditor(skeleton, scene, onBoneSelect);
217
- const [newBoneName, setNewBoneName] = useState('');
218
- const selectedBone = useMemo(() => bones.find((b) => b.id === selectedBoneId), [bones, selectedBoneId]);
219
- const rootBones = useMemo(() => bones.filter((b) => !b.parentId), [bones]);
220
- const renderBoneTree = (boneInfo, depth = 0) => {
221
- const isSelected = boneInfo.id === selectedBoneId;
222
- const childBones = bones.filter((b) => b.parentId === boneInfo.id);
223
- return (jsxs("div", { children: [jsxs("div", { onClick: () => selectBone(boneInfo.id), role: "button", tabIndex: 0, onKeyDown: (e) => e.key === 'Enter' && selectBone(boneInfo.id), style: {
224
- padding: '4px 8px',
225
- paddingLeft: depth * 16 + 8,
226
- background: isSelected ? 'var(--nice-color-accent, #6366f1)' : 'transparent',
227
- color: isSelected ? 'var(--nice-color-white, #fff)' : 'inherit',
228
- cursor: 'pointer',
229
- fontSize: 13,
230
- borderRadius: 4,
231
- }, children: [jsx(NiceIcon, { name: "target", size: 14 }), " ", boneInfo.name] }), childBones.map((child) => renderBoneTree(child, depth + 1))] }, boneInfo.id));
232
- };
233
- return (jsxs("div", { className: `nice-armature-editor ${className}`, style: { position: 'relative', display: 'flex', flexDirection: 'column', gap: 8 }, children: [jsx("span", { style: { position: 'absolute', top: 0, right: 0, zIndex: 20 }, children: jsx(NiceTutorialButton, { steps: resolveTutorial(tutorial, ARMATURE_EDITOR_TOUR) }) }), jsxs("div", { style: { display: 'flex', gap: 4 }, children: [jsxs("button", { type: "button", onClick: resetPose, title: t('armatureEditor.resetPose', 'Reset Pose'), children: [jsx(NiceIcon, { name: "refresh", size: 14 }), " ", t('armatureEditor.reset', 'Reset')] }), jsxs("button", { type: "button", onClick: () => savePose(`Pose ${savedPoses.length + 1}`), title: t('armatureEditor.savePose', 'Save Pose'), children: [jsx(NiceIcon, { name: "save", size: 14 }), " ", t('armatureEditor.savePose', 'Save Pose')] })] }), jsx("div", { style: {
234
- maxHeight: 200,
235
- overflowY: 'auto',
236
- border: '1px solid var(--nice-border, #e2e8f0)',
237
- borderRadius: 6,
238
- padding: 4,
239
- }, children: rootBones.length === 0 ? (jsx("div", { style: { padding: 16, textAlign: 'center', color: 'var(--nice-text-muted, #999)' }, children: t('armatureEditor.noSkeletonFound', 'No skeleton found') })) : (rootBones.map((b) => renderBoneTree(b))) }), selectedBoneId && (jsxs("div", { style: { display: 'flex', gap: 4 }, children: [jsx("input", { type: "text", value: newBoneName, onChange: (e) => setNewBoneName(e.target.value), placeholder: t('armatureEditor.newBoneName', 'New bone name'), style: {
240
- flex: 1,
241
- padding: '4px 8px',
242
- borderRadius: 4,
243
- border: '1px solid var(--nice-border, #e2e8f0)',
244
- } }), jsx("button", { type: "button", disabled: !newBoneName.trim(), onClick: () => {
245
- addBone(selectedBoneId, newBoneName.trim());
246
- setNewBoneName('');
247
- }, children: t('armatureEditor.add', '+ Add') }), jsx("button", { type: "button", onClick: () => removeBone(selectedBoneId), "aria-label": t('armatureEditor.removeBone', 'Remove bone'), children: jsx(NiceIcon, { name: "trash", size: 14 }) })] })), selectedBone && (jsxs("div", { style: {
248
- display: 'grid',
249
- gap: 4,
250
- fontSize: 12,
251
- border: '1px solid var(--nice-border, #e2e8f0)',
252
- borderRadius: 6,
253
- padding: 8,
254
- }, children: [jsx("strong", { children: selectedBone.name }), jsx("label", { children: t('armatureEditor.rotationX', 'Rotation X: {x}°').replace('{x}', String((selectedBone.localRotation.x * (180 / Math.PI)).toFixed(1))) }), jsx("input", { type: "range", min: -180, max: 180, value: selectedBone.localRotation.x * (180 / Math.PI), onChange: (e) => {
255
- const rot = selectedBone.localRotation.clone();
256
- rot.x = parseFloat(e.target.value) * (Math.PI / 180);
257
- setBoneRotation(selectedBone.id, rot);
258
- } }), jsx("label", { children: t('armatureEditor.rotationY', 'Rotation Y: {y}°').replace('{y}', String((selectedBone.localRotation.y * (180 / Math.PI)).toFixed(1))) }), jsx("input", { type: "range", min: -180, max: 180, value: selectedBone.localRotation.y * (180 / Math.PI), onChange: (e) => {
259
- const rot = selectedBone.localRotation.clone();
260
- rot.y = parseFloat(e.target.value) * (Math.PI / 180);
261
- setBoneRotation(selectedBone.id, rot);
262
- } }), jsx("label", { children: t('armatureEditor.rotationZ', 'Rotation Z: {z}°').replace('{z}', String((selectedBone.localRotation.z * (180 / Math.PI)).toFixed(1))) }), jsx("input", { type: "range", min: -180, max: 180, value: selectedBone.localRotation.z * (180 / Math.PI), onChange: (e) => {
263
- const rot = selectedBone.localRotation.clone();
264
- rot.z = parseFloat(e.target.value) * (Math.PI / 180);
265
- setBoneRotation(selectedBone.id, rot);
266
- } })] })), savedPoses.length > 0 && (jsxs("div", { style: { display: 'grid', gap: 4 }, children: [jsx("strong", { style: { fontSize: 12 }, children: t('armatureEditor.savedPoses', 'Saved Poses') }), savedPoses.map((pose) => (jsx("button", { type: "button", onClick: () => loadPose(pose), style: { textAlign: 'left', padding: '4px 8px', fontSize: 12 }, children: pose.name }, pose.id)))] }))] }));
360
+ ),
361
+ /* @__PURE__ */ jsx(
362
+ "button",
363
+ {
364
+ type: "button",
365
+ disabled: !newBoneName.trim(),
366
+ onClick: () => {
367
+ addBone(selectedBoneId, newBoneName.trim());
368
+ setNewBoneName("");
369
+ },
370
+ children: t("armatureEditor.add", "+ Add")
371
+ }
372
+ ),
373
+ /* @__PURE__ */ jsx(
374
+ "button",
375
+ {
376
+ type: "button",
377
+ onClick: () => removeBone(selectedBoneId),
378
+ "aria-label": t("armatureEditor.removeBone", "Remove bone"),
379
+ children: /* @__PURE__ */ jsx(NiceIcon, { name: "trash", size: 14 })
380
+ }
381
+ )
382
+ ] }),
383
+ selectedBone && /* @__PURE__ */ jsxs(
384
+ "div",
385
+ {
386
+ style: {
387
+ display: "grid",
388
+ gap: 4,
389
+ fontSize: 12,
390
+ border: "1px solid var(--nice-border, #e2e8f0)",
391
+ borderRadius: 6,
392
+ padding: 8
393
+ },
394
+ children: [
395
+ /* @__PURE__ */ jsx("strong", { children: selectedBone.name }),
396
+ /* @__PURE__ */ jsx("label", { children: t("armatureEditor.rotationX", "Rotation X: {x}°").replace(
397
+ "{x}",
398
+ String((selectedBone.localRotation.x * (180 / Math.PI)).toFixed(1))
399
+ ) }),
400
+ /* @__PURE__ */ jsx(
401
+ "input",
402
+ {
403
+ type: "range",
404
+ min: -180,
405
+ max: 180,
406
+ value: selectedBone.localRotation.x * (180 / Math.PI),
407
+ onChange: (e) => {
408
+ const rot = selectedBone.localRotation.clone();
409
+ rot.x = parseFloat(e.target.value) * (Math.PI / 180);
410
+ setBoneRotation(selectedBone.id, rot);
411
+ }
412
+ }
413
+ ),
414
+ /* @__PURE__ */ jsx("label", { children: t("armatureEditor.rotationY", "Rotation Y: {y}°").replace(
415
+ "{y}",
416
+ String((selectedBone.localRotation.y * (180 / Math.PI)).toFixed(1))
417
+ ) }),
418
+ /* @__PURE__ */ jsx(
419
+ "input",
420
+ {
421
+ type: "range",
422
+ min: -180,
423
+ max: 180,
424
+ value: selectedBone.localRotation.y * (180 / Math.PI),
425
+ onChange: (e) => {
426
+ const rot = selectedBone.localRotation.clone();
427
+ rot.y = parseFloat(e.target.value) * (Math.PI / 180);
428
+ setBoneRotation(selectedBone.id, rot);
429
+ }
430
+ }
431
+ ),
432
+ /* @__PURE__ */ jsx("label", { children: t("armatureEditor.rotationZ", "Rotation Z: {z}°").replace(
433
+ "{z}",
434
+ String((selectedBone.localRotation.z * (180 / Math.PI)).toFixed(1))
435
+ ) }),
436
+ /* @__PURE__ */ jsx(
437
+ "input",
438
+ {
439
+ type: "range",
440
+ min: -180,
441
+ max: 180,
442
+ value: selectedBone.localRotation.z * (180 / Math.PI),
443
+ onChange: (e) => {
444
+ const rot = selectedBone.localRotation.clone();
445
+ rot.z = parseFloat(e.target.value) * (Math.PI / 180);
446
+ setBoneRotation(selectedBone.id, rot);
447
+ }
448
+ }
449
+ )
450
+ ]
451
+ }
452
+ ),
453
+ savedPoses.length > 0 && /* @__PURE__ */ jsxs("div", { style: { display: "grid", gap: 4 }, children: [
454
+ /* @__PURE__ */ jsx("strong", { style: { fontSize: 12 }, children: t("armatureEditor.savedPoses", "Saved Poses") }),
455
+ savedPoses.map((pose) => /* @__PURE__ */ jsx(
456
+ "button",
457
+ {
458
+ type: "button",
459
+ onClick: () => loadPose(pose),
460
+ style: { textAlign: "left", padding: "4px 8px", fontSize: 12 },
461
+ children: pose.name
462
+ },
463
+ pose.id
464
+ ))
465
+ ] })
466
+ ]
467
+ }
468
+ );
469
+ };
470
+ export {
471
+ NiceArmatureEditor,
472
+ useArmatureEditor
267
473
  };
268
-
269
- export { NiceArmatureEditor, useArmatureEditor };