@nice2dev/ui-3d 1.2.0 → 1.2.2

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 (76) hide show
  1. package/CHANGELOG.md +142 -0
  2. package/dist/cjs/core/editorAsk.js +48 -0
  3. package/dist/cjs/core/i18n.js +7 -3
  4. package/dist/cjs/dance/DanceScoreEngine.js +7 -0
  5. package/dist/cjs/dance/PoseDetector.js +4 -2
  6. package/dist/cjs/material/NiceMaterialEditor.js +8 -3
  7. package/dist/cjs/material/materialEditorTypes.js +1 -1
  8. package/dist/cjs/material/materialNodeDefinitions.js +14 -11
  9. package/dist/cjs/model/ModelEditor.js +9 -2
  10. package/dist/cjs/model/ModelEditorLeftPanel.js +10 -4
  11. package/dist/cjs/model/ModelEditorMenuBar.js +10 -8
  12. package/dist/cjs/model/ModelEditorRightPanel.js +28 -22
  13. package/dist/cjs/model/ModelEditorSubComponents.js +17 -9
  14. package/dist/cjs/model/ModelEditorTimeline.js +8 -4
  15. package/dist/cjs/model/ModelEditorToolbar.js +6 -4
  16. package/dist/cjs/model/ModelEditorViewport.js +19 -6
  17. package/dist/cjs/model/ModelViewer.js +9 -7
  18. package/dist/cjs/model/NiceArmatureEditor.js +9 -7
  19. package/dist/cjs/model/NiceMorphTargetEditor.js +7 -3
  20. package/dist/cjs/model/NiceOctree.js +9 -4
  21. package/dist/cjs/model/NicePhysicsSimulation.js +7 -5
  22. package/dist/cjs/model/NiceProceduralGeometry.js +6 -4
  23. package/dist/cjs/model/NiceTerrainEditor.js +9 -7
  24. package/dist/cjs/model/NiceWeightPainter.js +6 -4
  25. package/dist/cjs/model/NiceXRPreview.js +9 -4
  26. package/dist/cjs/model/useModelEditor.js +113 -87
  27. package/dist/cjs/model/useModelViewer.js +13 -6
  28. package/dist/cjs/nice2dev-ui-3d.css +1 -1
  29. package/dist/cjs/particle/NiceParticleEditor.js +23 -11
  30. package/dist/cjs/rendering/NiceCascadedShadows.js +3 -1
  31. package/dist/cjs/rendering/NiceRenderExport.js +9 -3
  32. package/dist/cjs/rendering/NiceSSAO.js +4 -1
  33. package/dist/cjs/rendering/NiceSSR.js +4 -1
  34. package/dist/cjs/rendering/NiceWebGPURenderer.js +5 -2
  35. package/dist/cjs/uv/NiceUVEditor.js +16 -12
  36. package/dist/cjs/uv/uvEditorTypes.js +1 -0
  37. package/dist/esm/core/editorAsk.js +45 -0
  38. package/dist/esm/core/i18n.js +8 -4
  39. package/dist/esm/dance/DanceScoreEngine.js +7 -0
  40. package/dist/esm/dance/PoseDetector.js +4 -2
  41. package/dist/esm/material/NiceMaterialEditor.js +8 -3
  42. package/dist/esm/material/materialEditorTypes.js +1 -1
  43. package/dist/esm/material/materialNodeDefinitions.js +14 -11
  44. package/dist/esm/model/ModelEditor.js +9 -2
  45. package/dist/esm/model/ModelEditorLeftPanel.js +10 -4
  46. package/dist/esm/model/ModelEditorMenuBar.js +10 -8
  47. package/dist/esm/model/ModelEditorRightPanel.js +28 -22
  48. package/dist/esm/model/ModelEditorSubComponents.js +17 -9
  49. package/dist/esm/model/ModelEditorTimeline.js +8 -4
  50. package/dist/esm/model/ModelEditorToolbar.js +6 -4
  51. package/dist/esm/model/ModelEditorViewport.js +19 -6
  52. package/dist/esm/model/ModelViewer.js +9 -7
  53. package/dist/esm/model/NiceArmatureEditor.js +9 -7
  54. package/dist/esm/model/NiceMorphTargetEditor.js +7 -3
  55. package/dist/esm/model/NiceOctree.js +9 -4
  56. package/dist/esm/model/NicePhysicsSimulation.js +7 -5
  57. package/dist/esm/model/NiceProceduralGeometry.js +6 -4
  58. package/dist/esm/model/NiceTerrainEditor.js +9 -7
  59. package/dist/esm/model/NiceWeightPainter.js +6 -4
  60. package/dist/esm/model/NiceXRPreview.js +9 -4
  61. package/dist/esm/model/useModelEditor.js +113 -87
  62. package/dist/esm/model/useModelViewer.js +13 -6
  63. package/dist/esm/nice2dev-ui-3d.css +1 -1
  64. package/dist/esm/particle/NiceParticleEditor.js +23 -11
  65. package/dist/esm/rendering/NiceCascadedShadows.js +3 -1
  66. package/dist/esm/rendering/NiceRenderExport.js +9 -3
  67. package/dist/esm/rendering/NiceSSAO.js +4 -1
  68. package/dist/esm/rendering/NiceSSR.js +4 -1
  69. package/dist/esm/rendering/NiceWebGPURenderer.js +5 -2
  70. package/dist/esm/uv/NiceUVEditor.js +16 -12
  71. package/dist/esm/uv/uvEditorTypes.js +1 -0
  72. package/dist/types/core/editorAsk.d.ts +26 -0
  73. package/dist/types/core/i18n.d.ts +1 -17
  74. package/dist/types/dance/DanceScoreEngine.d.ts +0 -6
  75. package/dist/types/model/useModelEditor.d.ts +3 -0
  76. package/package.json +3 -3
@@ -4,6 +4,7 @@ var jsxRuntime = require('react/jsx-runtime');
4
4
  var ui = require('@nice2dev/ui');
5
5
  var React = require('react');
6
6
  var THREE = require('three');
7
+ var i18n = require('../core/i18n.js');
7
8
 
8
9
  function _interopNamespaceDefault(e) {
9
10
  var n = Object.create(null);
@@ -301,6 +302,7 @@ function useSSAO(renderer, scene, camera, configOverride) {
301
302
  blurMat.dispose();
302
303
  ortho.remove(quad);
303
304
  };
305
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- the uniforms' first values; the next effect updates them without rebuilding the render targets
304
306
  }, [renderer, camera, config.resolution]);
305
307
  // Update uniforms
