@nice2dev/ui-3d 1.1.10 → 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.
Files changed (86) hide show
  1. package/CHANGELOG.md +173 -0
  2. package/README.md +11 -15
  3. package/dist/cjs/core/editorAsk.js +48 -0
  4. package/dist/cjs/core/i18n.js +7 -3
  5. package/dist/cjs/core/threeCompat.js +52 -0
  6. package/dist/cjs/dance/DanceScoreEngine.js +7 -0
  7. package/dist/cjs/dance/PoseDetector.js +116 -64
  8. package/dist/cjs/index.js +0 -3
  9. package/dist/cjs/material/NiceMaterialEditor.js +7 -3
  10. package/dist/cjs/material/materialEditorTypes.js +1 -1
  11. package/dist/cjs/material/materialNodeDefinitions.js +14 -11
  12. package/dist/cjs/model/ModelEditor.js +3 -5
  13. package/dist/cjs/model/ModelEditorLeftPanel.js +10 -4
  14. package/dist/cjs/model/ModelEditorMenuBar.js +7 -5
  15. package/dist/cjs/model/ModelEditorRightPanel.js +18 -16
  16. package/dist/cjs/model/ModelEditorSubComponents.js +8 -6
  17. package/dist/cjs/model/ModelEditorTimeline.js +6 -4
  18. package/dist/cjs/model/ModelEditorToolbar.js +6 -4
  19. package/dist/cjs/model/ModelEditorViewport.js +16 -6
  20. package/dist/cjs/model/ModelViewer.js +6 -7
  21. package/dist/cjs/model/NiceArmatureEditor.js +8 -6
  22. package/dist/cjs/model/NiceMorphTargetEditor.js +5 -3
  23. package/dist/cjs/model/NiceOctree.js +3 -1
  24. package/dist/cjs/model/NicePhysicsSimulation.js +7 -5
  25. package/dist/cjs/model/NiceProceduralGeometry.js +5 -3
  26. package/dist/cjs/model/NiceTerrainEditor.js +10 -8
  27. package/dist/cjs/model/NiceWeightPainter.js +6 -4
  28. package/dist/cjs/model/NiceXRPreview.js +7 -5
  29. package/dist/cjs/model/useModelEditor.js +30 -18
  30. package/dist/cjs/model/useModelViewer.js +13 -9
  31. package/dist/cjs/nice2dev-ui-3d.css +1 -1
  32. package/dist/cjs/particle/NiceParticleEditor.js +14 -9
  33. package/dist/cjs/rendering/NiceCascadedShadows.js +3 -1
  34. package/dist/cjs/rendering/NiceRenderExport.js +11 -5
  35. package/dist/cjs/rendering/NiceSSAO.js +3 -1
  36. package/dist/cjs/rendering/NiceSSR.js +3 -1
  37. package/dist/cjs/rendering/NiceWebGPURenderer.js +7 -4
  38. package/dist/cjs/uv/NiceUVEditor.js +16 -12
  39. package/dist/cjs/uv/uvEditorTypes.js +1 -0
  40. package/dist/esm/core/editorAsk.js +45 -0
  41. package/dist/esm/core/i18n.js +8 -4
  42. package/dist/esm/core/threeCompat.js +30 -0
  43. package/dist/esm/dance/DanceScoreEngine.js +7 -0
  44. package/dist/esm/dance/PoseDetector.js +118 -65
  45. package/dist/esm/index.js +3 -3
  46. package/dist/esm/material/NiceMaterialEditor.js +7 -3
  47. package/dist/esm/material/materialEditorTypes.js +1 -1
  48. package/dist/esm/material/materialNodeDefinitions.js +14 -11
  49. package/dist/esm/model/ModelEditor.js +4 -5
  50. package/dist/esm/model/ModelEditorLeftPanel.js +10 -4
  51. package/dist/esm/model/ModelEditorMenuBar.js +7 -5
  52. package/dist/esm/model/ModelEditorRightPanel.js +18 -16
  53. package/dist/esm/model/ModelEditorSubComponents.js +8 -6
  54. package/dist/esm/model/ModelEditorTimeline.js +6 -4
  55. package/dist/esm/model/ModelEditorToolbar.js +6 -4
  56. package/dist/esm/model/ModelEditorViewport.js +16 -6
  57. package/dist/esm/model/ModelViewer.js +7 -7
  58. package/dist/esm/model/NiceArmatureEditor.js +9 -7
  59. package/dist/esm/model/NiceMorphTargetEditor.js +5 -3
  60. package/dist/esm/model/NiceOctree.js +3 -1
  61. package/dist/esm/model/NicePhysicsSimulation.js +7 -5
  62. package/dist/esm/model/NiceProceduralGeometry.js +5 -3
  63. package/dist/esm/model/NiceTerrainEditor.js +10 -8
  64. package/dist/esm/model/NiceWeightPainter.js +6 -4
  65. package/dist/esm/model/NiceXRPreview.js +7 -5
  66. package/dist/esm/model/useModelEditor.js +30 -18
  67. package/dist/esm/model/useModelViewer.js +13 -9
  68. package/dist/esm/nice2dev-ui-3d.css +1 -1
  69. package/dist/esm/particle/NiceParticleEditor.js +14 -9
  70. package/dist/esm/rendering/NiceCascadedShadows.js +3 -1
  71. package/dist/esm/rendering/NiceRenderExport.js +11 -5
  72. package/dist/esm/rendering/NiceSSAO.js +3 -1
  73. package/dist/esm/rendering/NiceSSR.js +3 -1
  74. package/dist/esm/rendering/NiceWebGPURenderer.js +7 -4
  75. package/dist/esm/uv/NiceUVEditor.js +16 -12
  76. package/dist/esm/uv/uvEditorTypes.js +1 -0
  77. package/dist/types/core/editorAsk.d.ts +26 -0
  78. package/dist/types/core/i18n.d.ts +1 -17
  79. package/dist/types/core/threeCompat.d.ts +14 -0
  80. package/dist/types/dance/DanceScoreEngine.d.ts +0 -6
  81. package/dist/types/dance/PoseDetector.d.ts +14 -13
  82. package/dist/types/index.d.ts +4 -4
  83. package/dist/types/model/ModelEditor.d.ts +0 -4
  84. package/dist/types/model/ModelViewer.d.ts +0 -4
  85. package/dist/types/model/useModelEditor.d.ts +3 -0
  86. package/package.json +3 -3
