@nice2dev/ui-3d 1.2.1 → 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 (54) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/dist/cjs/material/NiceMaterialEditor.js +1 -0
  3. package/dist/cjs/model/ModelEditor.js +7 -1
  4. package/dist/cjs/model/ModelEditorMenuBar.js +5 -5
  5. package/dist/cjs/model/ModelEditorRightPanel.js +13 -9
  6. package/dist/cjs/model/ModelEditorSubComponents.js +9 -3
  7. package/dist/cjs/model/ModelEditorTimeline.js +3 -1
  8. package/dist/cjs/model/ModelEditorToolbar.js +1 -1
  9. package/dist/cjs/model/ModelEditorViewport.js +5 -2
  10. package/dist/cjs/model/ModelViewer.js +4 -4
  11. package/dist/cjs/model/NiceArmatureEditor.js +4 -4
  12. package/dist/cjs/model/NiceMorphTargetEditor.js +3 -1
  13. package/dist/cjs/model/NiceOctree.js +7 -4
  14. package/dist/cjs/model/NicePhysicsSimulation.js +4 -4
  15. package/dist/cjs/model/NiceProceduralGeometry.js +3 -3
  16. package/dist/cjs/model/NiceTerrainEditor.js +2 -2
  17. package/dist/cjs/model/NiceWeightPainter.js +2 -2
  18. package/dist/cjs/model/NiceXRPreview.js +6 -3
  19. package/dist/cjs/model/useModelEditor.js +92 -73
  20. package/dist/cjs/model/useModelViewer.js +13 -6
  21. package/dist/cjs/particle/NiceParticleEditor.js +10 -3
  22. package/dist/cjs/rendering/NiceCascadedShadows.js +1 -1
  23. package/dist/cjs/rendering/NiceRenderExport.js +2 -2
  24. package/dist/cjs/rendering/NiceSSAO.js +2 -1
  25. package/dist/cjs/rendering/NiceSSR.js +2 -1
  26. package/dist/cjs/rendering/NiceWebGPURenderer.js +2 -1
  27. package/dist/cjs/uv/NiceUVEditor.js +3 -3
  28. package/dist/esm/material/NiceMaterialEditor.js +1 -0
  29. package/dist/esm/model/ModelEditor.js +7 -1
  30. package/dist/esm/model/ModelEditorMenuBar.js +5 -5
  31. package/dist/esm/model/ModelEditorRightPanel.js +13 -9
  32. package/dist/esm/model/ModelEditorSubComponents.js +9 -3
  33. package/dist/esm/model/ModelEditorTimeline.js +3 -1
  34. package/dist/esm/model/ModelEditorToolbar.js +1 -1
  35. package/dist/esm/model/ModelEditorViewport.js +5 -2
  36. package/dist/esm/model/ModelViewer.js +4 -4
  37. package/dist/esm/model/NiceArmatureEditor.js +4 -4
  38. package/dist/esm/model/NiceMorphTargetEditor.js +3 -1
  39. package/dist/esm/model/NiceOctree.js +7 -4
  40. package/dist/esm/model/NicePhysicsSimulation.js +4 -4
  41. package/dist/esm/model/NiceProceduralGeometry.js +3 -3
  42. package/dist/esm/model/NiceTerrainEditor.js +2 -2
  43. package/dist/esm/model/NiceWeightPainter.js +2 -2
  44. package/dist/esm/model/NiceXRPreview.js +6 -3
  45. package/dist/esm/model/useModelEditor.js +92 -73
  46. package/dist/esm/model/useModelViewer.js +13 -6
  47. package/dist/esm/particle/NiceParticleEditor.js +10 -3
  48. package/dist/esm/rendering/NiceCascadedShadows.js +1 -1
  49. package/dist/esm/rendering/NiceRenderExport.js +2 -2
  50. package/dist/esm/rendering/NiceSSAO.js +2 -1
  51. package/dist/esm/rendering/NiceSSR.js +2 -1
  52. package/dist/esm/rendering/NiceWebGPURenderer.js +2 -1
  53. package/dist/esm/uv/NiceUVEditor.js +3 -3
  54. package/package.json +2 -2
@@ -346,7 +346,7 @@ const NiceRenderExport = ({ renderer, scene, camera, className = '', }) => {
346
346
  color: config.width === p.width && config.height === p.height
347
347
  ? 'var(--nice-color-white, #fff)'
348
348
  : 'inherit',
349
- }, children: p.label }, p.label))) }), jsxRuntime.jsxs("div", { style: { display: 'flex', gap: 4, alignItems: 'center' }, children: [jsxRuntime.jsx("input", { type: "number", value: config.width, min: 1, max: 8192, onChange: (e) => setConfig({ width: parseInt(e.target.value, 10) || 1 }), style: { width: 70, padding: '3px 6px' } }), jsxRuntime.jsx("span", { children: "\u00D7" }), jsxRuntime.jsx("input", { type: "number", value: config.height, min: 1, max: 8192, onChange: (e) => setConfig({ height: parseInt(e.target.value, 10) || 1 }), style: { width: 70, padding: '3px 6px' } })] }), jsxRuntime.jsxs("label", { children: ["Format:", jsxRuntime.jsxs("select", { value: config.format, onChange: (e) => setConfig({ format: e.target.value }), style: { width: '100%', marginTop: 2 }, children: [jsxRuntime.jsx("option", { value: "png", children: "PNG (lossless + alpha)" }), jsxRuntime.jsx("option", { value: "jpeg", children: "JPEG (lossy, no alpha)" }), jsxRuntime.jsx("option", { value: "exr", children: "EXR (HDR float)" })] })] }), jsxRuntime.jsxs("label", { children: ["Background:", jsxRuntime.jsxs("select", { value: config.backgroundMode, onChange: (e) => setConfig({ backgroundMode: e.target.value }), style: { width: '100%', marginTop: 2 }, children: [jsxRuntime.jsx("option", { value: "transparent", children: t('renderExport.transparent', 'Transparent') }), jsxRuntime.jsx("option", { value: "color", children: t('renderExport.solidColor', 'Solid Color') }), jsxRuntime.jsx("option", { value: "scene", children: t('renderExport.sceneBackground', 'Scene Background') })] })] }), config.backgroundMode === 'color' && (jsxRuntime.jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 6 }, children: ["Color:", jsxRuntime.jsx("input", { type: "color", value: config.backgroundColor, onChange: (e) => setConfig({ backgroundColor: e.target.value }) })] })), jsxRuntime.jsxs("label", { children: ["Supersampling: ", config.samples, "x", jsxRuntime.jsxs("select", { value: config.samples, onChange: (e) => setConfig({ samples: parseInt(e.target.value, 10) }), style: { width: '100%', marginTop: 2 }, children: [jsxRuntime.jsx("option", { value: 1, children: "1x (none)" }), jsxRuntime.jsx("option", { value: 2, children: "2x" }), jsxRuntime.jsx("option", { value: 4, children: "4x (best quality)" })] })] }), jsxRuntime.jsx("button", { type: "button", onClick: handleRender, disabled: isRendering, style: {
349
+ }, children: p.label }, p.label))) }), jsxRuntime.jsxs("div", { style: { display: 'flex', gap: 4, alignItems: 'center' }, children: [jsxRuntime.jsx("input", { type: "number", value: config.width, min: 1, max: 8192, onChange: (e) => setConfig({ width: parseInt(e.target.value, 10) || 1 }), style: { width: 70, padding: '3px 6px' } }), jsxRuntime.jsx("span", { children: "\u00D7" }), jsxRuntime.jsx("input", { type: "number", value: config.height, min: 1, max: 8192, onChange: (e) => setConfig({ height: parseInt(e.target.value, 10) || 1 }), style: { width: 70, padding: '3px 6px' } })] }), jsxRuntime.jsxs("label", { children: [t('renderExport.format', 'Format:'), jsxRuntime.jsxs("select", { value: config.format, onChange: (e) => setConfig({ format: e.target.value }), style: { width: '100%', marginTop: 2 }, children: [jsxRuntime.jsx("option", { value: "png", children: t('renderExport.pngLosslessAlpha', 'PNG (lossless + alpha)') }), jsxRuntime.jsx("option", { value: "jpeg", children: t('renderExport.jpegLossyNoAlpha', 'JPEG (lossy, no alpha)') }), jsxRuntime.jsx("option", { value: "exr", children: t('renderExport.exrHdrFloat', 'EXR (HDR float)') })] })] }), jsxRuntime.jsxs("label", { children: [t('renderExport.background', 'Background:'), jsxRuntime.jsxs("select", { value: config.backgroundMode, onChange: (e) => setConfig({ backgroundMode: e.target.value }), style: { width: '100%', marginTop: 2 }, children: [jsxRuntime.jsx("option", { value: "transparent", children: t('renderExport.transparent', 'Transparent') }), jsxRuntime.jsx("option", { value: "color", children: t('renderExport.solidColor', 'Solid Color') }), jsxRuntime.jsx("option", { value: "scene", children: t('renderExport.sceneBackground', 'Scene Background') })] })] }), config.backgroundMode === 'color' && (jsxRuntime.jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 6 }, children: [t('renderExport.color', 'Color:'), jsxRuntime.jsx("input", { type: "color", value: config.backgroundColor, onChange: (e) => setConfig({ backgroundColor: e.target.value }) })] })), jsxRuntime.jsxs("label", { children: [t('renderExport.supersamplingX', 'Supersampling: {samples}x').replace('{samples}', String(config.samples)), jsxRuntime.jsxs("select", { value: config.samples, onChange: (e) => setConfig({ samples: parseInt(e.target.value, 10) }), style: { width: '100%', marginTop: 2 }, children: [jsxRuntime.jsx("option", { value: 1, children: t('renderExport.n1xNone', '1x (none)') }), jsxRuntime.jsx("option", { value: 2, children: "2x" }), jsxRuntime.jsx("option", { value: 4, children: t('renderExport.n4xBestQuality', '4x (best quality)') })] })] }), jsxRuntime.jsx("button", { type: "button", onClick: handleRender, disabled: isRendering, style: {
350
350
  background: isRendering
351
351
  ? 'var(--nice-bg-tertiary, #94a3b8)'
352
352
  : 'var(--nice-color-accent, #6366f1)',
@@ -355,7 +355,7 @@ const NiceRenderExport = ({ renderer, scene, camera, className = '', }) => {
355
355
  borderRadius: 4,
356
356
  padding: '8px 12px',
357
357
  cursor: isRendering ? 'wait' : 'pointer',
358
- }, children: isRendering ? (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(ui.NiceIcon, { name: "clock", size: 14 }), " Rendering\u2026"] })) : (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(ui.NiceIcon, { name: "camera", size: 14 }), ' ', t('renderExport.render', 'Render {width}×{height} {format}')
358
+ }, children: isRendering ? (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(ui.NiceIcon, { name: "clock", size: 14 }), " ", t('renderExport.rendering', 'Rendering…')] })) : (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(ui.NiceIcon, { name: "camera", size: 14 }), ' ', t('renderExport.render', 'Render {width}×{height} {format}')
359
359
  .replace('{width}', String(config.width))
360
360
  .replace('{height}', String(config.height))
361
361
  .replace('{format}', config.format.toUpperCase())] })) })] }));
