@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,284 +1,407 @@
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
- /* ── Weight painting utilities ── */
24
+ const THREE__namespace = /* @__PURE__ */ _interopNamespaceDefault(THREE);
29
25
  function getFalloff(distance, radius, type) {
30
- const t = Math.min(distance / radius, 1);
31
- switch (type) {
32
- case 'constant':
33
- return 1;
34
- case 'linear':
35
- return 1 - t;
36
- case 'smooth':
37
- return 1 - t * t * (3 - 2 * t); // smoothstep
38
- case 'sharp':
39
- return Math.pow(1 - t, 3);
40
- default:
41
- return 1 - t;
42
- }
26
+ const t = Math.min(distance / radius, 1);
27
+ switch (type) {
28
+ case "constant":
29
+ return 1;
30
+ case "linear":
31
+ return 1 - t;
32
+ case "smooth":
33
+ return 1 - t * t * (3 - 2 * t);
34
+ // smoothstep
35
+ case "sharp":
36
+ return Math.pow(1 - t, 3);
37
+ default:
38
+ return 1 - t;
39
+ }
43
40
  }
44
- /* ── Hook ── */
45
41
  function useWeightPainter(skinnedMesh, scene, camera, canvas, onWeightsChange) {
46
- var _a, _b;
47
- const [brush, setBrushState] = React.useState({
48
- radius: 0.5,
49
- strength: 0.5,
50
- falloff: 'smooth',
51
- mode: 'add',
52
- });
53
- const [isPainting, setIsPainting] = React.useState(false);
54
- const [activeBoneIndex, setActiveBoneIndex] = React.useState(0);
55
- const [showHeatMap, setShowHeatMap] = React.useState(true);
56
- const raycaster = React.useRef(new THREE__namespace.Raycaster());
57
- const heatMapMaterial = React.useRef(null);
58
- const originalMaterial = React.useRef(null);
59
- // Get bone names from skeleton
60
- const boneNames = (_b = (_a = skinnedMesh === null || skinnedMesh === void 0 ? void 0 : skinnedMesh.skeleton) === null || _a === void 0 ? void 0 : _a.bones.map((b, i) => b.name || `Bone ${i}`)) !== null && _b !== void 0 ? _b : [];
61
- // Create heat map overlay material
62
- React.useEffect(() => {
63
- if (!skinnedMesh || !showHeatMap) {
64
- return;
42
+ const [brush, setBrushState] = React.useState({
43
+ radius: 0.5,
44
+ strength: 0.5,
45
+ falloff: "smooth",
46
+ mode: "add"
47
+ });
48
+ const [isPainting, setIsPainting] = React.useState(false);
49
+ const [activeBoneIndex, setActiveBoneIndex] = React.useState(0);
50
+ const [showHeatMap, setShowHeatMap] = React.useState(true);
51
+ const raycaster = React.useRef(new THREE__namespace.Raycaster());
52
+ const heatMapMaterial = React.useRef(null);
53
+ const originalMaterial = React.useRef(null);
54
+ const boneNames = skinnedMesh?.skeleton?.bones.map((b, i) => b.name || `Bone ${i}`) ?? [];
55
+ React.useEffect(() => {
56
+ if (!skinnedMesh || !showHeatMap) {
57
+ return;
58
+ }
59
+ originalMaterial.current = skinnedMesh.material;
60
+ const weightColors = new Float32Array(skinnedMesh.geometry.attributes.position.count * 3);
61
+ updateHeatMapColors(skinnedMesh, activeBoneIndex, weightColors);
62
+ skinnedMesh.geometry.setAttribute("weightColor", new THREE__namespace.BufferAttribute(weightColors, 3));
63
+ const mat = new THREE__namespace.ShaderMaterial({
64
+ vertexShader: `
65
+ attribute vec3 weightColor;
66
+ varying vec3 vColor;
67
+ void main() {
68
+ vColor = weightColor;
69
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
65
70
  }
66
- originalMaterial.current = skinnedMesh.material;
67
- const weightColors = new Float32Array(skinnedMesh.geometry.attributes.position.count * 3);
68
- updateHeatMapColors(skinnedMesh, activeBoneIndex, weightColors);
69
- skinnedMesh.geometry.setAttribute('weightColor', new THREE__namespace.BufferAttribute(weightColors, 3));
70
- const mat = new THREE__namespace.ShaderMaterial({
71
- vertexShader: `
72
- attribute vec3 weightColor;
73
- varying vec3 vColor;
74
- void main() {
75
- vColor = weightColor;
76
- gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
77
- }
78
- `,
79
- fragmentShader: `
80
- varying vec3 vColor;
81
- void main() {
82
- gl_FragColor = vec4(vColor, 1.0);
83
- }
84
71
  `,
85
- side: THREE__namespace.DoubleSide,
86
- });
87
- heatMapMaterial.current = mat;
88
- skinnedMesh.material = mat;
89
- return () => {
90
- if (originalMaterial.current) {
91
- skinnedMesh.material = originalMaterial.current;
92
- }
93
- skinnedMesh.geometry.deleteAttribute('weightColor');
94
- mat.dispose();
95
- };
96
- }, [skinnedMesh, showHeatMap, activeBoneIndex]);
97
- const setBrush = React.useCallback((partial) => {
98
- setBrushState((prev) => ({ ...prev, ...partial }));
99
- }, []);
100
- const paintAtPoint = React.useCallback((point) => {
101
- if (!skinnedMesh) {
102
- return;
103
- }
104
- const geometry = skinnedMesh.geometry;
105
- const posAttr = geometry.attributes.position;
106
- const skinWeightAttr = geometry.attributes.skinWeight;
107
- const skinIndexAttr = geometry.attributes.skinIndex;
108
- if (!skinWeightAttr || !skinIndexAttr) {
109
- return;
72
+ fragmentShader: `
73
+ varying vec3 vColor;
74
+ void main() {
75
+ gl_FragColor = vec4(vColor, 1.0);
110
76
  }
111
- const vertex = new THREE__namespace.Vector3();
112
- for (let i = 0; i < posAttr.count; i++) {
113
- vertex.fromBufferAttribute(posAttr, i);
114
- skinnedMesh.localToWorld(vertex);
115
- const dist = vertex.distanceTo(point);
116
- if (dist > brush.radius) {
117
- continue;
118
- }
119
- const influence = getFalloff(dist, brush.radius, brush.falloff) * brush.strength;
120
- // Get current skin weights for this vertex
121
- const weights = new THREE__namespace.Vector4().fromBufferAttribute(skinWeightAttr, i);
122
- const indices = new THREE__namespace.Vector4().fromBufferAttribute(skinIndexAttr, i);
123
- // Find which component (x/y/z/w) has our bone index
124
- const components = [indices.x, indices.y, indices.z, indices.w];
125
- let slot = components.indexOf(activeBoneIndex);
126
- // If bone not assigned to this vertex, find an empty or lowest-weight slot
127
- if (slot === -1) {
128
- const weightArr = [weights.x, weights.y, weights.z, weights.w];
129
- slot = weightArr.indexOf(Math.min(...weightArr));
130
- // Set the bone index in that slot
131
- const compKey = ['x', 'y', 'z', 'w'][slot];
132
- indices[compKey] = activeBoneIndex;
133
- skinIndexAttr.setXYZW(i, indices.x, indices.y, indices.z, indices.w);
134
- }
135
- const compKey = ['x', 'y', 'z', 'w'][slot];
136
- let currentWeight = weights[compKey];
137
- switch (brush.mode) {
138
- case 'add':
139
- currentWeight = Math.min(1, currentWeight + influence);
140
- break;
141
- case 'subtract':
142
- currentWeight = Math.max(0, currentWeight - influence);
143
- break;
144
- case 'smooth': {
145
- // Average with neighbors — simplified: move toward 0.5
146
- currentWeight = currentWeight + (0.5 - currentWeight) * influence;
147
- break;
148
- }
149
- case 'blur': {
150
- currentWeight = currentWeight * (1 - influence * 0.5);
151
- break;
152
- }
153
- }
154
- weights[compKey] = currentWeight;
155
- // Normalize weights so they sum to 1
156
- const total = weights.x + weights.y + weights.z + weights.w;
157
- if (total > 0) {
158
- weights.divideScalar(total);
159
- }
160
- skinWeightAttr.setXYZW(i, weights.x, weights.y, weights.z, weights.w);
161
- }
162
- skinWeightAttr.needsUpdate = true;
163
- skinIndexAttr.needsUpdate = true;
164
- // Update heat map if shown
165
- if (showHeatMap && heatMapMaterial.current) {
166
- const colorAttr = geometry.attributes.weightColor;
167
- if (colorAttr) {
168
- const colors = new Float32Array(posAttr.count * 3);
169
- updateHeatMapColors(skinnedMesh, activeBoneIndex, colors);
170
- colorAttr.set(colors);
171
- colorAttr.needsUpdate = true;
172
- }
77
+ `,
78
+ side: THREE__namespace.DoubleSide
79
+ });
80
+ heatMapMaterial.current = mat;
81
+ skinnedMesh.material = mat;
82
+ return () => {
83
+ if (originalMaterial.current) {
84
+ skinnedMesh.material = originalMaterial.current;
85
+ }
86
+ skinnedMesh.geometry.deleteAttribute("weightColor");
87
+ mat.dispose();
88
+ };
89
+ }, [skinnedMesh, showHeatMap, activeBoneIndex]);
90
+ const setBrush = React.useCallback((partial) => {
91
+ setBrushState((prev) => ({ ...prev, ...partial }));
92
+ }, []);
93
+ const paintAtPoint = React.useCallback(
94
+ (point) => {
95
+ if (!skinnedMesh) {
96
+ return;
97
+ }
98
+ const geometry = skinnedMesh.geometry;
99
+ const posAttr = geometry.attributes.position;
100
+ const skinWeightAttr = geometry.attributes.skinWeight;
101
+ const skinIndexAttr = geometry.attributes.skinIndex;
102
+ if (!skinWeightAttr || !skinIndexAttr) {
103
+ return;
104
+ }
105
+ const vertex = new THREE__namespace.Vector3();
106
+ for (let i = 0; i < posAttr.count; i++) {
107
+ vertex.fromBufferAttribute(posAttr, i);
108
+ skinnedMesh.localToWorld(vertex);
109
+ const dist = vertex.distanceTo(point);
110
+ if (dist > brush.radius) {
111
+ continue;
173
112
  }
174
- onWeightsChange === null || onWeightsChange === void 0 ? void 0 : onWeightsChange(skinnedMesh);
175
- }, [skinnedMesh, brush, activeBoneIndex, showHeatMap, onWeightsChange]);
176
- // Mouse event handlers
177
- React.useEffect(() => {
178
- if (!canvas || !camera || !isPainting || !skinnedMesh) {
179
- return;
113
+ const influence = getFalloff(dist, brush.radius, brush.falloff) * brush.strength;
114
+ const weights = new THREE__namespace.Vector4().fromBufferAttribute(skinWeightAttr, i);
115
+ const indices = new THREE__namespace.Vector4().fromBufferAttribute(skinIndexAttr, i);
116
+ const components = [indices.x, indices.y, indices.z, indices.w];
117
+ let slot = components.indexOf(activeBoneIndex);
118
+ if (slot === -1) {
119
+ const weightArr = [weights.x, weights.y, weights.z, weights.w];
120
+ slot = weightArr.indexOf(Math.min(...weightArr));
121
+ const compKey2 = ["x", "y", "z", "w"][slot];
122
+ indices[compKey2] = activeBoneIndex;
123
+ skinIndexAttr.setXYZW(i, indices.x, indices.y, indices.z, indices.w);
180
124
  }
181
- const onMouseMove = (e) => {
182
- const rect = canvas.getBoundingClientRect();
183
- const mouse = new THREE__namespace.Vector2(((e.clientX - rect.left) / rect.width) * 2 - 1, -((e.clientY - rect.top) / rect.height) * 2 + 1);
184
- raycaster.current.setFromCamera(mouse, camera);
185
- const intersects = raycaster.current.intersectObject(skinnedMesh);
186
- if (intersects.length > 0) {
187
- paintAtPoint(intersects[0].point);
188
- }
189
- };
190
- canvas.addEventListener('mousemove', onMouseMove);
191
- return () => canvas.removeEventListener('mousemove', onMouseMove);
192
- }, [canvas, camera, isPainting, skinnedMesh, paintAtPoint]);
193
- const normalizeWeights = React.useCallback(() => {
194
- if (!skinnedMesh) {
195
- return;
125
+ const compKey = ["x", "y", "z", "w"][slot];
126
+ let currentWeight = weights[compKey];
127
+ switch (brush.mode) {
128
+ case "add":
129
+ currentWeight = Math.min(1, currentWeight + influence);
130
+ break;
131
+ case "subtract":
132
+ currentWeight = Math.max(0, currentWeight - influence);
133
+ break;
134
+ case "smooth": {
135
+ currentWeight = currentWeight + (0.5 - currentWeight) * influence;
136
+ break;
137
+ }
138
+ case "blur": {
139
+ currentWeight = currentWeight * (1 - influence * 0.5);
140
+ break;
141
+ }
196
142
  }
197
- const skinWeightAttr = skinnedMesh.geometry.attributes.skinWeight;
198
- if (!skinWeightAttr) {
199
- return;
143
+ weights[compKey] = currentWeight;
144
+ const total = weights.x + weights.y + weights.z + weights.w;
145
+ if (total > 0) {
146
+ weights.divideScalar(total);
200
147
  }
201
- for (let i = 0; i < skinWeightAttr.count; i++) {
202
- const w = new THREE__namespace.Vector4().fromBufferAttribute(skinWeightAttr, i);
203
- const total = w.x + w.y + w.z + w.w;
204
- if (total > 0) {
205
- w.divideScalar(total);
206
- skinWeightAttr.setXYZW(i, w.x, w.y, w.z, w.w);
207
- }
148
+ skinWeightAttr.setXYZW(i, weights.x, weights.y, weights.z, weights.w);
149
+ }
150
+ skinWeightAttr.needsUpdate = true;
151
+ skinIndexAttr.needsUpdate = true;
152
+ if (showHeatMap && heatMapMaterial.current) {
153
+ const colorAttr = geometry.attributes.weightColor;
154
+ if (colorAttr) {
155
+ const colors = new Float32Array(posAttr.count * 3);
156
+ updateHeatMapColors(skinnedMesh, activeBoneIndex, colors);
157
+ colorAttr.set(colors);
158
+ colorAttr.needsUpdate = true;
208
159
  }
209
- skinWeightAttr.needsUpdate = true;
210
- }, [skinnedMesh]);
211
- return {
212
- brush,
213
- setBrush,
214
- isPainting,
215
- activeBoneIndex,
216
- setActiveBoneIndex,
217
- boneNames,
218
- startPainting: () => setIsPainting(true),
219
- stopPainting: () => setIsPainting(false),
220
- normalizeWeights,
221
- showHeatMap,
222
- setShowHeatMap,
160
+ }
161
+ onWeightsChange?.(skinnedMesh);
162
+ },
163
+ [skinnedMesh, brush, activeBoneIndex, showHeatMap, onWeightsChange]
164
+ );
165
+ React.useEffect(() => {
166
+ if (!canvas || !camera || !isPainting || !skinnedMesh) {
167
+ return;
168
+ }
169
+ const onMouseMove = (e) => {
170
+ const rect = canvas.getBoundingClientRect();
171
+ const mouse = new THREE__namespace.Vector2(
172
+ (e.clientX - rect.left) / rect.width * 2 - 1,
173
+ -((e.clientY - rect.top) / rect.height) * 2 + 1
174
+ );
175
+ raycaster.current.setFromCamera(mouse, camera);
176
+ const intersects = raycaster.current.intersectObject(skinnedMesh);
177
+ if (intersects.length > 0) {
178
+ paintAtPoint(intersects[0].point);
179
+ }
223
180
  };
224
- }
225
- /* ── Helpers ── */
226
- function updateHeatMapColors(mesh, boneIndex, colors) {
227
- const skinWeightAttr = mesh.geometry.attributes.skinWeight;
228
- const skinIndexAttr = mesh.geometry.attributes.skinIndex;
229
- if (!skinWeightAttr || !skinIndexAttr) {
230
- return;
181
+ canvas.addEventListener("mousemove", onMouseMove);
182
+ return () => canvas.removeEventListener("mousemove", onMouseMove);
183
+ }, [canvas, camera, isPainting, skinnedMesh, paintAtPoint]);
184
+ const normalizeWeights = React.useCallback(() => {
185
+ if (!skinnedMesh) {
186
+ return;
187
+ }
188
+ const skinWeightAttr = skinnedMesh.geometry.attributes.skinWeight;
189
+ if (!skinWeightAttr) {
190
+ return;
231
191
  }
232
192
  for (let i = 0; i < skinWeightAttr.count; i++) {
233
- const indices = new THREE__namespace.Vector4().fromBufferAttribute(skinIndexAttr, i);
234
- const weights = new THREE__namespace.Vector4().fromBufferAttribute(skinWeightAttr, i);
235
- const components = [indices.x, indices.y, indices.z, indices.w];
236
- const weightArr = [weights.x, weights.y, weights.z, weights.w];
237
- const slot = components.indexOf(boneIndex);
238
- const weight = slot >= 0 ? weightArr[slot] : 0;
239
- // Blue → Green → Red heat map
240
- const r = Math.min(1, weight * 2);
241
- const g = weight < 0.5 ? weight * 2 : 2 - weight * 2;
242
- const b = Math.max(0, 1 - weight * 2);
243
- colors[i * 3] = r;
244
- colors[i * 3 + 1] = g;
245
- colors[i * 3 + 2] = b;
193
+ const w = new THREE__namespace.Vector4().fromBufferAttribute(skinWeightAttr, i);
194
+ const total = w.x + w.y + w.z + w.w;
195
+ if (total > 0) {
196
+ w.divideScalar(total);
197
+ skinWeightAttr.setXYZW(i, w.x, w.y, w.z, w.w);
198
+ }
246
199
  }
200
+ skinWeightAttr.needsUpdate = true;
201
+ }, [skinnedMesh]);
202
+ return {
203
+ brush,
204
+ setBrush,
205
+ isPainting,
206
+ activeBoneIndex,
207
+ setActiveBoneIndex,
208
+ boneNames,
209
+ startPainting: () => setIsPainting(true),
210
+ stopPainting: () => setIsPainting(false),
211
+ normalizeWeights,
212
+ showHeatMap,
213
+ setShowHeatMap
214
+ };
215
+ }
216
+ function updateHeatMapColors(mesh, boneIndex, colors) {
217
+ const skinWeightAttr = mesh.geometry.attributes.skinWeight;
218
+ const skinIndexAttr = mesh.geometry.attributes.skinIndex;
219
+ if (!skinWeightAttr || !skinIndexAttr) {
220
+ return;
221
+ }
222
+ for (let i = 0; i < skinWeightAttr.count; i++) {
223
+ const indices = new THREE__namespace.Vector4().fromBufferAttribute(skinIndexAttr, i);
224
+ const weights = new THREE__namespace.Vector4().fromBufferAttribute(skinWeightAttr, i);
225
+ const components = [indices.x, indices.y, indices.z, indices.w];
226
+ const weightArr = [weights.x, weights.y, weights.z, weights.w];
227
+ const slot = components.indexOf(boneIndex);
228
+ const weight = slot >= 0 ? weightArr[slot] : 0;
229
+ const r = Math.min(1, weight * 2);
230
+ const g = weight < 0.5 ? weight * 2 : 2 - weight * 2;
231
+ const b = Math.max(0, 1 - weight * 2);
232
+ colors[i * 3] = r;
233
+ colors[i * 3 + 1] = g;
234
+ colors[i * 3 + 2] = b;
235
+ }
247
236
  }
248
- /* ── Component ── */
249
- const NiceWeightPainter = ({ skinnedMesh, activeBoneIndex: initialBoneIndex, scene, camera, canvas, onWeightsChange, className = '', }) => {
250
- const { t } = i18n.useNiceTranslation();
251
- const { brush, setBrush, isPainting, activeBoneIndex, setActiveBoneIndex, boneNames, startPainting, stopPainting, normalizeWeights, showHeatMap, setShowHeatMap, } = useWeightPainter(skinnedMesh, scene, camera, canvas, onWeightsChange);
252
- React.useEffect(() => {
253
- setActiveBoneIndex(initialBoneIndex);
254
- }, [initialBoneIndex, setActiveBoneIndex]);
255
- return (jsxRuntime.jsxs("div", { className: `nice-weight-painter ${className}`, style: { display: 'grid', gap: 8, fontSize: 13 }, children: [jsxRuntime.jsxs("label", { children: [t('weightPainter.bone', 'Bone:'), jsxRuntime.jsx("select", { value: activeBoneIndex, onChange: (e) => setActiveBoneIndex(parseInt(e.target.value, 10)), style: { marginLeft: 8, width: '100%' }, children: boneNames.map((name, i) => (jsxRuntime.jsx("option", { value: i, children: name }, i))) })] }), jsxRuntime.jsxs("fieldset", { style: {
256
- display: 'flex',
257
- gap: 4,
258
- border: '1px solid var(--nice-border, #e2e8f0)',
259
- borderRadius: 6,
260
- padding: 4,
261
- }, children: [jsxRuntime.jsx("legend", { style: { fontSize: 11 }, children: t('weightPainter.mode', 'Mode') }), ['add', 'subtract', 'smooth', 'blur'].map((mode) => (jsxRuntime.jsx("button", { type: "button", onClick: () => setBrush({ mode }), style: {
262
- flex: 1,
263
- padding: '4px 6px',
264
- background: brush.mode === mode ? 'var(--nice-color-accent, #6366f1)' : 'transparent',
265
- color: brush.mode === mode ? 'var(--nice-color-white, #fff)' : 'inherit',
266
- border: '1px solid var(--nice-border, #e2e8f0)',
267
- borderRadius: 4,
268
- cursor: 'pointer',
269
- fontSize: 11,
270
- }, children: mode }, mode)))] }), jsxRuntime.jsxs("label", { children: [t('weightPainter.radius', 'Radius: {radius}').replace('{radius}', String(brush.radius.toFixed(2))), jsxRuntime.jsx("input", { type: "range", min: 0.05, max: 2, step: 0.05, value: brush.radius, onChange: (e) => setBrush({ radius: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsxRuntime.jsxs("label", { children: [t('weightPainter.strength', 'Strength: {strength}').replace('{strength}', String(brush.strength.toFixed(2))), jsxRuntime.jsx("input", { type: "range", min: 0.01, max: 1, step: 0.01, value: brush.strength, onChange: (e) => setBrush({ strength: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsxRuntime.jsxs("div", { style: { display: 'flex', gap: 4 }, children: [jsxRuntime.jsx("button", { type: "button", onClick: isPainting ? stopPainting : startPainting, style: {
271
- flex: 1,
272
- background: isPainting
273
- ? 'var(--nice-danger, #ef4444)'
274
- : 'var(--nice-color-accent, #6366f1)',
275
- color: 'var(--nice-color-white, #fff)',
276
- border: 'none',
277
- borderRadius: 4,
278
- padding: '6px 12px',
279
- cursor: 'pointer',
280
- }, children: isPainting ? (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(ui.NiceIcon, { name: "stop", size: 14 }), " ", t('weightPainter.stopPainting', 'Stop Painting')] })) : (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(ui.NiceIcon, { name: "brush", size: 14 }), ' ', t('weightPainter.startPainting', 'Start Painting')] })) }), jsxRuntime.jsxs("button", { type: "button", onClick: normalizeWeights, title: t('weightPainter.normalizeAllWeights', 'Normalize all weights'), children: [jsxRuntime.jsx(ui.NiceIcon, { name: "scale", size: 14 }), " ", t('weightPainter.normalize', 'Normalize')] })] }), jsxRuntime.jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 6 }, children: [jsxRuntime.jsx("input", { type: "checkbox", checked: showHeatMap, onChange: (e) => setShowHeatMap(e.target.checked) }), t('weightPainter.showWeightHeatMap', 'Show weight heat map')] })] }));
237
+ const NiceWeightPainter = ({
238
+ skinnedMesh,
239
+ activeBoneIndex: initialBoneIndex,
240
+ scene,
241
+ camera,
242
+ canvas,
243
+ onWeightsChange,
244
+ className = ""
245
+ }) => {
246
+ const { t } = i18n.useNiceTranslation();
247
+ const {
248
+ brush,
249
+ setBrush,
250
+ isPainting,
251
+ activeBoneIndex,
252
+ setActiveBoneIndex,
253
+ boneNames,
254
+ startPainting,
255
+ stopPainting,
256
+ normalizeWeights,
257
+ showHeatMap,
258
+ setShowHeatMap
259
+ } = useWeightPainter(skinnedMesh, scene, camera, canvas, onWeightsChange);
260
+ React.useEffect(() => {
261
+ setActiveBoneIndex(initialBoneIndex);
262
+ }, [initialBoneIndex, setActiveBoneIndex]);
263
+ return /* @__PURE__ */ jsxRuntime.jsxs(
264
+ "div",
265
+ {
266
+ className: `nice-weight-painter ${className}`,
267
+ style: { display: "grid", gap: 8, fontSize: 13 },
268
+ children: [
269
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
270
+ t("weightPainter.bone", "Bone:"),
271
+ /* @__PURE__ */ jsxRuntime.jsx(
272
+ "select",
273
+ {
274
+ value: activeBoneIndex,
275
+ onChange: (e) => setActiveBoneIndex(parseInt(e.target.value, 10)),
276
+ style: { marginLeft: 8, width: "100%" },
277
+ children: boneNames.map((name, i) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: i, children: name }, i))
278
+ }
279
+ )
280
+ ] }),
281
+ /* @__PURE__ */ jsxRuntime.jsxs(
282
+ "fieldset",
283
+ {
284
+ style: {
285
+ display: "flex",
286
+ gap: 4,
287
+ border: "1px solid var(--nice-border, #e2e8f0)",
288
+ borderRadius: 6,
289
+ padding: 4
290
+ },
291
+ children: [
292
+ /* @__PURE__ */ jsxRuntime.jsx("legend", { style: { fontSize: 11 }, children: t("weightPainter.mode", "Mode") }),
293
+ ["add", "subtract", "smooth", "blur"].map((mode) => /* @__PURE__ */ jsxRuntime.jsx(
294
+ "button",
295
+ {
296
+ type: "button",
297
+ onClick: () => setBrush({ mode }),
298
+ style: {
299
+ flex: 1,
300
+ padding: "4px 6px",
301
+ background: brush.mode === mode ? "var(--nice-color-accent, #6366f1)" : "transparent",
302
+ color: brush.mode === mode ? "var(--nice-color-white, #fff)" : "inherit",
303
+ border: "1px solid var(--nice-border, #e2e8f0)",
304
+ borderRadius: 4,
305
+ cursor: "pointer",
306
+ fontSize: 11
307
+ },
308
+ children: mode
309
+ },
310
+ mode
311
+ ))
312
+ ]
313
+ }
314
+ ),
315
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
316
+ t("weightPainter.radius", "Radius: {radius}").replace(
317
+ "{radius}",
318
+ String(brush.radius.toFixed(2))
319
+ ),
320
+ /* @__PURE__ */ jsxRuntime.jsx(
321
+ "input",
322
+ {
323
+ type: "range",
324
+ min: 0.05,
325
+ max: 2,
326
+ step: 0.05,
327
+ value: brush.radius,
328
+ onChange: (e) => setBrush({ radius: parseFloat(e.target.value) }),
329
+ style: { width: "100%" }
330
+ }
331
+ )
332
+ ] }),
333
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
334
+ t("weightPainter.strength", "Strength: {strength}").replace(
335
+ "{strength}",
336
+ String(brush.strength.toFixed(2))
337
+ ),
338
+ /* @__PURE__ */ jsxRuntime.jsx(
339
+ "input",
340
+ {
341
+ type: "range",
342
+ min: 0.01,
343
+ max: 1,
344
+ step: 0.01,
345
+ value: brush.strength,
346
+ onChange: (e) => setBrush({ strength: parseFloat(e.target.value) }),
347
+ style: { width: "100%" }
348
+ }
349
+ )
350
+ ] }),
351
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", gap: 4 }, children: [
352
+ /* @__PURE__ */ jsxRuntime.jsx(
353
+ "button",
354
+ {
355
+ type: "button",
356
+ onClick: isPainting ? stopPainting : startPainting,
357
+ style: {
358
+ flex: 1,
359
+ background: isPainting ? "var(--nice-danger, #ef4444)" : "var(--nice-color-accent, #6366f1)",
360
+ color: "var(--nice-color-white, #fff)",
361
+ border: "none",
362
+ borderRadius: 4,
363
+ padding: "6px 12px",
364
+ cursor: "pointer"
365
+ },
366
+ children: isPainting ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
367
+ /* @__PURE__ */ jsxRuntime.jsx(ui.NiceIcon, { name: "stop", size: 14 }),
368
+ " ",
369
+ t("weightPainter.stopPainting", "Stop Painting")
370
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
371
+ /* @__PURE__ */ jsxRuntime.jsx(ui.NiceIcon, { name: "brush", size: 14 }),
372
+ " ",
373
+ t("weightPainter.startPainting", "Start Painting")
374
+ ] })
375
+ }
376
+ ),
377
+ /* @__PURE__ */ jsxRuntime.jsxs(
378
+ "button",
379
+ {
380
+ type: "button",
381
+ onClick: normalizeWeights,
382
+ title: t("weightPainter.normalizeAllWeights", "Normalize all weights"),
383
+ children: [
384
+ /* @__PURE__ */ jsxRuntime.jsx(ui.NiceIcon, { name: "scale", size: 14 }),
385
+ " ",
386
+ t("weightPainter.normalize", "Normalize")
387
+ ]
388
+ }
389
+ )
390
+ ] }),
391
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { style: { display: "flex", alignItems: "center", gap: 6 }, children: [
392
+ /* @__PURE__ */ jsxRuntime.jsx(
393
+ "input",
394
+ {
395
+ type: "checkbox",
396
+ checked: showHeatMap,
397
+ onChange: (e) => setShowHeatMap(e.target.checked)
398
+ }
399
+ ),
400
+ t("weightPainter.showWeightHeatMap", "Show weight heat map")
401
+ ] })
402
+ ]
403
+ }
404
+ );
281
405
  };
282
-
283
406
  exports.NiceWeightPainter = NiceWeightPainter;
284
407
  exports.useWeightPainter = useWeightPainter;