306
308
  React.useEffect(() => {
@@ -357,8 +359,9 @@ function useSSAO(renderer, scene, camera, configOverride) {
357
359
  }
358
360
  /* ── Component ── */
359
361
  const NiceSSAO = ({ renderer, scene, camera, config: configProp, className = '', }) => {
362
+ const { t } = i18n.useNiceTranslation();
360
363
  const { config, setConfig } = useSSAO(renderer, scene, camera, configProp);
361
- return (jsxRuntime.jsxs("div", { className: `nice-ssao ${className}`, style: { display: 'grid', gap: 8, fontSize: 13 }, children: [jsxRuntime.jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' }, children: [jsxRuntime.jsxs("strong", { children: [jsxRuntime.jsx(ui.NiceIcon, { name: "circle", size: 14 }), " Ambient Occlusion"] }), jsxRuntime.jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 4, fontSize: 11 }, children: [jsxRuntime.jsx("input", { type: "checkbox", checked: config.enabled, onChange: (e) => setConfig({ enabled: e.target.checked }) }), "Enabled"] })] }), jsxRuntime.jsxs("label", { children: ["Mode:", jsxRuntime.jsxs("select", { value: config.mode, onChange: (e) => setConfig({ mode: e.target.value }), style: { width: '100%', marginTop: 2 }, children: [jsxRuntime.jsx("option", { value: "ssao", children: "SSAO" }), jsxRuntime.jsx("option", { value: "hbao", children: "HBAO" })] })] }), jsxRuntime.jsxs("label", { children: ["Samples: ", config.samples, jsxRuntime.jsx("input", { type: "range", min: 8, max: 128, step: 8, value: config.samples, onChange: (e) => setConfig({ samples: parseInt(e.target.value, 10) }), style: { width: '100%' } })] }), jsxRuntime.jsxs("label", { children: ["Radius: ", config.radius.toFixed(2), jsxRuntime.jsx("input", { type: "range", min: 0.1, max: 5, step: 0.1, value: config.radius, onChange: (e) => setConfig({ radius: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsxRuntime.jsxs("label", { children: ["Intensity: ", config.intensity.toFixed(2), jsxRuntime.jsx("input", { type: "range", min: 0, max: 3, step: 0.1, value: config.intensity, onChange: (e) => setConfig({ intensity: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsxRuntime.jsxs("label", { children: ["Bias: ", config.bias.toFixed(3), jsxRuntime.jsx("input", { type: "range", min: 0.001, max: 0.1, step: 0.001, value: config.bias, onChange: (e) => setConfig({ bias: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsxRuntime.jsxs("label", { children: ["Resolution: ", config.resolution.toFixed(2), jsxRuntime.jsx("input", { type: "range", min: 0.25, max: 1, step: 0.25, value: config.resolution, onChange: (e) => setConfig({ resolution: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsxRuntime.jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 6 }, children: [jsxRuntime.jsx("input", { type: "checkbox", checked: config.blurEnabled, onChange: (e) => setConfig({ blurEnabled: e.target.checked }) }), "Bilateral Blur (size: ", config.blurSize, ")"] })] }));
364
+ return (jsxRuntime.jsxs("div", { className: `nice-ssao ${className}`, style: { display: 'grid', gap: 8, fontSize: 13 }, children: [jsxRuntime.jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' }, children: [jsxRuntime.jsxs("strong", { children: [jsxRuntime.jsx(ui.NiceIcon, { name: "circle", size: 14 }), " ", t('ssao.ambientOcclusion', 'Ambient Occlusion')] }), jsxRuntime.jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 4, fontSize: 11 }, children: [jsxRuntime.jsx("input", { type: "checkbox", checked: config.enabled, onChange: (e) => setConfig({ enabled: e.target.checked }) }), t('ssao.enabled', 'Enabled')] })] }), jsxRuntime.jsxs("label", { children: [t('ssao.mode', 'Mode:'), jsxRuntime.jsxs("select", { value: config.mode, onChange: (e) => setConfig({ mode: e.target.value }), style: { width: '100%', marginTop: 2 }, children: [jsxRuntime.jsx("option", { value: "ssao", children: t('ssao.ssao', 'SSAO') }), jsxRuntime.jsx("option", { value: "hbao", children: t('ssao.hbao', 'HBAO') })] })] }), jsxRuntime.jsxs("label", { children: [t('ssao.samples', 'Samples: {samples}').replace('{samples}', String(config.samples)), jsxRuntime.jsx("input", { type: "range", min: 8, max: 128, step: 8, value: config.samples, onChange: (e) => setConfig({ samples: parseInt(e.target.value, 10) }), style: { width: '100%' } })] }), jsxRuntime.jsxs("label", { children: [t('ssao.radius', 'Radius: {radius}').replace('{radius}', String(config.radius.toFixed(2))), jsxRuntime.jsx("input", { type: "range", min: 0.1, max: 5, step: 0.1, value: config.radius, onChange: (e) => setConfig({ radius: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsxRuntime.jsxs("label", { children: [t('ssao.intensity', 'Intensity: {intensity}').replace('{intensity}', String(config.intensity.toFixed(2))), jsxRuntime.jsx("input", { type: "range", min: 0, max: 3, step: 0.1, value: config.intensity, onChange: (e) => setConfig({ intensity: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsxRuntime.jsxs("label", { children: [t('ssao.bias', 'Bias: {bias}').replace('{bias}', String(config.bias.toFixed(3))), jsxRuntime.jsx("input", { type: "range", min: 0.001, max: 0.1, step: 0.001, value: config.bias, onChange: (e) => setConfig({ bias: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsxRuntime.jsxs("label", { children: [t('ssao.resolution', 'Resolution: {resolution}').replace('{resolution}', String(config.resolution.toFixed(2))), jsxRuntime.jsx("input", { type: "range", min: 0.25, max: 1, step: 0.25, value: config.resolution, onChange: (e) => setConfig({ resolution: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsxRuntime.jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 6 }, children: [jsxRuntime.jsx("input", { type: "checkbox", checked: config.blurEnabled, onChange: (e) => setConfig({ blurEnabled: e.target.checked }) }), t('ssao.bilateralBlurSize', 'Bilateral Blur (size: {blurSize})').replace('{blurSize}', String(config.blurSize))] })] }));
362
365
  };
363
366
 
364
367
  exports.NiceSSAO = NiceSSAO;
@@ -4,6 +4,7 @@ var jsxRuntime = require('react/jsx-runtime');
4
4
  var ui = require('@nice2dev/ui');
5
5
  var React = require('react');
6
6
  var THREE = require('three');
7
+ var i18n = require('../core/i18n.js');
7
8
 
8
9
  function _interopNamespaceDefault(e) {
9
10
  var n = Object.create(null);
@@ -227,6 +228,7 @@ function useSSR(renderer, scene, camera, configOverride) {
227
228
  material.dispose();
228
229
  ortho.remove(quad);
229
230
  };
231
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- the uniforms' first values; the next effect updates them without rebuilding the render targets
230
232
  }, [renderer, camera, config.resolution]);
231
233
  // Update uniforms when config changes
232
234
  React.useEffect(() => {
@@ -274,8 +276,9 @@ function useSSR(renderer, scene, camera, configOverride) {
274
276
  }
275
277
  /* ── Component ── */
276
278
  const NiceSSR = ({ renderer, scene, camera, config: configProp, className = '', }) => {
279
+ const { t } = i18n.useNiceTranslation();
277
280
  const { config, setConfig } = useSSR(renderer, scene, camera, configProp);
278
- return (jsxRuntime.jsxs("div", { className: `nice-ssr ${className}`, style: { display: 'grid', gap: 8, fontSize: 13 }, children: [jsxRuntime.jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' }, children: [jsxRuntime.jsxs("strong", { children: [jsxRuntime.jsx(ui.NiceIcon, { name: "copy", size: 14 }), " Screen-Space Reflections"] }), jsxRuntime.jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 4, fontSize: 11 }, children: [jsxRuntime.jsx("input", { type: "checkbox", checked: config.enabled, onChange: (e) => setConfig({ enabled: e.target.checked }) }), "Enabled"] })] }), jsxRuntime.jsxs("label", { children: ["Max Steps: ", config.maxSteps, jsxRuntime.jsx("input", { type: "range", min: 16, max: 256, step: 16, value: config.maxSteps, onChange: (e) => setConfig({ maxSteps: parseInt(e.target.value, 10) }), style: { width: '100%' } })] }), jsxRuntime.jsxs("label", { children: ["Max Distance: ", config.maxDistance, jsxRuntime.jsx("input", { type: "range", min: 5, max: 200, step: 5, value: config.maxDistance, onChange: (e) => setConfig({ maxDistance: parseInt(e.target.value, 10) }), style: { width: '100%' } })] }), jsxRuntime.jsxs("label", { children: ["Intensity: ", config.intensity.toFixed(2), jsxRuntime.jsx("input", { type: "range", min: 0, max: 1, step: 0.05, value: config.intensity, onChange: (e) => setConfig({ intensity: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsxRuntime.jsxs("label", { children: ["Thickness: ", config.thickness.toFixed(2), jsxRuntime.jsx("input", { type: "range", min: 0.05, max: 2, step: 0.05, value: config.thickness, onChange: (e) => setConfig({ thickness: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsxRuntime.jsxs("label", { children: ["Jitter: ", config.jitter.toFixed(2), jsxRuntime.jsx("input", { type: "range", min: 0, max: 1, step: 0.05, value: config.jitter, onChange: (e) => setConfig({ jitter: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsxRuntime.jsxs("label", { children: ["Resolution Scale: ", config.resolution.toFixed(2), jsxRuntime.jsx("input", { type: "range", min: 0.25, max: 1, step: 0.25, value: config.resolution, onChange: (e) => setConfig({ resolution: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsxRuntime.jsxs("label", { children: ["Fresnel: ", config.fresnelExponent.toFixed(1), jsxRuntime.jsx("input", { type: "range", min: 1, max: 8, step: 0.5, value: config.fresnelExponent, onChange: (e) => setConfig({ fresnelExponent: parseFloat(e.target.value) }), style: { width: '100%' } })] })] }));
281
+ return (jsxRuntime.jsxs("div", { className: `nice-ssr ${className}`, style: { display: 'grid', gap: 8, fontSize: 13 }, children: [jsxRuntime.jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' }, children: [jsxRuntime.jsxs("strong", { children: [jsxRuntime.jsx(ui.NiceIcon, { name: "copy", size: 14 }), ' ', t('ssr.screenSpaceReflections', 'Screen-Space Reflections')] }), jsxRuntime.jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 4, fontSize: 11 }, children: [jsxRuntime.jsx("input", { type: "checkbox", checked: config.enabled, onChange: (e) => setConfig({ enabled: e.target.checked }) }), t('ssr.enabled', 'Enabled')] })] }), jsxRuntime.jsxs("label", { children: [t('ssr.maxSteps', 'Max Steps: {maxSteps}').replace('{maxSteps}', String(config.maxSteps)), jsxRuntime.jsx("input", { type: "range", min: 16, max: 256, step: 16, value: config.maxSteps, onChange: (e) => setConfig({ maxSteps: parseInt(e.target.value, 10) }), style: { width: '100%' } })] }), jsxRuntime.jsxs("label", { children: [t('ssr.maxDistance', 'Max Distance: {maxDistance}').replace('{maxDistance}', String(config.maxDistance)), jsxRuntime.jsx("input", { type: "range", min: 5, max: 200, step: 5, value: config.maxDistance, onChange: (e) => setConfig({ maxDistance: parseInt(e.target.value, 10) }), style: { width: '100%' } })] }), jsxRuntime.jsxs("label", { children: [t('ssr.intensity', 'Intensity: {intensity}').replace('{intensity}', String(config.intensity.toFixed(2))), jsxRuntime.jsx("input", { type: "range", min: 0, max: 1, step: 0.05, value: config.intensity, onChange: (e) => setConfig({ intensity: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsxRuntime.jsxs("label", { children: [t('ssr.thickness', 'Thickness: {thickness}').replace('{thickness}', String(config.thickness.toFixed(2))), jsxRuntime.jsx("input", { type: "range", min: 0.05, max: 2, step: 0.05, value: config.thickness, onChange: (e) => setConfig({ thickness: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsxRuntime.jsxs("label", { children: [t('ssr.jitter', 'Jitter: {jitter}').replace('{jitter}', String(config.jitter.toFixed(2))), jsxRuntime.jsx("input", { type: "range", min: 0, max: 1, step: 0.05, value: config.jitter, onChange: (e) => setConfig({ jitter: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsxRuntime.jsxs("label", { children: [t('ssr.resolutionScale', 'Resolution Scale: {resolution}').replace('{resolution}', String(config.resolution.toFixed(2))), jsxRuntime.jsx("input", { type: "range", min: 0.25, max: 1, step: 0.25, value: config.resolution, onChange: (e) => setConfig({ resolution: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsxRuntime.jsxs("label", { children: [t('ssr.fresnel', 'Fresnel: {fresnelExponent}').replace('{fresnelExponent}', String(config.fresnelExponent.toFixed(1))), jsxRuntime.jsx("input", { type: "range", min: 1, max: 8, step: 0.5, value: config.fresnelExponent, onChange: (e) => setConfig({ fresnelExponent: parseFloat(e.target.value) }), style: { width: '100%' } })] })] }));
279
282
  };
280
283
 
281
284
  exports.NiceSSR = NiceSSR;
@@ -4,6 +4,7 @@ var jsxRuntime = require('react/jsx-runtime');
4
4
  var ui = require('@nice2dev/ui');
5
5
  var React = require('react');
6
6
  var THREE = require('three');
7
+ var i18n = require('../core/i18n.js');
7
8
  var threeCompat = require('../core/threeCompat.js');
8
9
 
9
10
  function _interopNamespaceDefault(e) {
@@ -151,6 +152,7 @@ function useWebGPURenderer(canvas, scene, camera, width = 800, height = 600, onR
151
152
  disposed = true;
152
153
  (_a = rendererRef.current) === null || _a === void 0 ? void 0 : _a.dispose();
153
154
  };
155
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- one renderer per canvas, made with the first settings; the effects below apply later changes, and onRendererReady reports that one renderer
154
156
  }, [canvas]);
155
157
  // Update renderer settings reactively
156
158
  React.useEffect(() => {
@@ -230,14 +232,15 @@ const TONE_MAPPINGS = [
230
232
  ];
231
233
  /* ── Component ── */
232
234
  const NiceWebGPURenderer = ({ canvas, scene, camera, width, height, onRendererReady, className = '', }) => {
235
+ const { t } = i18n.useNiceTranslation();
233
236
  const { isWebGPU, capabilities, isReady, toneMapping, setToneMapping, toneMappingExposure, setToneMappingExposure, outputColorSpace, setOutputColorSpace, pixelRatio, setPixelRatio, } = useWebGPURenderer(canvas, scene, camera, width, height, onRendererReady);
234
- return (jsxRuntime.jsxs("div", { className: `nice-webgpu-renderer ${className}`, style: { display: 'grid', gap: 8, fontSize: 13 }, children: [jsxRuntime.jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' }, children: [jsxRuntime.jsxs("strong", { children: [jsxRuntime.jsx(ui.NiceIcon, { name: "monitor", size: 14 }), " Renderer"] }), jsxRuntime.jsx("span", { style: {
237
+ return (jsxRuntime.jsxs("div", { className: `nice-webgpu-renderer ${className}`, style: { display: 'grid', gap: 8, fontSize: 13 }, children: [jsxRuntime.jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' }, children: [jsxRuntime.jsxs("strong", { children: [jsxRuntime.jsx(ui.NiceIcon, { name: "monitor", size: 14 }), " ", t('webGPURenderer.renderer', 'Renderer')] }), jsxRuntime.jsx("span", { style: {
235
238
  fontSize: 10,
236
239
  padding: '2px 6px',
237
240
  borderRadius: 4,
238
241
  background: isWebGPU ? 'var(--nice-success, #22c55e)' : 'var(--nice-warning, #f59e0b)',
239
242
  color: 'var(--nice-color-white, #fff)',
240
- }, children: isWebGPU ? 'WebGPU' : 'WebGL' })] }), !isReady && jsxRuntime.jsx("div", { style: { opacity: 0.6, fontSize: 11 }, children: "Initializing\u2026" }), capabilities && (jsxRuntime.jsxs("div", { style: { fontSize: 10, opacity: 0.7, lineHeight: 1.5 }, children: ["GPU: ", capabilities.adapterName, jsxRuntime.jsx("br", {}), "Vendor: ", capabilities.vendor, jsxRuntime.jsx("br", {}), "Max Texture: ", capabilities.maxTextureDimension2D, "px", capabilities.features.length > 0 && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("br", {}), "Features: ", capabilities.features.length] }))] })), jsxRuntime.jsxs("label", { children: ["Tone Mapping:", jsxRuntime.jsx("select", { value: toneMapping, onChange: (e) => setToneMapping(parseInt(e.target.value, 10)), style: { width: '100%', marginTop: 2 }, children: TONE_MAPPINGS.map((tm) => (jsxRuntime.jsx("option", { value: tm.value, children: tm.label }, tm.value))) })] }), jsxRuntime.jsxs("label", { children: ["Exposure: ", toneMappingExposure.toFixed(2), jsxRuntime.jsx("input", { type: "range", min: 0.1, max: 3, step: 0.05, value: toneMappingExposure, onChange: (e) => setToneMappingExposure(parseFloat(e.target.value)), style: { width: '100%' } })] }), jsxRuntime.jsxs("label", { children: ["Output Color Space:", jsxRuntime.jsxs("select", { value: outputColorSpace, onChange: (e) => setOutputColorSpace(e.target.value), style: { width: '100%', marginTop: 2 }, children: [jsxRuntime.jsx("option", { value: THREE__namespace.SRGBColorSpace, children: "sRGB" }), jsxRuntime.jsx("option", { value: THREE__namespace.LinearSRGBColorSpace, children: "Linear sRGB" })] })] }), jsxRuntime.jsxs("label", { children: ["Pixel Ratio: ", pixelRatio.toFixed(1), jsxRuntime.jsx("input", { type: "range", min: 0.5, max: 3, step: 0.5, value: pixelRatio, onChange: (e) => setPixelRatio(parseFloat(e.target.value)), style: { width: '100%' } })] })] }));
243
+ }, children: isWebGPU ? 'WebGPU' : 'WebGL' })] }), !isReady && (jsxRuntime.jsx("div", { style: { opacity: 0.6, fontSize: 11 }, children: t('webGPURenderer.initializing', 'Initializing…') })), capabilities && (jsxRuntime.jsxs("div", { style: { fontSize: 10, opacity: 0.7, lineHeight: 1.5 }, children: [t('webGPURenderer.gpu', 'GPU: {adapterName}').replace('{adapterName}', String(capabilities.adapterName)), jsxRuntime.jsx("br", {}), t('webGPURenderer.vendor', 'Vendor: {vendor}').replace('{vendor}', String(capabilities.vendor)), jsxRuntime.jsx("br", {}), t('webGPURenderer.maxTexturePx', 'Max Texture: {maxTextureDimension2D}px').replace('{maxTextureDimension2D}', String(capabilities.maxTextureDimension2D)), capabilities.features.length > 0 && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("br", {}), t('webGPURenderer.features', 'Features: {features}').replace('{features}', String(capabilities.features.length))] }))] })), jsxRuntime.jsxs("label", { children: [t('webGPURenderer.toneMapping', 'Tone Mapping:'), jsxRuntime.jsx("select", { value: toneMapping, onChange: (e) => setToneMapping(parseInt(e.target.value, 10)), style: { width: '100%', marginTop: 2 }, children: TONE_MAPPINGS.map((tm) => (jsxRuntime.jsx("option", { value: tm.value, children: tm.label }, tm.value))) })] }), jsxRuntime.jsxs("label", { children: [t('webGPURenderer.exposure', 'Exposure: {toneMappingExposure}').replace('{toneMappingExposure}', String(toneMappingExposure.toFixed(2))), jsxRuntime.jsx("input", { type: "range", min: 0.1, max: 3, step: 0.05, value: toneMappingExposure, onChange: (e) => setToneMappingExposure(parseFloat(e.target.value)), style: { width: '100%' } })] }), jsxRuntime.jsxs("label", { children: [t('webGPURenderer.outputColorSpace', 'Output Color Space:'), jsxRuntime.jsxs("select", { value: outputColorSpace, onChange: (e) => setOutputColorSpace(e.target.value), style: { width: '100%', marginTop: 2 }, children: [jsxRuntime.jsx("option", { value: THREE__namespace.SRGBColorSpace, children: "sRGB" }), jsxRuntime.jsx("option", { value: THREE__namespace.LinearSRGBColorSpace, children: t('webGPURenderer.linearSrgb', 'Linear sRGB') })] })] }), jsxRuntime.jsxs("label", { children: [t('webGPURenderer.pixelRatio', 'Pixel Ratio: {pixelRatio}').replace('{pixelRatio}', String(pixelRatio.toFixed(1))), jsxRuntime.jsx("input", { type: "range", min: 0.5, max: 3, step: 0.5, value: pixelRatio, onChange: (e) => setPixelRatio(parseFloat(e.target.value)), style: { width: '100%' } })] })] }));
241
244
  };