@@ -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
- const ModelEditorViewport = React.memo(({ api }) => {
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: [jsxs("span", { className: styles.viewportBadge, children: [editorMode.toUpperCase(), " MODE"] }), 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) => {
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"] })) : (jsxs(Fragment, { children: [jsx(NiceIcon, { name: "globe", size: 14 }), " World"] })) }) }), dragOver && (jsx("div", { className: styles.viewportDropZone, children: jsx("span", { children: "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: "Add Cube" }), jsx("span", { className: styles.contextKey, children: "Shift+A" })] }), jsx("div", { className: styles.contextItem, onClick: () => addPrimitive('sphere'), children: jsx("span", { children: "Add Sphere" }) }), jsx("div", { className: styles.contextItem, onClick: () => addSceneLight('point'), children: jsx("span", { children: "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: "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: "Delete" }), jsx("span", { className: styles.contextKey, children: "Del" })] }), jsx("div", { className: styles.contextSep }), jsxs("div", { className: styles.contextItem, onClick: () => {
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: "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 }), " Snap to Grid"] }) })] }))] }));
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,26 +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: "Open file\u2026", onClick: () => { var _a; return (_a = fileInputRef.current) === null || _a === void 0 ? void 0 : _a.click(); }, children: [jsx(NiceIcon, { name: "folder", size: 14 }), " Open"] }), jsx("input", { ref: fileInputRef, type: "file", accept: ".fbx,.glb,.gltf,.obj,.dae,.stl,.ply", style: { display: 'none' }, onChange: (e) => {
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: "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: "Wireframe", "aria-label": "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: "Grid", "aria-label": "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: "Clipping plane", "aria-label": "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: [
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: "Zoom to fit", "aria-label": "Zoom to fit", children: jsx(NiceIcon, { name: "search", size: 16 }) }), jsx(NiceButton, { className: styles.toolBtn, variant: "ghost", size: "sm", onClick: api.resetCamera, title: "Reset camera", "aria-label": "Reset camera", children: jsx(NiceIcon, { name: "home", size: 16 }) }), jsx("div", { className: styles.toolSep }), jsx(NiceButton, { className: styles.toolBtn, variant: "ghost", size: "sm", onClick: () => {
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: "Screenshot", "aria-label": "Screenshot", children: jsx(NiceIcon, { name: "camera", size: 16 }) }), jsx(NiceButton, { className: styles.toolBtn, variant: "ghost", size: "sm", onClick: api.toggleFullscreen, title: "Fullscreen", "aria-label": "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: "Info panel", "aria-label": "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: "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: "Model Info" }), jsxs("div", { className: styles.panelRow, children: [jsx("label", { children: "Triangles" }), jsx("span", { children: api.polyCount.toLocaleString() })] }), jsxs("div", { className: styles.panelRow, children: [jsx("label", { children: "Meshes" }), jsx("span", { children: api.meshCount })] }), api.dimensions && (jsxs(Fragment, { children: [jsxs("div", { className: styles.panelRow, children: [jsx("label", { children: "Width" }), jsx("span", { children: api.dimensions.width })] }), jsxs("div", { className: styles.panelRow, children: [jsx("label", { children: "Height" }), jsx("span", { children: api.dimensions.height })] }), jsxs("div", { className: styles.panelRow, children: [jsx("label", { children: "Depth" }), jsx("span", { children: api.dimensions.depth })] })] }))] }), jsxs("div", { className: styles.panelSection, children: [jsx("div", { className: styles.panelTitle, children: "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: "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: "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": "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)] }));
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
- /** @deprecated Use NiceModelViewer */
64
- const ModelViewer = NiceModelViewer;
65
65
 
66
- export { ModelViewer, NiceModelViewer };
66
+ export { NiceModelViewer };
@@ -1,7 +1,8 @@
1
1
  import { jsxs, jsx } from 'react/jsx-runtime';
2
- import { NiceTutorialButton, resolveTutorial, NiceIcon } from '@nice2dev/ui';
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 = [
@@ -145,7 +146,7 @@ function useArmatureEditor(skeleton, scene, onBoneSelect) {
145
146
  }
146
147
  }
147
148
  const pose = {
148
- id: crypto.randomUUID(),
149
+ id: createId(),
149
150
  name,
150
151
  timestamp: Date.now(),
151
152
  boneTransforms: transforms,
@@ -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]);
@@ -222,19 +224,19 @@ const NiceArmatureEditor = ({ skeleton, skinnedMesh: _skinnedMesh, scene, camera
222
224
  padding: '4px 8px',
223
225
  paddingLeft: depth * 16 + 8,
224
226
  background: isSelected ? 'var(--nice-color-accent, #6366f1)' : 'transparent',
225
- color: isSelected ? 'var(--nice-text-on-primary, #fff)' : 'inherit',
227
+ color: isSelected ? 'var(--nice-color-white, #fff)' : 'inherit',
226
228
  cursor: 'pointer',
227
229
  fontSize: 13,
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: "Reset Pose", children: [jsx(NiceIcon, { name: "refresh", size: 14 }), " Reset"] }), jsxs("button", { type: "button", onClick: () => savePose(`Pose ${savedPoses.length + 1}`), title: "Save Pose", children: [jsx(NiceIcon, { name: "save", size: 14 }), " Save Pose"] })] }), jsx("div", { style: {
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: "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: "New bone name", style: {
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": "Remove bone", children: jsx(NiceIcon, { name: "trash", size: 14 }) })] })), selectedBone && (jsxs("div", { style: {
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: "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)))] }))] }));
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: "Morph Targets" }), jsx("button", { type: "button", onClick: resetAllInfluences, style: { fontSize: 11 }, children: "Reset All" })] }), targets.length === 0 && (jsx("div", { style: { opacity: 0.6, fontStyle: 'italic' }, children: "No morph targets found." })), targets.map((target) => (jsxs("div", { style: {
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: "Delete", "aria-label": "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: "New morph target name\u2026", onKeyDown: (e) => e.key === 'Enter' && handleCreate(), style: {
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: "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": "Remove animation", children: jsx(NiceIcon, { name: "trash", size: 14 }) })] }, anim.name)))] }))] }));
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 };
@@ -2,6 +2,7 @@ import { jsxs, jsx } from 'react/jsx-runtime';
2
2
  import { NiceIcon } from '@nice2dev/ui';
3
3
  import { useEffect, useMemo, useState, useRef, useCallback } from 'react';
4
4
  import * as THREE from 'three';
5
+ import { useNiceTranslation } from '../core/i18n.js';
5
6
 
6
7
  /* ── Octree implementation ── */