@@ -302,6 +302,7 @@ function useSSAO(renderer, scene, camera, configOverride) {
302
302
  blurMat.dispose();
303
303
  ortho.remove(quad);
304
304
  };
305
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- the uniforms' first values; the next effect updates them without rebuilding the render targets
305
306
  }, [renderer, camera, config.resolution]);
306
307
  // Update uniforms
307
308
  React.useEffect(() => {
@@ -360,7 +361,7 @@ function useSSAO(renderer, scene, camera, configOverride) {
360
361
  const NiceSSAO = ({ renderer, scene, camera, config: configProp, className = '', }) => {
361
362
  const { t } = i18n.useNiceTranslation();
362
363
  const { config, setConfig } = useSSAO(renderer, scene, camera, configProp);
363
- 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: ["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: ["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))] })] }));
364
365
  };
365
366
 
366
367
  exports.NiceSSAO = NiceSSAO;
@@ -228,6 +228,7 @@ function useSSR(renderer, scene, camera, configOverride) {
228
228
  material.dispose();
229
229
  ortho.remove(quad);
230
230
  };
231
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- the uniforms' first values; the next effect updates them without rebuilding the render targets
231
232
  }, [renderer, camera, config.resolution]);
232
233
  // Update uniforms when config changes
233
234
  React.useEffect(() => {
@@ -277,7 +278,7 @@ function useSSR(renderer, scene, camera, configOverride) {
277
278
  const NiceSSR = ({ renderer, scene, camera, config: configProp, className = '', }) => {
278
279
  const { t } = i18n.useNiceTranslation();
279
280
  const { config, setConfig } = useSSR(renderer, scene, camera, configProp);
280
- 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: ["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%' } })] })] }));
281
282
  };
282
283
 
283
284
  exports.NiceSSR = NiceSSR;
@@ -152,6 +152,7 @@ function useWebGPURenderer(canvas, scene, camera, width = 800, height = 600, onR
152
152
  disposed = true;
153
153
  (_a = rendererRef.current) === null || _a === void 0 ? void 0 : _a.dispose();
154
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
155
156
  }, [canvas]);
156
157
  // Update renderer settings reactively
157
158
  React.useEffect(() => {
@@ -239,7 +240,7 @@ const NiceWebGPURenderer = ({ canvas, scene, camera, width, height, onRendererRe
239
240
  borderRadius: 4,
240
241
  background: isWebGPU ? 'var(--nice-success, #22c55e)' : 'var(--nice-warning, #f59e0b)',
241
242
  color: 'var(--nice-color-white, #fff)',
242
- }, 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: t('webGPURenderer.linearSrgb', '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%' } })] })] }));
243
244
  };
244
245
 
245
246
  exports.NiceWebGPURenderer = NiceWebGPURenderer;
@@ -553,14 +553,14 @@ const NiceUVEditor = ({ geometry, texture, channel = 'uv', onUVChange, onSelecti
553
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) => ({
554
554
  ...prev,
555
555
  display: { ...prev.display, showIslandBounds: e.target.checked },
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: "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: t('uvEditor.snapping', '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) => ({
557
557
  ...prev,
558
558
  snapping: e.target.checked,
559
- })) }), t('uvEditor.enableSnapping', '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) => ({
560
560
  ...prev,
561
561
  snapSize: parseFloat(e.target.value),
562
562
  })) })] })] })] })) : undefined;
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.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: t('uvEditor.noGeometrySelected', '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') }))] }) })] }));
564
564
  };
565
565
 
566
566
  exports.NiceUVEditor = NiceUVEditor;
@@ -362,6 +362,7 @@ function useMaterialEditor(initialGraph) {
362
362
  }, 300);
363
363
  return () => clearTimeout(timeout);
364
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
365
366
  }, [state.graph, state.autoCompile]);
366
367
  return { state, dispatch };
367
368
  }
@@ -1,6 +1,7 @@
1
1
  import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
2
2
  import { NiceAppShell } from '@nice2dev/ui';
3
3
  import { EditorAskDialog } from '../core/editorAsk.js';
4
+ import { useNiceTranslation } from '../core/i18n.js';
4
5
  import styles from './ModelEditor.module.css.js';
5
6
  import ModelEditorLeftPanel from './ModelEditorLeftPanel.js';
6
7
  import ModelEditorMenuBar from './ModelEditorMenuBar.js';
@@ -13,6 +14,7 @@ import { useModelEditor } from './useModelEditor.js';
13
14
 
14
15
  const ACCEPT = /* @__PURE__ */ SUPPORTED_EXTENSIONS.map((e) => `.${e}`).join(',');
15
16
  const NiceModelEditor = ({ className, onSaveToLibrary, onAIPoseVideo, }) => {
17
+ const { t } = useNiceTranslation();
16
18
  const api = useModelEditor({ onSaveToLibrary, onAIPoseVideo });
17
19
  return (jsxs(Fragment, { children: [jsx("input", { ref: api.fileInputRef, type: "file", accept: ACCEPT, style: { display: 'none' }, onChange: (e) => {
18
20
  var _a;
@@ -28,7 +30,11 @@ const NiceModelEditor = ({ className, onSaveToLibrary, onAIPoseVideo, }) => {
28
30
  api.mergeAnimationFBX(f);
29
31
  }
30
32
  e.target.value = '';
31
- } }), 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 }), 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 }) })] }));
32
38
  };
33
39
 
34
40
  export { NiceModelEditor };