242
245
 
243
246
  exports.NiceWebGPURenderer = NiceWebGPURenderer;
@@ -3,6 +3,7 @@
3
3
  var jsxRuntime = require('react/jsx-runtime');
4
4
  var ui = require('@nice2dev/ui');
5
5
  var React = require('react');
6
+ var i18n = require('../core/i18n.js');
6
7
  var UVEditor_module = require('./UVEditor.module.css.js');
7
8
  var uvEditorTypes = require('./uvEditorTypes.js');
8
9
  var uvEditorUtils = require('./uvEditorUtils.js');
@@ -22,6 +23,7 @@ const UV_EDITOR_TOUR = [
22
23
  Component
23
24
  ═══════════════════════════════════════════ */
24
25
  const NiceUVEditor = ({ geometry, texture, channel = 'uv', onUVChange, onSelectionChange, width = '100%', height = 600, showToolbar = true, showSidebar = true, className, readOnly = false, tutorial, }) => {
26
+ const { t } = i18n.useNiceTranslation();
25
27
  const canvasRef = React.useRef(null);
26
28
  const containerRef = React.useRef(null);
27
29
  // State
@@ -124,12 +126,14 @@ const NiceUVEditor = ({ geometry, texture, channel = 'uv', onUVChange, onSelecti
124
126
  ctx.closePath();
125
127
  ctx.stroke();
126
128
  }
127
- // Draw faces
129
+ // Draw faces — the two fills resolved once per frame, not once per face
130
+ const faceSelectedFill = ui.resolveCssColor('var(--nice-warning-tint-30, rgba(255, 170, 0, 0.3))', ctx.canvas);
131
+ const faceFill = ui.resolveCssColor('var(--nice-overlay-10, rgba(100, 100, 100, 0.1))', ctx.canvas);
128
132
  for (const face of meshData.faces.values()) {
129
133
  const verts = face.vertices.map((id) => meshData.vertices.get(id));
130
134
  const screenVerts = verts.map((v) => toScreen(v.position));
131
135
  // Fill
132
- ctx.fillStyle = face.selected ? 'rgba(255, 170, 0, 0.3)' : 'rgba(100, 100, 100, 0.1)';
136
+ ctx.fillStyle = face.selected ? faceSelectedFill : faceFill;
133
137
  ctx.beginPath();
134
138
  ctx.moveTo(screenVerts[0].x, screenVerts[0].y);
135
139
  screenVerts.slice(1).forEach((sv) => ctx.lineTo(sv.x, sv.y));
@@ -530,33 +534,33 @@ const NiceUVEditor = ({ geometry, texture, channel = 'uv', onUVChange, onSelecti
530
534
  setMeshData(newData);
531
535
  onUVChange === null || onUVChange === void 0 ? void 0 : onUVChange(geometry);
532
536
  }, [meshData, geometry, state.activeChannel, readOnly, pushUndo, onUVChange]);
533
- const toolbarSlot = showToolbar ? (jsxRuntime.jsxs("div", { className: UVEditor_module.toolbar, children: [jsxRuntime.jsx("div", { className: UVEditor_module.toolGroup, children: uvEditorTypes.UV_TOOLS.slice(0, 5).map((tool) => (jsxRuntime.jsx("button", { className: `${UVEditor_module.toolBtn} ${state.activeTool === tool ? UVEditor_module.active : ''}`, onClick: () => setState((prev) => ({ ...prev, activeTool: tool })), title: tool, disabled: readOnly, children: tool[0].toUpperCase() }, tool))) }), jsxRuntime.jsx("div", { className: UVEditor_module.toolGroup, children: uvEditorTypes.UV_SELECTION_MODES.map((mode) => (jsxRuntime.jsx("button", { className: `${UVEditor_module.toolBtn} ${state.selectionMode === mode ? UVEditor_module.active : ''}`, onClick: () => setState((prev) => ({ ...prev, selectionMode: mode })), title: mode, children: mode[0].toUpperCase() }, mode))) }), jsxRuntime.jsxs("div", { className: UVEditor_module.toolGroup, children: [jsxRuntime.jsx("button", { className: UVEditor_module.toolBtn, onClick: handlePack, disabled: readOnly || !meshData, title: "Pack Islands (P)", children: "Pack" }), jsxRuntime.jsx("button", { className: UVEditor_module.toolBtn, onClick: handleRelax, disabled: readOnly || !meshData, title: "Relax UVs", children: "Relax" })] }), jsxRuntime.jsx("div", { className: UVEditor_module.toolGroup, children: jsxRuntime.jsxs("select", { className: UVEditor_module.select, onChange: (e) => handleProject(e.target.value), disabled: readOnly || !meshData, defaultValue: "", children: [jsxRuntime.jsx("option", { value: "", disabled: true, children: "Project..." }), uvEditorTypes.PROJECTION_TYPES.map((type) => (jsxRuntime.jsx("option", { value: type, children: type }, type)))] }) }), jsxRuntime.jsx("div", { className: UVEditor_module.toolGroup, children: jsxRuntime.jsx("select", { className: UVEditor_module.select, value: state.activeChannel, onChange: (e) => setState((prev) => ({
537
+ const toolbarSlot = showToolbar ? (jsxRuntime.jsxs("div", { className: UVEditor_module.toolbar, children: [jsxRuntime.jsx("div", { className: UVEditor_module.toolGroup, children: uvEditorTypes.UV_TOOLS.slice(0, 5).map((tool) => (jsxRuntime.jsx("button", { className: `${UVEditor_module.toolBtn} ${state.activeTool === tool ? UVEditor_module.active : ''}`, onClick: () => setState((prev) => ({ ...prev, activeTool: tool })), title: tool, disabled: readOnly, children: tool[0].toUpperCase() }, tool))) }), jsxRuntime.jsx("div", { className: UVEditor_module.toolGroup, children: uvEditorTypes.UV_SELECTION_MODES.map((mode) => (jsxRuntime.jsx("button", { className: `${UVEditor_module.toolBtn} ${state.selectionMode === mode ? UVEditor_module.active : ''}`, onClick: () => setState((prev) => ({ ...prev, selectionMode: mode })), title: mode, children: mode[0].toUpperCase() }, mode))) }), jsxRuntime.jsxs("div", { className: UVEditor_module.toolGroup, children: [jsxRuntime.jsx("button", { className: UVEditor_module.toolBtn, onClick: handlePack, disabled: readOnly || !meshData, title: t('uvEditor.packIslandsP', 'Pack Islands (P)'), children: t('uvEditor.pack', 'Pack') }), jsxRuntime.jsx("button", { className: UVEditor_module.toolBtn, onClick: handleRelax, disabled: readOnly || !meshData, title: t('uvEditor.relaxUvs', 'Relax UVs'), children: t('uvEditor.relax', 'Relax') })] }), jsxRuntime.jsx("div", { className: UVEditor_module.toolGroup, children: jsxRuntime.jsxs("select", { className: UVEditor_module.select, onChange: (e) => handleProject(e.target.value), disabled: readOnly || !meshData, defaultValue: "", children: [jsxRuntime.jsx("option", { value: "", disabled: true, children: t('uvEditor.project', 'Project...') }), uvEditorTypes.PROJECTION_TYPES.map((type) => (jsxRuntime.jsx("option", { value: type, children: type }, type)))] }) }), jsxRuntime.jsx("div", { className: UVEditor_module.toolGroup, children: jsxRuntime.jsx("select", { className: UVEditor_module.select, value: state.activeChannel, onChange: (e) => setState((prev) => ({
534
538
  ...prev,
535
539
  activeChannel: e.target.value,
536
- })), children: uvEditorTypes.UV_CHANNELS.map((ch) => (jsxRuntime.jsx("option", { value: ch, children: ch.toUpperCase() }, ch))) }) }), jsxRuntime.jsxs("div", { className: UVEditor_module.toolGroup, children: [jsxRuntime.jsx("button", { className: UVEditor_module.toolBtn, onClick: undo, disabled: undoStack.length === 0, title: "Undo (Ctrl+Z)", "aria-label": "Undo", children: jsxRuntime.jsx(ui.NiceIcon, { name: "arrow-left", size: 16 }) }), jsxRuntime.jsx("button", { className: UVEditor_module.toolBtn, onClick: redo, disabled: redoStack.length === 0, title: "Redo (Ctrl+Y)", "aria-label": "Redo", children: jsxRuntime.jsx(ui.NiceIcon, { name: "arrow-right", size: 16 }) })] })] })) : undefined;
537
- const sidebarSlot = showSidebar && meshData ? (jsxRuntime.jsxs("div", { className: UVEditor_module.sidebar, children: [jsxRuntime.jsxs("div", { className: UVEditor_module.panel, children: [jsxRuntime.jsx("h4", { className: UVEditor_module.panelTitle, children: "Display" }), jsxRuntime.jsxs("label", { className: UVEditor_module.checkbox, children: [jsxRuntime.jsx("input", { type: "checkbox", checked: state.display.showGrid, onChange: (e) => setState((prev) => ({
540
+ })), children: uvEditorTypes.UV_CHANNELS.map((ch) => (jsxRuntime.jsx("option", { value: ch, children: ch.toUpperCase() }, ch))) }) }), jsxRuntime.jsxs("div", { className: UVEditor_module.toolGroup, children: [jsxRuntime.jsx("button", { className: UVEditor_module.toolBtn, onClick: undo, disabled: undoStack.length === 0, title: t('uvEditor.undoCtrlZ', 'Undo (Ctrl+Z)'), "aria-label": t('uvEditor.undo', 'Undo'), children: jsxRuntime.jsx(ui.NiceIcon, { name: "arrow-left", size: 16 }) }), jsxRuntime.jsx("button", { className: UVEditor_module.toolBtn, onClick: redo, disabled: redoStack.length === 0, title: t('uvEditor.redoCtrlY', 'Redo (Ctrl+Y)'), "aria-label": t('uvEditor.redo', 'Redo'), children: jsxRuntime.jsx(ui.NiceIcon, { name: "arrow-right", size: 16 }) })] })] })) : undefined;
541
+ const sidebarSlot = showSidebar && meshData ? (jsxRuntime.jsxs("div", { className: UVEditor_module.sidebar, children: [jsxRuntime.jsxs("div", { className: UVEditor_module.panel, children: [jsxRuntime.jsx("h4", { className: UVEditor_module.panelTitle, children: t('uvEditor.display', 'Display') }), jsxRuntime.jsxs("label", { className: UVEditor_module.checkbox, children: [jsxRuntime.jsx("input", { type: "checkbox", checked: state.display.showGrid, onChange: (e) => setState((prev) => ({
538
542
  ...prev,
539
543
  display: { ...prev.display, showGrid: e.target.checked },
540
- })) }), "Show Grid"] }), jsxRuntime.jsxs("label", { className: UVEditor_module.checkbox, children: [jsxRuntime.jsx("input", { type: "checkbox", checked: state.display.showTexture, onChange: (e) => setState((prev) => ({
544
+ })) }), t('uvEditor.showGrid', 'Show Grid')] }), jsxRuntime.jsxs("label", { className: UVEditor_module.checkbox, children: [jsxRuntime.jsx("input", { type: "checkbox", checked: state.display.showTexture, onChange: (e) => setState((prev) => ({
541
545
  ...prev,
542
546
  display: { ...prev.display, showTexture: e.target.checked },
543
- })) }), "Show Texture"] }), jsxRuntime.jsxs("label", { className: UVEditor_module.checkbox, children: [jsxRuntime.jsx("input", { type: "checkbox", checked: state.display.showSeams, onChange: (e) => setState((prev) => ({
547
+ })) }), t('uvEditor.showTexture', 'Show Texture')] }), jsxRuntime.jsxs("label", { className: UVEditor_module.checkbox, children: [jsxRuntime.jsx("input", { type: "checkbox", checked: state.display.showSeams, onChange: (e) => setState((prev) => ({
544
548
  ...prev,
545
549
  display: { ...prev.display, showSeams: e.target.checked },
546
- })) }), "Show Seams"] }), jsxRuntime.jsxs("label", { className: UVEditor_module.checkbox, children: [jsxRuntime.jsx("input", { type: "checkbox", checked: state.display.showDistortion, onChange: (e) => setState((prev) => ({
550
+ })) }), t('uvEditor.showSeams', 'Show Seams')] }), jsxRuntime.jsxs("label", { className: UVEditor_module.checkbox, children: [jsxRuntime.jsx("input", { type: "checkbox", checked: state.display.showDistortion, onChange: (e) => setState((prev) => ({
547
551
  ...prev,
548
552
  display: { ...prev.display, showDistortion: e.target.checked },
549
- })) }), "Show Distortion"] }), jsxRuntime.jsxs("label", { className: UVEditor_module.checkbox, children: [jsxRuntime.jsx("input", { type: "checkbox", checked: state.display.showIslandBounds, onChange: (e) => setState((prev) => ({
553
+ })) }), t('uvEditor.showDistortion', 'Show Distortion')] }), jsxRuntime.jsxs("label", { className: UVEditor_module.checkbox, children: [jsxRuntime.jsx("input", { type: "checkbox", checked: state.display.showIslandBounds, onChange: (e) => setState((prev) => ({
550
554
  ...prev,
551
555
  display: { ...prev.display, showIslandBounds: e.target.checked },
552
- })) }), "Show Island Bounds"] })] }), jsxRuntime.jsxs("div", { className: UVEditor_module.panel, children: [jsxRuntime.jsx("h4", { className: UVEditor_module.panelTitle, children: "Statistics" }), jsxRuntime.jsxs("div", { className: UVEditor_module.stat, children: [jsxRuntime.jsx("span", { children: "Vertices:" }), jsxRuntime.jsx("span", { children: meshData.vertices.size })] }), jsxRuntime.jsxs("div", { className: UVEditor_module.stat, children: [jsxRuntime.jsx("span", { children: "Faces:" }), jsxRuntime.jsx("span", { children: meshData.faces.size })] }), jsxRuntime.jsxs("div", { className: UVEditor_module.stat, children: [jsxRuntime.jsx("span", { children: "Islands:" }), jsxRuntime.jsx("span", { children: meshData.islands.size })] }), jsxRuntime.jsxs("div", { className: UVEditor_module.stat, children: [jsxRuntime.jsx("span", { children: "Selected:" }), jsxRuntime.jsx("span", { children: uvEditorUtils.getSelectedVertices(meshData).length })] })] }), jsxRuntime.jsxs("div", { className: UVEditor_module.panel, children: [jsxRuntime.jsx("h4", { className: UVEditor_module.panelTitle, children: "Snapping" }), jsxRuntime.jsxs("label", { className: UVEditor_module.checkbox, children: [jsxRuntime.jsx("input", { type: "checkbox", checked: state.snapping, onChange: (e) => setState((prev) => ({
556
+ })) }), t('uvEditor.showIslandBounds', 'Show Island Bounds')] })] }), jsxRuntime.jsxs("div", { className: UVEditor_module.panel, children: [jsxRuntime.jsx("h4", { className: UVEditor_module.panelTitle, children: t('uvEditor.statistics', 'Statistics') }), jsxRuntime.jsxs("div", { className: UVEditor_module.stat, children: [jsxRuntime.jsx("span", { children: t('uvEditor.vertices', 'Vertices:') }), jsxRuntime.jsx("span", { children: meshData.vertices.size })] }), jsxRuntime.jsxs("div", { className: UVEditor_module.stat, children: [jsxRuntime.jsx("span", { children: t('uvEditor.faces', 'Faces:') }), jsxRuntime.jsx("span", { children: meshData.faces.size })] }), jsxRuntime.jsxs("div", { className: UVEditor_module.stat, children: [jsxRuntime.jsx("span", { children: t('uvEditor.islands', 'Islands:') }), jsxRuntime.jsx("span", { children: meshData.islands.size })] }), jsxRuntime.jsxs("div", { className: UVEditor_module.stat, children: [jsxRuntime.jsx("span", { children: t('uvEditor.selected', 'Selected:') }), jsxRuntime.jsx("span", { children: uvEditorUtils.getSelectedVertices(meshData).length })] })] }), jsxRuntime.jsxs("div", { className: UVEditor_module.panel, children: [jsxRuntime.jsx("h4", { className: UVEditor_module.panelTitle, children: t('uvEditor.snapping', 'Snapping') }), jsxRuntime.jsxs("label", { className: UVEditor_module.checkbox, children: [jsxRuntime.jsx("input", { type: "checkbox", checked: state.snapping, onChange: (e) => setState((prev) => ({
553
557
  ...prev,
554
558
  snapping: e.target.checked,
555
- })) }), "Enable Snapping"] }), jsxRuntime.jsxs("label", { className: UVEditor_module.rangeLabel, children: ["Snap Size: ", state.snapSize.toFixed(4), jsxRuntime.jsx("input", { type: "range", min: "0.001", max: "0.25", step: "0.001", value: state.snapSize, onChange: (e) => setState((prev) => ({
559
+ })) }), t('uvEditor.enableSnapping', 'Enable Snapping')] }), jsxRuntime.jsxs("label", { className: UVEditor_module.rangeLabel, children: [t('uvEditor.snapSize', 'Snap Size: {snapSize}').replace('{snapSize}', String(state.snapSize.toFixed(4))), jsxRuntime.jsx("input", { type: "range", min: "0.001", max: "0.25", step: "0.001", value: state.snapSize, onChange: (e) => setState((prev) => ({
556
560
  ...prev,
557
561
  snapSize: parseFloat(e.target.value),
558
562
  })) })] })] })] })) : undefined;
559
- return (jsxRuntime.jsxs("div", { style: { position: 'relative', width, height }, children: [jsxRuntime.jsx("span", { style: { position: 'absolute', top: 6, right: 6, zIndex: 30 }, children: jsxRuntime.jsx(ui.NiceTutorialButton, { steps: ui.resolveTutorial(tutorial, UV_EDITOR_TOUR) }) }), jsxRuntime.jsx(ui.NiceAppShell, { className: `nice-uv-editor ${UVEditor_module.uvEditor} ${className !== null && className !== void 0 ? className : ''}`, flush: true, toolbar: toolbarSlot, rightSidebar: sidebarSlot, statusBar: jsxRuntime.jsxs("div", { className: UVEditor_module.statusBar, children: [jsxRuntime.jsxs("span", { children: ["Tool: ", state.activeTool] }), jsxRuntime.jsxs("span", { children: ["Mode: ", state.selectionMode] }), jsxRuntime.jsxs("span", { children: ["Zoom: ", (state.zoom * 100).toFixed(0), "%"] }), jsxRuntime.jsxs("span", { children: ["Channel: ", state.activeChannel] })] }), children: jsxRuntime.jsxs("div", { ref: containerRef, className: UVEditor_module.viewport, onMouseDown: handleMouseDown, onMouseMove: handleMouseMove, onMouseUp: handleMouseUp, onMouseLeave: handleMouseUp, onWheel: handleWheel, children: [jsxRuntime.jsx("canvas", { ref: canvasRef, className: UVEditor_module.canvas }), !meshData && jsxRuntime.jsx("div", { className: UVEditor_module.noGeometry, children: "No geometry selected" })] }) })] }));
563
+ return (jsxRuntime.jsxs("div", { style: { position: 'relative', width, height }, children: [jsxRuntime.jsx("span", { style: { position: 'absolute', top: 6, right: 6, zIndex: 30 }, children: jsxRuntime.jsx(ui.NiceTutorialButton, { steps: ui.resolveTutorial(tutorial, UV_EDITOR_TOUR) }) }), jsxRuntime.jsx(ui.NiceAppShell, { className: `nice-uv-editor ${UVEditor_module.uvEditor} ${className !== null && className !== void 0 ? className : ''}`, flush: true, toolbar: toolbarSlot, rightSidebar: sidebarSlot, statusBar: jsxRuntime.jsxs("div", { className: UVEditor_module.statusBar, children: [jsxRuntime.jsx("span", { children: t('uvEditor.tool', 'Tool: {activeTool}').replace('{activeTool}', String(state.activeTool)) }), jsxRuntime.jsx("span", { children: t('uvEditor.mode', 'Mode: {selectionMode}').replace('{selectionMode}', String(state.selectionMode)) }), jsxRuntime.jsx("span", { children: t('uvEditor.zoom', 'Zoom: {zoom}%').replace('{zoom}', String((state.zoom * 100).toFixed(0))) }), jsxRuntime.jsx("span", { children: t('uvEditor.channel', 'Channel: {activeChannel}').replace('{activeChannel}', String(state.activeChannel)) })] }), children: jsxRuntime.jsxs("div", { ref: containerRef, className: UVEditor_module.viewport, onMouseDown: handleMouseDown, onMouseMove: handleMouseMove, onMouseUp: handleMouseUp, onMouseLeave: handleMouseUp, onWheel: handleWheel, children: [jsxRuntime.jsx("canvas", { ref: canvasRef, className: UVEditor_module.canvas }), !meshData && (jsxRuntime.jsx("div", { className: UVEditor_module.noGeometry, children: t('uvEditor.noGeometrySelected', 'No geometry selected') }))] }) })] }));
560
564
  };
561
565
 
562
566
  exports.NiceUVEditor = NiceUVEditor;
@@ -50,6 +50,7 @@ const DEFAULT_UV_DISPLAY = {
50
50
  showDistortion: false,
51
51
  distortionGradient: { low: 'var(--nice-success, #00ff00)', high: 'var(--nice-danger, #ff0000)' },
52
52
  showSeams: true,
53
+ // audit-allow: literal-color — the seam colour setting's default
53
54
  seamColor: '#ff6600',
54
55
  highlightSelected: true,
55
56
  selectionColor: 'var(--nice-warning, #ffaa00)',
@@ -0,0 +1,45 @@
1
+ import { jsx } from 'react/jsx-runtime';
2
+ import { NiceConfirmDialog, NicePromptDialog } from '@nice2dev/ui';
3
+ import { useState, useRef, useCallback } from 'react';
4
+
5
+ /**
6
+ * The library's dialogs in place of `window.prompt` / `window.confirm` for the editor hooks,
7
+ * which hold state but render nothing: `ask` resolves with the answer (`null` when cancelled)
8
+ * and `pendingAsk` is the open question, for the editor component to pass to
9
+ * {@link EditorAskDialog}. One question at a time: a new one settles the open one as cancelled.
10
+ */
11
+ function useEditorAsk() {
12
+ const [pendingAsk, setPendingAsk] = useState(null);
13
+ const pendingRef = useRef(null);
14
+ const ask = useCallback((kind, text, defaultValue) => new Promise((resolve) => {
15
+ var _a;
16
+ (_a = pendingRef.current) === null || _a === void 0 ? void 0 : _a.answer(null);
17
+ const next = {
18
+ kind,
19
+ text,
20
+ defaultValue,
21
+ answer: (value) => {
22
+ if (pendingRef.current === next) {
23
+ pendingRef.current = null;
24
+ setPendingAsk(null);
25
+ }
26
+ resolve(value);
27
+ },
28
+ };
29
+ pendingRef.current = next;
30
+ setPendingAsk(next);
31
+ }), []);
32
+ return { pendingAsk, ask };
33
+ }
34
+ /** Shows the question {@link useEditorAsk} is waiting on, if any. */
35
+ const EditorAskDialog = ({ ask }) => {
36
+ if (!ask) {
37
+ return null;
38
+ }
39
+ if (ask.kind === 'confirm') {
40
+ return (jsx(NiceConfirmDialog, { visible: true, message: ask.text, onAccept: () => ask.answer(''), onReject: () => ask.answer(null) }));
41
+ }
42
+ return (jsx(NicePromptDialog, { open: true, title: ask.text, defaultValue: ask.defaultValue, onSubmit: (value) => ask.answer(value), onCancel: () => ask.answer(null) }));
43
+ };
44
+
45
+ export { EditorAskDialog, useEditorAsk };
@@ -1,11 +1,15 @@
1
1
  import { jsx } from 'react/jsx-runtime';
2
- import { createContext, useContext } from 'react';
2
+ import { useNiceTranslation as useNiceTranslation$1 } from '@nice2dev/ui';
3
+ import { useContext, useMemo, createContext } from 'react';
3
4
 
4
- const fallback = (_key, defaultValue) => defaultValue;
5
- const NiceI18nContext = createContext(fallback);
5
+ /** `null` outside this package's provider: the global translator applies. */
6
+ const NiceI18nContext = /* @__PURE__ */ createContext(null);
7
+ /** Overrides the app's global translator for the 3D controls below it. */
6
8
  const NiceI18nProvider = ({ t, children }) => (jsx(NiceI18nContext.Provider, { value: t, children: children }));
7
9
  function useNiceTranslation() {
8
- const t = useContext(NiceI18nContext);
10
+ const own = useContext(NiceI18nContext);
11
+ const { t: global } = useNiceTranslation$1();
12
+ const t = useMemo(() => own !== null && own !== void 0 ? own : ((key, defaultValue) => global(key, defaultValue)), [own, global]);
9
13
  return { t };
10
14
  }
11
15
 
@@ -1,5 +1,11 @@
1
1
  import { useRef, useState, useCallback } from 'react';
2
2
 
3
+ /**
4
+ * DanceScoreEngine — Compares detected poses against reference choreography
5
+ *
6
+ * Calculates real-time dance accuracy by comparing camera-detected Pose3DFrames
7
+ * against a reference sequence (from choreography DSL or recorded poses).
8
+ */
3
9
  // ── Default config ──────────────────────────────────────────────
4
10
  const DEFAULT_CONFIG = {
5
11
  keypointWeights: {},
@@ -190,6 +196,7 @@ class DanceScoreEngine {
190
196
  return 'F';
191
197
  }
192
198
  }
199
+ // ── React hook ──────────────────────────────────────────────────
193
200
  function useDanceScore(config) {
194
201
  const engineRef = useRef(new DanceScoreEngine(config));
195
202
  const [lastScore, setLastScore] = useState(null);
@@ -1,6 +1,7 @@
1
1
  import { jsxs, jsx } from 'react/jsx-runtime';
2
2
  import { resolveCssColor } from '@nice2dev/ui';
3
3
  import { forwardRef, useRef, useImperativeHandle, useCallback, useEffect, useState } from 'react';
4
+ import { useNiceTranslation } from '../core/i18n.js';
4
5
 
5
6
  // ── Standard keypoint definitions (COCO 17-keypoint model) ──────
6
7
  const COCO_KEYPOINTS = [
@@ -147,7 +148,8 @@ function usePoseDetection(config = {}) {
147
148
  return { start, stop, isDetecting, getLatestFrame: () => latestFrameRef.current };
148
149
  }
149
150
  // ── Component: PoseDetector ─────────────────────────────────────
150
- const NicePoseDetector = forwardRef(function NicePoseDetector(props, ref) {
151
+ const NicePoseDetector = /* @__PURE__ */ forwardRef(function NicePoseDetector(props, ref) {
152
+ const { t } = useNiceTranslation();
151
153
  const { onFrame, onStart, onStop, onError, className, style, width = 640, height = 480, mirror = true, showSkeleton = true, showConfidence = false, ...config } = props;
152
154
  const videoRef = useRef(null);
153
155
  const canvasRef = useRef(null);
@@ -283,7 +285,7 @@ const NicePoseDetector = forwardRef(function NicePoseDetector(props, ref) {
283
285
  cursor: 'pointer',
284
286
  fontSize: 14,
285
287
  fontWeight: 600,
286
- }, children: "Start Camera" }))] }));
288
+ }, children: t('poseDetector.startCamera', 'Start Camera') }))] }));
287
289
  });
288
290
 
289
291
  export { NicePoseDetector, usePoseDetection };
@@ -1,6 +1,7 @@
1
1
  import { jsxs, jsx } from 'react/jsx-runtime';
2
2
  import { NiceTutorialButton, resolveTutorial, NiceAppShell, NiceIcon, resolveCssColor } from '@nice2dev/ui';
3
3
  import { useRef, useState, useMemo, useEffect, useCallback, useReducer } from 'react';
4
+ import { useNiceTranslation } from '../core/i18n.js';
4
5
  import styles from './MaterialEditor.module.css.js';
5
6
  import { MAX_ZOOM, MIN_ZOOM, NODE_WIDTH, NODE_HEADER_HEIGHT, PORT_HEIGHT, PORT_TYPE_COLORS, NODE_CATEGORY_COLORS, NODE_CATEGORY_NAMES, DEFAULT_GRID_SIZE } from './materialEditorTypes.js';
6
7
  import { compileShader, screenToGraph, importGraphFromJSON, exportGraphToJSON, updateNodeInGraph, calculateZoomToFit, cloneNode, generateNodeId, removeNodesFromGraph, removeConnectionFromGraph, getNodeDefinition, canConnect, createConnection, addConnectionToGraph, moveNodesInGraph, createNode, addNodeToGraph, createEmptyGraph } from './materialEditorUtils.js';
@@ -361,10 +362,12 @@ function useMaterialEditor(initialGraph) {
361
362
  }, 300);
362
363
  return () => clearTimeout(timeout);
363
364
  }
365
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- compiling only stops a second compile; as a dependency its true -> false flip would compile the same graph again, forever
364
366
  }, [state.graph, state.autoCompile]);
365
367
  return { state, dispatch };
366
368
  }
367
369
  function NodePalette({ searchQuery, expandedCategories, onSearch, onToggleCategory, onAddNode, onDragStart, }) {
370
+ const { t } = useNiceTranslation();
368
371
  const filteredNodes = searchQuery ? searchNodes(searchQuery) : MATERIAL_NODE_DEFINITIONS;
369
372
  const categories = useMemo(() => {
370
373
  const cats = new Map();
@@ -376,7 +379,7 @@ function NodePalette({ searchQuery, expandedCategories, onSearch, onToggleCatego
376
379
  }
377
380
  return cats;
378
381
  }, [filteredNodes]);
379
- return (jsxs("div", { className: styles.palette, children: [jsx("div", { className: styles.paletteSearch, children: jsx("input", { type: "text", placeholder: "Search nodes...", value: searchQuery, onChange: (e) => onSearch(e.target.value), className: styles.searchInput }) }), jsx("div", { className: styles.paletteCategories, children: Array.from(categories.entries()).map(([category, nodes]) => (jsxs("div", { className: styles.paletteCategory, children: [jsxs("button", { className: styles.categoryHeader, onClick: () => onToggleCategory(category), style: { borderLeftColor: NODE_CATEGORY_COLORS[category] }, children: [jsx("span", { className: styles.categoryIcon, children: jsx(NiceIcon, { name: expandedCategories.includes(category) ? 'chevron-down' : 'chevron-right', size: 14 }) }), jsx("span", { children: NODE_CATEGORY_NAMES[category] }), jsx("span", { className: styles.categoryCount, children: nodes.length })] }), expandedCategories.includes(category) && (jsx("div", { className: styles.categoryNodes, children: nodes.map((nodeDef) => (jsxs("div", { className: styles.paletteNode, draggable: true, onDragStart: (e) => onDragStart(nodeDef.type, e), onDoubleClick: () => onAddNode(nodeDef.type), title: nodeDef.description, children: [jsx("span", { className: styles.nodeColor, style: {
382
+ return (jsxs("div", { className: styles.palette, children: [jsx("div", { className: styles.paletteSearch, children: jsx("input", { type: "text", placeholder: t('materialEditor.searchNodes', 'Search nodes...'), value: searchQuery, onChange: (e) => onSearch(e.target.value), className: styles.searchInput }) }), jsx("div", { className: styles.paletteCategories, children: Array.from(categories.entries()).map(([category, nodes]) => (jsxs("div", { className: styles.paletteCategory, children: [jsxs("button", { className: styles.categoryHeader, onClick: () => onToggleCategory(category), style: { borderLeftColor: NODE_CATEGORY_COLORS[category] }, children: [jsx("span", { className: styles.categoryIcon, children: jsx(NiceIcon, { name: expandedCategories.includes(category) ? 'chevron-down' : 'chevron-right', size: 14 }) }), jsx("span", { children: NODE_CATEGORY_NAMES[category] }), jsx("span", { className: styles.categoryCount, children: nodes.length })] }), expandedCategories.includes(category) && (jsx("div", { className: styles.categoryNodes, children: nodes.map((nodeDef) => (jsxs("div", { className: styles.paletteNode, draggable: true, onDragStart: (e) => onDragStart(nodeDef.type, e), onDoubleClick: () => onAddNode(nodeDef.type), title: nodeDef.description, children: [jsx("span", { className: styles.nodeColor, style: {
380
383
  backgroundColor: nodeDef.color || NODE_CATEGORY_COLORS[nodeDef.category],
381
384
  } }), jsx("span", { className: styles.nodeName, children: nodeDef.name })] }, nodeDef.type))) }))] }, category))) })] }));
382
385
  }
@@ -452,9 +455,11 @@ function GraphNode({ node, definition, selected, viewTransform, connections, onS
452
455
  }) })] })), node.comment && jsx("div", { className: styles.nodeComment, children: node.comment })] }));
453
456
  }
454
457
  function Toolbar({ canUndo, canRedo, canPaste, hasSelection, autoCompile, compiling, showGrid, snapToGrid, onUndo, onRedo, onCopy, onCut, onPaste, onDuplicate, onDelete, onSelectAll, onZoomToFit, onToggleGrid, onToggleSnap, onToggleAutoCompile, onCompile, onNew, onExport, onImport, }) {
455
- return (jsxs("div", { className: styles.toolbar, children: [jsxs("div", { className: styles.toolbarGroup, children: [jsx("button", { onClick: onNew, title: "New Material (Ctrl+N)", "aria-label": "New Material", children: jsx(NiceIcon, { name: "file", size: 16 }) }), jsx("button", { onClick: onImport, title: "Import (Ctrl+O)", "aria-label": "Import", children: jsx(NiceIcon, { name: "folder", size: 16 }) }), jsx("button", { onClick: onExport, title: "Export (Ctrl+S)", "aria-label": "Export", children: jsx(NiceIcon, { name: "save", size: 16 }) })] }), jsx("div", { className: styles.toolbarSeparator }), jsxs("div", { className: styles.toolbarGroup, children: [jsx("button", { onClick: onUndo, disabled: !canUndo, title: "Undo (Ctrl+Z)", "aria-label": "Undo", children: jsx(NiceIcon, { name: "arrow-left", size: 16 }) }), jsx("button", { onClick: onRedo, disabled: !canRedo, title: "Redo (Ctrl+Y)", "aria-label": "Redo", children: jsx(NiceIcon, { name: "arrow-right", size: 16 }) })] }), jsx("div", { className: styles.toolbarSeparator }), jsxs("div", { className: styles.toolbarGroup, children: [jsx("button", { onClick: onCut, disabled: !hasSelection, title: "Cut (Ctrl+X)", "aria-label": "Cut", children: jsx(NiceIcon, { name: "scissors", size: 16 }) }), jsx("button", { onClick: onCopy, disabled: !hasSelection, title: "Copy (Ctrl+C)", "aria-label": "Copy", children: jsx(NiceIcon, { name: "clipboard", size: 16 }) }), jsx("button", { onClick: onPaste, disabled: !canPaste, title: "Paste (Ctrl+V)", "aria-label": "Paste", children: jsx(NiceIcon, { name: "download", size: 16 }) }), jsx("button", { onClick: onDuplicate, disabled: !hasSelection, title: "Duplicate (Ctrl+D)", "aria-label": "Duplicate", children: jsx(NiceIcon, { name: "refresh", size: 16 }) }), jsx("button", { onClick: onDelete, disabled: !hasSelection, title: "Delete (Del)", "aria-label": "Delete", children: jsx(NiceIcon, { name: "trash", size: 16 }) })] }), jsx("div", { className: styles.toolbarSeparator }), jsxs("div", { className: styles.toolbarGroup, children: [jsx("button", { onClick: onSelectAll, title: "Select All (Ctrl+A)", "aria-label": "Select All", children: jsx(NiceIcon, { name: "square", size: 16 }) }), jsx("button", { onClick: onZoomToFit, title: "Zoom to Fit (F)", "aria-label": "Zoom to Fit", children: jsx(NiceIcon, { name: "search", size: 16 }) }), jsx("button", { onClick: onToggleGrid, className: showGrid ? styles.active : '', title: "Toggle Grid (G)", "aria-label": "Toggle Grid", children: jsx(NiceIcon, { name: "hash", size: 16 }) }), jsx("button", { onClick: onToggleSnap, className: snapToGrid ? styles.active : '', title: "Toggle Snap (S)", "aria-label": "Toggle Snap", children: jsx(NiceIcon, { name: "target", size: 16 }) })] }), jsx("div", { className: styles.toolbarSeparator }), jsxs("div", { className: styles.toolbarGroup, children: [jsx("button", { onClick: onToggleAutoCompile, className: autoCompile ? styles.active : '', title: "Auto Compile", "aria-label": "Auto Compile", children: jsx(NiceIcon, { name: "zap", size: 16 }) }), jsx("button", { onClick: onCompile, disabled: compiling, title: "Compile (F5)", "aria-label": "Compile", children: jsx(NiceIcon, { name: compiling ? 'clock' : 'play', size: 16 }) })] })] }));
458
+ const { t } = useNiceTranslation();
459
+ return (jsxs("div", { className: styles.toolbar, children: [jsxs("div", { className: styles.toolbarGroup, children: [jsx("button", { onClick: onNew, title: t('materialEditor.newMaterialCtrlN', 'New Material (Ctrl+N)'), "aria-label": t('materialEditor.newMaterial', 'New Material'), children: jsx(NiceIcon, { name: "file", size: 16 }) }), jsx("button", { onClick: onImport, title: t('materialEditor.importCtrlO', 'Import (Ctrl+O)'), "aria-label": t('materialEditor.import', 'Import'), children: jsx(NiceIcon, { name: "folder", size: 16 }) }), jsx("button", { onClick: onExport, title: t('materialEditor.exportCtrlS', 'Export (Ctrl+S)'), "aria-label": t('materialEditor.export', 'Export'), children: jsx(NiceIcon, { name: "save", size: 16 }) })] }), jsx("div", { className: styles.toolbarSeparator }), jsxs("div", { className: styles.toolbarGroup, children: [jsx("button", { onClick: onUndo, disabled: !canUndo, title: t('materialEditor.undoCtrlZ', 'Undo (Ctrl+Z)'), "aria-label": t('materialEditor.undo', 'Undo'), children: jsx(NiceIcon, { name: "arrow-left", size: 16 }) }), jsx("button", { onClick: onRedo, disabled: !canRedo, title: t('materialEditor.redoCtrlY', 'Redo (Ctrl+Y)'), "aria-label": t('materialEditor.redo', 'Redo'), children: jsx(NiceIcon, { name: "arrow-right", size: 16 }) })] }), jsx("div", { className: styles.toolbarSeparator }), jsxs("div", { className: styles.toolbarGroup, children: [jsx("button", { onClick: onCut, disabled: !hasSelection, title: t('materialEditor.cutCtrlX', 'Cut (Ctrl+X)'), "aria-label": t('materialEditor.cut', 'Cut'), children: jsx(NiceIcon, { name: "scissors", size: 16 }) }), jsx("button", { onClick: onCopy, disabled: !hasSelection, title: t('materialEditor.copyCtrlC', 'Copy (Ctrl+C)'), "aria-label": t('materialEditor.copy', 'Copy'), children: jsx(NiceIcon, { name: "clipboard", size: 16 }) }), jsx("button", { onClick: onPaste, disabled: !canPaste, title: t('materialEditor.pasteCtrlV', 'Paste (Ctrl+V)'), "aria-label": t('materialEditor.paste', 'Paste'), children: jsx(NiceIcon, { name: "download", size: 16 }) }), jsx("button", { onClick: onDuplicate, disabled: !hasSelection, title: t('materialEditor.duplicateCtrlD', 'Duplicate (Ctrl+D)'), "aria-label": t('materialEditor.duplicate', 'Duplicate'), children: jsx(NiceIcon, { name: "refresh", size: 16 }) }), jsx("button", { onClick: onDelete, disabled: !hasSelection, title: t('materialEditor.deleteDel', 'Delete (Del)'), "aria-label": t('materialEditor.delete', 'Delete'), children: jsx(NiceIcon, { name: "trash", size: 16 }) })] }), jsx("div", { className: styles.toolbarSeparator }), jsxs("div", { className: styles.toolbarGroup, children: [jsx("button", { onClick: onSelectAll, title: t('materialEditor.selectAllCtrlA', 'Select All (Ctrl+A)'), "aria-label": t('materialEditor.selectAll', 'Select All'), children: jsx(NiceIcon, { name: "square", size: 16 }) }), jsx("button", { onClick: onZoomToFit, title: t('materialEditor.zoomToFitF', 'Zoom to Fit (F)'), "aria-label": t('materialEditor.zoomToFit', 'Zoom to Fit'), children: jsx(NiceIcon, { name: "search", size: 16 }) }), jsx("button", { onClick: onToggleGrid, className: showGrid ? styles.active : '', title: t('materialEditor.toggleGridG', 'Toggle Grid (G)'), "aria-label": t('materialEditor.toggleGrid', 'Toggle Grid'), children: jsx(NiceIcon, { name: "hash", size: 16 }) }), jsx("button", { onClick: onToggleSnap, className: snapToGrid ? styles.active : '', title: t('materialEditor.toggleSnapS', 'Toggle Snap (S)'), "aria-label": t('materialEditor.toggleSnap', 'Toggle Snap'), children: jsx(NiceIcon, { name: "target", size: 16 }) })] }), jsx("div", { className: styles.toolbarSeparator }), jsxs("div", { className: styles.toolbarGroup, children: [jsx("button", { onClick: onToggleAutoCompile, className: autoCompile ? styles.active : '', title: t('materialEditor.autoCompile', 'Auto Compile'), "aria-label": t('materialEditor.autoCompile', 'Auto Compile'), children: jsx(NiceIcon, { name: "zap", size: 16 }) }), jsx("button", { onClick: onCompile, disabled: compiling, title: t('materialEditor.compileF5', 'Compile (F5)'), "aria-label": t('materialEditor.compile', 'Compile'), children: jsx(NiceIcon, { name: compiling ? 'clock' : 'play', size: 16 }) })] })] }));
456
460
  }
457
461
  function PreviewPanel({ shader, mode, compiling, onModeChange }) {
462
+ const { t } = useNiceTranslation();
458
463
  const canvasRef = useRef(null);
459
464
  const previewModes = ['sphere', 'cube', 'plane', 'cylinder', 'torus'];
460
465
  // Simple preview rendering (in real implementation would use Three.js)
@@ -484,7 +489,7 @@ function PreviewPanel({ shader, mode, compiling, onModeChange }) {
484
489
  ctx.arc(100, 100, 80, 0, Math.PI * 2);
485
490
  ctx.fill();
486
491
  }, [shader, mode]);
487
- return (jsxs("div", { className: styles.previewPanel, children: [jsxs("div", { className: styles.previewHeader, children: [jsx("span", { children: "Preview" }), jsx("select", { value: mode, onChange: (e) => onModeChange(e.target.value), className: styles.previewModeSelect, children: previewModes.map((m) => (jsx("option", { value: m, children: m.charAt(0).toUpperCase() + m.slice(1) }, m))) })] }), jsxs("div", { className: styles.previewCanvas, children: [compiling && jsx("div", { className: styles.previewCompiling, children: "Compiling..." }), jsx("canvas", { ref: canvasRef, width: 200, height: 200 })] }), (shader === null || shader === void 0 ? void 0 : shader.errors.length) ? (jsx("div", { className: styles.previewErrors, children: shader.errors.slice(0, 3).map((err, i) => (jsx("div", { className: styles.previewError, children: err.message }, i))) })) : null] }));
492
+ return (jsxs("div", { className: styles.previewPanel, children: [jsxs("div", { className: styles.previewHeader, children: [jsx("span", { children: t('materialEditor.preview', 'Preview') }), jsx("select", { value: mode, onChange: (e) => onModeChange(e.target.value), className: styles.previewModeSelect, children: previewModes.map((m) => (jsx("option", { value: m, children: m.charAt(0).toUpperCase() + m.slice(1) }, m))) })] }), jsxs("div", { className: styles.previewCanvas, children: [compiling && (jsx("div", { className: styles.previewCompiling, children: t('materialEditor.compiling', 'Compiling...') })), jsx("canvas", { ref: canvasRef, width: 200, height: 200 })] }), (shader === null || shader === void 0 ? void 0 : shader.errors.length) ? (jsx("div", { className: styles.previewErrors, children: shader.errors.slice(0, 3).map((err, i) => (jsx("div", { className: styles.previewError, children: err.message }, i))) })) : null] }));
488
493
  }
489
494
  /* ═══════════════════════════════════════════════════════════════════════════
490
495
  Main Component
@@ -42,7 +42,7 @@ const PORT_TYPE_COLORS = {
42
42
  vec4: 'var(--nice-warning, #ffc107)',
43
43
  color: 'var(--nice-danger, #ff5722)',
44
44
  texture2d: 'var(--nice-info, #00bcd4)',
45
- textureCube: '#03A9F4',
45
+ textureCube: 'var(--nice-color-primary, #03A9F4)',
46
46
  sampler: 'var(--nice-accent, #3f51b5)',
47
47
  matrix3: 'var(--nice-accent, #673ab7)',
48
48
  matrix4: 'var(--nice-accent, #9c27b0)',
@@ -1450,17 +1450,20 @@ const logicNodes = [
1450
1450
  EXPORTS
1451
1451
  ═══════════════════════════════════════════════════════════════════════════ */
1452
1452
  /** All built-in node definitions */
1453
- const MATERIAL_NODE_DEFINITIONS = [
1454
- ...inputNodes,
1455
- ...outputNodes,
1456
- ...mathNodes,
1457
- ...vectorNodes,
1458
- ...colorNodes,
1459
- ...textureNodes,
1460
- ...proceduralNodes,
1461
- ...utilityNodes,
1462
- ...logicNodes,
1463
- ];
1453
+ const MATERIAL_NODE_DEFINITIONS =
1454
+ /* @__PURE__ */ (function () {
1455
+ return [
1456
+ ...inputNodes,
1457
+ ...outputNodes,
1458
+ ...mathNodes,
1459
+ ...vectorNodes,
1460
+ ...colorNodes,
1461
+ ...textureNodes,
1462
+ ...proceduralNodes,
1463
+ ...utilityNodes,
1464
+ ...logicNodes,
1465
+ ];
1466
+ })();
1464
1467
  /** Node definitions map by type */
1465
1468
  const MATERIAL_NODE_MAP = new Map(MATERIAL_NODE_DEFINITIONS.map((def) => [def.type, def]));
1466
1469
  /** Get nodes by category */
@@ -1,5 +1,7 @@
1
1
  import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
2
2
  import { NiceAppShell } from '@nice2dev/ui';
3
+ import { EditorAskDialog } from '../core/editorAsk.js';
4
+ import { useNiceTranslation } from '../core/i18n.js';
3
5
  import styles from './ModelEditor.module.css.js';
4
6
  import ModelEditorLeftPanel from './ModelEditorLeftPanel.js';
5
7
  import ModelEditorMenuBar from './ModelEditorMenuBar.js';
@@ -10,8 +12,9 @@ import { SUPPORTED_EXTENSIONS } from './modelEditorTypes.js';
10
12
  import ModelEditorViewport from './ModelEditorViewport.js';
11
13
  import { useModelEditor } from './useModelEditor.js';
12
14
 
13
- const ACCEPT = SUPPORTED_EXTENSIONS.map((e) => `.${e}`).join(',');
15
+ const ACCEPT = /* @__PURE__ */ SUPPORTED_EXTENSIONS.map((e) => `.${e}`).join(',');
14
16
  const NiceModelEditor = ({ className, onSaveToLibrary, onAIPoseVideo, }) => {
17
+ const { t } = useNiceTranslation();
15
18
  const api = useModelEditor({ onSaveToLibrary, onAIPoseVideo });
16
19
  return (jsxs(Fragment, { children: [jsx("input", { ref: api.fileInputRef, type: "file", accept: ACCEPT, style: { display: 'none' }, onChange: (e) => {
17
20
  var _a;
@@ -27,7 +30,11 @@ const NiceModelEditor = ({ className, onSaveToLibrary, onAIPoseVideo, }) => {
27
30
  api.mergeAnimationFBX(f);
28
31
  }
29
32
  e.target.value = '';
30
- } }), jsx("input", { ref: api.videoInputRef, type: "file", accept: "video/*", style: { display: 'none' }, onChange: api.handleAIVideo }), jsx(NiceAppShell, { className: `${styles.root} ${className !== null && className !== void 0 ? className : ''}`, flush: true, menubar: jsx(ModelEditorMenuBar, { api: api, hasOnSaveToLibrary: !!onSaveToLibrary }), sidebar: jsxs("div", { style: { display: 'flex', minHeight: 0 }, children: [jsx(ModelEditorLeftPanel, { api: api }), jsx(ModelEditorToolbar, { api: api })] }), rightSidebar: jsx(ModelEditorRightPanel, { api: api }), bottomPanel: jsx(ModelEditorTimeline, { api: api }), statusBar: jsxs("div", { className: styles.statusBar, children: [jsx("span", { children: api.statusText }), jsxs("span", { style: { marginLeft: 'auto' }, children: [api.polyCount.toLocaleString(), " triangles \u00B7 ", api.meshCount, " meshes \u00B7 ", api.boneCount, ' ', "bones \u00B7 ", api.animations.length, " anim(s)"] })] }), children: jsx(ModelEditorViewport, { api: api }) })] }));
33
+ } }), jsx("input", { ref: api.videoInputRef, type: "file", accept: "video/*", style: { display: 'none' }, onChange: api.handleAIVideo }), jsx(EditorAskDialog, { ask: api.pendingAsk }), jsx(NiceAppShell, { className: `${styles.root} ${className !== null && className !== void 0 ? className : ''}`, flush: true, menubar: jsx(ModelEditorMenuBar, { api: api, hasOnSaveToLibrary: !!onSaveToLibrary }), sidebar: jsxs("div", { style: { display: 'flex', minHeight: 0 }, children: [jsx(ModelEditorLeftPanel, { api: api }), jsx(ModelEditorToolbar, { api: api })] }), rightSidebar: jsx(ModelEditorRightPanel, { api: api }), bottomPanel: jsx(ModelEditorTimeline, { api: api }), statusBar: jsxs("div", { className: styles.statusBar, children: [jsx("span", { children: api.statusText }), jsx("span", { style: { marginLeft: 'auto' }, children: t('modelEditor.trianglesMeshesBonesAnimS', '{polyCount} triangles · {meshCount} meshes · {boneCount} bones · {animations} anim(s)')
34
+ .replace('{polyCount}', String(api.polyCount.toLocaleString()))
35
+ .replace('{meshCount}', String(api.meshCount))
36
+ .replace('{boneCount}', String(api.boneCount))
37
+ .replace('{animations}', String(api.animations.length)) })] }), children: jsx(ModelEditorViewport, { api: api }) })] }));
31
38
  };
32
39
 
33
40
  export { NiceModelEditor };
@@ -1,9 +1,12 @@
1
1
  import { jsx, jsxs } from 'react/jsx-runtime';
2
2
  import { NiceIcon, NiceButton, NiceTextInput } 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 ModelEditorLeftPanel = React.memo(({ api }) => {
7
+ const ModelEditorLeftPanel = /* @__PURE__ */ Object.assign(
8
+ /* @__PURE__ */ React.memo(({ api }) => {
9
+ const { t } = useNiceTranslation();
7
10
  const { sceneTree, selectedId, outlinerSearch, setOutlinerSearch, selectObject, rebuildTree, toggleNodeExpanded, toggleNodeVisibility, } = api;
8
11
  const renderTreeNode = (node, depth) => {
9
12
  // Outliner search filter
@@ -37,8 +40,11 @@ const ModelEditorLeftPanel = React.memo(({ api }) => {
37
40
  toggleNodeVisibility(node);
38
41
  }, children: jsx(NiceIcon, { name: node.visible ? 'eye' : 'eye-off', size: 14 }) })] }), node.expanded && node.children.map((c) => renderTreeNode(c, depth + 1))] }, node.id));
39
42
  };
40
- return (jsx("div", { className: styles.leftPanel, children: jsxs("div", { className: styles.panelSection, children: [jsxs("div", { className: styles.panelTitle, children: [jsx(NiceIcon, { name: "clipboard", size: 14 }), " Outliner", jsx(NiceButton, { variant: "ghost", size: "sm", onClick: rebuildTree, title: "Refresh", "aria-label": "Refresh outliner", children: jsx(NiceIcon, { name: "refresh", size: 14 }) })] }), jsx("div", { className: styles.outlinerSearch, children: jsx(NiceTextInput, { className: styles.propInput, placeholder: "Filter...", value: outlinerSearch, onChange: (val) => setOutlinerSearch(val), style: { width: '100%', marginBottom: 4 } }) }), jsxs("div", { className: styles.panelContent, style: { overflowY: 'auto', flex: 1 }, children: [sceneTree.length === 0 && (jsx("div", { style: { color: 'var(--nice-text-secondary, #666)', fontSize: 10, padding: '8px 0' }, children: "Drop a 3D file to begin" })), sceneTree.map((n) => renderTreeNode(n, 0))] })] }) }));
41
- });
42
- ModelEditorLeftPanel.displayName = 'ModelEditorLeftPanel';
43
+ return (jsx("div", { className: styles.leftPanel, children: jsxs("div", { className: styles.panelSection, children: [jsxs("div", { className: styles.panelTitle, children: [jsx(NiceIcon, { name: "clipboard", size: 14 }), " ", t('modelEditorLeftPanel.outliner', 'Outliner'), jsx(NiceButton, { variant: "ghost", size: "sm", onClick: rebuildTree, title: t('modelEditorLeftPanel.refresh', 'Refresh'), "aria-label": t('modelEditorLeftPanel.refreshOutliner', 'Refresh outliner'), children: jsx(NiceIcon, { name: "refresh", size: 14 }) })] }), jsx("div", { className: styles.outlinerSearch, children: jsx(NiceTextInput, { className: styles.propInput, placeholder: t('modelEditorLeftPanel.filter', 'Filter...'), value: outlinerSearch, onChange: (val) => setOutlinerSearch(val), style: { width: '100%', marginBottom: 4 } }) }), jsxs("div", { className: styles.panelContent, style: { overflowY: 'auto', flex: 1 }, children: [sceneTree.length === 0 && (jsx("div", { style: {
44
+ color: 'var(--nice-text-secondary, #666)',
45
+ fontSize: 10,
46
+ padding: '8px 0',
47
+ }, children: t('modelEditorLeftPanel.dropA3dFileTo', 'Drop a 3D file to begin') })), sceneTree.map((n) => renderTreeNode(n, 0))] })] }) }));
48
+ }), { displayName: 'ModelEditorLeftPanel' });
43
49
 
44
50
  export { ModelEditorLeftPanel as default };