7
8
  const DEFAULT_CONFIG = {
@@ -324,6 +325,7 @@ function useOctree(scene, configOverride) {
324
325
  }
325
326
  /* ── Component ── */
326
327
  const NiceOctree = ({ scene, config, showDebug: initialShowDebug, className = '', }) => {
328
+ const { t } = useNiceTranslation();
327
329
  const octree = useOctree(scene, config);
328
330
  useEffect(() => {
329
331
  if (initialShowDebug !== undefined) {
@@ -337,7 +339,7 @@ const NiceOctree = ({ scene, config, showDebug: initialShowDebug, className = ''
337
339
  borderRadius: 4,
338
340
  padding: '6px 12px',
339
341
  cursor: 'pointer',
340
- }, children: [jsx(NiceIcon, { name: "zap", size: 14 }), " Build Octree"] }), jsxs("div", { style: { fontSize: 11, opacity: 0.7 }, children: ["Nodes: ", octree.nodeCount, " | Objects: ", octree.objectCount] }), jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 6 }, children: [jsx("input", { type: "checkbox", checked: octree.showDebug, onChange: (e) => octree.setShowDebug(e.target.checked) }), "Show debug wireframes"] })] }));
342
+ }, children: [jsx(NiceIcon, { name: "zap", size: 14 }), " ", t('octree.buildOctree', 'Build Octree')] }), jsxs("div", { style: { fontSize: 11, opacity: 0.7 }, children: ["Nodes: ", octree.nodeCount, " | Objects: ", octree.objectCount] }), jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 6 }, children: [jsx("input", { type: "checkbox", checked: octree.showDebug, onChange: (e) => octree.setShowDebug(e.target.checked) }), t('octree.showDebugWireframes', 'Show debug wireframes')] })] }));
341
343
  };
342
344
 
343
345
  export { NiceOctree, useOctree };
