@nice2dev/ui-3d 1.2.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +103 -0
- package/dist/cjs/core/editorAsk.js +48 -0
- package/dist/cjs/core/i18n.js +7 -3
- package/dist/cjs/dance/DanceScoreEngine.js +7 -0
- package/dist/cjs/dance/PoseDetector.js +4 -2
- package/dist/cjs/material/NiceMaterialEditor.js +7 -3
- package/dist/cjs/material/materialEditorTypes.js +1 -1
- package/dist/cjs/material/materialNodeDefinitions.js +14 -11
- package/dist/cjs/model/ModelEditor.js +3 -2
- package/dist/cjs/model/ModelEditorLeftPanel.js +10 -4
- package/dist/cjs/model/ModelEditorMenuBar.js +7 -5
- package/dist/cjs/model/ModelEditorRightPanel.js +18 -16
- package/dist/cjs/model/ModelEditorSubComponents.js +8 -6
- package/dist/cjs/model/ModelEditorTimeline.js +6 -4
- package/dist/cjs/model/ModelEditorToolbar.js +6 -4
- package/dist/cjs/model/ModelEditorViewport.js +16 -6
- package/dist/cjs/model/ModelViewer.js +6 -4
- package/dist/cjs/model/NiceArmatureEditor.js +6 -4
- package/dist/cjs/model/NiceMorphTargetEditor.js +5 -3
- package/dist/cjs/model/NiceOctree.js +3 -1
- package/dist/cjs/model/NicePhysicsSimulation.js +6 -4
- package/dist/cjs/model/NiceProceduralGeometry.js +5 -3
- package/dist/cjs/model/NiceTerrainEditor.js +8 -6
- package/dist/cjs/model/NiceWeightPainter.js +4 -2
- package/dist/cjs/model/NiceXRPreview.js +4 -2
- package/dist/cjs/model/useModelEditor.js +22 -15
- package/dist/cjs/nice2dev-ui-3d.css +1 -1
- package/dist/cjs/particle/NiceParticleEditor.js +14 -9
- package/dist/cjs/rendering/NiceCascadedShadows.js +3 -1
- package/dist/cjs/rendering/NiceRenderExport.js +9 -3
- package/dist/cjs/rendering/NiceSSAO.js +3 -1
- package/dist/cjs/rendering/NiceSSR.js +3 -1
- package/dist/cjs/rendering/NiceWebGPURenderer.js +4 -2
- package/dist/cjs/uv/NiceUVEditor.js +16 -12
- package/dist/cjs/uv/uvEditorTypes.js +1 -0
- package/dist/esm/core/editorAsk.js +45 -0
- package/dist/esm/core/i18n.js +8 -4
- package/dist/esm/dance/DanceScoreEngine.js +7 -0
- package/dist/esm/dance/PoseDetector.js +4 -2
- package/dist/esm/material/NiceMaterialEditor.js +7 -3
- package/dist/esm/material/materialEditorTypes.js +1 -1
- package/dist/esm/material/materialNodeDefinitions.js +14 -11
- package/dist/esm/model/ModelEditor.js +3 -2
- package/dist/esm/model/ModelEditorLeftPanel.js +10 -4
- package/dist/esm/model/ModelEditorMenuBar.js +7 -5
- package/dist/esm/model/ModelEditorRightPanel.js +18 -16
- package/dist/esm/model/ModelEditorSubComponents.js +8 -6
- package/dist/esm/model/ModelEditorTimeline.js +6 -4
- package/dist/esm/model/ModelEditorToolbar.js +6 -4
- package/dist/esm/model/ModelEditorViewport.js +16 -6
- package/dist/esm/model/ModelViewer.js +6 -4
- package/dist/esm/model/NiceArmatureEditor.js +6 -4
- package/dist/esm/model/NiceMorphTargetEditor.js +5 -3
- package/dist/esm/model/NiceOctree.js +3 -1
- package/dist/esm/model/NicePhysicsSimulation.js +6 -4
- package/dist/esm/model/NiceProceduralGeometry.js +5 -3
- package/dist/esm/model/NiceTerrainEditor.js +8 -6
- package/dist/esm/model/NiceWeightPainter.js +4 -2
- package/dist/esm/model/NiceXRPreview.js +4 -2
- package/dist/esm/model/useModelEditor.js +22 -15
- package/dist/esm/nice2dev-ui-3d.css +1 -1
- package/dist/esm/particle/NiceParticleEditor.js +14 -9
- package/dist/esm/rendering/NiceCascadedShadows.js +3 -1
- package/dist/esm/rendering/NiceRenderExport.js +9 -3
- package/dist/esm/rendering/NiceSSAO.js +3 -1
- package/dist/esm/rendering/NiceSSR.js +3 -1
- package/dist/esm/rendering/NiceWebGPURenderer.js +4 -2
- package/dist/esm/uv/NiceUVEditor.js +16 -12
- package/dist/esm/uv/uvEditorTypes.js +1 -0
- package/dist/types/core/editorAsk.d.ts +26 -0
- package/dist/types/core/i18n.d.ts +1 -17
- package/dist/types/dance/DanceScoreEngine.d.ts +0 -6
- package/dist/types/model/useModelEditor.d.ts +3 -0
- package/package.json +3 -3
|
@@ -2,11 +2,14 @@ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
|
2
2
|
import { NiceButton, NiceIcon, NiceTextInput, NiceColorPicker, NiceSlider, NiceCheckbox, NiceNumberInput } from '@nice2dev/ui';
|
|
3
3
|
import React from 'react';
|
|
4
4
|
import * as THREE from 'three';
|
|
5
|
+
import { useNiceTranslation } from '../core/i18n.js';
|
|
5
6
|
import styles from './ModelEditor.module.css.js';
|
|
6
7
|
import { Vec3Row, Vec3RowDeg, MaterialCard } from './ModelEditorSubComponents.js';
|
|
7
8
|
import { DEG } from './modelEditorTypes.js';
|
|
8
9
|
|
|
9
|
-
const ModelEditorRightPanel =
|
|
10
|
+
const ModelEditorRightPanel = /* @__PURE__ */ Object.assign(
|
|
11
|
+
/* @__PURE__ */ React.memo(({ api }) => {
|
|
12
|
+
const { t } = useNiceTranslation();
|
|
10
13
|
const { propTab, setPropTab, selectedNode, selectedMaterials, selMaterialIdx, setSelMaterialIdx, setMatRefresh, rebuildTree, aiBusy, aiStatus, rootObjectRef, videoInputRef, bgColor, setBgColor, fogEnabled, setFogEnabled, fogColor, setFogColor, fogNear, setFogNear, fogFar, setFogFar, showLightHelpers, setShowLightHelpers, showGrid, setShowGrid, showAxes, setShowAxes, showSkeleton, setShowSkeleton, snapEnabled, setSnapEnabled, snapGrid, setSnapGrid, setStatusText, } = api;
|
|
11
14
|
return (jsxs("div", { className: styles.rightPanel, children: [jsx("div", { className: styles.propTabs, children: ['object', 'material', 'world', 'modifiers', 'physics'].map((t) => (jsx(NiceButton, { className: styles.propTabBtn, variant: propTab === t ? 'primary' : 'ghost', size: "sm", onClick: () => setPropTab(t), title: t.charAt(0).toUpperCase() + t.slice(1), "aria-label": t.charAt(0).toUpperCase() + t.slice(1), children: jsx(NiceIcon, { name: t === 'object'
|
|
12
15
|
? 'settings'
|
|
@@ -16,13 +19,13 @@ const ModelEditorRightPanel = React.memo(({ api }) => {
|
|
|
16
19
|
? 'globe'
|
|
17
20
|
: t === 'modifiers'
|
|
18
21
|
? 'settings'
|
|
19
|
-
: 'zap', size: 16 }) }, t))) }), propTab === 'object' && (jsxs(Fragment, { children: [jsxs("div", { className: styles.panelSection, children: [jsxs("div", { className: styles.panelTitle, children: [jsx(NiceIcon, { name: "settings", size: 14 }),
|
|
22
|
+
: 'zap', size: 16 }) }, t))) }), propTab === 'object' && (jsxs(Fragment, { children: [jsxs("div", { className: styles.panelSection, children: [jsxs("div", { className: styles.panelTitle, children: [jsx(NiceIcon, { name: "settings", size: 14 }), ' ', t('modelEditorRightPanel.transform', 'Transform')] }), jsx("div", { className: styles.panelContent, children: selectedNode ? (jsxs(Fragment, { children: [jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: t('modelEditorRightPanel.name', 'Name') }), jsx(NiceTextInput, { className: styles.propInput, value: selectedNode.name, onChange: (val) => {
|
|
20
23
|
selectedNode.object.name = val;
|
|
21
24
|
rebuildTree();
|
|
22
|
-
} })] }), jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children:
|
|
25
|
+
} })] }), jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: t('modelEditorRightPanel.type', 'Type') }), jsx("span", { style: { fontSize: 10, color: 'var(--nice-text-secondary, #888)' }, children: selectedNode.type })] }), jsx(Vec3Row, { label: t('modelEditorRightPanel.position', 'Position'), value: selectedNode.object.position, onChange: () => setMatRefresh((n) => n + 1) }), jsx(Vec3RowDeg, { label: t('modelEditorRightPanel.rotation', 'Rotation'), value: selectedNode.object.rotation, onChange: () => setMatRefresh((n) => n + 1) }), jsx(Vec3Row, { label: t('modelEditorRightPanel.scale', 'Scale'), value: selectedNode.object.scale, onChange: () => setMatRefresh((n) => n + 1) })] })) : (jsx("div", { style: { color: 'var(--nice-text-secondary, #666)', fontSize: 10 }, children: t('modelEditorRightPanel.selectAnObject', 'Select an object') })) })] }), selectedNode && selectedNode.object instanceof THREE.Light && (jsxs("div", { className: styles.panelSection, children: [jsxs("div", { className: styles.panelTitle, children: [jsx(NiceIcon, { name: "lightbulb", size: 14 }), ' ', t('modelEditorRightPanel.light', 'Light')] }), jsxs("div", { className: styles.panelContent, children: [jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: t('modelEditorRightPanel.color', 'Color') }), jsx(NiceColorPicker, { value: `#${selectedNode.object.color.getHexString()}`, onChange: (c) => {
|
|
23
26
|
selectedNode.object.color.set(c);
|
|
24
27
|
setMatRefresh((n) => n + 1);
|
|
25
|
-
} })] }), jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children:
|
|
28
|
+
} })] }), jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: t('modelEditorRightPanel.intensity', 'Intensity') }), jsx(NiceSlider, { min: 0, max: 10, step: 0.1, value: selectedNode.object.intensity, onChange: (val) => {
|
|
26
29
|
selectedNode.object.intensity = val;
|
|
27
30
|
setMatRefresh((n) => n + 1);
|
|
28
31
|
}, style: { flex: 1 } }), jsx("span", { style: {
|
|
@@ -30,10 +33,10 @@ const ModelEditorRightPanel = React.memo(({ api }) => {
|
|
|
30
33
|
color: 'var(--nice-text-secondary, #888)',
|
|
31
34
|
width: 28,
|
|
32
35
|
textAlign: 'right',
|
|
33
|
-
}, children: selectedNode.object.intensity.toFixed(1) })] }), 'castShadow' in selectedNode.object && (jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children:
|
|
36
|
+
}, children: selectedNode.object.intensity.toFixed(1) })] }), 'castShadow' in selectedNode.object && (jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: t('modelEditorRightPanel.shadow', 'Shadow') }), jsx(NiceCheckbox, { checked: selectedNode.object.castShadow, onChange: (checked) => {
|
|
34
37
|
selectedNode.object.castShadow = checked;
|
|
35
38
|
setMatRefresh((n) => n + 1);
|
|
36
|
-
} })] })), selectedNode.object instanceof THREE.SpotLight && (jsxs(Fragment, { children: [jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children:
|
|
39
|
+
} })] })), selectedNode.object instanceof THREE.SpotLight && (jsxs(Fragment, { children: [jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: t('modelEditorRightPanel.angle', 'Angle') }), jsx(NiceSlider, { min: 0, max: 90, step: 1, value: selectedNode.object.angle * DEG, onChange: (val) => {
|
|
37
40
|
selectedNode.object.angle = val / DEG;
|
|
38
41
|
setMatRefresh((n) => n + 1);
|
|
39
42
|
}, style: { flex: 1 } }), jsxs("span", { style: {
|
|
@@ -41,10 +44,10 @@ const ModelEditorRightPanel = React.memo(({ api }) => {
|
|
|
41
44
|
color: 'var(--nice-text-secondary, #888)',
|
|
42
45
|
width: 28,
|
|
43
46
|
textAlign: 'right',
|
|
44
|
-
}, children: [(selectedNode.object.angle * DEG).toFixed(0), "\u00B0"] })] }), jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children:
|
|
47
|
+
}, children: [(selectedNode.object.angle * DEG).toFixed(0), "\u00B0"] })] }), jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: t('modelEditorRightPanel.penumbra', 'Penumbra') }), jsx(NiceSlider, { min: 0, max: 1, step: 0.01, value: selectedNode.object.penumbra, onChange: (val) => {
|
|
45
48
|
selectedNode.object.penumbra = val;
|
|
46
49
|
setMatRefresh((n) => n + 1);
|
|
47
|
-
}, style: { flex: 1 } })] })] }))] })] })), selectedNode && selectedNode.object instanceof THREE.PerspectiveCamera && (jsxs("div", { className: styles.panelSection, children: [jsxs("div", { className: styles.panelTitle, children: [jsx(NiceIcon, { name: "camera", size: 14 }), " Camera
|
|
50
|
+
}, style: { flex: 1 } })] })] }))] })] })), selectedNode && selectedNode.object instanceof THREE.PerspectiveCamera && (jsxs("div", { className: styles.panelSection, children: [jsxs("div", { className: styles.panelTitle, children: [jsx(NiceIcon, { name: "camera", size: 14 }), " ", t('modelEditorRightPanel.camera', 'Camera')] }), jsxs("div", { className: styles.panelContent, children: [jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: "FOV" }), jsx(NiceSlider, { min: 10, max: 120, step: 1, value: selectedNode.object.fov, onChange: (val) => {
|
|
48
51
|
const cam = selectedNode.object;
|
|
49
52
|
cam.fov = val;
|
|
50
53
|
cam.updateProjectionMatrix();
|
|
@@ -54,7 +57,7 @@ const ModelEditorRightPanel = React.memo(({ api }) => {
|
|
|
54
57
|
color: 'var(--nice-text-secondary, #888)',
|
|
55
58
|
width: 28,
|
|
56
59
|
textAlign: 'right',
|
|
57
|
-
}, children: [selectedNode.object.fov.toFixed(0), "\u00B0"] })] }), jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children:
|
|
60
|
+
}, children: [selectedNode.object.fov.toFixed(0), "\u00B0"] })] }), jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: t('modelEditorRightPanel.near', 'Near') }), jsx(NiceNumberInput, { className: styles.propInput, step: 0.01, value: selectedNode.object.near, onChange: (val) => {
|
|
58
61
|
const cam = selectedNode.object;
|
|
59
62
|
cam.near = val !== null && val !== void 0 ? val : 0.1;
|
|
60
63
|
cam.updateProjectionMatrix();
|
|
@@ -64,11 +67,11 @@ const ModelEditorRightPanel = React.memo(({ api }) => {
|
|
|
64
67
|
cam.far = val !== null && val !== void 0 ? val : 1000;
|
|
65
68
|
cam.updateProjectionMatrix();
|
|
66
69
|
setMatRefresh((n) => n + 1);
|
|
67
|
-
}, style: { width: 80 } })] })] })] })), jsxs("div", { className: styles.panelSection, children: [jsxs("div", { className: styles.panelTitle, children: [jsx(NiceIcon, { name: "wand", size: 14 }),
|
|
70
|
+
}, style: { width: 80 } })] })] })] })), jsxs("div", { className: styles.panelSection, children: [jsxs("div", { className: styles.panelTitle, children: [jsx(NiceIcon, { name: "wand", size: 14 }), ' ', t('modelEditorRightPanel.aiAnimation', 'AI Animation')] }), jsx("div", { className: styles.panelContent, children: jsxs("div", { className: styles.aiPanel, children: [jsxs("div", { className: styles.aiHeader, children: [jsx(NiceIcon, { name: "wand", size: 14 }), ' ', t('modelEditorRightPanel.generateAnimationFromVideo', 'Generate animation from video')] }), jsx("p", { style: {
|
|
68
71
|
fontSize: 10,
|
|
69
72
|
color: 'var(--nice-text-secondary, #888)',
|
|
70
73
|
margin: '0 0 6px',
|
|
71
|
-
}, children: "Upload a video and AI will extract 3D pose data to create an animation clip for the current model." }), jsx("div", { className: styles.aiBtnRow, children: jsx(NiceButton, { className: styles.aiBtn, variant: "ghost", size: "sm", disabled: aiBusy || !rootObjectRef.current, onClick: () => { var _a; return (_a = videoInputRef.current) === null || _a === void 0 ? void 0 : _a.click(); }, children: aiBusy ? 'Processing...' : 'Upload Video' }) }), aiStatus && jsx("div", { className: styles.aiStatus, children: aiStatus })] }) })] })] })), propTab === 'material' && (jsxs("div", { className: styles.panelSection, children: [jsxs("div", { className: styles.panelTitle, children: [jsx(NiceIcon, { name: "palette", size: 14 }),
|
|
74
|
+
}, children: "Upload a video and AI will extract 3D pose data to create an animation clip for the current model." }), jsx("div", { className: styles.aiBtnRow, children: jsx(NiceButton, { className: styles.aiBtn, variant: "ghost", size: "sm", disabled: aiBusy || !rootObjectRef.current, onClick: () => { var _a; return (_a = videoInputRef.current) === null || _a === void 0 ? void 0 : _a.click(); }, children: aiBusy ? 'Processing...' : 'Upload Video' }) }), aiStatus && jsx("div", { className: styles.aiStatus, children: aiStatus })] }) })] })] })), propTab === 'material' && (jsxs("div", { className: styles.panelSection, children: [jsxs("div", { className: styles.panelTitle, children: [jsx(NiceIcon, { name: "palette", size: 14 }), ' ', t('modelEditorRightPanel.materials', 'Materials')] }), jsx("div", { className: styles.panelContent, children: selectedMaterials.length > 0 ? (selectedMaterials.map((mat, i) => (jsx(MaterialCard, { material: mat, active: i === selMaterialIdx, onClick: () => setSelMaterialIdx(i), onUpdate: () => setMatRefresh((n) => n + 1) }, i)))) : (jsx("div", { style: { color: 'var(--nice-text-secondary, #666)', fontSize: 10 }, children: selectedNode ? 'No materials on this object' : 'Select a mesh to inspect' })) })] })), propTab === 'world' && (jsxs(Fragment, { children: [jsxs("div", { className: styles.panelSection, children: [jsxs("div", { className: styles.panelTitle, children: [jsx(NiceIcon, { name: "globe", size: 14 }), ' ', t('modelEditorRightPanel.environment', 'Environment')] }), jsx("div", { className: styles.panelContent, children: jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: t('modelEditorRightPanel.background', 'Background') }), jsx(NiceColorPicker, { value: bgColor, onChange: (c) => setBgColor(c) }), jsx("span", { style: { fontSize: 9, color: 'var(--nice-text-secondary, #888)' }, children: bgColor })] }) })] }), jsxs("div", { className: styles.panelSection, children: [jsxs("div", { className: styles.panelTitle, children: [jsx(NiceIcon, { name: "cloud", size: 14 }), " Fog"] }), jsxs("div", { className: styles.panelContent, children: [jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: t('modelEditorRightPanel.enable', 'Enable') }), jsx(NiceCheckbox, { checked: fogEnabled, onChange: () => setFogEnabled((v) => !v) })] }), fogEnabled && (jsxs(Fragment, { children: [jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: t('modelEditorRightPanel.color', 'Color') }), jsx(NiceColorPicker, { value: fogColor, onChange: (c) => setFogColor(c) })] }), jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: t('modelEditorRightPanel.near', 'Near') }), jsx(NiceNumberInput, { className: styles.propInput, value: fogNear, onChange: (val) => setFogNear(val !== null && val !== void 0 ? val : 1), style: { width: 60 } })] }), jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: "Far" }), jsx(NiceNumberInput, { className: styles.propInput, value: fogFar, onChange: (val) => setFogFar(val !== null && val !== void 0 ? val : 100), style: { width: 60 } })] })] }))] })] }), jsxs("div", { className: styles.panelSection, children: [jsxs("div", { className: styles.panelTitle, children: [jsx(NiceIcon, { name: "lightbulb", size: 14 }), ' ', t('modelEditorRightPanel.sceneLights', 'Scene Lights')] }), jsxs("div", { className: styles.panelContent, children: [jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: t('modelEditorRightPanel.showHelpers', 'Show Helpers') }), jsx(NiceCheckbox, { checked: showLightHelpers, onChange: () => setShowLightHelpers((v) => !v) })] }), jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: t('modelEditorRightPanel.grid', 'Grid') }), jsx(NiceCheckbox, { checked: showGrid, onChange: () => setShowGrid((v) => !v) })] }), jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: t('modelEditorRightPanel.axes', 'Axes') }), jsx(NiceCheckbox, { checked: showAxes, onChange: () => setShowAxes((v) => !v) })] }), jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: t('modelEditorRightPanel.skeleton', 'Skeleton') }), jsx(NiceCheckbox, { checked: showSkeleton, onChange: () => setShowSkeleton((v) => !v) })] })] })] }), jsxs("div", { className: styles.panelSection, children: [jsxs("div", { className: styles.panelTitle, children: [jsx(NiceIcon, { name: "grid", size: 14 }), " ", t('modelEditorRightPanel.snapping', 'Snapping')] }), jsxs("div", { className: styles.panelContent, children: [jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: t('modelEditorRightPanel.snap', 'Snap') }), jsx(NiceCheckbox, { checked: snapEnabled, onChange: () => setSnapEnabled((v) => !v) })] }), snapEnabled && (jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: t('modelEditorRightPanel.gridSize', 'Grid Size') }), jsx(NiceNumberInput, { className: styles.propInput, step: 0.25, min: 0.1, value: snapGrid, onChange: (val) => setSnapGrid(val !== null && val !== void 0 ? val : 1), style: { width: 60 } })] }))] })] })] })), propTab === 'modifiers' && (jsxs("div", { className: styles.panelSection, children: [jsxs("div", { className: styles.panelTitle, children: [jsx(NiceIcon, { name: "settings", size: 14 }), ' ', t('modelEditorRightPanel.modifiers', 'Modifiers')] }), jsx("div", { className: styles.panelContent, children: selectedNode && selectedNode.object instanceof THREE.Mesh ? (jsxs(Fragment, { children: [jsx(NiceButton, { className: styles.btnSecondary, variant: "ghost", size: "sm", style: { width: '100%', marginBottom: 4 }, onClick: () => {
|
|
72
75
|
const mesh = selectedNode.object;
|
|
73
76
|
const geo = mesh.geometry;
|
|
74
77
|
const pos = geo.attributes.position;
|
|
@@ -82,13 +85,13 @@ const ModelEditorRightPanel = React.memo(({ api }) => {
|
|
|
82
85
|
const mesh = selectedNode.object;
|
|
83
86
|
mesh.geometry.computeVertexNormals();
|
|
84
87
|
setStatusText('Recomputed normals');
|
|
85
|
-
}, children:
|
|
88
|
+
}, children: t('modelEditorRightPanel.recomputeNormals', 'Recompute Normals') }), jsx(NiceButton, { className: styles.btnSecondary, variant: "ghost", size: "sm", style: { width: '100%', marginBottom: 4 }, onClick: () => {
|
|
86
89
|
const mesh = selectedNode.object;
|
|
87
90
|
const geo = mesh.geometry;
|
|
88
91
|
geo.center();
|
|
89
92
|
setStatusText('Centered geometry');
|
|
90
93
|
setMatRefresh((n) => n + 1);
|
|
91
|
-
}, children:
|
|
94
|
+
}, children: t('modelEditorRightPanel.centerGeometry', 'Center Geometry') }), jsx(NiceButton, { className: styles.btnSecondary, variant: "ghost", size: "sm", style: { width: '100%', marginBottom: 4 }, onClick: () => {
|
|
92
95
|
const mesh = selectedNode.object;
|
|
93
96
|
const geo = mesh.geometry;
|
|
94
97
|
const idx = geo.index;
|
|
@@ -103,8 +106,7 @@ const ModelEditorRightPanel = React.memo(({ api }) => {
|
|
|
103
106
|
geo.computeVertexNormals();
|
|
104
107
|
setStatusText('Flipped normals');
|
|
105
108
|
}
|
|
106
|
-
}, children:
|
|
107
|
-
});
|
|
108
|
-
ModelEditorRightPanel.displayName = 'ModelEditorRightPanel';
|
|
109
|
+
}, children: t('modelEditorRightPanel.flipNormals', 'Flip Normals') })] })) : (jsx("div", { style: { color: 'var(--nice-text-secondary, #666)', fontSize: 10 }, children: t('modelEditorRightPanel.selectAMeshToApply', 'Select a mesh to apply modifiers') })) })] })), propTab === 'physics' && (jsxs("div", { className: styles.panelSection, children: [jsxs("div", { className: styles.panelTitle, children: [jsx(NiceIcon, { name: "zap", size: 14 }), " Physics (Info)"] }), jsx("div", { className: styles.panelContent, children: jsxs("div", { style: { color: 'var(--nice-text-secondary, #666)', fontSize: 10, lineHeight: 1.5 }, children: [t('modelEditorRightPanel.physicsSimulationPreviewIsNot', 'Physics simulation preview is not yet available.'), jsx("br", {}), jsx("br", {}), "Planned features:", jsx("br", {}), "\u2022 Rigid body simulation", jsx("br", {}), "\u2022 Soft body dynamics", jsx("br", {}), "\u2022 Cloth simulation", jsx("br", {}), "\u2022 Particle systems"] }) })] }))] }));
|
|
110
|
+
}), { displayName: 'ModelEditorRightPanel' });
|
|
109
111
|
|
|
110
112
|
export { ModelEditorRightPanel as default };
|
|
@@ -2,6 +2,7 @@ import { jsxs, jsx } from 'react/jsx-runtime';
|
|
|
2
2
|
import { NiceTextInput, NiceColorPicker, NiceSlider, NiceSelect } from '@nice2dev/ui';
|
|
3
3
|
import { useState } from 'react';
|
|
4
4
|
import * as THREE from 'three';
|
|
5
|
+
import { useNiceTranslation } from '../core/i18n.js';
|
|
5
6
|
import styles from './ModelEditor.module.css.js';
|
|
6
7
|
import { DEG } from './modelEditorTypes.js';
|
|
7
8
|
|
|
@@ -37,6 +38,7 @@ const Vec3RowDeg = ({ label, value, onChange }) => {
|
|
|
37
38
|
/** Material card with editable properties */
|
|
38
39
|
const MaterialCard = ({ material, active, onClick, onUpdate }) => {
|
|
39
40
|
var _a, _b, _c, _d;
|
|
41
|
+
const { t } = useNiceTranslation();
|
|
40
42
|
const mat = material;
|
|
41
43
|
const hasColor = 'color' in mat;
|
|
42
44
|
const hasMetalness = 'metalness' in mat;
|
|
@@ -56,21 +58,21 @@ const MaterialCard = ({ material, active, onClick, onUpdate }) => {
|
|
|
56
58
|
mat.needsUpdate = true;
|
|
57
59
|
onUpdate();
|
|
58
60
|
};
|
|
59
|
-
return (jsxs("div", { className: `${styles.materialCard} ${active ? styles.materialCardActive : ''}`, onClick: onClick, children: [jsx("div", { className: styles.materialHeader, children: jsx("span", { className: styles.materialName, children: mat.name || mat.type }) }), hasColor && (jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children:
|
|
61
|
+
return (jsxs("div", { className: `${styles.materialCard} ${active ? styles.materialCardActive : ''}`, onClick: onClick, children: [jsx("div", { className: styles.materialHeader, children: jsx("span", { className: styles.materialName, children: mat.name || mat.type }) }), hasColor && (jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: t('modelEditorSubComponents.color', 'Color') }), jsx(NiceColorPicker, { value: `#${mat.color.getHexString()}`, onChange: (c) => setColor(c) }), jsxs("span", { style: { fontSize: 10, color: 'var(--nice-text-secondary, #888)' }, children: ["#", mat.color.getHexString()] })] })), hasMetalness && (jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: t('modelEditorSubComponents.metal', 'Metal') }), jsx(NiceSlider, { min: 0, max: 1, step: 0.01, value: mat.metalness, onChange: (val) => setNum('metalness', val), style: { flex: 1 } }), jsx("span", { style: {
|
|
60
62
|
fontSize: 9,
|
|
61
63
|
color: 'var(--nice-text-secondary, #888)',
|
|
62
64
|
width: 28,
|
|
63
65
|
textAlign: 'right',
|
|
64
|
-
}, children: mat.metalness.toFixed(2) })] })), hasRoughness && (jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children:
|
|
66
|
+
}, children: mat.metalness.toFixed(2) })] })), hasRoughness && (jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: t('modelEditorSubComponents.rough', 'Rough') }), jsx(NiceSlider, { min: 0, max: 1, step: 0.01, value: mat.roughness, onChange: (val) => setNum('roughness', val), style: { flex: 1 } }), jsx("span", { style: {
|
|
65
67
|
fontSize: 9,
|
|
66
68
|
color: 'var(--nice-text-secondary, #888)',
|
|
67
69
|
width: 28,
|
|
68
70
|
textAlign: 'right',
|
|
69
|
-
}, children: mat.roughness.toFixed(2) })] })), hasEmissive && mat.emissive && (jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children:
|
|
71
|
+
}, children: mat.roughness.toFixed(2) })] })), hasEmissive && mat.emissive && (jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: t('modelEditorSubComponents.emissive', 'Emissive') }), jsx(NiceColorPicker, { value: `#${mat.emissive.getHexString()}`, onChange: (c) => {
|
|
70
72
|
mat.emissive.set(c);
|
|
71
73
|
mat.needsUpdate = true;
|
|
72
74
|
onUpdate();
|
|
73
|
-
} })] })), jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children:
|
|
75
|
+
} })] })), jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: t('modelEditorSubComponents.opacity', 'Opacity') }), jsx(NiceSlider, { min: 0, max: 1, step: 0.01, value: mat.opacity, onChange: (val) => {
|
|
74
76
|
mat.opacity = val;
|
|
75
77
|
mat.transparent = val < 1;
|
|
76
78
|
mat.needsUpdate = true;
|
|
@@ -80,7 +82,7 @@ const MaterialCard = ({ material, active, onClick, onUpdate }) => {
|
|
|
80
82
|
color: 'var(--nice-text-secondary, #888)',
|
|
81
83
|
width: 28,
|
|
82
84
|
textAlign: 'right',
|
|
83
|
-
}, children: mat.opacity.toFixed(2) })] }), jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children:
|
|
85
|
+
}, children: mat.opacity.toFixed(2) })] }), jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: t('modelEditorSubComponents.side', 'Side') }), jsx(NiceSelect, { className: styles.propSelect, value: String(mat.side), onChange: (val) => {
|
|
84
86
|
mat.side = parseInt(val);
|
|
85
87
|
mat.needsUpdate = true;
|
|
86
88
|
onUpdate();
|
|
@@ -88,7 +90,7 @@ const MaterialCard = ({ material, active, onClick, onUpdate }) => {
|
|
|
88
90
|
{ value: String(THREE.FrontSide), label: 'Front' },
|
|
89
91
|
{ value: String(THREE.BackSide), label: 'Back' },
|
|
90
92
|
{ value: String(THREE.DoubleSide), label: 'Double' },
|
|
91
|
-
] })] }), hasMap && mat.map && (jsxs("div", { className: styles.textureSlot, children: [jsx("div", { className: styles.textureThumb, children: mat.map.image != null && (jsx("img", { src: (_a = mat.map.image.src) !== null && _a !== void 0 ? _a : '', alt: "diffuse" })) }), jsxs("div", { className: styles.textureInfo, children: [jsx("div", { className: styles.textureName, children:
|
|
93
|
+
] })] }), hasMap && mat.map && (jsxs("div", { className: styles.textureSlot, children: [jsx("div", { className: styles.textureThumb, children: mat.map.image != null && (jsx("img", { src: (_a = mat.map.image.src) !== null && _a !== void 0 ? _a : '', alt: "diffuse" })) }), jsxs("div", { className: styles.textureInfo, children: [jsx("div", { className: styles.textureName, children: t('modelEditorSubComponents.diffuseMap', 'Diffuse Map') }), jsxs("div", { style: { fontSize: 9, color: 'var(--nice-text-secondary, #666)' }, children: [(_b = mat.map.image) === null || _b === void 0 ? void 0 : _b.width, "\u00D7", (_c = mat.map.image) === null || _c === void 0 ? void 0 : _c.height] })] })] })), hasNormalMap && mat.normalMap && (jsxs("div", { className: styles.textureSlot, children: [jsx("div", { className: styles.textureThumb, children: mat.normalMap.image != null && (jsx("img", { src: (_d = mat.normalMap.image.src) !== null && _d !== void 0 ? _d : '', alt: "normal" })) }), jsx("div", { className: styles.textureInfo, children: jsx("div", { className: styles.textureName, children: t('modelEditorSubComponents.normalMap', 'Normal Map') }) })] }))] }));
|
|
92
94
|
};
|
|
93
95
|
|
|
94
96
|
export { MaterialCard, Vec3Row, Vec3RowDeg };
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
2
2
|
import { NiceIcon, NiceButton, NiceSlider } from '@nice2dev/ui';
|
|
3
3
|
import React from 'react';
|
|
4
|
+
import { useNiceTranslation } from '../core/i18n.js';
|
|
4
5
|
import styles from './ModelEditor.module.css.js';
|
|
5
6
|
import { fmtTime } from './modelEditorTypes.js';
|
|
6
7
|
|
|
7
|
-
const ModelEditorTimeline =
|
|
8
|
+
const ModelEditorTimeline = /* @__PURE__ */ Object.assign(
|
|
9
|
+
/* @__PURE__ */ React.memo(({ api }) => {
|
|
10
|
+
const { t } = useNiceTranslation();
|
|
8
11
|
const { bottomCollapsed, setBottomCollapsed, animations, activeAnimIdx, isPlaying, animTime, animDuration, animSpeed, setAnimSpeed, loopAnim, setLoopAnim, seekAnim, togglePlay, stopAnim, playClip, mergeInputRef, } = api;
|
|
9
|
-
return (jsxs("div", { className: `${styles.bottomPanel} ${bottomCollapsed ? styles.bottomPanelCollapsed : ''}`, children: [jsxs("div", { className: styles.bottomPanelHeader, children: [jsx("span", { className: styles.bottomToggle, onClick: () => setBottomCollapsed((c) => !c), children: jsx(NiceIcon, { name: bottomCollapsed ? 'chevron-right' : 'chevron-down', size: 14 }) }), jsxs("span", { className: styles.bottomPanelTitle, onClick: () => setBottomCollapsed((c) => !c), children: ["Animation (", animations.length, " clip", animations.length !== 1 ? 's' : '', ")"] }), jsx("div", { className: styles.menuSep }), jsxs("div", { className: styles.transportBar, children: [jsx(NiceButton, { className: styles.transportBtn, variant: "ghost", size: "sm", onClick: () => seekAnim(0), title:
|
|
12
|
+
return (jsxs("div", { className: `${styles.bottomPanel} ${bottomCollapsed ? styles.bottomPanelCollapsed : ''}`, children: [jsxs("div", { className: styles.bottomPanelHeader, children: [jsx("span", { className: styles.bottomToggle, onClick: () => setBottomCollapsed((c) => !c), children: jsx(NiceIcon, { name: bottomCollapsed ? 'chevron-right' : 'chevron-down', size: 14 }) }), jsxs("span", { className: styles.bottomPanelTitle, onClick: () => setBottomCollapsed((c) => !c), children: ["Animation (", animations.length, " clip", animations.length !== 1 ? 's' : '', ")"] }), jsx("div", { className: styles.menuSep }), jsxs("div", { className: styles.transportBar, children: [jsx(NiceButton, { className: styles.transportBtn, variant: "ghost", size: "sm", onClick: () => seekAnim(0), title: t('modelEditorTimeline.goToStart', 'Go to start'), "aria-label": t('modelEditorTimeline.goToStart', 'Go to start'), children: jsx(NiceIcon, { name: "chevron-left", size: 16 }) }), jsx(NiceButton, { className: styles.transportBtn, variant: isPlaying ? 'primary' : 'ghost', size: "sm", onClick: togglePlay, title: t('modelEditorTimeline.playPauseSpace', 'Play/Pause (Space)'), "aria-label": t('modelEditorTimeline.playPauseSpace', 'Play/Pause (Space)'), children: jsx(NiceIcon, { name: isPlaying ? 'pause' : 'play', size: 16 }) }), jsx(NiceButton, { className: styles.transportBtn, variant: "ghost", size: "sm", onClick: stopAnim, title: t('modelEditorTimeline.stop', 'Stop'), "aria-label": t('modelEditorTimeline.stop', 'Stop'), children: jsx(NiceIcon, { name: "stop", size: 16 }) }), jsx(NiceButton, { className: styles.transportBtn, variant: loopAnim ? 'primary' : 'ghost', size: "sm", onClick: () => setLoopAnim((v) => !v), title: t('modelEditorTimeline.loop', 'Loop'), "aria-label": t('modelEditorTimeline.loop', 'Loop'), children: jsx(NiceIcon, { name: "refresh", size: 16 }) })] }), jsxs("span", { className: styles.transportTime, children: [fmtTime(animTime), " / ", fmtTime(animDuration)] }), jsx("div", { className: styles.menuSep }), jsx("label", { className: styles.menuLabel, children: t('modelEditorTimeline.speed', 'Speed') }), jsx(NiceSlider, { min: 0.1, max: 3, step: 0.1, value: animSpeed, onChange: (val) => setAnimSpeed(val), style: { width: 60 } }), jsxs("span", { className: styles.menuLabel, children: [animSpeed.toFixed(1), "\u00D7"] })] }), !bottomCollapsed && (jsxs("div", { className: styles.animListArea, children: [jsxs("div", { className: styles.animList, children: [animations.length === 0 && (jsx("div", { style: { color: 'var(--nice-text-secondary, #666)', fontSize: 10, padding: 8 }, children: t('modelEditorTimeline.noAnimationsLoaded', 'No animations loaded') })), animations.map((anim, i) => (jsxs("div", { className: `${styles.animItem} ${i === activeAnimIdx ? styles.animItemActive : ''}`, onClick: () => playClip(i), children: [jsx("span", { children: anim.name }), jsxs("span", { className: styles.animDuration, children: [anim.duration.toFixed(1), "s"] })] }, `${anim.name}-${i}`))), jsx("div", { style: { padding: '4px 8px' }, children: jsx(NiceButton, { className: styles.btnSecondary, variant: "ghost", size: "sm", style: { width: '100%', fontSize: 10 }, onClick: () => { var _a; return (_a = mergeInputRef.current) === null || _a === void 0 ? void 0 : _a.click(); }, children: "+ Add Animation File" }) })] }), jsxs("div", { className: styles.timelineArea, children: [jsxs("div", { className: styles.timelineRuler, children: [animDuration > 0 &&
|
|
10
13
|
Array.from({ length: Math.ceil(animDuration) + 1 }, (_, i) => {
|
|
11
14
|
const pct = (i / animDuration) * 100;
|
|
12
15
|
return (jsxs(React.Fragment, { children: [jsx("div", { className: styles.rulerTick, style: { left: `${pct}%` } }), jsxs("span", { className: styles.rulerLabel, style: { left: `${pct}%` }, children: [i, "s"] })] }, i));
|
|
@@ -24,7 +27,6 @@ const ModelEditorTimeline = React.memo(({ api }) => {
|
|
|
24
27
|
left: `${(animTime / animDuration) * 100}%`,
|
|
25
28
|
height: '100%',
|
|
26
29
|
} })) })] })] }))] }));
|
|
27
|
-
});
|
|
28
|
-
ModelEditorTimeline.displayName = 'ModelEditorTimeline';
|
|
30
|
+
}), { displayName: 'ModelEditorTimeline' });
|
|
29
31
|
|
|
30
32
|
export { ModelEditorTimeline as default };
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
2
2
|
import { NiceButton, NiceIcon } from '@nice2dev/ui';
|
|
3
3
|
import React from 'react';
|
|
4
|
+
import { useNiceTranslation } from '../core/i18n.js';
|
|
4
5
|
import styles from './ModelEditor.module.css.js';
|
|
5
6
|
|
|
6
|
-
const ModelEditorToolbar =
|
|
7
|
+
const ModelEditorToolbar = /* @__PURE__ */ Object.assign(
|
|
8
|
+
/* @__PURE__ */ React.memo(({ api }) => {
|
|
9
|
+
const { t } = useNiceTranslation();
|
|
7
10
|
const { transformMode, setTransformMode, snapEnabled, setSnapEnabled, gizmoSpace, setGizmoSpace, rootObjectRef, focusOnObject, setCameraPreset, deleteSelected, duplicateSelected, selectedNode, } = api;
|
|
8
|
-
return (jsxs("div", { className: styles.toolbar, children: [jsx(NiceButton, { className: styles.toolBtn, variant: transformMode === 'translate' ? 'primary' : 'ghost', size: "sm", onClick: () => setTransformMode('translate'), title:
|
|
9
|
-
});
|
|
10
|
-
ModelEditorToolbar.displayName = 'ModelEditorToolbar';
|
|
11
|
+
return (jsxs("div", { className: styles.toolbar, children: [jsx(NiceButton, { className: styles.toolBtn, variant: transformMode === 'translate' ? 'primary' : 'ghost', size: "sm", onClick: () => setTransformMode('translate'), title: t('modelEditorToolbar.translateG', 'Translate (G)'), "aria-label": t('modelEditorToolbar.translateG', 'Translate (G)'), children: jsx(NiceIcon, { name: "move", size: 16 }) }), jsx(NiceButton, { className: styles.toolBtn, variant: transformMode === 'rotate' ? 'primary' : 'ghost', size: "sm", onClick: () => setTransformMode('rotate'), title: t('modelEditorToolbar.rotateR', 'Rotate (R)'), "aria-label": t('modelEditorToolbar.rotateR', 'Rotate (R)'), children: jsx(NiceIcon, { name: "refresh", size: 16 }) }), jsx(NiceButton, { className: styles.toolBtn, variant: transformMode === 'scale' ? 'primary' : 'ghost', size: "sm", onClick: () => setTransformMode('scale'), title: t('modelEditorToolbar.scaleS', 'Scale (S)'), "aria-label": t('modelEditorToolbar.scaleS', 'Scale (S)'), children: jsx(NiceIcon, { name: "hexagon", size: 16 }) }), jsx("div", { className: styles.toolSep }), jsx(NiceButton, { className: styles.toolBtn, variant: snapEnabled ? 'primary' : 'ghost', size: "sm", onClick: () => setSnapEnabled((v) => !v), title: `Snap to Grid (${snapEnabled ? 'ON' : 'OFF'})`, "aria-label": t('modelEditorToolbar.snapToGrid', 'Snap to Grid'), children: jsx(NiceIcon, { name: "grid", size: 16 }) }), jsx(NiceButton, { className: styles.toolBtn, variant: "ghost", size: "sm", onClick: () => setGizmoSpace((s) => (s === 'local' ? 'world' : 'local')), title: `Orientation: ${gizmoSpace}`, "aria-label": `Orientation: ${gizmoSpace}`, children: jsx(NiceIcon, { name: gizmoSpace === 'local' ? 'target' : 'globe', size: 16 }) }), jsx("div", { className: styles.toolSep }), jsx(NiceButton, { className: styles.toolBtn, variant: "ghost", size: "sm", onClick: () => rootObjectRef.current && focusOnObject(rootObjectRef.current), title: t('modelEditorToolbar.focusF', 'Focus (F)'), "aria-label": t('modelEditorToolbar.focusF', 'Focus (F)'), children: jsx(NiceIcon, { name: "target", size: 16 }) }), jsx(NiceButton, { className: styles.toolBtn, variant: "ghost", size: "sm", onClick: () => setCameraPreset('front'), title: t('modelEditorToolbar.frontViewNumpad1', 'Front view (Numpad 1)'), children: "1" }), jsx(NiceButton, { className: styles.toolBtn, variant: "ghost", size: "sm", onClick: () => setCameraPreset('right'), title: t('modelEditorToolbar.rightViewNumpad3', 'Right view (Numpad 3)'), children: "3" }), jsx(NiceButton, { className: styles.toolBtn, variant: "ghost", size: "sm", onClick: () => setCameraPreset('top'), title: t('modelEditorToolbar.topViewNumpad7', 'Top view (Numpad 7)'), children: "7" }), jsx("div", { className: styles.toolSep }), jsx(NiceButton, { className: styles.toolBtn, variant: "ghost", size: "sm", onClick: deleteSelected, title: t('modelEditorToolbar.deleteSelectedDel', 'Delete selected (Del)'), "aria-label": t('modelEditorToolbar.deleteSelectedDel', 'Delete selected (Del)'), children: jsx(NiceIcon, { name: "trash", size: 16 }) }), jsx(NiceButton, { className: styles.toolBtn, variant: "ghost", size: "sm", onClick: duplicateSelected, title: t('modelEditorToolbar.duplicateShiftD', 'Duplicate (Shift+D)'), disabled: !selectedNode, "aria-label": t('modelEditorToolbar.duplicateShiftD', 'Duplicate (Shift+D)'), children: jsx(NiceIcon, { name: "clipboard", size: 16 }) })] }));
|
|
12
|
+
}), { displayName: 'ModelEditorToolbar' });
|
|
11
13
|
|
|
12
14
|
export { ModelEditorToolbar as default };
|
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
|
|
2
2
|
import { NiceIcon } from '@nice2dev/ui';
|
|
3
3
|
import React from 'react';
|
|
4
|
+
import { useNiceTranslation } from '../core/i18n.js';
|
|
4
5
|
import styles from './ModelEditor.module.css.js';
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
/** The viewport badge per editor mode: [key, English]. */
|
|
8
|
+
const MODE_BADGE = {
|
|
9
|
+
object: ['modelEditorViewport.objectMode', 'OBJECT MODE'],
|
|
10
|
+
edit: ['modelEditorViewport.editMode', 'EDIT MODE'],
|
|
11
|
+
pose: ['modelEditorViewport.poseMode', 'POSE MODE'],
|
|
12
|
+
cad: ['modelEditorViewport.cadMode', 'CAD MODE'],
|
|
13
|
+
sims: ['modelEditorViewport.simsMode', 'SIMS MODE'],
|
|
14
|
+
};
|
|
15
|
+
const ModelEditorViewport = /* @__PURE__ */ Object.assign(
|
|
16
|
+
/* @__PURE__ */ React.memo(({ api }) => {
|
|
17
|
+
const { t } = useNiceTranslation();
|
|
7
18
|
const { mountRef, dragOver, handleDragOver, handleDragLeave, handleDrop, handleViewportClick, handleContextMenu, editorMode, polyCount, meshCount, boneCount, shadingMode, setShadingMode, gizmoSpace, setGizmoSpace, contextMenu, closeContextMenu, addPrimitive, addSceneLight, duplicateSelected, deleteSelected, selectedNode, rootObjectRef, focusOnObject, snapEnabled, setSnapEnabled, } = api;
|
|
8
|
-
return (jsxs(Fragment, { children: [jsxs("div", { ref: mountRef, className: `${styles.viewportContainer} ${dragOver ? styles.dragOver : ''}`, onDragOver: handleDragOver, onDragLeave: handleDragLeave, onDrop: handleDrop, onClick: handleViewportClick, onContextMenu: handleContextMenu, children: [jsxs("div", { className: styles.viewportOverlay, children: [
|
|
19
|
+
return (jsxs(Fragment, { children: [jsxs("div", { ref: mountRef, className: `${styles.viewportContainer} ${dragOver ? styles.dragOver : ''}`, onDragOver: handleDragOver, onDragLeave: handleDragLeave, onDrop: handleDrop, onClick: handleViewportClick, onContextMenu: handleContextMenu, children: [jsxs("div", { className: styles.viewportOverlay, children: [jsx("span", { className: styles.viewportBadge, children: t(...MODE_BADGE[editorMode]) }), jsxs("span", { className: styles.viewportBadge, children: [polyCount.toLocaleString(), " tris \u00B7 ", meshCount, " meshes \u00B7 ", boneCount, " bones"] })] }), jsx("div", { className: styles.viewportShadingBar, children: ['wireframe', 'solid', 'material', 'rendered'].map((m) => (jsx("button", { className: `${styles.shadingBtn} ${shadingMode === m ? styles.shadingBtnActive : ''}`, onClick: (e) => {
|
|
9
20
|
e.stopPropagation();
|
|
10
21
|
setShadingMode(m);
|
|
11
22
|
}, title: m.charAt(0).toUpperCase() + m.slice(1), "aria-label": m.charAt(0).toUpperCase() + m.slice(1), children: jsx(NiceIcon, { name: m === 'wireframe'
|
|
@@ -17,12 +28,11 @@ const ModelEditorViewport = React.memo(({ api }) => {
|
|
|
17
28
|
: 'lightbulb', size: 16 }) }, m))) }), jsx("div", { className: styles.viewportGizmoInfo, children: jsx("button", { className: styles.shadingBtn, onClick: (e) => {
|
|
18
29
|
e.stopPropagation();
|
|
19
30
|
setGizmoSpace((s) => (s === 'local' ? 'world' : 'local'));
|
|
20
|
-
}, title: `Orientation: ${gizmoSpace}`, "aria-label": `Orientation: ${gizmoSpace}`, children: gizmoSpace === 'local' ? (jsxs(Fragment, { children: [jsx(NiceIcon, { name: "target", size: 14 }), " Local
|
|
31
|
+
}, title: `Orientation: ${gizmoSpace}`, "aria-label": `Orientation: ${gizmoSpace}`, children: gizmoSpace === 'local' ? (jsxs(Fragment, { children: [jsx(NiceIcon, { name: "target", size: 14 }), " ", t('modelEditorViewport.local', 'Local')] })) : (jsxs(Fragment, { children: [jsx(NiceIcon, { name: "globe", size: 14 }), " ", t('modelEditorViewport.world', 'World')] })) }) }), dragOver && (jsx("div", { className: styles.viewportDropZone, children: jsx("span", { children: t('modelEditorViewport.drop3dFilesHere', 'Drop 3D files here') }) }))] }), contextMenu && (jsxs("div", { className: styles.contextMenu, style: { top: contextMenu.y, left: contextMenu.x }, onClick: closeContextMenu, children: [jsxs("div", { className: styles.contextItem, onClick: () => addPrimitive('cube'), children: [jsx("span", { children: t('modelEditorViewport.addCube', 'Add Cube') }), jsx("span", { className: styles.contextKey, children: "Shift+A" })] }), jsx("div", { className: styles.contextItem, onClick: () => addPrimitive('sphere'), children: jsx("span", { children: t('modelEditorViewport.addSphere', 'Add Sphere') }) }), jsx("div", { className: styles.contextItem, onClick: () => addSceneLight('point'), children: jsx("span", { children: t('modelEditorViewport.addPointLight', 'Add Point Light') }) }), jsx("div", { className: styles.contextSep }), jsxs("div", { className: styles.contextItem, onClick: duplicateSelected, style: !selectedNode ? { opacity: 0.4, pointerEvents: 'none' } : {}, children: [jsx("span", { children: t('modelEditorViewport.duplicate', 'Duplicate') }), jsx("span", { className: styles.contextKey, children: "Shift+D" })] }), jsxs("div", { className: styles.contextItem, onClick: deleteSelected, style: !selectedNode ? { opacity: 0.4, pointerEvents: 'none' } : {}, children: [jsx("span", { children: t('modelEditorViewport.delete', 'Delete') }), jsx("span", { className: styles.contextKey, children: "Del" })] }), jsx("div", { className: styles.contextSep }), jsxs("div", { className: styles.contextItem, onClick: () => {
|
|
21
32
|
if (rootObjectRef.current) {
|
|
22
33
|
focusOnObject(rootObjectRef.current);
|
|
23
34
|
}
|
|
24
|
-
}, children: [jsx("span", { children:
|
|
25
|
-
});
|
|
26
|
-
ModelEditorViewport.displayName = 'ModelEditorViewport';
|
|
35
|
+
}, children: [jsx("span", { children: t('modelEditorViewport.focusCamera', 'Focus Camera') }), jsx("span", { className: styles.contextKey, children: "F" })] }), jsx("div", { className: styles.contextItem, onClick: () => setSnapEnabled((v) => !v), children: jsxs("span", { children: [snapEnabled && jsx(NiceIcon, { name: "check", size: 14 }), ' ', t('modelEditorViewport.snapToGrid', 'Snap to Grid')] }) })] }))] }));
|
|
36
|
+
}), { displayName: 'ModelEditorViewport' });
|
|
27
37
|
|
|
28
38
|
export { ModelEditorViewport as default };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
2
2
|
import { NiceButton, NiceIcon, NiceSelect, NiceSlider } from '@nice2dev/ui';
|
|
3
3
|
import { useState, useRef, useCallback } from 'react';
|
|
4
|
+
import { useNiceTranslation } from '../core/i18n.js';
|
|
4
5
|
import styles from './ModelViewer.module.css.js';
|
|
5
6
|
import { useModelViewer } from './useModelViewer.js';
|
|
6
7
|
|
|
@@ -12,6 +13,7 @@ const NAV_META = {
|
|
|
12
13
|
};
|
|
13
14
|
const NavLabel = ({ mode }) => (jsxs(Fragment, { children: [jsx(NiceIcon, { name: NAV_META[mode].icon, size: 14 }), " ", NAV_META[mode].label] }));
|
|
14
15
|
const NiceModelViewer = ({ className, showToolbar = true, showStatusBar = true, showInfoPanel = true, toolbarExtra, onFileDrop, children, ...viewerProps }) => {
|
|
16
|
+
const { t } = useNiceTranslation();
|
|
15
17
|
const api = useModelViewer(viewerProps);
|
|
16
18
|
const [panelOpen, setPanelOpen] = useState(false);
|
|
17
19
|
const [dragOver, setDragOver] = useState(false);
|
|
@@ -41,24 +43,24 @@ const NiceModelViewer = ({ className, showToolbar = true, showStatusBar = true,
|
|
|
41
43
|
const idx = navModes.indexOf(api.navigationMode);
|
|
42
44
|
api.setNavigationMode(navModes[(idx + 1) % navModes.length]);
|
|
43
45
|
}, [api]);
|
|
44
|
-
return (jsxs("div", { className: `${styles.root} ${className !== null && className !== void 0 ? className : ''}`, children: [showToolbar && (jsxs("div", { className: styles.toolbar, children: [jsxs(NiceButton, { className: styles.toolBtn, variant: "ghost", size: "sm", title:
|
|
46
|
+
return (jsxs("div", { className: `${styles.root} ${className !== null && className !== void 0 ? className : ''}`, children: [showToolbar && (jsxs("div", { className: styles.toolbar, children: [jsxs(NiceButton, { className: styles.toolBtn, variant: "ghost", size: "sm", title: t('modelViewer.openFile', 'Open file…'), onClick: () => { var _a; return (_a = fileInputRef.current) === null || _a === void 0 ? void 0 : _a.click(); }, children: [jsx(NiceIcon, { name: "folder", size: 14 }), " ", t('modelViewer.open', 'Open')] }), jsx("input", { ref: fileInputRef, type: "file", accept: ".fbx,.glb,.gltf,.obj,.dae,.stl,.ply", style: { display: 'none' }, onChange: (e) => {
|
|
45
47
|
var _a;
|
|
46
48
|
const f = (_a = e.target.files) === null || _a === void 0 ? void 0 : _a[0];
|
|
47
49
|
if (f) {
|
|
48
50
|
api.loadFile(f);
|
|
49
51
|
}
|
|
50
52
|
e.target.value = '';
|
|
51
|
-
} }), jsx("div", { className: styles.toolSep }), jsx(NiceButton, { className: styles.toolBtn, variant: "ghost", size: "sm", title:
|
|
53
|
+
} }), jsx("div", { className: styles.toolSep }), jsx(NiceButton, { className: styles.toolBtn, variant: "ghost", size: "sm", title: t('modelViewer.navigationMode', 'Navigation mode'), onClick: cycleNav, children: jsx(NavLabel, { mode: api.navigationMode }) }), jsx(NiceButton, { className: styles.toolBtn, variant: api.wireframe ? 'primary' : 'ghost', size: "sm", onClick: () => api.setWireframe(!api.wireframe), title: t('modelViewer.wireframe', 'Wireframe'), "aria-label": t('modelViewer.wireframe', 'Wireframe'), children: jsx(NiceIcon, { name: "square", size: 16 }) }), jsx(NiceButton, { className: styles.toolBtn, variant: api.showGrid ? 'primary' : 'ghost', size: "sm", onClick: () => api.setShowGrid(!api.showGrid), title: t('modelViewer.grid', 'Grid'), "aria-label": t('modelViewer.grid', 'Grid'), children: jsx(NiceIcon, { name: "grid", size: 16 }) }), jsx(NiceButton, { className: styles.toolBtn, variant: api.clippingEnabled ? 'primary' : 'ghost', size: "sm", onClick: () => api.setClippingEnabled(!api.clippingEnabled), title: t('modelViewer.clippingPlane', 'Clipping plane'), "aria-label": t('modelViewer.clippingPlane', 'Clipping plane'), children: jsx(NiceIcon, { name: "scissors", size: 16 }) }), api.clippingEnabled && (jsxs(Fragment, { children: [jsx(NiceSelect, { className: styles.animSelect, value: api.clippingAxis, onChange: (val) => api.setClippingAxis(val), options: [
|
|
52
54
|
{ value: 'x', label: 'X' },
|
|
53
55
|
{ value: 'y', label: 'Y' },
|
|
54
56
|
{ value: 'z', label: 'Z' },
|
|
55
|
-
] }), jsx(NiceSlider, { min: -10, max: 10, step: 0.1, value: api.clippingPosition, onChange: (val) => api.setClippingPosition(val), style: { width: 60 } })] })), jsx("div", { className: styles.toolSep }), jsx(NiceButton, { className: styles.toolBtn, variant: "ghost", size: "sm", onClick: api.zoomToFit, title:
|
|
57
|
+
] }), jsx(NiceSlider, { min: -10, max: 10, step: 0.1, value: api.clippingPosition, onChange: (val) => api.setClippingPosition(val), style: { width: 60 } })] })), jsx("div", { className: styles.toolSep }), jsx(NiceButton, { className: styles.toolBtn, variant: "ghost", size: "sm", onClick: api.zoomToFit, title: t('modelViewer.zoomToFit', 'Zoom to fit'), "aria-label": t('modelViewer.zoomToFit', 'Zoom to fit'), children: jsx(NiceIcon, { name: "search", size: 16 }) }), jsx(NiceButton, { className: styles.toolBtn, variant: "ghost", size: "sm", onClick: api.resetCamera, title: t('modelViewer.resetCamera', 'Reset camera'), "aria-label": t('modelViewer.resetCamera', 'Reset camera'), children: jsx(NiceIcon, { name: "home", size: 16 }) }), jsx("div", { className: styles.toolSep }), jsx(NiceButton, { className: styles.toolBtn, variant: "ghost", size: "sm", onClick: () => {
|
|
56
58
|
const url = api.screenshot();
|
|
57
59
|
const a = document.createElement('a');
|
|
58
60
|
a.href = url;
|
|
59
61
|
a.download = 'screenshot.png';
|
|
60
62
|
a.click();
|
|
61
|
-
}, title:
|
|
63
|
+
}, title: t('modelViewer.screenshot', 'Screenshot'), "aria-label": t('modelViewer.screenshot', 'Screenshot'), children: jsx(NiceIcon, { name: "camera", size: 16 }) }), jsx(NiceButton, { className: styles.toolBtn, variant: "ghost", size: "sm", onClick: api.toggleFullscreen, title: t('modelViewer.fullscreen', 'Fullscreen'), "aria-label": t('modelViewer.fullscreen', 'Fullscreen'), children: jsx(NiceIcon, { name: api.isFullscreen ? 'collapse' : 'expand', size: 16 }) }), toolbarExtra, showInfoPanel && (jsx("div", { className: styles.toolRight, children: jsx(NiceButton, { className: `${styles.toolBtn} ${panelOpen ? styles.active : ''}`, variant: panelOpen ? 'primary' : 'ghost', size: "sm", onClick: () => setPanelOpen(!panelOpen), title: t('modelViewer.infoPanel', 'Info panel'), "aria-label": t('modelViewer.infoPanel', 'Info panel'), children: jsx(NiceIcon, { name: "info", size: 16 }) }) }))] })), jsxs("div", { className: styles.viewport, ref: api.mountRef, tabIndex: 0, onDragOver: handleDragOver, onDragLeave: handleDragLeave, onDrop: handleDrop, children: [api.loading && (jsxs("div", { className: styles.loadingOverlay, children: [jsx("span", { className: styles.loadingText, children: "Loading model\u2026" }), jsx("div", { className: styles.loadingBar, children: jsx("div", { className: styles.loadingBarFill, style: { width: `${(api.loadProgress * 100).toFixed(0)}%` } }) })] })), api.error && (jsxs("div", { className: styles.errorOverlay, children: [jsx(NiceIcon, { name: "warning", size: 28 }), jsx("span", { style: { marginTop: 8 }, children: api.error })] })), dragOver && (jsx("div", { className: styles.dropZone, children: t('modelViewer.drop3dModelFileHere', 'Drop 3D model file here') }))] }), jsxs("div", { className: `${styles.sidePanel} ${!panelOpen ? styles.hidden : ''}`, children: [jsxs("div", { className: styles.panelSection, children: [jsx("div", { className: styles.panelTitle, children: t('modelViewer.modelInfo', 'Model Info') }), jsxs("div", { className: styles.panelRow, children: [jsx("label", { children: t('modelViewer.triangles', 'Triangles') }), jsx("span", { children: api.polyCount.toLocaleString() })] }), jsxs("div", { className: styles.panelRow, children: [jsx("label", { children: t('modelViewer.meshes', 'Meshes') }), jsx("span", { children: api.meshCount })] }), api.dimensions && (jsxs(Fragment, { children: [jsxs("div", { className: styles.panelRow, children: [jsx("label", { children: t('modelViewer.width', 'Width') }), jsx("span", { children: api.dimensions.width })] }), jsxs("div", { className: styles.panelRow, children: [jsx("label", { children: t('modelViewer.height', 'Height') }), jsx("span", { children: api.dimensions.height })] }), jsxs("div", { className: styles.panelRow, children: [jsx("label", { children: t('modelViewer.depth', 'Depth') }), jsx("span", { children: api.dimensions.depth })] })] }))] }), jsxs("div", { className: styles.panelSection, children: [jsx("div", { className: styles.panelTitle, children: t('modelViewer.navigation', 'Navigation') }), navModes.map((m) => (jsx(NiceButton, { className: styles.toolBtn, variant: api.navigationMode === m ? 'primary' : 'ghost', size: "sm", onClick: () => api.setNavigationMode(m), children: jsx(NavLabel, { mode: m }) }, m)))] }), jsxs("div", { className: styles.panelSection, children: [jsx("div", { className: styles.panelTitle, children: t('modelViewer.autoRotate', 'Auto Rotate') }), jsx(NiceSlider, { min: 0, max: 10, step: 0.5, value: api.autoRotateSpeed, onChange: (val) => api.setAutoRotateSpeed(val), style: { width: '100%' } })] }), api.animations.length > 0 && (jsxs("div", { className: styles.panelSection, children: [jsx("div", { className: styles.panelTitle, children: t('modelViewer.animations', 'Animations') }), api.animations.map((name, i) => (jsxs(NiceButton, { className: styles.toolBtn, variant: api.activeAnimIdx === i ? 'primary' : 'ghost', size: "sm", onClick: () => api.playAnimation(i), children: [jsx(NiceIcon, { name: "play", size: 14 }), " ", name] }, i)))] }))] }), jsxs("div", { className: `${styles.animBar} ${api.animations.length === 0 ? styles.hidden : ''}`, children: [jsx(NiceButton, { className: styles.toolBtn, variant: "ghost", size: "sm", onClick: api.togglePlay, "aria-label": t('modelViewer.playPause', 'Play/Pause'), children: jsx(NiceIcon, { name: api.isPlaying ? 'pause' : 'play', size: 16 }) }), jsx(NiceSelect, { className: styles.animSelect, value: String(api.activeAnimIdx), onChange: (val) => api.playAnimation(parseInt(val)), options: api.animations.map((n, i) => ({ value: String(i), label: n })) }), jsx(NiceSlider, { className: styles.animSlider, min: 0, max: api.animDuration || 1, step: 0.01, value: api.animTime, onChange: (val) => api.seekAnim(val) }), jsxs("span", { className: styles.toolLabel, children: [api.animTime.toFixed(1), "s / ", api.animDuration.toFixed(1), "s"] })] }), showStatusBar && (jsxs("div", { className: styles.statusBar, children: [jsx("span", { children: api.statusText }), jsxs("div", { className: styles.statusRight, children: [jsx("span", { children: jsx(NavLabel, { mode: api.navigationMode }) }), api.dimensions && (jsxs("span", { children: [api.dimensions.width, "\u00D7", api.dimensions.height, "\u00D7", api.dimensions.depth] }))] })] })), children === null || children === void 0 ? void 0 : children(api)] }));
|
|
62
64
|
};
|
|
63
65
|
|
|
64
66
|
export { NiceModelViewer };
|
|
@@ -2,6 +2,7 @@ import { jsxs, jsx } from 'react/jsx-runtime';
|
|
|
2
2
|
import { NiceTutorialButton, resolveTutorial, NiceIcon, createId } from '@nice2dev/ui';
|
|
3
3
|
import { useState, useMemo, useRef, useEffect, useCallback } from 'react';
|
|
4
4
|
import * as THREE from 'three';
|
|
5
|
+
import { useNiceTranslation } from '../core/i18n.js';
|
|
5
6
|
|
|
6
7
|
/** Built-in guided tour (EN inline + i18n keys → PL and other locales). */
|
|
7
8
|
const ARMATURE_EDITOR_TOUR = [
|
|
@@ -211,6 +212,7 @@ function useArmatureEditor(skeleton, scene, onBoneSelect) {
|
|
|
211
212
|
}
|
|
212
213
|
/* ── Component ── */
|
|
213
214
|
const NiceArmatureEditor = ({ skeleton, skinnedMesh: _skinnedMesh, scene, camera: _camera, onBoneSelect, onPoseChange: _onPoseChange, className = '', tutorial, }) => {
|
|
215
|
+
const { t } = useNiceTranslation();
|
|
214
216
|
const { bones, selectedBoneId, selectBone, setBonePosition: _setBonePosition, setBoneRotation, addBone, removeBone, savePose, loadPose, resetPose, savedPoses, skeletonHelper: _skeletonHelper, } = useArmatureEditor(skeleton, scene, onBoneSelect);
|
|
215
217
|
const [newBoneName, setNewBoneName] = useState('');
|
|
216
218
|
const selectedBone = useMemo(() => bones.find((b) => b.id === selectedBoneId), [bones, selectedBoneId]);
|
|
@@ -228,13 +230,13 @@ const NiceArmatureEditor = ({ skeleton, skinnedMesh: _skinnedMesh, scene, camera
|
|
|
228
230
|
borderRadius: 4,
|
|
229
231
|
}, children: [jsx(NiceIcon, { name: "target", size: 14 }), " ", boneInfo.name] }), childBones.map((child) => renderBoneTree(child, depth + 1))] }, boneInfo.id));
|
|
230
232
|
};
|
|
231
|
-
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:
|
|
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: {
|
|
232
234
|
maxHeight: 200,
|
|
233
235
|
overflowY: 'auto',
|
|
234
236
|
border: '1px solid var(--nice-border, #e2e8f0)',
|
|
235
237
|
borderRadius: 6,
|
|
236
238
|
padding: 4,
|
|
237
|
-
}, children: rootBones.length === 0 ? (jsx("div", { style: { padding: 16, textAlign: 'center', color: 'var(--nice-text-muted, #999)' }, children:
|
|
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: {
|
|
238
240
|
flex: 1,
|
|
239
241
|
padding: '4px 8px',
|
|
240
242
|
borderRadius: 4,
|
|
@@ -242,7 +244,7 @@ const NiceArmatureEditor = ({ skeleton, skinnedMesh: _skinnedMesh, scene, camera
|
|
|
242
244
|
} }), jsx("button", { type: "button", disabled: !newBoneName.trim(), onClick: () => {
|
|
243
245
|
addBone(selectedBoneId, newBoneName.trim());
|
|
244
246
|
setNewBoneName('');
|
|
245
|
-
}, children: "+ Add" }), jsx("button", { type: "button", onClick: () => removeBone(selectedBoneId), "aria-label":
|
|
247
|
+
}, children: "+ 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: {
|
|
246
248
|
display: 'grid',
|
|
247
249
|
gap: 4,
|
|
248
250
|
fontSize: 12,
|
|
@@ -261,7 +263,7 @@ const NiceArmatureEditor = ({ skeleton, skinnedMesh: _skinnedMesh, scene, camera
|
|
|
261
263
|
const rot = selectedBone.localRotation.clone();
|
|
262
264
|
rot.z = parseFloat(e.target.value) * (Math.PI / 180);
|
|
263
265
|
setBoneRotation(selectedBone.id, rot);
|
|
264
|
-
} })] })), savedPoses.length > 0 && (jsxs("div", { style: { display: 'grid', gap: 4 }, children: [jsx("strong", { style: { fontSize: 12 }, children:
|
|
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)))] }))] }));
|
|
265
267
|
};
|
|
266
268
|
|
|
267
269
|
export { NiceArmatureEditor, useArmatureEditor };
|
|
@@ -2,6 +2,7 @@ import { jsxs, jsx } from 'react/jsx-runtime';
|
|
|
2
2
|
import { NiceTutorialButton, resolveTutorial, NiceIcon } from '@nice2dev/ui';
|
|
3
3
|
import { useState, useRef, useEffect, useCallback } from 'react';
|
|
4
4
|
import * as THREE from 'three';
|
|
5
|
+
import { useNiceTranslation } from '../core/i18n.js';
|
|
5
6
|
|
|
6
7
|
/** Built-in guided tour (EN inline + i18n keys → PL and other locales). */
|
|
7
8
|
const MORPH_TARGET_EDITOR_TOUR = [
|
|
@@ -190,6 +191,7 @@ function useMorphTargetEditor(mesh, onInfluenceChange) {
|
|
|
190
191
|
}
|
|
191
192
|
/* ── Component ── */
|
|
192
193
|
const NiceMorphTargetEditor = ({ mesh, onInfluenceChange, className = '', tutorial, }) => {
|
|
194
|
+
const { t } = useNiceTranslation();
|
|
193
195
|
const { targets, setInfluence, createMorphTarget, deleteMorphTarget, resetAllInfluences, isAnimating, startAnimation, stopAnimation, animations, addAnimation: _addAnimation, removeAnimation, } = useMorphTargetEditor(mesh, onInfluenceChange);
|
|
194
196
|
const [newName, setNewName] = useState('');
|
|
195
197
|
const handleCreate = () => {
|
|
@@ -200,17 +202,17 @@ const NiceMorphTargetEditor = ({ mesh, onInfluenceChange, className = '', tutori
|
|
|
200
202
|
createMorphTarget(name);
|
|
201
203
|
setNewName('');
|
|
202
204
|
};
|
|
203
|
-
return (jsxs("div", { className: `nice-morph-target-editor ${className}`, style: { position: 'relative', display: 'grid', gap: 8, fontSize: 13 }, children: [jsx("span", { style: { position: 'absolute', top: 0, right: 0, zIndex: 20 }, children: jsx(NiceTutorialButton, { steps: resolveTutorial(tutorial, MORPH_TARGET_EDITOR_TOUR) }) }), jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' }, children: [jsx("strong", { children:
|
|
205
|
+
return (jsxs("div", { className: `nice-morph-target-editor ${className}`, style: { position: 'relative', display: 'grid', gap: 8, fontSize: 13 }, children: [jsx("span", { style: { position: 'absolute', top: 0, right: 0, zIndex: 20 }, children: jsx(NiceTutorialButton, { steps: resolveTutorial(tutorial, MORPH_TARGET_EDITOR_TOUR) }) }), jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' }, children: [jsx("strong", { children: t('morphTargetEditor.morphTargets', 'Morph Targets') }), jsx("button", { type: "button", onClick: resetAllInfluences, style: { fontSize: 11 }, children: t('morphTargetEditor.resetAll', 'Reset All') })] }), targets.length === 0 && (jsx("div", { style: { opacity: 0.6, fontStyle: 'italic' }, children: t('morphTargetEditor.noMorphTargetsFound', 'No morph targets found.') })), targets.map((target) => (jsxs("div", { style: {
|
|
204
206
|
display: 'grid',
|
|
205
207
|
gridTemplateColumns: '1fr 50px auto',
|
|
206
208
|
alignItems: 'center',
|
|
207
209
|
gap: 6,
|
|
208
|
-
}, children: [jsxs("div", { children: [jsx("label", { style: { display: 'block', fontSize: 11, marginBottom: 2 }, children: target.name }), jsx("input", { type: "range", min: 0, max: 1, step: 0.01, value: target.influence, onChange: (e) => setInfluence(target.index, parseFloat(e.target.value)), style: { width: '100%' } })] }), jsx("span", { style: { fontSize: 11, textAlign: 'right' }, children: target.influence.toFixed(2) }), jsx("button", { type: "button", onClick: () => deleteMorphTarget(target.index), title:
|
|
210
|
+
}, children: [jsxs("div", { children: [jsx("label", { style: { display: 'block', fontSize: 11, marginBottom: 2 }, children: target.name }), jsx("input", { type: "range", min: 0, max: 1, step: 0.01, value: target.influence, onChange: (e) => setInfluence(target.index, parseFloat(e.target.value)), style: { width: '100%' } })] }), jsx("span", { style: { fontSize: 11, textAlign: 'right' }, children: target.influence.toFixed(2) }), jsx("button", { type: "button", onClick: () => deleteMorphTarget(target.index), title: t('morphTargetEditor.delete', 'Delete'), "aria-label": t('morphTargetEditor.delete', 'Delete'), style: { background: 'none', border: 'none', cursor: 'pointer' }, children: jsx(NiceIcon, { name: "trash", size: 14 }) })] }, target.index))), jsxs("div", { style: { display: 'flex', gap: 4, marginTop: 4 }, children: [jsx("input", { type: "text", value: newName, onChange: (e) => setNewName(e.target.value), placeholder: t('morphTargetEditor.newMorphTargetName', 'New morph target name…'), onKeyDown: (e) => e.key === 'Enter' && handleCreate(), style: {
|
|
209
211
|
flex: 1,
|
|
210
212
|
padding: '4px 8px',
|
|
211
213
|
border: '1px solid var(--nice-border, #e2e8f0)',
|
|
212
214
|
borderRadius: 4,
|
|
213
|
-
} }), jsx("button", { type: "button", onClick: handleCreate, children: "+ Add" })] }), animations.length > 0 && (jsxs("div", { style: { marginTop: 8 }, children: [jsx("strong", { style: { fontSize: 12 }, children:
|
|
215
|
+
} }), jsx("button", { type: "button", onClick: handleCreate, children: "+ Add" })] }), animations.length > 0 && (jsxs("div", { style: { marginTop: 8 }, children: [jsx("strong", { style: { fontSize: 12 }, children: t('morphTargetEditor.animations', 'Animations') }), animations.map((anim) => (jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 4, marginTop: 4 }, children: [jsx("span", { style: { flex: 1 }, children: anim.name }), jsx("button", { type: "button", onClick: () => (isAnimating ? stopAnimation() : startAnimation(anim)), style: { fontSize: 11 }, "aria-label": isAnimating ? 'Stop' : 'Play', children: jsx(NiceIcon, { name: isAnimating ? 'stop' : 'play', size: 14 }) }), jsx("button", { type: "button", onClick: () => removeAnimation(anim.name), style: { fontSize: 11 }, "aria-label": t('morphTargetEditor.removeAnimation', 'Remove animation'), children: jsx(NiceIcon, { name: "trash", size: 14 }) })] }, anim.name)))] }))] }));
|
|
214
216
|
};
|
|
215
217
|
|
|
216
218
|
export { NiceMorphTargetEditor, useMorphTargetEditor };
|