@@ -9,10 +9,10 @@ const ModelEditorMenuBar = /* @__PURE__ */ Object.assign(
9
9
  const { t } = useNiceTranslation();
10
10
  const { editorMode, setEditorMode, fileInputRef, mergeInputRef, addMenuOpen, setAddMenuOpen, addPrimitive, addSceneLight, addCameraObject, addEmpty, selectedNode, duplicateSelected, deleteSelected, exportGLTF, exportOBJ, exportSTL, exportPLY, exportUSDZ, saveToLibrary, clearScene, showGrid, setShowGrid, showAxes, setShowAxes, wireframe, setWireframe, showSkeleton, setShowSkeleton, showLightHelpers, setShowLightHelpers, bgColor, setBgColor, } = api;
11
11
  return (jsxs("div", { className: styles.menuBar, children: [jsx(NiceSelect, { className: styles.modeSelect, value: editorMode, onChange: (val) => setEditorMode(val), options: [
12
- { value: 'object', label: 'Object Mode' },
13
- { value: 'edit', label: 'Edit Mode' },
14
- { value: 'pose', label: 'Pose Mode' },
15
- ] }), jsx("div", { className: styles.menuSep }), jsxs(NiceButton, { className: styles.menuBtn, variant: "ghost", size: "sm", onClick: () => { var _a; return (_a = fileInputRef.current) === null || _a === void 0 ? void 0 : _a.click(); }, children: [jsx(NiceIcon, { name: "folder", size: 14 }), " ", t('modelEditorMenuBar.open', 'Open')] }), jsxs(NiceButton, { className: styles.menuBtn, variant: "ghost", size: "sm", onClick: () => { var _a; return (_a = mergeInputRef.current) === null || _a === void 0 ? void 0 : _a.click(); }, title: t('modelEditorMenuBar.mergeAnimationsFromFbxGltf', 'Merge animations from FBX/GLTF onto current model'), children: [jsx(NiceIcon, { name: "plus", size: 14 }), " ", t('modelEditorMenuBar.mergeAnim', 'Merge Anim')] }), jsx("div", { className: styles.menuSep }), jsxs("div", { className: styles.menuDropdownWrap, children: [jsxs(NiceButton, { className: styles.menuBtn, variant: "ghost", size: "sm", onClick: () => setAddMenuOpen(addMenuOpen ? null : 'mesh'), children: [jsx(NiceIcon, { name: "plus", size: 14 }), " Add ", jsx(NiceIcon, { name: "chevron-down", size: 14 })] }), addMenuOpen && (jsxs("div", { className: styles.dropdownMenu, children: [jsx("div", { className: styles.dropdownTitle, children: t('modelEditorMenuBar.mesh', 'Mesh') }), [
12
+ { value: 'object', label: t('modelEditorMenuBar.objectMode', 'Object Mode') },
13
+ { value: 'edit', label: t('modelEditorMenuBar.editMode', 'Edit Mode') },
14
+ { value: 'pose', label: t('modelEditorMenuBar.poseMode', 'Pose Mode') },
15
+ ] }), jsx("div", { className: styles.menuSep }), jsxs(NiceButton, { className: styles.menuBtn, variant: "ghost", size: "sm", onClick: () => { var _a; return (_a = fileInputRef.current) === null || _a === void 0 ? void 0 : _a.click(); }, children: [jsx(NiceIcon, { name: "folder", size: 14 }), " ", t('modelEditorMenuBar.open', 'Open')] }), jsxs(NiceButton, { className: styles.menuBtn, variant: "ghost", size: "sm", onClick: () => { var _a; return (_a = mergeInputRef.current) === null || _a === void 0 ? void 0 : _a.click(); }, title: t('modelEditorMenuBar.mergeAnimationsFromFbxGltf', 'Merge animations from FBX/GLTF onto current model'), children: [jsx(NiceIcon, { name: "plus", size: 14 }), " ", t('modelEditorMenuBar.mergeAnim', 'Merge Anim')] }), jsx("div", { className: styles.menuSep }), jsxs("div", { className: styles.menuDropdownWrap, children: [jsxs(NiceButton, { className: styles.menuBtn, variant: "ghost", size: "sm", onClick: () => setAddMenuOpen(addMenuOpen ? null : 'mesh'), children: [jsx(NiceIcon, { name: "plus", size: 14 }), " ", t('modelEditorMenuBar.add', 'Add'), ' ', jsx(NiceIcon, { name: "chevron-down", size: 14 })] }), addMenuOpen && (jsxs("div", { className: styles.dropdownMenu, children: [jsx("div", { className: styles.dropdownTitle, children: t('modelEditorMenuBar.mesh', 'Mesh') }), [
16
16
  'cube',
17
17
  'sphere',
18
18
  'cylinder',
@@ -26,7 +26,7 @@ const ModelEditorMenuBar = /* @__PURE__ */ Object.assign(
26
26
  'octahedron',
27
27
  'tetrahedron',
28
28
  'torusKnot',
29
- ].map((t) => (jsx("div", { className: styles.dropdownItem, onClick: () => addPrimitive(t), children: t.charAt(0).toUpperCase() + t.slice(1) }, t))), jsx("div", { className: styles.dropdownSep }), jsx("div", { className: styles.dropdownTitle, children: t('modelEditorMenuBar.light', 'Light') }), ['point', 'spot', 'directional', 'hemisphere'].map((t) => (jsxs("div", { className: styles.dropdownItem, onClick: () => addSceneLight(t), children: [jsx(NiceIcon, { name: "lightbulb", size: 14 }), " ", t.charAt(0).toUpperCase() + t.slice(1)] }, t))), jsx("div", { className: styles.dropdownSep }), jsx("div", { className: styles.dropdownTitle, children: t('modelEditorMenuBar.other', 'Other') }), jsxs("div", { className: styles.dropdownItem, onClick: addCameraObject, children: [jsx(NiceIcon, { name: "camera", size: 14 }), " ", t('modelEditorMenuBar.camera', 'Camera')] }), jsxs("div", { className: styles.dropdownItem, onClick: () => addEmpty('arrows'), children: [jsx(NiceIcon, { name: "move", size: 14 }), " Empty (Arrows)"] })] }))] }), jsxs(NiceButton, { className: styles.menuBtn, variant: "ghost", size: "sm", onClick: duplicateSelected, disabled: !selectedNode, title: t('modelEditorMenuBar.duplicateSelectedShiftD', 'Duplicate selected (Shift+D)'), children: [jsx(NiceIcon, { name: "clipboard", size: 14 }), " ", t('modelEditorMenuBar.duplicate', 'Duplicate')] }), jsxs(NiceButton, { className: styles.menuBtn, variant: "ghost", size: "sm", onClick: deleteSelected, disabled: !selectedNode, title: t('modelEditorMenuBar.deleteSelectedDelete', 'Delete selected (Delete)'), children: [jsx(NiceIcon, { name: "close", size: 14 }), " ", t('modelEditorMenuBar.delete', 'Delete')] }), jsx("div", { className: styles.menuSep }), jsxs(NiceButton, { className: styles.menuBtn, variant: "ghost", size: "sm", onClick: () => exportGLTF(true), children: [jsx(NiceIcon, { name: "save", size: 14 }), " ", t('modelEditorMenuBar.exportGlb', 'Export GLB')] }), jsxs(NiceButton, { className: styles.menuBtn, variant: "ghost", size: "sm", onClick: () => exportGLTF(false), children: [jsx(NiceIcon, { name: "file", size: 14 }), " ", t('modelEditorMenuBar.exportGltf', 'Export GLTF')] }), jsxs(NiceButton, { className: styles.menuBtn, variant: "ghost", size: "sm", onClick: exportOBJ, children: [jsx(NiceIcon, { name: "file", size: 14 }), " ", t('modelEditorMenuBar.exportObj', 'Export OBJ')] }), jsxs(NiceButton, { className: styles.menuBtn, variant: "ghost", size: "sm", onClick: () => exportSTL(true), children: [jsx(NiceIcon, { name: "file", size: 14 }), " ", t('modelEditorMenuBar.exportStl', 'Export STL')] }), jsxs(NiceButton, { className: styles.menuBtn, variant: "ghost", size: "sm", onClick: () => exportPLY(true), children: [jsx(NiceIcon, { name: "file", size: 14 }), " ", t('modelEditorMenuBar.exportPly', 'Export PLY')] }), jsxs(NiceButton, { className: styles.menuBtn, variant: "ghost", size: "sm", onClick: exportUSDZ, children: [jsx(NiceIcon, { name: "file", size: 14 }), " ", t('modelEditorMenuBar.exportUsdz', 'Export USDZ')] }), hasOnSaveToLibrary && (jsxs(Fragment, { children: [jsx("div", { className: styles.menuSep }), jsxs(NiceButton, { className: styles.menuBtn, variant: "ghost", size: "sm", onClick: saveToLibrary, children: [jsx(NiceIcon, { name: "box", size: 14 }), ' ', t('modelEditorMenuBar.saveToLibrary', 'Save to Library')] })] })), jsx("div", { className: styles.menuSep }), jsxs(NiceButton, { className: styles.menuBtn, variant: "ghost", size: "sm", onClick: clearScene, children: [jsx(NiceIcon, { name: "trash", size: 14 }), " ", t('modelEditorMenuBar.clear', 'Clear')] }), jsx("div", { className: styles.menuSep }), jsx(NiceCheckbox, { checked: showGrid, onChange: () => setShowGrid((v) => !v), label: t('modelEditorMenuBar.grid', 'Grid') }), jsx(NiceCheckbox, { checked: showAxes, onChange: () => setShowAxes((v) => !v), label: t('modelEditorMenuBar.axes', 'Axes') }), jsx(NiceCheckbox, { checked: wireframe, onChange: () => setWireframe((v) => !v), label: t('modelEditorMenuBar.wire', 'Wire') }), jsx(NiceCheckbox, { checked: showSkeleton, onChange: () => setShowSkeleton((v) => !v), label: t('modelEditorMenuBar.skel', 'Skel') }), jsx(NiceCheckbox, { checked: showLightHelpers, onChange: () => setShowLightHelpers((v) => !v), label: t('modelEditorMenuBar.helpers', 'Helpers') }), jsxs("div", { className: styles.menuRight, children: [jsx("label", { className: styles.menuLabel, children: "BG" }), jsx(NiceColorPicker, { value: bgColor, onChange: (c) => setBgColor(c) })] })] }));
29
+ ].map((t) => (jsx("div", { className: styles.dropdownItem, onClick: () => addPrimitive(t), children: t.charAt(0).toUpperCase() + t.slice(1) }, t))), jsx("div", { className: styles.dropdownSep }), jsx("div", { className: styles.dropdownTitle, children: t('modelEditorMenuBar.light', 'Light') }), ['point', 'spot', 'directional', 'hemisphere'].map((t) => (jsxs("div", { className: styles.dropdownItem, onClick: () => addSceneLight(t), children: [jsx(NiceIcon, { name: "lightbulb", size: 14 }), " ", t.charAt(0).toUpperCase() + t.slice(1)] }, t))), jsx("div", { className: styles.dropdownSep }), jsx("div", { className: styles.dropdownTitle, children: t('modelEditorMenuBar.other', 'Other') }), jsxs("div", { className: styles.dropdownItem, onClick: addCameraObject, children: [jsx(NiceIcon, { name: "camera", size: 14 }), " ", t('modelEditorMenuBar.camera', 'Camera')] }), jsxs("div", { className: styles.dropdownItem, onClick: () => addEmpty('arrows'), children: [jsx(NiceIcon, { name: "move", size: 14 }), ' ', t('modelEditorMenuBar.emptyArrows', 'Empty (Arrows)')] })] }))] }), jsxs(NiceButton, { className: styles.menuBtn, variant: "ghost", size: "sm", onClick: duplicateSelected, disabled: !selectedNode, title: t('modelEditorMenuBar.duplicateSelectedShiftD', 'Duplicate selected (Shift+D)'), children: [jsx(NiceIcon, { name: "clipboard", size: 14 }), " ", t('modelEditorMenuBar.duplicate', 'Duplicate')] }), jsxs(NiceButton, { className: styles.menuBtn, variant: "ghost", size: "sm", onClick: deleteSelected, disabled: !selectedNode, title: t('modelEditorMenuBar.deleteSelectedDelete', 'Delete selected (Delete)'), children: [jsx(NiceIcon, { name: "close", size: 14 }), " ", t('modelEditorMenuBar.delete', 'Delete')] }), jsx("div", { className: styles.menuSep }), jsxs(NiceButton, { className: styles.menuBtn, variant: "ghost", size: "sm", onClick: () => exportGLTF(true), children: [jsx(NiceIcon, { name: "save", size: 14 }), " ", t('modelEditorMenuBar.exportGlb', 'Export GLB')] }), jsxs(NiceButton, { className: styles.menuBtn, variant: "ghost", size: "sm", onClick: () => exportGLTF(false), children: [jsx(NiceIcon, { name: "file", size: 14 }), " ", t('modelEditorMenuBar.exportGltf', 'Export GLTF')] }), jsxs(NiceButton, { className: styles.menuBtn, variant: "ghost", size: "sm", onClick: exportOBJ, children: [jsx(NiceIcon, { name: "file", size: 14 }), " ", t('modelEditorMenuBar.exportObj', 'Export OBJ')] }), jsxs(NiceButton, { className: styles.menuBtn, variant: "ghost", size: "sm", onClick: () => exportSTL(true), children: [jsx(NiceIcon, { name: "file", size: 14 }), " ", t('modelEditorMenuBar.exportStl', 'Export STL')] }), jsxs(NiceButton, { className: styles.menuBtn, variant: "ghost", size: "sm", onClick: () => exportPLY(true), children: [jsx(NiceIcon, { name: "file", size: 14 }), " ", t('modelEditorMenuBar.exportPly', 'Export PLY')] }), jsxs(NiceButton, { className: styles.menuBtn, variant: "ghost", size: "sm", onClick: exportUSDZ, children: [jsx(NiceIcon, { name: "file", size: 14 }), " ", t('modelEditorMenuBar.exportUsdz', 'Export USDZ')] }), hasOnSaveToLibrary && (jsxs(Fragment, { children: [jsx("div", { className: styles.menuSep }), jsxs(NiceButton, { className: styles.menuBtn, variant: "ghost", size: "sm", onClick: saveToLibrary, children: [jsx(NiceIcon, { name: "box", size: 14 }), ' ', t('modelEditorMenuBar.saveToLibrary', 'Save to Library')] })] })), jsx("div", { className: styles.menuSep }), jsxs(NiceButton, { className: styles.menuBtn, variant: "ghost", size: "sm", onClick: clearScene, children: [jsx(NiceIcon, { name: "trash", size: 14 }), " ", t('modelEditorMenuBar.clear', 'Clear')] }), jsx("div", { className: styles.menuSep }), jsx(NiceCheckbox, { checked: showGrid, onChange: () => setShowGrid((v) => !v), label: t('modelEditorMenuBar.grid', 'Grid') }), jsx(NiceCheckbox, { checked: showAxes, onChange: () => setShowAxes((v) => !v), label: t('modelEditorMenuBar.axes', 'Axes') }), jsx(NiceCheckbox, { checked: wireframe, onChange: () => setWireframe((v) => !v), label: t('modelEditorMenuBar.wire', 'Wire') }), jsx(NiceCheckbox, { checked: showSkeleton, onChange: () => setShowSkeleton((v) => !v), label: t('modelEditorMenuBar.skel', 'Skel') }), jsx(NiceCheckbox, { checked: showLightHelpers, onChange: () => setShowLightHelpers((v) => !v), label: t('modelEditorMenuBar.helpers', 'Helpers') }), jsxs("div", { className: styles.menuRight, children: [jsx("label", { className: styles.menuLabel, children: t('modelEditorMenuBar.bg', 'BG') }), jsx(NiceColorPicker, { value: bgColor, onChange: (c) => setBgColor(c) })] })] }));
30
30
  }), { displayName: 'ModelEditorMenuBar' });