@@ -2,6 +2,7 @@ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
2
2
  import { 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
  function inferColliderShape(object) {
7
8
  if (object instanceof THREE.Mesh) {
@@ -261,19 +262,20 @@ function usePhysicsSimulation(scene, onSimulationStep) {
261
262
  }
262
263
  /* ── Component ── */
263
264
  const NicePhysicsSimulation = ({ scene, selectedObject, onSimulationStep, className = '', }) => {
265
+ const { t } = useNiceTranslation();
264
266
  const { worldConfig, setWorldConfig, bodies, addBody, removeBody, updateBody, getBodyForObject, isRunning, play, pause, step, reset, debugDraw, setDebugDraw, } = usePhysicsSimulation(scene, onSimulationStep);
265
267
  const selectedBody = selectedObject
266
268
  ? getBodyForObject(selectedObject.name || selectedObject.uuid)
267
269
  : undefined;
268
- return (jsxs("div", { className: `nice-physics-simulation ${className}`, style: { display: 'grid', gap: 8, fontSize: 13 }, children: [jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' }, children: [jsxs("strong", { children: [jsx(NiceIcon, { name: "zap", size: 14 }), " Physics"] }), jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 4, fontSize: 11 }, children: [jsx("input", { type: "checkbox", checked: debugDraw, onChange: (e) => setDebugDraw(e.target.checked) }), "Debug"] })] }), jsxs("div", { style: { display: 'flex', gap: 4 }, children: [jsx("button", { type: "button", onClick: isRunning ? pause : play, style: {
270
+ return (jsxs("div", { className: `nice-physics-simulation ${className}`, style: { display: 'grid', gap: 8, fontSize: 13 }, children: [jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' }, children: [jsxs("strong", { children: [jsx(NiceIcon, { name: "zap", size: 14 }), " ", t('physicsSimulation.physics', 'Physics')] }), jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 4, fontSize: 11 }, children: [jsx("input", { type: "checkbox", checked: debugDraw, onChange: (e) => setDebugDraw(e.target.checked) }), t('physicsSimulation.debug', 'Debug')] })] }), jsxs("div", { style: { display: 'flex', gap: 4 }, children: [jsx("button", { type: "button", onClick: isRunning ? pause : play, style: {
269
271
  flex: 1,
270
272
  background: isRunning ? 'var(--nice-danger, #ef4444)' : 'var(--nice-success, #22c55e)',
271
- color: 'var(--nice-text-on-primary, #fff)',
273
+ color: 'var(--nice-color-white, #fff)',
272
274
  border: 'none',
273
275
  borderRadius: 4,
274
276
  padding: '5px 8px',
275
277
  cursor: 'pointer',
276
- }, children: isRunning ? (jsxs(Fragment, { children: [jsx(NiceIcon, { name: "pause", size: 14 }), " Pause"] })) : (jsxs(Fragment, { children: [jsx(NiceIcon, { name: "play", size: 14 }), " Play"] })) }), jsxs("button", { type: "button", onClick: step, disabled: isRunning, style: { padding: '5px 8px', cursor: 'pointer' }, children: [jsx(NiceIcon, { name: "skip-forward", size: 14 }), " Step"] }), jsxs("button", { type: "button", onClick: reset, style: { padding: '5px 8px', cursor: 'pointer' }, children: [jsx(NiceIcon, { name: "refresh", size: 14 }), " Reset"] })] }), jsxs("label", { children: ["Gravity Y: ", worldConfig.gravity.y.toFixed(1), jsx("input", { type: "range", min: -20, max: 0, step: 0.1, value: worldConfig.gravity.y, onChange: (e) => setWorldConfig({ gravity: new THREE.Vector3(0, parseFloat(e.target.value), 0) }), style: { width: '100%' } })] }), selectedObject && (jsxs("fieldset", { style: {
278
+ }, children: isRunning ? (jsxs(Fragment, { children: [jsx(NiceIcon, { name: "pause", size: 14 }), " ", t('physicsSimulation.pause', 'Pause')] })) : (jsxs(Fragment, { children: [jsx(NiceIcon, { name: "play", size: 14 }), " ", t('physicsSimulation.play', 'Play')] })) }), jsxs("button", { type: "button", onClick: step, disabled: isRunning, style: { padding: '5px 8px', cursor: 'pointer' }, children: [jsx(NiceIcon, { name: "skip-forward", size: 14 }), " ", t('physicsSimulation.step', 'Step')] }), jsxs("button", { type: "button", onClick: reset, style: { padding: '5px 8px', cursor: 'pointer' }, children: [jsx(NiceIcon, { name: "refresh", size: 14 }), " ", t('physicsSimulation.reset', 'Reset')] })] }), jsxs("label", { children: ["Gravity Y: ", worldConfig.gravity.y.toFixed(1), jsx("input", { type: "range", min: -20, max: 0, step: 0.1, value: worldConfig.gravity.y, onChange: (e) => setWorldConfig({ gravity: new THREE.Vector3(0, parseFloat(e.target.value), 0) }), style: { width: '100%' } })] }), selectedObject && (jsxs("fieldset", { style: {
277
279
  border: '1px solid var(--nice-border, #e2e8f0)',
278
280
  borderRadius: 6,
279
281
  padding: 6,
@@ -286,7 +288,7 @@ const NicePhysicsSimulation = ({ scene, selectedObject, onSimulationStep, classN
286
288
  borderRadius: 4,
287
289
  padding: '5px 8px',
288
290
  cursor: 'pointer',
289
- }, children: "+ Add Rigid Body" })) : (jsxs(Fragment, { children: [jsxs("label", { children: ["Type:", jsxs("select", { value: selectedBody.bodyType, onChange: (e) => updateBody(selectedBody.id, { bodyType: e.target.value }), style: { marginLeft: 8, width: '100%' }, children: [jsx("option", { value: "dynamic", children: "Dynamic" }), jsx("option", { value: "static", children: "Static" }), jsx("option", { value: "kinematic", children: "Kinematic" })] })] }), jsxs("label", { children: ["Shape:", jsxs("select", { value: selectedBody.colliderShape, onChange: (e) => updateBody(selectedBody.id, { colliderShape: e.target.value }), style: { marginLeft: 8, width: '100%' }, children: [jsx("option", { value: "box", children: "Box" }), jsx("option", { value: "sphere", children: "Sphere" }), jsx("option", { value: "capsule", children: "Capsule" }), jsx("option", { value: "convexHull", children: "Convex Hull" }), jsx("option", { value: "trimesh", children: "Trimesh" })] })] }), jsxs("label", { children: ["Mass: ", selectedBody.mass.toFixed(1), jsx("input", { type: "range", min: 0.1, max: 100, step: 0.1, value: selectedBody.mass, onChange: (e) => updateBody(selectedBody.id, { mass: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Restitution: ", selectedBody.restitution.toFixed(2), jsx("input", { type: "range", min: 0, max: 1, step: 0.01, value: selectedBody.restitution, onChange: (e) => updateBody(selectedBody.id, { restitution: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Friction: ", selectedBody.friction.toFixed(2), jsx("input", { type: "range", min: 0, max: 1, step: 0.01, value: selectedBody.friction, onChange: (e) => updateBody(selectedBody.id, { friction: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 6 }, children: [jsx("input", { type: "checkbox", checked: selectedBody.isSensor, onChange: (e) => updateBody(selectedBody.id, { isSensor: e.target.checked }) }), "Sensor (trigger only)"] }), jsx("button", { type: "button", onClick: () => removeBody(selectedBody.id), style: {
291
+ }, children: "+ Add Rigid Body" })) : (jsxs(Fragment, { children: [jsxs("label", { children: ["Type:", jsxs("select", { value: selectedBody.bodyType, onChange: (e) => updateBody(selectedBody.id, { bodyType: e.target.value }), style: { marginLeft: 8, width: '100%' }, children: [jsx("option", { value: "dynamic", children: t('physicsSimulation.dynamic', 'Dynamic') }), jsx("option", { value: "static", children: t('physicsSimulation.static', 'Static') }), jsx("option", { value: "kinematic", children: t('physicsSimulation.kinematic', 'Kinematic') })] })] }), jsxs("label", { children: ["Shape:", jsxs("select", { value: selectedBody.colliderShape, onChange: (e) => updateBody(selectedBody.id, { colliderShape: e.target.value }), style: { marginLeft: 8, width: '100%' }, children: [jsx("option", { value: "box", children: "Box" }), jsx("option", { value: "sphere", children: t('physicsSimulation.sphere', 'Sphere') }), jsx("option", { value: "capsule", children: t('physicsSimulation.capsule', 'Capsule') }), jsx("option", { value: "convexHull", children: t('physicsSimulation.convexHull', 'Convex Hull') }), jsx("option", { value: "trimesh", children: t('physicsSimulation.trimesh', 'Trimesh') })] })] }), jsxs("label", { children: ["Mass: ", selectedBody.mass.toFixed(1), jsx("input", { type: "range", min: 0.1, max: 100, step: 0.1, value: selectedBody.mass, onChange: (e) => updateBody(selectedBody.id, { mass: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Restitution: ", selectedBody.restitution.toFixed(2), jsx("input", { type: "range", min: 0, max: 1, step: 0.01, value: selectedBody.restitution, onChange: (e) => updateBody(selectedBody.id, { restitution: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Friction: ", selectedBody.friction.toFixed(2), jsx("input", { type: "range", min: 0, max: 1, step: 0.01, value: selectedBody.friction, onChange: (e) => updateBody(selectedBody.id, { friction: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 6 }, children: [jsx("input", { type: "checkbox", checked: selectedBody.isSensor, onChange: (e) => updateBody(selectedBody.id, { isSensor: e.target.checked }) }), "Sensor (trigger only)"] }), jsx("button", { type: "button", onClick: () => removeBody(selectedBody.id), style: {
290
292
  background: 'var(--nice-danger, #ef4444)',
291
293
  color: 'var(--nice-text-on-primary, #fff)',
292
294
  border: 'none',
@@ -294,7 +296,7 @@ const NicePhysicsSimulation = ({ scene, selectedObject, onSimulationStep, classN
294
296
  padding: '4px 8px',
295
297
  cursor: 'pointer',
296
298
  fontSize: 11,
297
- }, children: "Remove Rigid Body" })] }))] })), jsxs("div", { style: { fontSize: 11, opacity: 0.7 }, children: ["Total bodies: ", bodies.length] })] }));
299
+ }, children: t('physicsSimulation.removeRigidBody', 'Remove Rigid Body') })] }))] })), jsxs("div", { style: { fontSize: 11, opacity: 0.7 }, children: ["Total bodies: ", bodies.length] })] }));
298
300
  };
299
301
 
300
302
  export { NicePhysicsSimulation, usePhysicsSimulation };
@@ -2,6 +2,7 @@ import { jsxs, jsx } from 'react/jsx-runtime';
2
2
  import { NiceIcon } from '@nice2dev/ui';
3
3
  import { useState, useCallback } from 'react';
4
4
  import * as THREE from 'three';
5
+ import { useNiceTranslation } from '../core/i18n.js';
5
6
 
6
7
  /* ── Generators ── */
7
8
  function seededRng(seed) {
@@ -292,21 +293,22 @@ function useProceduralGeometry(scene, onGenerated) {
292
293
  }
293
294
  /* ── Component ── */
294
295
  const NiceProceduralGeometry = ({ scene, onGenerated, className = '', }) => {
296
+ const { t } = useNiceTranslation();
295
297
  const { type, setType, treeParams, setTreeParams, rockParams, setRockParams, buildingParams, setBuildingParams, gearParams, setGearParams, generate, } = useProceduralGeometry(scene, onGenerated);
296
- return (jsxs("div", { className: `nice-procedural-geometry ${className}`, style: { display: 'grid', gap: 8, fontSize: 13 }, children: [jsx("strong", { children: "Procedural Geometry" }), jsxs("select", { value: type, onChange: (e) => setType(e.target.value), style: { width: '100%', padding: '4px 8px' }, children: [jsx("option", { value: "tree", children: "Tree" }), jsx("option", { value: "rock", children: "Rock" }), jsx("option", { value: "building", children: "Building" }), jsx("option", { value: "pipe", children: "Pipe" }), jsx("option", { value: "gear", children: "Gear" })] }), type === 'tree' && (jsxs("div", { style: { display: 'grid', gap: 4 }, children: [jsxs("label", { children: ["Height: ", treeParams.trunkHeight.toFixed(1), jsx("input", { type: "range", min: 1, max: 15, step: 0.5, value: treeParams.trunkHeight, onChange: (e) => setTreeParams({ ...treeParams, trunkHeight: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Branches: ", treeParams.branches, jsx("input", { type: "range", min: 1, max: 6, value: treeParams.branches, onChange: (e) => setTreeParams({ ...treeParams, branches: parseInt(e.target.value, 10) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Depth: ", treeParams.depth, jsx("input", { type: "range", min: 1, max: 5, value: treeParams.depth, onChange: (e) => setTreeParams({ ...treeParams, depth: parseInt(e.target.value, 10) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Angle: ", treeParams.branchAngle, "\u00B0", jsx("input", { type: "range", min: 10, max: 80, value: treeParams.branchAngle, onChange: (e) => setTreeParams({ ...treeParams, branchAngle: parseInt(e.target.value, 10) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Seed: ", treeParams.seed, jsx("input", { type: "range", min: 1, max: 999, value: treeParams.seed, onChange: (e) => setTreeParams({ ...treeParams, seed: parseInt(e.target.value, 10) }), style: { width: '100%' } })] })] })), type === 'rock' && (jsxs("div", { style: { display: 'grid', gap: 4 }, children: [jsxs("label", { children: ["Radius: ", rockParams.radius.toFixed(1), jsx("input", { type: "range", min: 0.2, max: 5, step: 0.1, value: rockParams.radius, onChange: (e) => setRockParams({ ...rockParams, radius: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Detail: ", rockParams.detail, jsx("input", { type: "range", min: 1, max: 5, value: rockParams.detail, onChange: (e) => setRockParams({ ...rockParams, detail: parseInt(e.target.value, 10) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Noise: ", rockParams.noiseAmount.toFixed(2), jsx("input", { type: "range", min: 0, max: 0.5, step: 0.01, value: rockParams.noiseAmount, onChange: (e) => setRockParams({ ...rockParams, noiseAmount: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Seed: ", rockParams.seed, jsx("input", { type: "range", min: 1, max: 999, value: rockParams.seed, onChange: (e) => setRockParams({ ...rockParams, seed: parseInt(e.target.value, 10) }), style: { width: '100%' } })] })] })), type === 'building' && (jsxs("div", { style: { display: 'grid', gap: 4 }, children: [jsxs("label", { children: ["Width: ", buildingParams.width, jsx("input", { type: "range", min: 4, max: 20, step: 1, value: buildingParams.width, onChange: (e) => setBuildingParams({ ...buildingParams, width: parseInt(e.target.value, 10) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Floors: ", buildingParams.floors, jsx("input", { type: "range", min: 1, max: 20, value: buildingParams.floors, onChange: (e) => setBuildingParams({ ...buildingParams, floors: parseInt(e.target.value, 10) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Windows/Floor: ", buildingParams.windowsPerFloor, jsx("input", { type: "range", min: 1, max: 10, value: buildingParams.windowsPerFloor, onChange: (e) => setBuildingParams({
298
+ return (jsxs("div", { className: `nice-procedural-geometry ${className}`, style: { display: 'grid', gap: 8, fontSize: 13 }, children: [jsx("strong", { children: t('proceduralGeometry.proceduralGeometry', 'Procedural Geometry') }), jsxs("select", { value: type, onChange: (e) => setType(e.target.value), style: { width: '100%', padding: '4px 8px' }, children: [jsx("option", { value: "tree", children: t('proceduralGeometry.tree', 'Tree') }), jsx("option", { value: "rock", children: t('proceduralGeometry.rock', 'Rock') }), jsx("option", { value: "building", children: t('proceduralGeometry.building', 'Building') }), jsx("option", { value: "pipe", children: t('proceduralGeometry.pipe', 'Pipe') }), jsx("option", { value: "gear", children: t('proceduralGeometry.gear', 'Gear') })] }), type === 'tree' && (jsxs("div", { style: { display: 'grid', gap: 4 }, children: [jsxs("label", { children: ["Height: ", treeParams.trunkHeight.toFixed(1), jsx("input", { type: "range", min: 1, max: 15, step: 0.5, value: treeParams.trunkHeight, onChange: (e) => setTreeParams({ ...treeParams, trunkHeight: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Branches: ", treeParams.branches, jsx("input", { type: "range", min: 1, max: 6, value: treeParams.branches, onChange: (e) => setTreeParams({ ...treeParams, branches: parseInt(e.target.value, 10) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Depth: ", treeParams.depth, jsx("input", { type: "range", min: 1, max: 5, value: treeParams.depth, onChange: (e) => setTreeParams({ ...treeParams, depth: parseInt(e.target.value, 10) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Angle: ", treeParams.branchAngle, "\u00B0", jsx("input", { type: "range", min: 10, max: 80, value: treeParams.branchAngle, onChange: (e) => setTreeParams({ ...treeParams, branchAngle: parseInt(e.target.value, 10) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Seed: ", treeParams.seed, jsx("input", { type: "range", min: 1, max: 999, value: treeParams.seed, onChange: (e) => setTreeParams({ ...treeParams, seed: parseInt(e.target.value, 10) }), style: { width: '100%' } })] })] })), type === 'rock' && (jsxs("div", { style: { display: 'grid', gap: 4 }, children: [jsxs("label", { children: ["Radius: ", rockParams.radius.toFixed(1), jsx("input", { type: "range", min: 0.2, max: 5, step: 0.1, value: rockParams.radius, onChange: (e) => setRockParams({ ...rockParams, radius: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Detail: ", rockParams.detail, jsx("input", { type: "range", min: 1, max: 5, value: rockParams.detail, onChange: (e) => setRockParams({ ...rockParams, detail: parseInt(e.target.value, 10) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Noise: ", rockParams.noiseAmount.toFixed(2), jsx("input", { type: "range", min: 0, max: 0.5, step: 0.01, value: rockParams.noiseAmount, onChange: (e) => setRockParams({ ...rockParams, noiseAmount: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Seed: ", rockParams.seed, jsx("input", { type: "range", min: 1, max: 999, value: rockParams.seed, onChange: (e) => setRockParams({ ...rockParams, seed: parseInt(e.target.value, 10) }), style: { width: '100%' } })] })] })), type === 'building' && (jsxs("div", { style: { display: 'grid', gap: 4 }, children: [jsxs("label", { children: ["Width: ", buildingParams.width, jsx("input", { type: "range", min: 4, max: 20, step: 1, value: buildingParams.width, onChange: (e) => setBuildingParams({ ...buildingParams, width: parseInt(e.target.value, 10) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Floors: ", buildingParams.floors, jsx("input", { type: "range", min: 1, max: 20, value: buildingParams.floors, onChange: (e) => setBuildingParams({ ...buildingParams, floors: parseInt(e.target.value, 10) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Windows/Floor: ", buildingParams.windowsPerFloor, jsx("input", { type: "range", min: 1, max: 10, value: buildingParams.windowsPerFloor, onChange: (e) => setBuildingParams({
297
299
  ...buildingParams,
298
300
  windowsPerFloor: parseInt(e.target.value, 10),
299
301
  }), style: { width: '100%' } })] }), jsxs("label", { children: ["Roof:", jsxs("select", { value: buildingParams.roofStyle, onChange: (e) => setBuildingParams({
300
302
  ...buildingParams,
301
303
  roofStyle: e.target.value,
302
- }), style: { marginLeft: 8 }, children: [jsx("option", { value: "flat", children: "Flat" }), jsx("option", { value: "gable", children: "Gable" }), jsx("option", { value: "hip", children: "Hip" })] })] })] })), type === 'gear' && (jsxs("div", { style: { display: 'grid', gap: 4 }, children: [jsxs("label", { children: ["Outer Radius: ", gearParams.outerRadius.toFixed(1), jsx("input", { type: "range", min: 0.5, max: 5, step: 0.1, value: gearParams.outerRadius, onChange: (e) => setGearParams({ ...gearParams, outerRadius: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Teeth: ", gearParams.teeth, jsx("input", { type: "range", min: 4, max: 30, value: gearParams.teeth, onChange: (e) => setGearParams({ ...gearParams, teeth: parseInt(e.target.value, 10) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Tooth Depth: ", gearParams.toothDepth.toFixed(2), jsx("input", { type: "range", min: 0.05, max: 1, step: 0.05, value: gearParams.toothDepth, onChange: (e) => setGearParams({ ...gearParams, toothDepth: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Thickness: ", gearParams.thickness.toFixed(2), jsx("input", { type: "range", min: 0.1, max: 2, step: 0.1, value: gearParams.thickness, onChange: (e) => setGearParams({ ...gearParams, thickness: parseFloat(e.target.value) }), style: { width: '100%' } })] })] })), jsxs("button", { type: "button", onClick: generate, style: {
304
+ }), style: { marginLeft: 8 }, children: [jsx("option", { value: "flat", children: t('proceduralGeometry.flat', 'Flat') }), jsx("option", { value: "gable", children: t('proceduralGeometry.gable', 'Gable') }), jsx("option", { value: "hip", children: "Hip" })] })] })] })), type === 'gear' && (jsxs("div", { style: { display: 'grid', gap: 4 }, children: [jsxs("label", { children: ["Outer Radius: ", gearParams.outerRadius.toFixed(1), jsx("input", { type: "range", min: 0.5, max: 5, step: 0.1, value: gearParams.outerRadius, onChange: (e) => setGearParams({ ...gearParams, outerRadius: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Teeth: ", gearParams.teeth, jsx("input", { type: "range", min: 4, max: 30, value: gearParams.teeth, onChange: (e) => setGearParams({ ...gearParams, teeth: parseInt(e.target.value, 10) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Tooth Depth: ", gearParams.toothDepth.toFixed(2), jsx("input", { type: "range", min: 0.05, max: 1, step: 0.05, value: gearParams.toothDepth, onChange: (e) => setGearParams({ ...gearParams, toothDepth: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Thickness: ", gearParams.thickness.toFixed(2), jsx("input", { type: "range", min: 0.1, max: 2, step: 0.1, value: gearParams.thickness, onChange: (e) => setGearParams({ ...gearParams, thickness: parseFloat(e.target.value) }), style: { width: '100%' } })] })] })), jsxs("button", { type: "button", onClick: generate, style: {
303
305
  background: 'var(--nice-color-accent, #6366f1)',
304
306
  color: 'var(--nice-text-on-primary, #fff)',
305
307
  border: 'none',
306
308
  borderRadius: 4,
307
309
  padding: '6px 12px',
308
310
  cursor: 'pointer',
309
- }, children: [jsx(NiceIcon, { name: "wand", size: 14 }), " Generate"] })] }));
311
+ }, children: [jsx(NiceIcon, { name: "wand", size: 14 }), " ", t('proceduralGeometry.generate', 'Generate')] })] }));
310
312
  };
311
313
 
312
314
  export { NiceProceduralGeometry, generateBuilding, generateGear, generatePipe, generateRock, generateTree, useProceduralGeometry };
@@ -2,6 +2,7 @@ import { jsxs, jsx, Fragment } 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 TERRAIN_EDITOR_TOUR = [
@@ -319,6 +320,7 @@ function useTerrainEditor(scene, camera, canvas, config = { width: 100, depth: 1
319
320
  }
320
321
  /* ── Component ── */
321
322
  const NiceTerrainEditor = ({ scene, camera, canvas, config: configProp, onTerrainChange, className = '', tutorial, }) => {
323
+ const { t } = useNiceTranslation();
322
324
  const config = {
323
325
  width: 100,
324
326
  depth: 100,
@@ -329,43 +331,43 @@ const NiceTerrainEditor = ({ scene, camera, canvas, config: configProp, onTerrai
329
331
  const { brush, setBrush, isPainting, startPainting, stopPainting, generateFlat, generateNoise, exportHeightmap, textureLayers, setTextureLayer, activeTextureLayer, setActiveTextureLayer, } = useTerrainEditor(scene, camera, canvas, config, onTerrainChange);
330
332
  const [noiseOctaves, setNoiseOctaves] = useState(6);
331
333
  const [noisePersistence, setNoisePersistence] = useState(0.5);
332
- return (jsxs("div", { className: `nice-terrain-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, TERRAIN_EDITOR_TOUR) }) }), jsx("strong", { children: "Terrain Editor" }), jsxs("fieldset", { style: {
334
+ return (jsxs("div", { className: `nice-terrain-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, TERRAIN_EDITOR_TOUR) }) }), jsx("strong", { children: t('terrainEditor.terrainEditor', 'Terrain Editor') }), jsxs("fieldset", { style: {
333
335
  border: '1px solid var(--nice-border, #e2e8f0)',
334
336
  borderRadius: 6,
335
337
  padding: 4,
336
338
  display: 'flex',
337
339
  flexWrap: 'wrap',
338
340
  gap: 2,
339
- }, children: [jsx("legend", { style: { fontSize: 11 }, children: "Brush" }), ['raise', 'lower', 'flatten', 'smooth', 'noise', 'paint'].map((m) => (jsx("button", { type: "button", onClick: () => setBrush({ mode: m }), style: {
341
+ }, children: [jsx("legend", { style: { fontSize: 11 }, children: t('terrainEditor.brush', 'Brush') }), ['raise', 'lower', 'flatten', 'smooth', 'noise', 'paint'].map((m) => (jsx("button", { type: "button", onClick: () => setBrush({ mode: m }), style: {
340
342
  padding: '3px 6px',
341
343
  fontSize: 11,
342
344
  cursor: 'pointer',
343
345
  borderRadius: 4,
344
346
  border: '1px solid var(--nice-border, #e2e8f0)',
345
347
  background: brush.mode === m ? 'var(--nice-color-accent, #6366f1)' : 'transparent',
346
- color: brush.mode === m ? 'var(--nice-text-on-primary, #fff)' : 'inherit',
348
+ color: brush.mode === m ? 'var(--nice-color-white, #fff)' : 'inherit',
347
349
  }, children: m }, m)))] }), jsxs("label", { children: ["Radius: ", brush.radius.toFixed(1), jsx("input", { type: "range", min: 0.5, max: 20, step: 0.5, value: brush.radius, onChange: (e) => setBrush({ radius: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Strength: ", brush.strength.toFixed(2), 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%' } })] }), jsx("button", { type: "button", onClick: isPainting ? stopPainting : startPainting, style: {
348
350
  background: isPainting
349
351
  ? 'var(--nice-danger, #ef4444)'
350
352
  : 'var(--nice-color-accent, #6366f1)',
351
- color: 'var(--nice-text-on-primary, #fff)',
353
+ color: 'var(--nice-color-white, #fff)',
352
354
  border: 'none',
353
355
  borderRadius: 4,
354
356
  padding: '6px 12px',
355
357
  cursor: 'pointer',
356
- }, children: isPainting ? (jsxs(Fragment, { children: [jsx(NiceIcon, { name: "stop", size: 14 }), " Stop Painting"] })) : (jsxs(Fragment, { children: [jsx(NiceIcon, { name: "brush", size: 14 }), " Start Painting"] })) }), jsxs("fieldset", { style: {
358
+ }, children: isPainting ? (jsxs(Fragment, { children: [jsx(NiceIcon, { name: "stop", size: 14 }), " ", t('terrainEditor.stopPainting', 'Stop Painting')] })) : (jsxs(Fragment, { children: [jsx(NiceIcon, { name: "brush", size: 14 }), " ", t('terrainEditor.startPainting', 'Start Painting')] })) }), jsxs("fieldset", { style: {
357
359
  border: '1px solid var(--nice-border, #e2e8f0)',
358
360
  borderRadius: 6,
359
361
  padding: 6,
360
362
  display: 'grid',
361
363
  gap: 4,
362
- }, children: [jsx("legend", { style: { fontSize: 11 }, children: "Procedural" }), jsxs("label", { children: ["Octaves: ", noiseOctaves, jsx("input", { type: "range", min: 1, max: 10, value: noiseOctaves, onChange: (e) => setNoiseOctaves(parseInt(e.target.value, 10)), style: { width: '100%' } })] }), jsxs("label", { children: ["Persistence: ", noisePersistence.toFixed(2), jsx("input", { type: "range", min: 0.1, max: 1, step: 0.05, value: noisePersistence, onChange: (e) => setNoisePersistence(parseFloat(e.target.value)), style: { width: '100%' } })] }), jsxs("div", { style: { display: 'flex', gap: 4 }, children: [jsxs("button", { type: "button", onClick: () => generateNoise(noiseOctaves, noisePersistence), style: { flex: 1 }, children: [jsx(NiceIcon, { name: "mountain", size: 14 }), " Generate Noise"] }), jsxs("button", { type: "button", onClick: generateFlat, style: { flex: 1 }, children: [jsx(NiceIcon, { name: "square", size: 14 }), " Flatten"] })] })] }), brush.mode === 'paint' && (jsxs("fieldset", { style: {
364
+ }, children: [jsx("legend", { style: { fontSize: 11 }, children: t('terrainEditor.procedural', 'Procedural') }), jsxs("label", { children: ["Octaves: ", noiseOctaves, jsx("input", { type: "range", min: 1, max: 10, value: noiseOctaves, onChange: (e) => setNoiseOctaves(parseInt(e.target.value, 10)), style: { width: '100%' } })] }), jsxs("label", { children: ["Persistence: ", noisePersistence.toFixed(2), jsx("input", { type: "range", min: 0.1, max: 1, step: 0.05, value: noisePersistence, onChange: (e) => setNoisePersistence(parseFloat(e.target.value)), style: { width: '100%' } })] }), jsxs("div", { style: { display: 'flex', gap: 4 }, children: [jsxs("button", { type: "button", onClick: () => generateNoise(noiseOctaves, noisePersistence), style: { flex: 1 }, children: [jsx(NiceIcon, { name: "mountain", size: 14 }), ' ', t('terrainEditor.generateNoise', 'Generate Noise')] }), jsxs("button", { type: "button", onClick: generateFlat, style: { flex: 1 }, children: [jsx(NiceIcon, { name: "square", size: 14 }), " ", t('terrainEditor.flatten', 'Flatten')] })] })] }), brush.mode === 'paint' && (jsxs("fieldset", { style: {
363
365
  border: '1px solid var(--nice-border, #e2e8f0)',
364
366
  borderRadius: 6,
365
367
  padding: 6,
366
368
  display: 'grid',
367
369
  gap: 4,
368
- }, children: [jsx("legend", { style: { fontSize: 11 }, children: "Texture Layers" }), textureLayers.map((layer, i) => (jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 6 }, children: [jsx("input", { type: "radio", name: "texLayer", checked: activeTextureLayer === i, onChange: () => setActiveTextureLayer(i) }), jsx("input", { type: "text", value: layer.name, onChange: (e) => setTextureLayer(i, { name: e.target.value }), style: {
370
+ }, children: [jsx("legend", { style: { fontSize: 11 }, children: t('terrainEditor.textureLayers', 'Texture Layers') }), textureLayers.map((layer, i) => (jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 6 }, children: [jsx("input", { type: "radio", name: "texLayer", checked: activeTextureLayer === i, onChange: () => setActiveTextureLayer(i) }), jsx("input", { type: "text", value: layer.name, onChange: (e) => setTextureLayer(i, { name: e.target.value }), style: {
369
371
  flex: 1,
370
372
  padding: '2px 6px',
371
373
  border: '1px solid var(--nice-border, #e2e8f0)',
@@ -378,7 +380,7 @@ const NiceTerrainEditor = ({ scene, camera, canvas, config: configProp, onTerrai
378
380
  link.href = c.toDataURL('image/png');
379
381
  link.click();
380
382
  }
381
- }, style: { fontSize: 11 }, children: [jsx(NiceIcon, { name: "download", size: 14 }), " Export Heightmap PNG"] })] }));
383
+ }, style: { fontSize: 11 }, children: [jsx(NiceIcon, { name: "download", size: 14 }), ' ', t('terrainEditor.exportHeightmapPng', 'Export Heightmap PNG')] })] }));
382
384
  };
383
385
 
384
386
  export { NiceTerrainEditor, useTerrainEditor };
@@ -2,6 +2,7 @@ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
2
2
  import { NiceIcon } from '@nice2dev/ui';
3
3
  import { useEffect, useState, useRef, useCallback } from 'react';
4
4
  import * as THREE from 'three';
5
+ import { useNiceTranslation } from '../core/i18n.js';
5
6
 
6
7
  /* ── Weight painting utilities ── */
7
8
  function getFalloff(distance, radius, type) {
@@ -225,6 +226,7 @@ function updateHeatMapColors(mesh, boneIndex, colors) {
225
226
  }
226
227
  /* ── Component ── */
227
228
  const NiceWeightPainter = ({ skinnedMesh, activeBoneIndex: initialBoneIndex, scene, camera, canvas, onWeightsChange, className = '', }) => {
229
+ const { t } = useNiceTranslation();
228
230
  const { brush, setBrush, isPainting, activeBoneIndex, setActiveBoneIndex, boneNames, startPainting, stopPainting, normalizeWeights, showHeatMap, setShowHeatMap, } = useWeightPainter(skinnedMesh, scene, camera, canvas, onWeightsChange);
229
231
  useEffect(() => {
230
232
  setActiveBoneIndex(initialBoneIndex);
@@ -235,11 +237,11 @@ const NiceWeightPainter = ({ skinnedMesh, activeBoneIndex: initialBoneIndex, sce
235
237
  border: '1px solid var(--nice-border, #e2e8f0)',
236
238
  borderRadius: 6,
237
239
  padding: 4,
238
- }, children: [jsx("legend", { style: { fontSize: 11 }, children: "Mode" }), ['add', 'subtract', 'smooth', 'blur'].map((mode) => (jsx("button", { type: "button", onClick: () => setBrush({ mode }), style: {
240
+ }, children: [jsx("legend", { style: { fontSize: 11 }, children: t('weightPainter.mode', 'Mode') }), ['add', 'subtract', 'smooth', 'blur'].map((mode) => (jsx("button", { type: "button", onClick: () => setBrush({ mode }), style: {
239
241
  flex: 1,
240
242
  padding: '4px 6px',
241
243
  background: brush.mode === mode ? 'var(--nice-color-accent, #6366f1)' : 'transparent',
242
- color: brush.mode === mode ? 'var(--nice-text-on-primary, #fff)' : 'inherit',
244
+ color: brush.mode === mode ? 'var(--nice-color-white, #fff)' : 'inherit',
243
245
  border: '1px solid var(--nice-border, #e2e8f0)',
244
246
  borderRadius: 4,
245
247
  cursor: 'pointer',
@@ -249,12 +251,12 @@ const NiceWeightPainter = ({ skinnedMesh, activeBoneIndex: initialBoneIndex, sce
249
251
  background: isPainting
250
252
  ? 'var(--nice-danger, #ef4444)'
251
253
  : 'var(--nice-color-accent, #6366f1)',
252
- color: 'var(--nice-text-on-primary, #fff)',
254
+ color: 'var(--nice-color-white, #fff)',
253
255
  border: 'none',
254
256
  borderRadius: 4,
255
257
  padding: '6px 12px',
256
258
  cursor: 'pointer',
257
- }, children: isPainting ? (jsxs(Fragment, { children: [jsx(NiceIcon, { name: "stop", size: 14 }), " Stop Painting"] })) : (jsxs(Fragment, { children: [jsx(NiceIcon, { name: "brush", size: 14 }), " Start Painting"] })) }), jsxs("button", { type: "button", onClick: normalizeWeights, title: "Normalize all weights", children: [jsx(NiceIcon, { name: "scale", size: 14 }), " Normalize"] })] }), jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 6 }, children: [jsx("input", { type: "checkbox", checked: showHeatMap, onChange: (e) => setShowHeatMap(e.target.checked) }), "Show weight heat map"] })] }));
259
+ }, children: isPainting ? (jsxs(Fragment, { children: [jsx(NiceIcon, { name: "stop", size: 14 }), " ", t('weightPainter.stopPainting', 'Stop Painting')] })) : (jsxs(Fragment, { children: [jsx(NiceIcon, { name: "brush", size: 14 }), ' ', t('weightPainter.startPainting', 'Start Painting')] })) }), jsxs("button", { type: "button", onClick: normalizeWeights, title: t('weightPainter.normalizeAllWeights', 'Normalize all weights'), children: [jsx(NiceIcon, { name: "scale", size: 14 }), " ", t('weightPainter.normalize', 'Normalize')] })] }), jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 6 }, children: [jsx("input", { type: "checkbox", checked: showHeatMap, onChange: (e) => setShowHeatMap(e.target.checked) }), t('weightPainter.showWeightHeatMap', 'Show weight heat map')] })] }));
258
260
  };
259
261
 
260
262
  export { NiceWeightPainter, useWeightPainter };