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