31
31
 
32
32
  export { ModelEditorMenuBar as default };
@@ -47,7 +47,7 @@ const ModelEditorRightPanel = /* @__PURE__ */ Object.assign(
47
47
  }, children: [(selectedNode.object.angle * DEG).toFixed(0), "\u00B0"] })] }), jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: t('modelEditorRightPanel.penumbra', 'Penumbra') }), jsx(NiceSlider, { min: 0, max: 1, step: 0.01, value: selectedNode.object.penumbra, onChange: (val) => {
48
48
  selectedNode.object.penumbra = val;
49
49
  setMatRefresh((n) => n + 1);
50
- }, style: { flex: 1 } })] })] }))] })] })), selectedNode && selectedNode.object instanceof THREE.PerspectiveCamera && (jsxs("div", { className: styles.panelSection, children: [jsxs("div", { className: styles.panelTitle, children: [jsx(NiceIcon, { name: "camera", size: 14 }), " ", t('modelEditorRightPanel.camera', 'Camera')] }), jsxs("div", { className: styles.panelContent, children: [jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: "FOV" }), jsx(NiceSlider, { min: 10, max: 120, step: 1, value: selectedNode.object.fov, onChange: (val) => {
50
+ }, style: { flex: 1 } })] })] }))] })] })), selectedNode && selectedNode.object instanceof THREE.PerspectiveCamera && (jsxs("div", { className: styles.panelSection, children: [jsxs("div", { className: styles.panelTitle, children: [jsx(NiceIcon, { name: "camera", size: 14 }), " ", t('modelEditorRightPanel.camera', 'Camera')] }), jsxs("div", { className: styles.panelContent, children: [jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: t('modelEditorRightPanel.fov', 'FOV') }), jsx(NiceSlider, { min: 10, max: 120, step: 1, value: selectedNode.object.fov, onChange: (val) => {
51
51
  const cam = selectedNode.object;
52
52
  cam.fov = val;
53
53
  cam.updateProjectionMatrix();
@@ -62,7 +62,7 @@ const ModelEditorRightPanel = /* @__PURE__ */ Object.assign(
62
62
  cam.near = val !== null && val !== void 0 ? val : 0.1;
63
63
  cam.updateProjectionMatrix();
64
64
  setMatRefresh((n) => n + 1);
65
- }, style: { width: 60 } })] }), jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: "Far" }), jsx(NiceNumberInput, { className: styles.propInput, step: 1, value: selectedNode.object.far, onChange: (val) => {
65
+ }, style: { width: 60 } })] }), jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: t('modelEditorRightPanel.far', 'Far') }), jsx(NiceNumberInput, { className: styles.propInput, step: 1, value: selectedNode.object.far, onChange: (val) => {
66
66
  const cam = selectedNode.object;
67
67
  cam.far = val !== null && val !== void 0 ? val : 1000;
68
68
  cam.updateProjectionMatrix();
@@ -71,25 +71,29 @@ const ModelEditorRightPanel = /* @__PURE__ */ Object.assign(
71
71
  fontSize: 10,
72
72
  color: 'var(--nice-text-secondary, #888)',
73
73
  margin: '0 0 6px',
74
- }, children: "Upload a video and AI will extract 3D pose data to create an animation clip for the current model." }), jsx("div", { className: styles.aiBtnRow, children: jsx(NiceButton, { className: styles.aiBtn, variant: "ghost", size: "sm", disabled: aiBusy || !rootObjectRef.current, onClick: () => { var _a; return (_a = videoInputRef.current) === null || _a === void 0 ? void 0 : _a.click(); }, children: aiBusy ? 'Processing...' : 'Upload Video' }) }), aiStatus && jsx("div", { className: styles.aiStatus, children: aiStatus })] }) })] })] })), propTab === 'material' && (jsxs("div", { className: styles.panelSection, children: [jsxs("div", { className: styles.panelTitle, children: [jsx(NiceIcon, { name: "palette", size: 14 }), ' ', t('modelEditorRightPanel.materials', 'Materials')] }), jsx("div", { className: styles.panelContent, children: selectedMaterials.length > 0 ? (selectedMaterials.map((mat, i) => (jsx(MaterialCard, { material: mat, active: i === selMaterialIdx, onClick: () => setSelMaterialIdx(i), onUpdate: () => setMatRefresh((n) => n + 1) }, i)))) : (jsx("div", { style: { color: 'var(--nice-text-secondary, #666)', fontSize: 10 }, children: selectedNode ? 'No materials on this object' : 'Select a mesh to inspect' })) })] })), propTab === 'world' && (jsxs(Fragment, { children: [jsxs("div", { className: styles.panelSection, children: [jsxs("div", { className: styles.panelTitle, children: [jsx(NiceIcon, { name: "globe", size: 14 }), ' ', t('modelEditorRightPanel.environment', 'Environment')] }), jsx("div", { className: styles.panelContent, children: jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: t('modelEditorRightPanel.background', 'Background') }), jsx(NiceColorPicker, { value: bgColor, onChange: (c) => setBgColor(c) }), jsx("span", { style: { fontSize: 9, color: 'var(--nice-text-secondary, #888)' }, children: bgColor })] }) })] }), jsxs("div", { className: styles.panelSection, children: [jsxs("div", { className: styles.panelTitle, children: [jsx(NiceIcon, { name: "cloud", size: 14 }), " Fog"] }), jsxs("div", { className: styles.panelContent, children: [jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: t('modelEditorRightPanel.enable', 'Enable') }), jsx(NiceCheckbox, { checked: fogEnabled, onChange: () => setFogEnabled((v) => !v) })] }), fogEnabled && (jsxs(Fragment, { children: [jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: t('modelEditorRightPanel.color', 'Color') }), jsx(NiceColorPicker, { value: fogColor, onChange: (c) => setFogColor(c) })] }), jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: t('modelEditorRightPanel.near', 'Near') }), jsx(NiceNumberInput, { className: styles.propInput, value: fogNear, onChange: (val) => setFogNear(val !== null && val !== void 0 ? val : 1), style: { width: 60 } })] }), jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: "Far" }), jsx(NiceNumberInput, { className: styles.propInput, value: fogFar, onChange: (val) => setFogFar(val !== null && val !== void 0 ? val : 100), style: { width: 60 } })] })] }))] })] }), jsxs("div", { className: styles.panelSection, children: [jsxs("div", { className: styles.panelTitle, children: [jsx(NiceIcon, { name: "lightbulb", size: 14 }), ' ', t('modelEditorRightPanel.sceneLights', 'Scene Lights')] }), jsxs("div", { className: styles.panelContent, children: [jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: t('modelEditorRightPanel.showHelpers', 'Show Helpers') }), jsx(NiceCheckbox, { checked: showLightHelpers, onChange: () => setShowLightHelpers((v) => !v) })] }), jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: t('modelEditorRightPanel.grid', 'Grid') }), jsx(NiceCheckbox, { checked: showGrid, onChange: () => setShowGrid((v) => !v) })] }), jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: t('modelEditorRightPanel.axes', 'Axes') }), jsx(NiceCheckbox, { checked: showAxes, onChange: () => setShowAxes((v) => !v) })] }), jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: t('modelEditorRightPanel.skeleton', 'Skeleton') }), jsx(NiceCheckbox, { checked: showSkeleton, onChange: () => setShowSkeleton((v) => !v) })] })] })] }), jsxs("div", { className: styles.panelSection, children: [jsxs("div", { className: styles.panelTitle, children: [jsx(NiceIcon, { name: "grid", size: 14 }), " ", t('modelEditorRightPanel.snapping', 'Snapping')] }), jsxs("div", { className: styles.panelContent, children: [jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: t('modelEditorRightPanel.snap', 'Snap') }), jsx(NiceCheckbox, { checked: snapEnabled, onChange: () => setSnapEnabled((v) => !v) })] }), snapEnabled && (jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: t('modelEditorRightPanel.gridSize', 'Grid Size') }), jsx(NiceNumberInput, { className: styles.propInput, step: 0.25, min: 0.1, value: snapGrid, onChange: (val) => setSnapGrid(val !== null && val !== void 0 ? val : 1), style: { width: 60 } })] }))] })] })] })), propTab === 'modifiers' && (jsxs("div", { className: styles.panelSection, children: [jsxs("div", { className: styles.panelTitle, children: [jsx(NiceIcon, { name: "settings", size: 14 }), ' ', t('modelEditorRightPanel.modifiers', 'Modifiers')] }), jsx("div", { className: styles.panelContent, children: selectedNode && selectedNode.object instanceof THREE.Mesh ? (jsxs(Fragment, { children: [jsx(NiceButton, { className: styles.btnSecondary, variant: "ghost", size: "sm", style: { width: '100%', marginBottom: 4 }, onClick: () => {
74
+ }, children: t('modelEditorRightPanel.uploadAVideoAndAi', 'Upload a video and AI will extract 3D pose data to create an animation clip for the current model.') }), jsx("div", { className: styles.aiBtnRow, children: jsx(NiceButton, { className: styles.aiBtn, variant: "ghost", size: "sm", disabled: aiBusy || !rootObjectRef.current, onClick: () => { var _a; return (_a = videoInputRef.current) === null || _a === void 0 ? void 0 : _a.click(); }, children: aiBusy
75
+ ? t('modelEditorRightPanel.processing', 'Processing...')
76
+ : t('modelEditorRightPanel.uploadVideo', 'Upload Video') }) }), aiStatus && jsx("div", { className: styles.aiStatus, children: aiStatus })] }) })] })] })), propTab === 'material' && (jsxs("div", { className: styles.panelSection, children: [jsxs("div", { className: styles.panelTitle, children: [jsx(NiceIcon, { name: "palette", size: 14 }), ' ', t('modelEditorRightPanel.materials', 'Materials')] }), jsx("div", { className: styles.panelContent, children: selectedMaterials.length > 0 ? (selectedMaterials.map((mat, i) => (jsx(MaterialCard, { material: mat, active: i === selMaterialIdx, onClick: () => setSelMaterialIdx(i), onUpdate: () => setMatRefresh((n) => n + 1) }, i)))) : (jsx("div", { style: { color: 'var(--nice-text-secondary, #666)', fontSize: 10 }, children: selectedNode
77
+ ? t('modelEditorRightPanel.noMaterialsOnThisObject', 'No materials on this object')
78
+ : t('modelEditorRightPanel.selectAMeshToInspect', 'Select a mesh to inspect') })) })] })), propTab === 'world' && (jsxs(Fragment, { children: [jsxs("div", { className: styles.panelSection, children: [jsxs("div", { className: styles.panelTitle, children: [jsx(NiceIcon, { name: "globe", size: 14 }), ' ', t('modelEditorRightPanel.environment', 'Environment')] }), jsx("div", { className: styles.panelContent, children: jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: t('modelEditorRightPanel.background', 'Background') }), jsx(NiceColorPicker, { value: bgColor, onChange: (c) => setBgColor(c) }), jsx("span", { style: { fontSize: 9, color: 'var(--nice-text-secondary, #888)' }, children: bgColor })] }) })] }), jsxs("div", { className: styles.panelSection, children: [jsxs("div", { className: styles.panelTitle, children: [jsx(NiceIcon, { name: "cloud", size: 14 }), " ", t('modelEditorRightPanel.fog', 'Fog')] }), jsxs("div", { className: styles.panelContent, children: [jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: t('modelEditorRightPanel.enable', 'Enable') }), jsx(NiceCheckbox, { checked: fogEnabled, onChange: () => setFogEnabled((v) => !v) })] }), fogEnabled && (jsxs(Fragment, { children: [jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: t('modelEditorRightPanel.color', 'Color') }), jsx(NiceColorPicker, { value: fogColor, onChange: (c) => setFogColor(c) })] }), jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: t('modelEditorRightPanel.near', 'Near') }), jsx(NiceNumberInput, { className: styles.propInput, value: fogNear, onChange: (val) => setFogNear(val !== null && val !== void 0 ? val : 1), style: { width: 60 } })] }), jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: t('modelEditorRightPanel.far', 'Far') }), jsx(NiceNumberInput, { className: styles.propInput, value: fogFar, onChange: (val) => setFogFar(val !== null && val !== void 0 ? val : 100), style: { width: 60 } })] })] }))] })] }), jsxs("div", { className: styles.panelSection, children: [jsxs("div", { className: styles.panelTitle, children: [jsx(NiceIcon, { name: "lightbulb", size: 14 }), ' ', t('modelEditorRightPanel.sceneLights', 'Scene Lights')] }), jsxs("div", { className: styles.panelContent, children: [jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: t('modelEditorRightPanel.showHelpers', 'Show Helpers') }), jsx(NiceCheckbox, { checked: showLightHelpers, onChange: () => setShowLightHelpers((v) => !v) })] }), jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: t('modelEditorRightPanel.grid', 'Grid') }), jsx(NiceCheckbox, { checked: showGrid, onChange: () => setShowGrid((v) => !v) })] }), jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: t('modelEditorRightPanel.axes', 'Axes') }), jsx(NiceCheckbox, { checked: showAxes, onChange: () => setShowAxes((v) => !v) })] }), jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: t('modelEditorRightPanel.skeleton', 'Skeleton') }), jsx(NiceCheckbox, { checked: showSkeleton, onChange: () => setShowSkeleton((v) => !v) })] })] })] }), jsxs("div", { className: styles.panelSection, children: [jsxs("div", { className: styles.panelTitle, children: [jsx(NiceIcon, { name: "grid", size: 14 }), " ", t('modelEditorRightPanel.snapping', 'Snapping')] }), jsxs("div", { className: styles.panelContent, children: [jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: t('modelEditorRightPanel.snap', 'Snap') }), jsx(NiceCheckbox, { checked: snapEnabled, onChange: () => setSnapEnabled((v) => !v) })] }), snapEnabled && (jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: t('modelEditorRightPanel.gridSize', 'Grid Size') }), jsx(NiceNumberInput, { className: styles.propInput, step: 0.25, min: 0.1, value: snapGrid, onChange: (val) => setSnapGrid(val !== null && val !== void 0 ? val : 1), style: { width: 60 } })] }))] })] })] })), propTab === 'modifiers' && (jsxs("div", { className: styles.panelSection, children: [jsxs("div", { className: styles.panelTitle, children: [jsx(NiceIcon, { name: "settings", size: 14 }), ' ', t('modelEditorRightPanel.modifiers', 'Modifiers')] }), jsx("div", { className: styles.panelContent, children: selectedNode && selectedNode.object instanceof THREE.Mesh ? (jsxs(Fragment, { children: [jsx(NiceButton, { className: styles.btnSecondary, variant: "ghost", size: "sm", style: { width: '100%', marginBottom: 4 }, onClick: () => {
75
79
  const mesh = selectedNode.object;
76
80
  const geo = mesh.geometry;
77
81
  const pos = geo.attributes.position;
78
82
  if (pos) {
79
83
  const wireGeo = new THREE.WireframeGeometry(geo);
80
84
  mesh.geometry = wireGeo;
81
- setStatusText('Applied Wireframe modifier');
85
+ setStatusText(t('modelEditorRightPanel.appliedWireframeModifier', 'Applied Wireframe modifier'));
82
86
  rebuildTree();
83
87
  }
84
- }, children: "+ Wireframe Modifier" }), jsx(NiceButton, { className: styles.btnSecondary, variant: "ghost", size: "sm", style: { width: '100%', marginBottom: 4 }, onClick: () => {
88
+ }, children: t('modelEditorRightPanel.wireframeModifier', '+ Wireframe Modifier') }), jsx(NiceButton, { className: styles.btnSecondary, variant: "ghost", size: "sm", style: { width: '100%', marginBottom: 4 }, onClick: () => {
85
89
  const mesh = selectedNode.object;
86
90
  mesh.geometry.computeVertexNormals();
87
- setStatusText('Recomputed normals');
91
+ setStatusText(t('modelEditorRightPanel.recomputedNormals', 'Recomputed normals'));
88
92
  }, children: t('modelEditorRightPanel.recomputeNormals', 'Recompute Normals') }), jsx(NiceButton, { className: styles.btnSecondary, variant: "ghost", size: "sm", style: { width: '100%', marginBottom: 4 }, onClick: () => {
89
93
  const mesh = selectedNode.object;
90
94
  const geo = mesh.geometry;
91
95
  geo.center();
92
- setStatusText('Centered geometry');
96
+ setStatusText(t('modelEditorRightPanel.centeredGeometry', 'Centered geometry'));
93
97
  setMatRefresh((n) => n + 1);
94
98
  }, children: t('modelEditorRightPanel.centerGeometry', 'Center Geometry') }), jsx(NiceButton, { className: styles.btnSecondary, variant: "ghost", size: "sm", style: { width: '100%', marginBottom: 4 }, onClick: () => {
95
99
  const mesh = selectedNode.object;
@@ -104,9 +108,9 @@ const ModelEditorRightPanel = /* @__PURE__ */ Object.assign(
104
108
  }
105
109
  idx.needsUpdate = true;
106
110
  geo.computeVertexNormals();
107
- setStatusText('Flipped normals');
111
+ setStatusText(t('modelEditorRightPanel.flippedNormals', 'Flipped normals'));
108
112
  }
109
- }, children: t('modelEditorRightPanel.flipNormals', 'Flip Normals') })] })) : (jsx("div", { style: { color: 'var(--nice-text-secondary, #666)', fontSize: 10 }, children: t('modelEditorRightPanel.selectAMeshToApply', 'Select a mesh to apply modifiers') })) })] })), propTab === 'physics' && (jsxs("div", { className: styles.panelSection, children: [jsxs("div", { className: styles.panelTitle, children: [jsx(NiceIcon, { name: "zap", size: 14 }), " Physics (Info)"] }), jsx("div", { className: styles.panelContent, children: jsxs("div", { style: { color: 'var(--nice-text-secondary, #666)', fontSize: 10, lineHeight: 1.5 }, children: [t('modelEditorRightPanel.physicsSimulationPreviewIsNot', 'Physics simulation preview is not yet available.'), jsx("br", {}), jsx("br", {}), "Planned features:", jsx("br", {}), "\u2022 Rigid body simulation", jsx("br", {}), "\u2022 Soft body dynamics", jsx("br", {}), "\u2022 Cloth simulation", jsx("br", {}), "\u2022 Particle systems"] }) })] }))] }));
113
+ }, children: t('modelEditorRightPanel.flipNormals', 'Flip Normals') })] })) : (jsx("div", { style: { color: 'var(--nice-text-secondary, #666)', fontSize: 10 }, children: t('modelEditorRightPanel.selectAMeshToApply', 'Select a mesh to apply modifiers') })) })] })), propTab === 'physics' && (jsxs("div", { className: styles.panelSection, children: [jsxs("div", { className: styles.panelTitle, children: [jsx(NiceIcon, { name: "zap", size: 14 }), ' ', t('modelEditorRightPanel.physicsInfo', 'Physics (Info)')] }), jsx("div", { className: styles.panelContent, children: jsxs("div", { style: { color: 'var(--nice-text-secondary, #666)', fontSize: 10, lineHeight: 1.5 }, children: [t('modelEditorRightPanel.physicsSimulationPreviewIsNot', 'Physics simulation preview is not yet available.'), jsx("br", {}), jsx("br", {}), t('modelEditorRightPanel.plannedFeatures', 'Planned features:'), jsx("br", {}), t('modelEditorRightPanel.rigidBodySimulation', '• Rigid body simulation'), jsx("br", {}), t('modelEditorRightPanel.softBodyDynamics', '• Soft body dynamics'), jsx("br", {}), t('modelEditorRightPanel.clothSimulation', '• Cloth simulation'), jsx("br", {}), t('modelEditorRightPanel.particleSystems', '• Particle systems')] }) })] }))] }));
110
114
  }), { displayName: 'ModelEditorRightPanel' });
111
115
 
112
116
  export { ModelEditorRightPanel as default };
@@ -87,9 +87,15 @@ const MaterialCard = ({ material, active, onClick, onUpdate }) => {
87
87
  mat.needsUpdate = true;
88
88
  onUpdate();
89
89
  }, options: [
90
- { value: String(THREE.FrontSide), label: 'Front' },
91
- { value: String(THREE.BackSide), label: 'Back' },
92
- { value: String(THREE.DoubleSide), label: 'Double' },
90
+ {
91
+ value: String(THREE.FrontSide),
92
+ label: t('modelEditorSubComponents.front', 'Front'),
93
+ },
94
+ { value: String(THREE.BackSide), label: t('modelEditorSubComponents.back', 'Back') },
95
+ {
96
+ value: String(THREE.DoubleSide),
97
+ label: t('modelEditorSubComponents.double', 'Double'),
98
+ },
93
99
  ] })] }), hasMap && mat.map && (jsxs("div", { className: styles.textureSlot, children: [jsx("div", { className: styles.textureThumb, children: mat.map.image != null && (jsx("img", { src: (_a = mat.map.image.src) !== null && _a !== void 0 ? _a : '', alt: "diffuse" })) }), jsxs("div", { className: styles.textureInfo, children: [jsx("div", { className: styles.textureName, children: t('modelEditorSubComponents.diffuseMap', 'Diffuse Map') }), jsxs("div", { style: { fontSize: 9, color: 'var(--nice-text-secondary, #666)' }, children: [(_b = mat.map.image) === null || _b === void 0 ? void 0 : _b.width, "\u00D7", (_c = mat.map.image) === null || _c === void 0 ? void 0 : _c.height] })] })] })), hasNormalMap && mat.normalMap && (jsxs("div", { className: styles.textureSlot, children: [jsx("div", { className: styles.textureThumb, children: mat.normalMap.image != null && (jsx("img", { src: (_d = mat.normalMap.image.src) !== null && _d !== void 0 ? _d : '', alt: "normal" })) }), jsx("div", { className: styles.textureInfo, children: jsx("div", { className: styles.textureName, children: t('modelEditorSubComponents.normalMap', 'Normal Map') }) })] }))] }));
94
100
  };
95
101
 
@@ -9,7 +9,9 @@ const ModelEditorTimeline = /* @__PURE__ */ Object.assign(
9
9
  /* @__PURE__ */ React.memo(({ api }) => {
10
10
  const { t } = useNiceTranslation();
11
11
  const { bottomCollapsed, setBottomCollapsed, animations, activeAnimIdx, isPlaying, animTime, animDuration, animSpeed, setAnimSpeed, loopAnim, setLoopAnim, seekAnim, togglePlay, stopAnim, playClip, mergeInputRef, } = api;
12
- return (jsxs("div", { className: `${styles.bottomPanel} ${bottomCollapsed ? styles.bottomPanelCollapsed : ''}`, children: [jsxs("div", { className: styles.bottomPanelHeader, children: [jsx("span", { className: styles.bottomToggle, onClick: () => setBottomCollapsed((c) => !c), children: jsx(NiceIcon, { name: bottomCollapsed ? 'chevron-right' : 'chevron-down', size: 14 }) }), jsxs("span", { className: styles.bottomPanelTitle, onClick: () => setBottomCollapsed((c) => !c), children: ["Animation (", animations.length, " clip", animations.length !== 1 ? 's' : '', ")"] }), jsx("div", { className: styles.menuSep }), jsxs("div", { className: styles.transportBar, children: [jsx(NiceButton, { className: styles.transportBtn, variant: "ghost", size: "sm", onClick: () => seekAnim(0), title: t('modelEditorTimeline.goToStart', 'Go to start'), "aria-label": t('modelEditorTimeline.goToStart', 'Go to start'), children: jsx(NiceIcon, { name: "chevron-left", size: 16 }) }), jsx(NiceButton, { className: styles.transportBtn, variant: isPlaying ? 'primary' : 'ghost', size: "sm", onClick: togglePlay, title: t('modelEditorTimeline.playPauseSpace', 'Play/Pause (Space)'), "aria-label": t('modelEditorTimeline.playPauseSpace', 'Play/Pause (Space)'), children: jsx(NiceIcon, { name: isPlaying ? 'pause' : 'play', size: 16 }) }), jsx(NiceButton, { className: styles.transportBtn, variant: "ghost", size: "sm", onClick: stopAnim, title: t('modelEditorTimeline.stop', 'Stop'), "aria-label": t('modelEditorTimeline.stop', 'Stop'), children: jsx(NiceIcon, { name: "stop", size: 16 }) }), jsx(NiceButton, { className: styles.transportBtn, variant: loopAnim ? 'primary' : 'ghost', size: "sm", onClick: () => setLoopAnim((v) => !v), title: t('modelEditorTimeline.loop', 'Loop'), "aria-label": t('modelEditorTimeline.loop', 'Loop'), children: jsx(NiceIcon, { name: "refresh", size: 16 }) })] }), jsxs("span", { className: styles.transportTime, children: [fmtTime(animTime), " / ", fmtTime(animDuration)] }), jsx("div", { className: styles.menuSep }), jsx("label", { className: styles.menuLabel, children: t('modelEditorTimeline.speed', 'Speed') }), jsx(NiceSlider, { min: 0.1, max: 3, step: 0.1, value: animSpeed, onChange: (val) => setAnimSpeed(val), style: { width: 60 } }), jsxs("span", { className: styles.menuLabel, children: [animSpeed.toFixed(1), "\u00D7"] })] }), !bottomCollapsed && (jsxs("div", { className: styles.animListArea, children: [jsxs("div", { className: styles.animList, children: [animations.length === 0 && (jsx("div", { style: { color: 'var(--nice-text-secondary, #666)', fontSize: 10, padding: 8 }, children: t('modelEditorTimeline.noAnimationsLoaded', 'No animations loaded') })), animations.map((anim, i) => (jsxs("div", { className: `${styles.animItem} ${i === activeAnimIdx ? styles.animItemActive : ''}`, onClick: () => playClip(i), children: [jsx("span", { children: anim.name }), jsxs("span", { className: styles.animDuration, children: [anim.duration.toFixed(1), "s"] })] }, `${anim.name}-${i}`))), jsx("div", { style: { padding: '4px 8px' }, children: jsx(NiceButton, { className: styles.btnSecondary, variant: "ghost", size: "sm", style: { width: '100%', fontSize: 10 }, onClick: () => { var _a; return (_a = mergeInputRef.current) === null || _a === void 0 ? void 0 : _a.click(); }, children: "+ Add Animation File" }) })] }), jsxs("div", { className: styles.timelineArea, children: [jsxs("div", { className: styles.timelineRuler, children: [animDuration > 0 &&
12
+ return (jsxs("div", { className: `${styles.bottomPanel} ${bottomCollapsed ? styles.bottomPanelCollapsed : ''}`, children: [jsxs("div", { className: styles.bottomPanelHeader, children: [jsx("span", { className: styles.bottomToggle, onClick: () => setBottomCollapsed((c) => !c), children: jsx(NiceIcon, { name: bottomCollapsed ? 'chevron-right' : 'chevron-down', size: 14 }) }), jsx("span", { className: styles.bottomPanelTitle, onClick: () => setBottomCollapsed((c) => !c), children: animations.length !== 1
13
+ ? t('modelEditorTimeline.animationClipsMany', 'Animation ({animations} clips)').replace('{animations}', String(animations.length))
14
+ : t('modelEditorTimeline.animationClipsOne', 'Animation ({animations} clip)').replace('{animations}', String(animations.length)) }), jsx("div", { className: styles.menuSep }), jsxs("div", { className: styles.transportBar, children: [jsx(NiceButton, { className: styles.transportBtn, variant: "ghost", size: "sm", onClick: () => seekAnim(0), title: t('modelEditorTimeline.goToStart', 'Go to start'), "aria-label": t('modelEditorTimeline.goToStart', 'Go to start'), children: jsx(NiceIcon, { name: "chevron-left", size: 16 }) }), jsx(NiceButton, { className: styles.transportBtn, variant: isPlaying ? 'primary' : 'ghost', size: "sm", onClick: togglePlay, title: t('modelEditorTimeline.playPauseSpace', 'Play/Pause (Space)'), "aria-label": t('modelEditorTimeline.playPauseSpace', 'Play/Pause (Space)'), children: jsx(NiceIcon, { name: isPlaying ? 'pause' : 'play', size: 16 }) }), jsx(NiceButton, { className: styles.transportBtn, variant: "ghost", size: "sm", onClick: stopAnim, title: t('modelEditorTimeline.stop', 'Stop'), "aria-label": t('modelEditorTimeline.stop', 'Stop'), children: jsx(NiceIcon, { name: "stop", size: 16 }) }), jsx(NiceButton, { className: styles.transportBtn, variant: loopAnim ? 'primary' : 'ghost', size: "sm", onClick: () => setLoopAnim((v) => !v), title: t('modelEditorTimeline.loop', 'Loop'), "aria-label": t('modelEditorTimeline.loop', 'Loop'), children: jsx(NiceIcon, { name: "refresh", size: 16 }) })] }), jsxs("span", { className: styles.transportTime, children: [fmtTime(animTime), " / ", fmtTime(animDuration)] }), jsx("div", { className: styles.menuSep }), jsx("label", { className: styles.menuLabel, children: t('modelEditorTimeline.speed', 'Speed') }), jsx(NiceSlider, { min: 0.1, max: 3, step: 0.1, value: animSpeed, onChange: (val) => setAnimSpeed(val), style: { width: 60 } }), jsxs("span", { className: styles.menuLabel, children: [animSpeed.toFixed(1), "\u00D7"] })] }), !bottomCollapsed && (jsxs("div", { className: styles.animListArea, children: [jsxs("div", { className: styles.animList, children: [animations.length === 0 && (jsx("div", { style: { color: 'var(--nice-text-secondary, #666)', fontSize: 10, padding: 8 }, children: t('modelEditorTimeline.noAnimationsLoaded', 'No animations loaded') })), animations.map((anim, i) => (jsxs("div", { className: `${styles.animItem} ${i === activeAnimIdx ? styles.animItemActive : ''}`, onClick: () => playClip(i), children: [jsx("span", { children: anim.name }), jsxs("span", { className: styles.animDuration, children: [anim.duration.toFixed(1), "s"] })] }, `${anim.name}-${i}`))), jsx("div", { style: { padding: '4px 8px' }, children: jsx(NiceButton, { className: styles.btnSecondary, variant: "ghost", size: "sm", style: { width: '100%', fontSize: 10 }, onClick: () => { var _a; return (_a = mergeInputRef.current) === null || _a === void 0 ? void 0 : _a.click(); }, children: t('modelEditorTimeline.addAnimationFile', '+ Add Animation File') }) })] }), jsxs("div", { className: styles.timelineArea, children: [jsxs("div", { className: styles.timelineRuler, children: [animDuration > 0 &&
13
15
  Array.from({ length: Math.ceil(animDuration) + 1 }, (_, i) => {
14
16
  const pct = (i / animDuration) * 100;
15
17
  return (jsxs(React.Fragment, { children: [jsx("div", { className: styles.rulerTick, style: { left: `${pct}%` } }), jsxs("span", { className: styles.rulerLabel, style: { left: `${pct}%` }, children: [i, "s"] })] }, i));
@@ -8,7 +8,7 @@ const ModelEditorToolbar = /* @__PURE__ */ Object.assign(
8
8
  /* @__PURE__ */ React.memo(({ api }) => {
9
9
  const { t } = useNiceTranslation();
10
10
  const { transformMode, setTransformMode, snapEnabled, setSnapEnabled, gizmoSpace, setGizmoSpace, rootObjectRef, focusOnObject, setCameraPreset, deleteSelected, duplicateSelected, selectedNode, } = api;
11
- return (jsxs("div", { className: styles.toolbar, children: [jsx(NiceButton, { className: styles.toolBtn, variant: transformMode === 'translate' ? 'primary' : 'ghost', size: "sm", onClick: () => setTransformMode('translate'), title: t('modelEditorToolbar.translateG', 'Translate (G)'), "aria-label": t('modelEditorToolbar.translateG', 'Translate (G)'), children: jsx(NiceIcon, { name: "move", size: 16 }) }), jsx(NiceButton, { className: styles.toolBtn, variant: transformMode === 'rotate' ? 'primary' : 'ghost', size: "sm", onClick: () => setTransformMode('rotate'), title: t('modelEditorToolbar.rotateR', 'Rotate (R)'), "aria-label": t('modelEditorToolbar.rotateR', 'Rotate (R)'), children: jsx(NiceIcon, { name: "refresh", size: 16 }) }), jsx(NiceButton, { className: styles.toolBtn, variant: transformMode === 'scale' ? 'primary' : 'ghost', size: "sm", onClick: () => setTransformMode('scale'), title: t('modelEditorToolbar.scaleS', 'Scale (S)'), "aria-label": t('modelEditorToolbar.scaleS', 'Scale (S)'), children: jsx(NiceIcon, { name: "hexagon", size: 16 }) }), jsx("div", { className: styles.toolSep }), jsx(NiceButton, { className: styles.toolBtn, variant: snapEnabled ? 'primary' : 'ghost', size: "sm", onClick: () => setSnapEnabled((v) => !v), title: `Snap to Grid (${snapEnabled ? 'ON' : 'OFF'})`, "aria-label": t('modelEditorToolbar.snapToGrid', 'Snap to Grid'), children: jsx(NiceIcon, { name: "grid", size: 16 }) }), jsx(NiceButton, { className: styles.toolBtn, variant: "ghost", size: "sm", onClick: () => setGizmoSpace((s) => (s === 'local' ? 'world' : 'local')), title: `Orientation: ${gizmoSpace}`, "aria-label": `Orientation: ${gizmoSpace}`, children: jsx(NiceIcon, { name: gizmoSpace === 'local' ? 'target' : 'globe', size: 16 }) }), jsx("div", { className: styles.toolSep }), jsx(NiceButton, { className: styles.toolBtn, variant: "ghost", size: "sm", onClick: () => rootObjectRef.current && focusOnObject(rootObjectRef.current), title: t('modelEditorToolbar.focusF', 'Focus (F)'), "aria-label": t('modelEditorToolbar.focusF', 'Focus (F)'), children: jsx(NiceIcon, { name: "target", size: 16 }) }), jsx(NiceButton, { className: styles.toolBtn, variant: "ghost", size: "sm", onClick: () => setCameraPreset('front'), title: t('modelEditorToolbar.frontViewNumpad1', 'Front view (Numpad 1)'), children: "1" }), jsx(NiceButton, { className: styles.toolBtn, variant: "ghost", size: "sm", onClick: () => setCameraPreset('right'), title: t('modelEditorToolbar.rightViewNumpad3', 'Right view (Numpad 3)'), children: "3" }), jsx(NiceButton, { className: styles.toolBtn, variant: "ghost", size: "sm", onClick: () => setCameraPreset('top'), title: t('modelEditorToolbar.topViewNumpad7', 'Top view (Numpad 7)'), children: "7" }), jsx("div", { className: styles.toolSep }), jsx(NiceButton, { className: styles.toolBtn, variant: "ghost", size: "sm", onClick: deleteSelected, title: t('modelEditorToolbar.deleteSelectedDel', 'Delete selected (Del)'), "aria-label": t('modelEditorToolbar.deleteSelectedDel', 'Delete selected (Del)'), children: jsx(NiceIcon, { name: "trash", size: 16 }) }), jsx(NiceButton, { className: styles.toolBtn, variant: "ghost", size: "sm", onClick: duplicateSelected, title: t('modelEditorToolbar.duplicateShiftD', 'Duplicate (Shift+D)'), disabled: !selectedNode, "aria-label": t('modelEditorToolbar.duplicateShiftD', 'Duplicate (Shift+D)'), children: jsx(NiceIcon, { name: "clipboard", size: 16 }) })] }));
11
+ return (jsxs("div", { className: styles.toolbar, children: [jsx(NiceButton, { className: styles.toolBtn, variant: transformMode === 'translate' ? 'primary' : 'ghost', size: "sm", onClick: () => setTransformMode('translate'), title: t('modelEditorToolbar.translateG', 'Translate (G)'), "aria-label": t('modelEditorToolbar.translateG', 'Translate (G)'), children: jsx(NiceIcon, { name: "move", size: 16 }) }), jsx(NiceButton, { className: styles.toolBtn, variant: transformMode === 'rotate' ? 'primary' : 'ghost', size: "sm", onClick: () => setTransformMode('rotate'), title: t('modelEditorToolbar.rotateR', 'Rotate (R)'), "aria-label": t('modelEditorToolbar.rotateR', 'Rotate (R)'), children: jsx(NiceIcon, { name: "refresh", size: 16 }) }), jsx(NiceButton, { className: styles.toolBtn, variant: transformMode === 'scale' ? 'primary' : 'ghost', size: "sm", onClick: () => setTransformMode('scale'), title: t('modelEditorToolbar.scaleS', 'Scale (S)'), "aria-label": t('modelEditorToolbar.scaleS', 'Scale (S)'), children: jsx(NiceIcon, { name: "hexagon", size: 16 }) }), jsx("div", { className: styles.toolSep }), jsx(NiceButton, { className: styles.toolBtn, variant: snapEnabled ? 'primary' : 'ghost', size: "sm", onClick: () => setSnapEnabled((v) => !v), title: t('modelEditorToolbar.snapToGridState', 'Snap to Grid ({state})').replace('{state}', snapEnabled ? t('modelEditorToolbar.on', 'ON') : t('modelEditorToolbar.off', 'OFF')), "aria-label": t('modelEditorToolbar.snapToGrid', 'Snap to Grid'), children: jsx(NiceIcon, { name: "grid", size: 16 }) }), jsx(NiceButton, { className: styles.toolBtn, variant: "ghost", size: "sm", onClick: () => setGizmoSpace((s) => (s === 'local' ? 'world' : 'local')), title: t('modelEditorToolbar.orientation', 'Orientation: {gizmoSpace}').replace('{gizmoSpace}', String(gizmoSpace)), "aria-label": t('modelEditorToolbar.orientation', 'Orientation: {gizmoSpace}').replace('{gizmoSpace}', String(gizmoSpace)), children: jsx(NiceIcon, { name: gizmoSpace === 'local' ? 'target' : 'globe', size: 16 }) }), jsx("div", { className: styles.toolSep }), jsx(NiceButton, { className: styles.toolBtn, variant: "ghost", size: "sm", onClick: () => rootObjectRef.current && focusOnObject(rootObjectRef.current), title: t('modelEditorToolbar.focusF', 'Focus (F)'), "aria-label": t('modelEditorToolbar.focusF', 'Focus (F)'), children: jsx(NiceIcon, { name: "target", size: 16 }) }), jsx(NiceButton, { className: styles.toolBtn, variant: "ghost", size: "sm", onClick: () => setCameraPreset('front'), title: t('modelEditorToolbar.frontViewNumpad1', 'Front view (Numpad 1)'), children: "1" }), jsx(NiceButton, { className: styles.toolBtn, variant: "ghost", size: "sm", onClick: () => setCameraPreset('right'), title: t('modelEditorToolbar.rightViewNumpad3', 'Right view (Numpad 3)'), children: "3" }), jsx(NiceButton, { className: styles.toolBtn, variant: "ghost", size: "sm", onClick: () => setCameraPreset('top'), title: t('modelEditorToolbar.topViewNumpad7', 'Top view (Numpad 7)'), children: "7" }), jsx("div", { className: styles.toolSep }), jsx(NiceButton, { className: styles.toolBtn, variant: "ghost", size: "sm", onClick: deleteSelected, title: t('modelEditorToolbar.deleteSelectedDel', 'Delete selected (Del)'), "aria-label": t('modelEditorToolbar.deleteSelectedDel', 'Delete selected (Del)'), children: jsx(NiceIcon, { name: "trash", size: 16 }) }), jsx(NiceButton, { className: styles.toolBtn, variant: "ghost", size: "sm", onClick: duplicateSelected, title: t('modelEditorToolbar.duplicateShiftD', 'Duplicate (Shift+D)'), disabled: !selectedNode, "aria-label": t('modelEditorToolbar.duplicateShiftD', 'Duplicate (Shift+D)'), children: jsx(NiceIcon, { name: "clipboard", size: 16 }) })] }));
12
12
  }), { displayName: 'ModelEditorToolbar' });
13
13
 
14
14
  export { ModelEditorToolbar as default };
@@ -16,7 +16,10 @@ const ModelEditorViewport = /* @__PURE__ */ Object.assign(
16
16
  /* @__PURE__ */ React.memo(({ api }) => {
17
17
  const { t } = useNiceTranslation();
18
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;
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) => {
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]) }), jsx("span", { className: styles.viewportBadge, children: t('modelEditorViewport.trisMeshesBones', '{polyCount} tris · {meshCount} meshes · {boneCount} bones')
20
+ .replace('{polyCount}', String(polyCount.toLocaleString()))
21
+ .replace('{meshCount}', String(meshCount))
22
+ .replace('{boneCount}', String(boneCount)) })] }), jsx("div", { className: styles.viewportShadingBar, children: ['wireframe', 'solid', 'material', 'rendered'].map((m) => (jsx("button", { className: `${styles.shadingBtn} ${shadingMode === m ? styles.shadingBtnActive : ''}`, onClick: (e) => {
20
23
  e.stopPropagation();
21
24
  setShadingMode(m);
22
25
  }, title: m.charAt(0).toUpperCase() + m.slice(1), "aria-label": m.charAt(0).toUpperCase() + m.slice(1), children: jsx(NiceIcon, { name: m === 'wireframe'
@@ -28,7 +31,7 @@ const ModelEditorViewport = /* @__PURE__ */ Object.assign(
28
31
  : 'lightbulb', size: 16 }) }, m))) }), jsx("div", { className: styles.viewportGizmoInfo, children: jsx("button", { className: styles.shadingBtn, onClick: (e) => {
29
32
  e.stopPropagation();
30
33
  setGizmoSpace((s) => (s === 'local' ? 'world' : 'local'));
31
- }, title: `Orientation: ${gizmoSpace}`, "aria-label": `Orientation: ${gizmoSpace}`, children: gizmoSpace === 'local' ? (jsxs(Fragment, { children: [jsx(NiceIcon, { name: "target", size: 14 }), " ", t('modelEditorViewport.local', 'Local')] })) : (jsxs(Fragment, { children: [jsx(NiceIcon, { name: "globe", size: 14 }), " ", t('modelEditorViewport.world', 'World')] })) }) }), dragOver && (jsx("div", { className: styles.viewportDropZone, children: jsx("span", { children: t('modelEditorViewport.drop3dFilesHere', 'Drop 3D files here') }) }))] }), contextMenu && (jsxs("div", { className: styles.contextMenu, style: { top: contextMenu.y, left: contextMenu.x }, onClick: closeContextMenu, children: [jsxs("div", { className: styles.contextItem, onClick: () => addPrimitive('cube'), children: [jsx("span", { children: t('modelEditorViewport.addCube', 'Add Cube') }), jsx("span", { className: styles.contextKey, children: "Shift+A" })] }), jsx("div", { className: styles.contextItem, onClick: () => addPrimitive('sphere'), children: jsx("span", { children: t('modelEditorViewport.addSphere', 'Add Sphere') }) }), jsx("div", { className: styles.contextItem, onClick: () => addSceneLight('point'), children: jsx("span", { children: t('modelEditorViewport.addPointLight', 'Add Point Light') }) }), jsx("div", { className: styles.contextSep }), jsxs("div", { className: styles.contextItem, onClick: duplicateSelected, style: !selectedNode ? { opacity: 0.4, pointerEvents: 'none' } : {}, children: [jsx("span", { children: t('modelEditorViewport.duplicate', 'Duplicate') }), jsx("span", { className: styles.contextKey, children: "Shift+D" })] }), jsxs("div", { className: styles.contextItem, onClick: deleteSelected, style: !selectedNode ? { opacity: 0.4, pointerEvents: 'none' } : {}, children: [jsx("span", { children: t('modelEditorViewport.delete', 'Delete') }), jsx("span", { className: styles.contextKey, children: "Del" })] }), jsx("div", { className: styles.contextSep }), jsxs("div", { className: styles.contextItem, onClick: () => {
34
+ }, title: t('modelEditorViewport.orientation', 'Orientation: {gizmoSpace}').replace('{gizmoSpace}', String(gizmoSpace)), "aria-label": t('modelEditorViewport.orientation', 'Orientation: {gizmoSpace}').replace('{gizmoSpace}', String(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: () => {
32
35
  if (rootObjectRef.current) {
33
36
  focusOnObject(rootObjectRef.current);
34
37
  }
@@ -5,6 +5,7 @@ import { useNiceTranslation } from '../core/i18n.js';
5
5
  import styles from './ModelViewer.module.css.js';
6
6
  import { useModelViewer } from './useModelViewer.js';
7
7
 
8
+ const NAV_MODES = ['orbit', 'fps', 'fly', 'walkthrough'];
8
9
  const NAV_META = {
9
10
  orbit: { icon: 'globe', label: 'Orbit' },
10
11
  fps: { icon: 'user', label: 'FPS' },
@@ -38,10 +39,9 @@ const NiceModelViewer = ({ className, showToolbar = true, showStatusBar = true,
38
39
  }
39
40
  }, [api, onFileDrop]);
40
41
  /* ── nav cycle ── */
41
- const navModes = ['orbit', 'fps', 'fly', 'walkthrough'];
42
42
  const cycleNav = useCallback(() => {
43
- const idx = navModes.indexOf(api.navigationMode);
44
- api.setNavigationMode(navModes[(idx + 1) % navModes.length]);
43
+ const idx = NAV_MODES.indexOf(api.navigationMode);
44
+ api.setNavigationMode(NAV_MODES[(idx + 1) % NAV_MODES.length]);
45
45
  }, [api]);
46
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) => {
47
47
  var _a;
@@ -60,7 +60,7 @@ const NiceModelViewer = ({ className, showToolbar = true, showStatusBar = true,
60
60
  a.href = url;
61
61
  a.download = 'screenshot.png';
62
62
  a.click();
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)] }));
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: t('modelViewer.loadingModel', 'Loading model…') }), 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') }), NAV_MODES.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)] }));
64
64
  };
65
65
 
66
66
  export { NiceModelViewer };