@nice2dev/ui-3d 1.1.10 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/CHANGELOG.md +173 -0
  2. package/README.md +11 -15
  3. package/dist/cjs/core/editorAsk.js +48 -0
  4. package/dist/cjs/core/i18n.js +7 -3
  5. package/dist/cjs/core/threeCompat.js +52 -0
  6. package/dist/cjs/dance/DanceScoreEngine.js +7 -0
  7. package/dist/cjs/dance/PoseDetector.js +116 -64
  8. package/dist/cjs/index.js +0 -3
  9. package/dist/cjs/material/NiceMaterialEditor.js +7 -3
  10. package/dist/cjs/material/materialEditorTypes.js +1 -1
  11. package/dist/cjs/material/materialNodeDefinitions.js +14 -11
  12. package/dist/cjs/model/ModelEditor.js +3 -5
  13. package/dist/cjs/model/ModelEditorLeftPanel.js +10 -4
  14. package/dist/cjs/model/ModelEditorMenuBar.js +7 -5
  15. package/dist/cjs/model/ModelEditorRightPanel.js +18 -16
  16. package/dist/cjs/model/ModelEditorSubComponents.js +8 -6
  17. package/dist/cjs/model/ModelEditorTimeline.js +6 -4
  18. package/dist/cjs/model/ModelEditorToolbar.js +6 -4
  19. package/dist/cjs/model/ModelEditorViewport.js +16 -6
  20. package/dist/cjs/model/ModelViewer.js +6 -7
  21. package/dist/cjs/model/NiceArmatureEditor.js +8 -6
  22. package/dist/cjs/model/NiceMorphTargetEditor.js +5 -3
  23. package/dist/cjs/model/NiceOctree.js +3 -1
  24. package/dist/cjs/model/NicePhysicsSimulation.js +7 -5
  25. package/dist/cjs/model/NiceProceduralGeometry.js +5 -3
  26. package/dist/cjs/model/NiceTerrainEditor.js +10 -8
  27. package/dist/cjs/model/NiceWeightPainter.js +6 -4
  28. package/dist/cjs/model/NiceXRPreview.js +7 -5
  29. package/dist/cjs/model/useModelEditor.js +30 -18
  30. package/dist/cjs/model/useModelViewer.js +13 -9
  31. package/dist/cjs/nice2dev-ui-3d.css +1 -1
  32. package/dist/cjs/particle/NiceParticleEditor.js +14 -9
  33. package/dist/cjs/rendering/NiceCascadedShadows.js +3 -1
  34. package/dist/cjs/rendering/NiceRenderExport.js +11 -5
  35. package/dist/cjs/rendering/NiceSSAO.js +3 -1
  36. package/dist/cjs/rendering/NiceSSR.js +3 -1
  37. package/dist/cjs/rendering/NiceWebGPURenderer.js +7 -4
  38. package/dist/cjs/uv/NiceUVEditor.js +16 -12
  39. package/dist/cjs/uv/uvEditorTypes.js +1 -0
  40. package/dist/esm/core/editorAsk.js +45 -0
  41. package/dist/esm/core/i18n.js +8 -4
  42. package/dist/esm/core/threeCompat.js +30 -0
  43. package/dist/esm/dance/DanceScoreEngine.js +7 -0
  44. package/dist/esm/dance/PoseDetector.js +118 -65
  45. package/dist/esm/index.js +3 -3
  46. package/dist/esm/material/NiceMaterialEditor.js +7 -3
  47. package/dist/esm/material/materialEditorTypes.js +1 -1
  48. package/dist/esm/material/materialNodeDefinitions.js +14 -11
  49. package/dist/esm/model/ModelEditor.js +4 -5
  50. package/dist/esm/model/ModelEditorLeftPanel.js +10 -4
  51. package/dist/esm/model/ModelEditorMenuBar.js +7 -5
  52. package/dist/esm/model/ModelEditorRightPanel.js +18 -16
  53. package/dist/esm/model/ModelEditorSubComponents.js +8 -6
  54. package/dist/esm/model/ModelEditorTimeline.js +6 -4
  55. package/dist/esm/model/ModelEditorToolbar.js +6 -4
  56. package/dist/esm/model/ModelEditorViewport.js +16 -6
  57. package/dist/esm/model/ModelViewer.js +7 -7
  58. package/dist/esm/model/NiceArmatureEditor.js +9 -7
  59. package/dist/esm/model/NiceMorphTargetEditor.js +5 -3
  60. package/dist/esm/model/NiceOctree.js +3 -1
  61. package/dist/esm/model/NicePhysicsSimulation.js +7 -5
  62. package/dist/esm/model/NiceProceduralGeometry.js +5 -3
  63. package/dist/esm/model/NiceTerrainEditor.js +10 -8
  64. package/dist/esm/model/NiceWeightPainter.js +6 -4
  65. package/dist/esm/model/NiceXRPreview.js +7 -5
  66. package/dist/esm/model/useModelEditor.js +30 -18
  67. package/dist/esm/model/useModelViewer.js +13 -9
  68. package/dist/esm/nice2dev-ui-3d.css +1 -1
  69. package/dist/esm/particle/NiceParticleEditor.js +14 -9
  70. package/dist/esm/rendering/NiceCascadedShadows.js +3 -1
  71. package/dist/esm/rendering/NiceRenderExport.js +11 -5
  72. package/dist/esm/rendering/NiceSSAO.js +3 -1
  73. package/dist/esm/rendering/NiceSSR.js +3 -1
  74. package/dist/esm/rendering/NiceWebGPURenderer.js +7 -4
  75. package/dist/esm/uv/NiceUVEditor.js +16 -12
  76. package/dist/esm/uv/uvEditorTypes.js +1 -0
  77. package/dist/types/core/editorAsk.d.ts +26 -0
  78. package/dist/types/core/i18n.d.ts +1 -17
  79. package/dist/types/core/threeCompat.d.ts +14 -0
  80. package/dist/types/dance/DanceScoreEngine.d.ts +0 -6
  81. package/dist/types/dance/PoseDetector.d.ts +14 -13
  82. package/dist/types/index.d.ts +4 -4
  83. package/dist/types/model/ModelEditor.d.ts +0 -4
  84. package/dist/types/model/ModelViewer.d.ts +0 -4
  85. package/dist/types/model/useModelEditor.d.ts +3 -0
  86. package/package.json +3 -3
@@ -1,6 +1,7 @@
1
1
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
2
2
  import { NiceTutorialButton, resolveTutorial, NiceAppShell, resolveCssColor, NiceIcon } from '@nice2dev/ui';
3
3
  import { useRef, useMemo, useEffect, useReducer, useState } from 'react';
4
+ import { useNiceTranslation } from '../core/i18n.js';
4
5
  import styles from './ParticleEditor.module.css.js';
5
6
  import { MODULE_NAMES, BLEND_MODE_NAMES, RENDER_MODE_NAMES, EMITTER_SHAPE_NAMES } from './particleEditorTypes.js';
6
7
  import { applyPreset, createDefaultSystem, PARTICLE_PRESETS, importSystemsFromJSON, exportSystemsToJSON, cloneSystem } from './particleEditorUtils.js';
@@ -280,6 +281,7 @@ function useParticleEditor(initialSystems) {
280
281
  return { state, dispatch };
281
282
  }
282
283
  function SystemListPanel({ systems, selectedId, onSelect, onAdd, onRemove, onDuplicate, onRename, onAddPreset, }) {
284
+ const { t } = useNiceTranslation();
283
285
  const [editingId, setEditingId] = useState(null);
284
286
  const [editName, setEditName] = useState('');
285
287
  const [showPresets, setShowPresets] = useState(false);
@@ -293,20 +295,21 @@ function SystemListPanel({ systems, selectedId, onSelect, onAdd, onRemove, onDup
293
295
  }
294
296
  setEditingId(null);
295
297
  };
296
- return (jsxs("div", { className: styles.systemList, children: [jsxs("div", { className: styles.systemListHeader, children: [jsxs("span", { children: ["Systems (", systems.length, ")"] }), jsxs("div", { className: styles.systemListActions, children: [jsx("button", { onClick: onAdd, title: "New System", "aria-label": "New System", children: jsx(NiceIcon, { name: "plus", size: 14 }) }), jsx("button", { onClick: () => setShowPresets(!showPresets), title: "Add Preset", "aria-label": "Add Preset", children: jsx(NiceIcon, { name: "clipboard", size: 14 }) })] })] }), showPresets && (jsx("div", { className: styles.presetMenu, children: Object.keys(PARTICLE_PRESETS).map((preset) => (jsx("button", { className: styles.presetItem, onClick: () => {
298
+ return (jsxs("div", { className: styles.systemList, children: [jsxs("div", { className: styles.systemListHeader, children: [jsxs("span", { children: ["Systems (", systems.length, ")"] }), jsxs("div", { className: styles.systemListActions, children: [jsx("button", { onClick: onAdd, title: t('particleEditor.newSystem', 'New System'), "aria-label": t('particleEditor.newSystem', 'New System'), children: jsx(NiceIcon, { name: "plus", size: 14 }) }), jsx("button", { onClick: () => setShowPresets(!showPresets), title: t('particleEditor.addPreset', 'Add Preset'), "aria-label": t('particleEditor.addPreset', 'Add Preset'), children: jsx(NiceIcon, { name: "clipboard", size: 14 }) })] })] }), showPresets && (jsx("div", { className: styles.presetMenu, children: Object.keys(PARTICLE_PRESETS).map((preset) => (jsx("button", { className: styles.presetItem, onClick: () => {
297
299
  onAddPreset(preset);
298
300
  setShowPresets(false);
299
301
  }, children: preset.charAt(0).toUpperCase() + preset.slice(1) }, preset))) })), jsx("div", { className: styles.systemItems, children: systems.map((system) => (jsx("div", { className: `${styles.systemItem} ${selectedId === system.id ? styles.selected : ''}`, onClick: () => onSelect(system.id), children: editingId === system.id ? (jsx("input", { type: "text", value: editName, onChange: (e) => setEditName(e.target.value), onBlur: handleFinishRename, onKeyDown: (e) => e.key === 'Enter' && handleFinishRename(), autoFocus: true, className: styles.renameInput })) : (jsxs(Fragment, { children: [jsx("span", { className: styles.systemName, onDoubleClick: () => handleStartRename(system), children: system.name }), jsxs("div", { className: styles.systemItemActions, children: [jsx("button", { onClick: (e) => {
300
302
  e.stopPropagation();
301
303
  onDuplicate(system.id);
302
- }, title: "Duplicate", "aria-label": "Duplicate", children: jsx(NiceIcon, { name: "clipboard", size: 14 }) }), jsx("button", { onClick: (e) => {
304
+ }, title: t('particleEditor.duplicate', 'Duplicate'), "aria-label": t('particleEditor.duplicate', 'Duplicate'), children: jsx(NiceIcon, { name: "clipboard", size: 14 }) }), jsx("button", { onClick: (e) => {
303
305
  e.stopPropagation();
304
306
  onRemove(system.id);
305
- }, title: "Remove", "aria-label": "Remove", disabled: systems.length <= 1, children: jsx(NiceIcon, { name: "trash", size: 14 }) })] })] })) }, system.id))) })] }));
307
+ }, title: t('particleEditor.remove', 'Remove'), "aria-label": t('particleEditor.remove', 'Remove'), disabled: systems.length <= 1, children: jsx(NiceIcon, { name: "trash", size: 14 }) })] })] })) }, system.id))) })] }));
306
308
  }
307
309
  function InspectorPanel({ system, expandedModules, selectedModule, onToggleModule, onSelectModule: _onSelectModule, onUpdateSystem, }) {
310
+ const { t } = useNiceTranslation();
308
311
  if (!system) {
309
- return (jsx("div", { className: styles.inspector, children: jsx("div", { className: styles.emptyInspector, children: "No system selected" }) }));
312
+ return (jsx("div", { className: styles.inspector, children: jsx("div", { className: styles.emptyInspector, children: t('particleEditor.noSystemSelected', 'No system selected') }) }));
310
313
  }
311
314
  const modules = [
312
315
  { key: 'main', hasEnabled: false },
@@ -351,6 +354,7 @@ function InspectorPanel({ system, expandedModules, selectedModule, onToggleModul
351
354
  }, className: styles.moduleCheckbox })), jsx("span", { className: styles.moduleName, children: MODULE_NAMES[mod.key] || mod.key })] }), expandedModules.includes(mod.key) && (jsx("div", { className: styles.moduleContent, children: jsx(ModuleEditor, { system: system, moduleKey: mod.key, onUpdate: onUpdateSystem }) }))] }, mod.key))) })] }));
352
355
  }
353
356
  function ModuleEditor({ system, moduleKey, onUpdate }) {
357
+ const { t } = useNiceTranslation();
354
358
  const renderDistributionInput = (label, value, onChange) => (jsxs("div", { className: styles.propertyRow, children: [jsx("label", { children: label }), jsxs("div", { className: styles.distributionInput, children: [jsxs("select", { value: value.type, onChange: (e) => {
355
359
  const newType = e.target.value;
356
360
  if (newType === 'constant') {
@@ -368,7 +372,7 @@ function ModuleEditor({ system, moduleKey, onUpdate }) {
368
372
  ],
369
373
  });
370
374
  }
371
- }, className: styles.distributionType, children: [jsx("option", { value: "constant", children: "Constant" }), jsx("option", { value: "random", children: "Random" }), jsx("option", { value: "curve", children: "Curve" })] }), value.type === 'constant' && (jsx("input", { type: "number", value: value.value, onChange: (e) => onChange({ ...value, value: parseFloat(e.target.value) || 0 }), className: styles.numberInput })), value.type === 'random' && (jsxs(Fragment, { children: [jsx("input", { type: "number", value: value.min, onChange: (e) => onChange({ ...value, min: parseFloat(e.target.value) || 0 }), className: styles.numberInput, placeholder: "Min" }), jsx("input", { type: "number", value: value.max, onChange: (e) => onChange({ ...value, max: parseFloat(e.target.value) || 0 }), className: styles.numberInput, placeholder: "Max" })] }))] })] }));
375
+ }, className: styles.distributionType, children: [jsx("option", { value: "constant", children: t('particleEditor.constant', 'Constant') }), jsx("option", { value: "random", children: t('particleEditor.random', 'Random') }), jsx("option", { value: "curve", children: t('particleEditor.curve', 'Curve') })] }), value.type === 'constant' && (jsx("input", { type: "number", value: value.value, onChange: (e) => onChange({ ...value, value: parseFloat(e.target.value) || 0 }), className: styles.numberInput })), value.type === 'random' && (jsxs(Fragment, { children: [jsx("input", { type: "number", value: value.min, onChange: (e) => onChange({ ...value, min: parseFloat(e.target.value) || 0 }), className: styles.numberInput, placeholder: t('particleEditor.min', 'Min') }), jsx("input", { type: "number", value: value.max, onChange: (e) => onChange({ ...value, max: parseFloat(e.target.value) || 0 }), className: styles.numberInput, placeholder: t('particleEditor.max', 'Max') })] }))] })] }));
372
376
  const renderNumberInput = (label, value, onChange, props) => (jsxs("div", { className: styles.propertyRow, children: [jsx("label", { children: label }), jsx("input", { type: "number", value: value, onChange: (e) => onChange(parseFloat(e.target.value) || 0), className: styles.numberInput, ...props })] }));
373
377
  const renderToggle = (label, value, onChange) => (jsxs("div", { className: styles.propertyRow, children: [jsx("label", { children: label }), jsx("input", { type: "checkbox", checked: value, onChange: (e) => onChange(e.target.checked), className: styles.toggleInput })] }));
374
378
  const renderSelect = (label, value, options, onChange) => (jsxs("div", { className: styles.propertyRow, children: [jsx("label", { children: label }), jsx("select", { value: value, onChange: (e) => onChange(e.target.value), className: styles.selectInput, children: Object.entries(options).map(([key, name]) => (jsx("option", { value: key, children: name }, key))) })] }));
@@ -393,7 +397,7 @@ function ModuleEditor({ system, moduleKey, onUpdate }) {
393
397
  },
394
398
  ],
395
399
  },
396
- }), children: "Add Burst" })] })] }));
400
+ }), children: t('particleEditor.addBurst', 'Add Burst') })] })] }));
397
401
  case 'shape':
398
402
  return (jsxs(Fragment, { children: [renderSelect('Shape', system.shape.type, EMITTER_SHAPE_NAMES, (v) =>
399
403
  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- writing into discriminated-union variant; runtime narrowed by `type`
@@ -421,11 +425,12 @@ function ModuleEditor({ system, moduleKey, onUpdate }) {
421
425
  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- BLEND_MODE_NAMES is keyed by ParticleBlendMode but renderSelect is generic
422
426
  BLEND_MODE_NAMES, (v) => onUpdate({ material: { ...system.material, blendMode: v } }))] }));
423
427
  default:
424
- return jsx("div", { className: styles.moduleNotImplemented, children: "Module editor not yet implemented" });
428
+ return (jsx("div", { className: styles.moduleNotImplemented, children: t('particleEditor.moduleEditorNotYetImplemented', 'Module editor not yet implemented') }));
425
429
  }
426
430
  }
427
431
  function Toolbar({ isPlaying, isPaused, playbackSpeed, currentTime, canUndo, canRedo, showGrid, showBounds, showEmitterShape, viewMode, onPlay, onPause, onStop, onRestart, onSpeedChange, onUndo, onRedo, onToggleGrid, onToggleBounds, onToggleEmitterShape, onViewModeChange, onNew, onExport, onImport, }) {
428
- return (jsxs("div", { className: styles.toolbar, children: [jsxs("div", { className: styles.toolbarGroup, children: [jsx("button", { onClick: onNew, title: "New Project", "aria-label": "New Project", children: jsx(NiceIcon, { name: "file", size: 16 }) }), jsx("button", { onClick: onImport, title: "Import", "aria-label": "Import", children: jsx(NiceIcon, { name: "folder", size: 16 }) }), jsx("button", { onClick: onExport, title: "Export", "aria-label": "Export", children: jsx(NiceIcon, { name: "save", size: 16 }) })] }), jsx("div", { className: styles.toolbarSeparator }), jsxs("div", { className: styles.toolbarGroup, children: [jsx("button", { onClick: onUndo, disabled: !canUndo, title: "Undo", "aria-label": "Undo", children: jsx(NiceIcon, { name: "arrow-left", size: 16 }) }), jsx("button", { onClick: onRedo, disabled: !canRedo, title: "Redo", "aria-label": "Redo", children: jsx(NiceIcon, { name: "arrow-right", size: 16 }) })] }), jsx("div", { className: styles.toolbarSeparator }), jsxs("div", { className: styles.toolbarGroup, children: [isPlaying && !isPaused ? (jsx("button", { onClick: onPause, title: "Pause", "aria-label": "Pause", children: jsx(NiceIcon, { name: "pause", size: 16 }) })) : (jsx("button", { onClick: onPlay, title: "Play", "aria-label": "Play", children: jsx(NiceIcon, { name: "play", size: 16 }) })), jsx("button", { onClick: onStop, title: "Stop", "aria-label": "Stop", children: jsx(NiceIcon, { name: "stop", size: 16 }) }), jsx("button", { onClick: onRestart, title: "Restart", "aria-label": "Restart", children: jsx(NiceIcon, { name: "refresh", size: 16 }) }), jsxs("select", { value: playbackSpeed, onChange: (e) => onSpeedChange(parseFloat(e.target.value)), className: styles.speedSelect, title: "Playback Speed", children: [jsx("option", { value: 0.25, children: "0.25x" }), jsx("option", { value: 0.5, children: "0.5x" }), jsx("option", { value: 1, children: "1x" }), jsx("option", { value: 2, children: "2x" }), jsx("option", { value: 4, children: "4x" })] }), jsxs("span", { className: styles.timeDisplay, children: [currentTime.toFixed(2), "s"] })] }), jsx("div", { className: styles.toolbarSeparator }), jsxs("div", { className: styles.toolbarGroup, children: [jsx("button", { onClick: onToggleGrid, className: showGrid ? styles.active : '', title: "Toggle Grid", "aria-label": "Toggle Grid", children: jsx(NiceIcon, { name: "hash", size: 16 }) }), jsx("button", { onClick: onToggleBounds, className: showBounds ? styles.active : '', title: "Toggle Bounds", "aria-label": "Toggle Bounds", children: jsx(NiceIcon, { name: "square", size: 16 }) }), jsx("button", { onClick: onToggleEmitterShape, className: showEmitterShape ? styles.active : '', title: "Toggle Emitter Shape", "aria-label": "Toggle Emitter Shape", children: jsx(NiceIcon, { name: "target", size: 16 }) })] }), jsx("div", { className: styles.toolbarSeparator }), jsx("div", { className: styles.toolbarGroup, children: ['3d', 'top', 'side', 'front'].map((mode) => (jsx("button", { onClick: () => onViewModeChange(mode), className: viewMode === mode ? styles.active : '', title: mode.toUpperCase(), children: mode === '3d' ? jsx(NiceIcon, { name: "video", size: 16 }) : mode.charAt(0).toUpperCase() }, mode))) })] }));
432
+ const { t } = useNiceTranslation();
433
+ return (jsxs("div", { className: styles.toolbar, children: [jsxs("div", { className: styles.toolbarGroup, children: [jsx("button", { onClick: onNew, title: t('particleEditor.newProject', 'New Project'), "aria-label": t('particleEditor.newProject', 'New Project'), children: jsx(NiceIcon, { name: "file", size: 16 }) }), jsx("button", { onClick: onImport, title: t('particleEditor.import', 'Import'), "aria-label": t('particleEditor.import', 'Import'), children: jsx(NiceIcon, { name: "folder", size: 16 }) }), jsx("button", { onClick: onExport, title: t('particleEditor.export', 'Export'), "aria-label": t('particleEditor.export', 'Export'), children: jsx(NiceIcon, { name: "save", size: 16 }) })] }), jsx("div", { className: styles.toolbarSeparator }), jsxs("div", { className: styles.toolbarGroup, children: [jsx("button", { onClick: onUndo, disabled: !canUndo, title: t('particleEditor.undo', 'Undo'), "aria-label": t('particleEditor.undo', 'Undo'), children: jsx(NiceIcon, { name: "arrow-left", size: 16 }) }), jsx("button", { onClick: onRedo, disabled: !canRedo, title: t('particleEditor.redo', 'Redo'), "aria-label": t('particleEditor.redo', 'Redo'), children: jsx(NiceIcon, { name: "arrow-right", size: 16 }) })] }), jsx("div", { className: styles.toolbarSeparator }), jsxs("div", { className: styles.toolbarGroup, children: [isPlaying && !isPaused ? (jsx("button", { onClick: onPause, title: t('particleEditor.pause', 'Pause'), "aria-label": t('particleEditor.pause', 'Pause'), children: jsx(NiceIcon, { name: "pause", size: 16 }) })) : (jsx("button", { onClick: onPlay, title: t('particleEditor.play', 'Play'), "aria-label": t('particleEditor.play', 'Play'), children: jsx(NiceIcon, { name: "play", size: 16 }) })), jsx("button", { onClick: onStop, title: t('particleEditor.stop', 'Stop'), "aria-label": t('particleEditor.stop', 'Stop'), children: jsx(NiceIcon, { name: "stop", size: 16 }) }), jsx("button", { onClick: onRestart, title: t('particleEditor.restart', 'Restart'), "aria-label": t('particleEditor.restart', 'Restart'), children: jsx(NiceIcon, { name: "refresh", size: 16 }) }), jsxs("select", { value: playbackSpeed, onChange: (e) => onSpeedChange(parseFloat(e.target.value)), className: styles.speedSelect, title: t('particleEditor.playbackSpeed', 'Playback Speed'), children: [jsx("option", { value: 0.25, children: "0.25x" }), jsx("option", { value: 0.5, children: "0.5x" }), jsx("option", { value: 1, children: "1x" }), jsx("option", { value: 2, children: "2x" }), jsx("option", { value: 4, children: "4x" })] }), jsxs("span", { className: styles.timeDisplay, children: [currentTime.toFixed(2), "s"] })] }), jsx("div", { className: styles.toolbarSeparator }), jsxs("div", { className: styles.toolbarGroup, children: [jsx("button", { onClick: onToggleGrid, className: showGrid ? styles.active : '', title: t('particleEditor.toggleGrid', 'Toggle Grid'), "aria-label": t('particleEditor.toggleGrid', 'Toggle Grid'), children: jsx(NiceIcon, { name: "hash", size: 16 }) }), jsx("button", { onClick: onToggleBounds, className: showBounds ? styles.active : '', title: t('particleEditor.toggleBounds', 'Toggle Bounds'), "aria-label": t('particleEditor.toggleBounds', 'Toggle Bounds'), children: jsx(NiceIcon, { name: "square", size: 16 }) }), jsx("button", { onClick: onToggleEmitterShape, className: showEmitterShape ? styles.active : '', title: t('particleEditor.toggleEmitterShape', 'Toggle Emitter Shape'), "aria-label": t('particleEditor.toggleEmitterShape', 'Toggle Emitter Shape'), children: jsx(NiceIcon, { name: "target", size: 16 }) })] }), jsx("div", { className: styles.toolbarSeparator }), jsx("div", { className: styles.toolbarGroup, children: ['3d', 'top', 'side', 'front'].map((mode) => (jsx("button", { onClick: () => onViewModeChange(mode), className: viewMode === mode ? styles.active : '', title: mode.toUpperCase(), children: mode === '3d' ? jsx(NiceIcon, { name: "video", size: 16 }) : mode.charAt(0).toUpperCase() }, mode))) })] }));
429
434
  }
430
435
  function PreviewPanel({ systems, isPlaying, isPaused, currentTime, showGrid, showEmitterShape, }) {
431
436
  const canvasRef = useRef(null);
@@ -466,7 +471,7 @@ function PreviewPanel({ systems, isPlaying, isPaused, currentTime, showGrid, sho
466
471
  }
467
472
  // Draw emitter shape indicator
468
473
  if (showEmitterShape && systems.length > 0) {
469
- ctx.strokeStyle = 'rgba(0,200,255,0.5)';
474
+ ctx.strokeStyle = resolveCssColor('var(--nice-primary-tint-60, rgba(0,200,255,0.5))', ctx.canvas);
470
475
  ctx.lineWidth = 2;
471
476
  ctx.setLineDash([5, 5]);
472
477
  ctx.beginPath();
@@ -2,6 +2,7 @@ import { jsxs, jsx } from 'react/jsx-runtime';
2
2
  import { NiceIcon } from '@nice2dev/ui';
3
3
  import { useEffect, useState, useRef, useCallback } from 'react';
4
4
  import * as THREE from 'three';
5
+ import { useNiceTranslation } from '../core/i18n.js';
5
6
 
6
7
  /* ── Cascade split calculation ── */
7
8
  function computeCascadeSplits(cascadeCount, near, far, splitMode, lambda) {
@@ -245,13 +246,14 @@ function useCSM(scene, camera, configOverride) {
245
246
  }
246
247
  /* ── Component ── */
247
248
  const NiceCascadedShadows = ({ scene, camera, config: configProp, showDebug: initialDebug, className = '', }) => {
249
+ const { t } = useNiceTranslation();
248
250
  const { config, setConfig, cascadeSplits, showDebug, setShowDebug } = useCSM(scene, camera, configProp);
249
251
  useEffect(() => {
250
252
  if (initialDebug !== undefined) {
251
253
  setShowDebug(initialDebug);
252
254
  }
253
255
  }, [initialDebug, setShowDebug]);
254
- return (jsxs("div", { className: `nice-cascaded-shadows ${className}`, style: { display: 'grid', gap: 8, fontSize: 13 }, children: [jsxs("strong", { children: [jsx(NiceIcon, { name: "lightbulb", size: 14 }), " Cascaded Shadow Maps"] }), jsxs("label", { children: ["Cascades: ", config.cascadeCount, jsx("input", { type: "range", min: 1, max: 5, value: config.cascadeCount, onChange: (e) => setConfig({ cascadeCount: parseInt(e.target.value, 10) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Max Distance: ", config.maxFar, jsx("input", { type: "range", min: 50, max: 1000, step: 10, value: config.maxFar, onChange: (e) => setConfig({ maxFar: parseInt(e.target.value, 10) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Split Mode:", jsxs("select", { value: config.splitMode, onChange: (e) => setConfig({ splitMode: e.target.value }), style: { width: '100%', marginTop: 2 }, children: [jsx("option", { value: "practical", children: "Practical" }), jsx("option", { value: "logarithmic", children: "Logarithmic" }), jsx("option", { value: "linear", children: "Linear" })] })] }), config.splitMode === 'practical' && (jsxs("label", { children: ["Lambda: ", config.practicalSplitLambda.toFixed(2), jsx("input", { type: "range", min: 0, max: 1, step: 0.05, value: config.practicalSplitLambda, onChange: (e) => setConfig({ practicalSplitLambda: parseFloat(e.target.value) }), style: { width: '100%' } })] })), jsxs("label", { children: ["Shadow Map Size: ", config.shadowMapSize, jsxs("select", { value: config.shadowMapSize, onChange: (e) => setConfig({ shadowMapSize: parseInt(e.target.value, 10) }), style: { width: '100%', marginTop: 2 }, children: [jsx("option", { value: 512, children: "512" }), jsx("option", { value: 1024, children: "1024" }), jsx("option", { value: 2048, children: "2048" }), jsx("option", { value: 4096, children: "4096" })] })] }), jsxs("div", { style: { fontSize: 10, opacity: 0.7 }, children: ["Cascade splits: ", cascadeSplits.map((s) => s.toFixed(1)).join(' | ')] }), jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 6 }, children: [jsx("input", { type: "checkbox", checked: showDebug, onChange: (e) => setShowDebug(e.target.checked) }), "Show cascade frustums"] })] }));
256
+ return (jsxs("div", { className: `nice-cascaded-shadows ${className}`, style: { display: 'grid', gap: 8, fontSize: 13 }, children: [jsxs("strong", { children: [jsx(NiceIcon, { name: "lightbulb", size: 14 }), ' ', t('cascadedShadows.cascadedShadowMaps', 'Cascaded Shadow Maps')] }), jsxs("label", { children: ["Cascades: ", config.cascadeCount, jsx("input", { type: "range", min: 1, max: 5, value: config.cascadeCount, onChange: (e) => setConfig({ cascadeCount: parseInt(e.target.value, 10) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Max Distance: ", config.maxFar, jsx("input", { type: "range", min: 50, max: 1000, step: 10, value: config.maxFar, onChange: (e) => setConfig({ maxFar: parseInt(e.target.value, 10) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Split Mode:", jsxs("select", { value: config.splitMode, onChange: (e) => setConfig({ splitMode: e.target.value }), style: { width: '100%', marginTop: 2 }, children: [jsx("option", { value: "practical", children: t('cascadedShadows.practical', 'Practical') }), jsx("option", { value: "logarithmic", children: t('cascadedShadows.logarithmic', 'Logarithmic') }), jsx("option", { value: "linear", children: t('cascadedShadows.linear', 'Linear') })] })] }), config.splitMode === 'practical' && (jsxs("label", { children: ["Lambda: ", config.practicalSplitLambda.toFixed(2), jsx("input", { type: "range", min: 0, max: 1, step: 0.05, value: config.practicalSplitLambda, onChange: (e) => setConfig({ practicalSplitLambda: parseFloat(e.target.value) }), style: { width: '100%' } })] })), jsxs("label", { children: ["Shadow Map Size: ", config.shadowMapSize, jsxs("select", { value: config.shadowMapSize, onChange: (e) => setConfig({ shadowMapSize: parseInt(e.target.value, 10) }), style: { width: '100%', marginTop: 2 }, children: [jsx("option", { value: 512, children: "512" }), jsx("option", { value: 1024, children: "1024" }), jsx("option", { value: 2048, children: "2048" }), jsx("option", { value: 4096, children: "4096" })] })] }), jsxs("div", { style: { fontSize: 10, opacity: 0.7 }, children: ["Cascade splits: ", cascadeSplits.map((s) => s.toFixed(1)).join(' | ')] }), jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 6 }, children: [jsx("input", { type: "checkbox", checked: showDebug, onChange: (e) => setShowDebug(e.target.checked) }), t('cascadedShadows.showCascadeFrustums', 'Show cascade frustums')] })] }));
255
257
  };
256
258
 
257
259
  export { NiceCascadedShadows, useCSM };
@@ -2,6 +2,7 @@ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
2
2
  import { NiceIcon } from '@nice2dev/ui';
3
3
  import { useState, useCallback } from 'react';
4
4
  import * as THREE from 'three';
5
+ import { useNiceTranslation } from '../core/i18n.js';
5
6
 
6
7
  /* ── EXR writer (minimal, uncompressed) ── */
7
8
  function writeEXR(width, height, floatData) {
@@ -182,6 +183,7 @@ const DEFAULT_CONFIG = {
182
183
  // resolves to nothing, so the exported frame needs the literal (cf. 344d1b2).
183
184
  backgroundColor: '#000000',
184
185
  backgroundMode: 'transparent',
186
+ // audit-allow: literal-color +2 — the default sky gradient of the rendered image
185
187
  gradientTop: '#87CEEB',
186
188
  gradientBottom: '#2E4057',
187
189
  samples: 1,
@@ -296,6 +298,7 @@ function useRenderExport(renderer, scene, camera) {
296
298
  }
297
299
  /* ── Component ── */
298
300
  const NiceRenderExport = ({ renderer, scene, camera, className = '', }) => {
301
+ const { t } = useNiceTranslation();
299
302
  const { config, setConfig, render, isRendering, presetSizes } = useRenderExport(renderer, scene, camera);
300
303
  const handleRender = async () => {
301
304
  const blob = await render();
@@ -310,7 +313,7 @@ const NiceRenderExport = ({ renderer, scene, camera, className = '', }) => {
310
313
  link.click();
311
314
  URL.revokeObjectURL(url);
312
315
  };
313
- return (jsxs("div", { className: `nice-render-export ${className}`, style: { display: 'grid', gap: 8, fontSize: 13 }, children: [jsxs("strong", { children: [jsx(NiceIcon, { name: "camera", size: 14 }), " Render Export"] }), jsx("div", { style: { display: 'flex', flexWrap: 'wrap', gap: 2 }, children: presetSizes.map((p) => (jsx("button", { type: "button", onClick: () => setConfig({ width: p.width, height: p.height }), style: {
316
+ return (jsxs("div", { className: `nice-render-export ${className}`, style: { display: 'grid', gap: 8, fontSize: 13 }, children: [jsxs("strong", { children: [jsx(NiceIcon, { name: "camera", size: 14 }), " ", t('renderExport.renderExport', 'Render Export')] }), jsx("div", { style: { display: 'flex', flexWrap: 'wrap', gap: 2 }, children: presetSizes.map((p) => (jsx("button", { type: "button", onClick: () => setConfig({ width: p.width, height: p.height }), style: {
314
317
  padding: '2px 6px',
315
318
  fontSize: 10,
316
319
  cursor: 'pointer',
@@ -320,18 +323,21 @@ const NiceRenderExport = ({ renderer, scene, camera, className = '', }) => {
320
323
  ? 'var(--nice-color-accent, #6366f1)'
321
324
  : 'transparent',
322
325
  color: config.width === p.width && config.height === p.height
323
- ? 'var(--nice-text-on-primary, #fff)'
326
+ ? 'var(--nice-color-white, #fff)'
324
327
  : 'inherit',
325
- }, children: p.label }, p.label))) }), jsxs("div", { style: { display: 'flex', gap: 4, alignItems: 'center' }, children: [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' } }), jsx("span", { children: "\u00D7" }), 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' } })] }), jsxs("label", { children: ["Format:", jsxs("select", { value: config.format, onChange: (e) => setConfig({ format: e.target.value }), style: { width: '100%', marginTop: 2 }, children: [jsx("option", { value: "png", children: "PNG (lossless + alpha)" }), jsx("option", { value: "jpeg", children: "JPEG (lossy, no alpha)" }), jsx("option", { value: "exr", children: "EXR (HDR float)" })] })] }), jsxs("label", { children: ["Background:", jsxs("select", { value: config.backgroundMode, onChange: (e) => setConfig({ backgroundMode: e.target.value }), style: { width: '100%', marginTop: 2 }, children: [jsx("option", { value: "transparent", children: "Transparent" }), jsx("option", { value: "color", children: "Solid Color" }), jsx("option", { value: "scene", children: "Scene Background" })] })] }), config.backgroundMode === 'color' && (jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 6 }, children: ["Color:", jsx("input", { type: "color", value: config.backgroundColor, onChange: (e) => setConfig({ backgroundColor: e.target.value }) })] })), jsxs("label", { children: ["Supersampling: ", config.samples, "x", jsxs("select", { value: config.samples, onChange: (e) => setConfig({ samples: parseInt(e.target.value, 10) }), style: { width: '100%', marginTop: 2 }, children: [jsx("option", { value: 1, children: "1x (none)" }), jsx("option", { value: 2, children: "2x" }), jsx("option", { value: 4, children: "4x (best quality)" })] })] }), jsx("button", { type: "button", onClick: handleRender, disabled: isRendering, style: {
328
+ }, children: p.label }, p.label))) }), jsxs("div", { style: { display: 'flex', gap: 4, alignItems: 'center' }, children: [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' } }), jsx("span", { children: "\u00D7" }), 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' } })] }), jsxs("label", { children: ["Format:", jsxs("select", { value: config.format, onChange: (e) => setConfig({ format: e.target.value }), style: { width: '100%', marginTop: 2 }, children: [jsx("option", { value: "png", children: "PNG (lossless + alpha)" }), jsx("option", { value: "jpeg", children: "JPEG (lossy, no alpha)" }), jsx("option", { value: "exr", children: "EXR (HDR float)" })] })] }), jsxs("label", { children: ["Background:", jsxs("select", { value: config.backgroundMode, onChange: (e) => setConfig({ backgroundMode: e.target.value }), style: { width: '100%', marginTop: 2 }, children: [jsx("option", { value: "transparent", children: t('renderExport.transparent', 'Transparent') }), jsx("option", { value: "color", children: t('renderExport.solidColor', 'Solid Color') }), jsx("option", { value: "scene", children: t('renderExport.sceneBackground', 'Scene Background') })] })] }), config.backgroundMode === 'color' && (jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 6 }, children: ["Color:", jsx("input", { type: "color", value: config.backgroundColor, onChange: (e) => setConfig({ backgroundColor: e.target.value }) })] })), jsxs("label", { children: ["Supersampling: ", config.samples, "x", jsxs("select", { value: config.samples, onChange: (e) => setConfig({ samples: parseInt(e.target.value, 10) }), style: { width: '100%', marginTop: 2 }, children: [jsx("option", { value: 1, children: "1x (none)" }), jsx("option", { value: 2, children: "2x" }), jsx("option", { value: 4, children: "4x (best quality)" })] })] }), jsx("button", { type: "button", onClick: handleRender, disabled: isRendering, style: {
326
329
  background: isRendering
327
330
  ? 'var(--nice-bg-tertiary, #94a3b8)'
328
331
  : 'var(--nice-color-accent, #6366f1)',
329
- color: 'var(--nice-text-on-primary, #fff)',
332
+ color: 'var(--nice-color-white, #fff)',
330
333
  border: 'none',
331
334
  borderRadius: 4,
332
335
  padding: '8px 12px',
333
336
  cursor: isRendering ? 'wait' : 'pointer',
334
- }, children: isRendering ? (jsxs(Fragment, { children: [jsx(NiceIcon, { name: "clock", size: 14 }), " Rendering\u2026"] })) : (jsxs(Fragment, { children: [jsx(NiceIcon, { name: "camera", size: 14 }), " Render ", config.width, "\u00D7", config.height, ' ', config.format.toUpperCase()] })) })] }));
337
+ }, children: isRendering ? (jsxs(Fragment, { children: [jsx(NiceIcon, { name: "clock", size: 14 }), " Rendering\u2026"] })) : (jsxs(Fragment, { children: [jsx(NiceIcon, { name: "camera", size: 14 }), ' ', t('renderExport.render', 'Render {width}×{height} {format}')
338
+ .replace('{width}', String(config.width))
339
+ .replace('{height}', String(config.height))
340
+ .replace('{format}', config.format.toUpperCase())] })) })] }));
335
341
  };
336
342
 
337
343
  export { NiceRenderExport, useRenderExport };
@@ -2,6 +2,7 @@ import { jsxs, jsx } from 'react/jsx-runtime';
2
2
  import { NiceIcon } from '@nice2dev/ui';
3
3
  import { useState, useRef, useCallback, useEffect } from 'react';
4
4
  import * as THREE from 'three';
5
+ import { useNiceTranslation } from '../core/i18n.js';
5
6
 
6
7
  /* ── SSAO Shaders ── */
7
8
  const SSAO_VERTEX = `
@@ -336,8 +337,9 @@ function useSSAO(renderer, scene, camera, configOverride) {
336
337
  }
337
338
  /* ── Component ── */
338
339
  const NiceSSAO = ({ renderer, scene, camera, config: configProp, className = '', }) => {
340
+ const { t } = useNiceTranslation();
339
341
  const { config, setConfig } = useSSAO(renderer, scene, camera, configProp);
340
- return (jsxs("div", { className: `nice-ssao ${className}`, style: { display: 'grid', gap: 8, fontSize: 13 }, children: [jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' }, children: [jsxs("strong", { children: [jsx(NiceIcon, { name: "circle", size: 14 }), " Ambient Occlusion"] }), jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 4, fontSize: 11 }, children: [jsx("input", { type: "checkbox", checked: config.enabled, onChange: (e) => setConfig({ enabled: e.target.checked }) }), "Enabled"] })] }), jsxs("label", { children: ["Mode:", jsxs("select", { value: config.mode, onChange: (e) => setConfig({ mode: e.target.value }), style: { width: '100%', marginTop: 2 }, children: [jsx("option", { value: "ssao", children: "SSAO" }), jsx("option", { value: "hbao", children: "HBAO" })] })] }), jsxs("label", { children: ["Samples: ", config.samples, 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%' } })] }), jsxs("label", { children: ["Radius: ", config.radius.toFixed(2), 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%' } })] }), jsxs("label", { children: ["Intensity: ", config.intensity.toFixed(2), 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%' } })] }), jsxs("label", { children: ["Bias: ", config.bias.toFixed(3), 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%' } })] }), jsxs("label", { children: ["Resolution: ", config.resolution.toFixed(2), 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%' } })] }), jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 6 }, children: [jsx("input", { type: "checkbox", checked: config.blurEnabled, onChange: (e) => setConfig({ blurEnabled: e.target.checked }) }), "Bilateral Blur (size: ", config.blurSize, ")"] })] }));
342
+ return (jsxs("div", { className: `nice-ssao ${className}`, style: { display: 'grid', gap: 8, fontSize: 13 }, children: [jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' }, children: [jsxs("strong", { children: [jsx(NiceIcon, { name: "circle", size: 14 }), " ", t('ssao.ambientOcclusion', 'Ambient Occlusion')] }), jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 4, fontSize: 11 }, children: [jsx("input", { type: "checkbox", checked: config.enabled, onChange: (e) => setConfig({ enabled: e.target.checked }) }), t('ssao.enabled', 'Enabled')] })] }), jsxs("label", { children: ["Mode:", jsxs("select", { value: config.mode, onChange: (e) => setConfig({ mode: e.target.value }), style: { width: '100%', marginTop: 2 }, children: [jsx("option", { value: "ssao", children: t('ssao.ssao', 'SSAO') }), jsx("option", { value: "hbao", children: t('ssao.hbao', 'HBAO') })] })] }), jsxs("label", { children: ["Samples: ", config.samples, 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%' } })] }), jsxs("label", { children: ["Radius: ", config.radius.toFixed(2), 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%' } })] }), jsxs("label", { children: ["Intensity: ", config.intensity.toFixed(2), 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%' } })] }), jsxs("label", { children: ["Bias: ", config.bias.toFixed(3), 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%' } })] }), jsxs("label", { children: ["Resolution: ", config.resolution.toFixed(2), 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%' } })] }), jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 6 }, children: [jsx("input", { type: "checkbox", checked: config.blurEnabled, onChange: (e) => setConfig({ blurEnabled: e.target.checked }) }), "Bilateral Blur (size: ", config.blurSize, ")"] })] }));
341
343
  };
342
344
 
343
345
  export { NiceSSAO, useSSAO };
@@ -2,6 +2,7 @@ import { jsxs, jsx } from 'react/jsx-runtime';
2
2
  import { NiceIcon } from '@nice2dev/ui';
3
3
  import { useState, useRef, useCallback, useEffect } from 'react';
4
4
  import * as THREE from 'three';
5
+ import { useNiceTranslation } from '../core/i18n.js';
5
6
 
6
7
  /* ── SSR Shader ── */
7
8
  const SSR_VERTEX = `
@@ -253,8 +254,9 @@ function useSSR(renderer, scene, camera, configOverride) {
253
254
  }
254
255
  /* ── Component ── */
255
256
  const NiceSSR = ({ renderer, scene, camera, config: configProp, className = '', }) => {
257
+ const { t } = useNiceTranslation();
256
258
  const { config, setConfig } = useSSR(renderer, scene, camera, configProp);
257
- return (jsxs("div", { className: `nice-ssr ${className}`, style: { display: 'grid', gap: 8, fontSize: 13 }, children: [jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' }, children: [jsxs("strong", { children: [jsx(NiceIcon, { name: "copy", size: 14 }), " Screen-Space Reflections"] }), jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 4, fontSize: 11 }, children: [jsx("input", { type: "checkbox", checked: config.enabled, onChange: (e) => setConfig({ enabled: e.target.checked }) }), "Enabled"] })] }), jsxs("label", { children: ["Max Steps: ", config.maxSteps, 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%' } })] }), jsxs("label", { children: ["Max Distance: ", config.maxDistance, 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%' } })] }), jsxs("label", { children: ["Intensity: ", config.intensity.toFixed(2), 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%' } })] }), jsxs("label", { children: ["Thickness: ", config.thickness.toFixed(2), 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%' } })] }), jsxs("label", { children: ["Jitter: ", config.jitter.toFixed(2), 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%' } })] }), jsxs("label", { children: ["Resolution Scale: ", config.resolution.toFixed(2), 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%' } })] }), jsxs("label", { children: ["Fresnel: ", config.fresnelExponent.toFixed(1), 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%' } })] })] }));
259
+ return (jsxs("div", { className: `nice-ssr ${className}`, style: { display: 'grid', gap: 8, fontSize: 13 }, children: [jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' }, children: [jsxs("strong", { children: [jsx(NiceIcon, { name: "copy", size: 14 }), ' ', t('ssr.screenSpaceReflections', 'Screen-Space Reflections')] }), jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 4, fontSize: 11 }, children: [jsx("input", { type: "checkbox", checked: config.enabled, onChange: (e) => setConfig({ enabled: e.target.checked }) }), t('ssr.enabled', 'Enabled')] })] }), jsxs("label", { children: ["Max Steps: ", config.maxSteps, 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%' } })] }), jsxs("label", { children: ["Max Distance: ", config.maxDistance, 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%' } })] }), jsxs("label", { children: ["Intensity: ", config.intensity.toFixed(2), 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%' } })] }), jsxs("label", { children: ["Thickness: ", config.thickness.toFixed(2), 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%' } })] }), jsxs("label", { children: ["Jitter: ", config.jitter.toFixed(2), 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%' } })] }), jsxs("label", { children: ["Resolution Scale: ", config.resolution.toFixed(2), 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%' } })] }), jsxs("label", { children: ["Fresnel: ", config.fresnelExponent.toFixed(1), 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%' } })] })] }));
258
260
  };
259
261
 
260
262
  export { NiceSSR, useSSR };
@@ -2,6 +2,8 @@ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
2
2
  import { NiceIcon } from '@nice2dev/ui';
3
3
  import { useState, useRef, useEffect, useCallback } from 'react';
4
4
  import * as THREE from 'three';
5
+ import { useNiceTranslation } from '../core/i18n.js';
6
+ import { SOFT_SHADOW_MAP } from '../core/threeCompat.js';
5
7
 
6
8
  /* ── WebGPU detection ── */
7
9
  async function detectWebGPU() {
@@ -116,7 +118,7 @@ function useWebGPURenderer(canvas, scene, camera, width = 800, height = 600, onR
116
118
  r.toneMappingExposure = toneMappingExposure;
117
119
  r.outputColorSpace = outputColorSpace;
118
120
  r.shadowMap.enabled = true;
119
- r.shadowMap.type = THREE.PCFSoftShadowMap;
121
+ r.shadowMap.type = SOFT_SHADOW_MAP;
120
122
  rendererRef.current = r;
121
123
  setRenderer(r);
122
124
  setIsWebGPU(usingWebGPU);
@@ -208,14 +210,15 @@ const TONE_MAPPINGS = [
208
210
  ];
209
211
  /* ── Component ── */
210
212
  const NiceWebGPURenderer = ({ canvas, scene, camera, width, height, onRendererReady, className = '', }) => {
213
+ const { t } = useNiceTranslation();
211
214
  const { isWebGPU, capabilities, isReady, toneMapping, setToneMapping, toneMappingExposure, setToneMappingExposure, outputColorSpace, setOutputColorSpace, pixelRatio, setPixelRatio, } = useWebGPURenderer(canvas, scene, camera, width, height, onRendererReady);
212
- return (jsxs("div", { className: `nice-webgpu-renderer ${className}`, style: { display: 'grid', gap: 8, fontSize: 13 }, children: [jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' }, children: [jsxs("strong", { children: [jsx(NiceIcon, { name: "monitor", size: 14 }), " Renderer"] }), jsx("span", { style: {
215
+ return (jsxs("div", { className: `nice-webgpu-renderer ${className}`, style: { display: 'grid', gap: 8, fontSize: 13 }, children: [jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' }, children: [jsxs("strong", { children: [jsx(NiceIcon, { name: "monitor", size: 14 }), " ", t('webGPURenderer.renderer', 'Renderer')] }), jsx("span", { style: {
213
216
  fontSize: 10,
214
217
  padding: '2px 6px',
215
218
  borderRadius: 4,
216
219
  background: isWebGPU ? 'var(--nice-success, #22c55e)' : 'var(--nice-warning, #f59e0b)',
217
- color: 'var(--nice-text-on-primary, #fff)',
218
- }, children: isWebGPU ? 'WebGPU' : 'WebGL' })] }), !isReady && jsx("div", { style: { opacity: 0.6, fontSize: 11 }, children: "Initializing\u2026" }), capabilities && (jsxs("div", { style: { fontSize: 10, opacity: 0.7, lineHeight: 1.5 }, children: ["GPU: ", capabilities.adapterName, jsx("br", {}), "Vendor: ", capabilities.vendor, jsx("br", {}), "Max Texture: ", capabilities.maxTextureDimension2D, "px", capabilities.features.length > 0 && (jsxs(Fragment, { children: [jsx("br", {}), "Features: ", capabilities.features.length] }))] })), jsxs("label", { children: ["Tone Mapping:", jsx("select", { value: toneMapping, onChange: (e) => setToneMapping(parseInt(e.target.value, 10)), style: { width: '100%', marginTop: 2 }, children: TONE_MAPPINGS.map((tm) => (jsx("option", { value: tm.value, children: tm.label }, tm.value))) })] }), jsxs("label", { children: ["Exposure: ", toneMappingExposure.toFixed(2), jsx("input", { type: "range", min: 0.1, max: 3, step: 0.05, value: toneMappingExposure, onChange: (e) => setToneMappingExposure(parseFloat(e.target.value)), style: { width: '100%' } })] }), jsxs("label", { children: ["Output Color Space:", jsxs("select", { value: outputColorSpace, onChange: (e) => setOutputColorSpace(e.target.value), style: { width: '100%', marginTop: 2 }, children: [jsx("option", { value: THREE.SRGBColorSpace, children: "sRGB" }), jsx("option", { value: THREE.LinearSRGBColorSpace, children: "Linear sRGB" })] })] }), jsxs("label", { children: ["Pixel Ratio: ", pixelRatio.toFixed(1), jsx("input", { type: "range", min: 0.5, max: 3, step: 0.5, value: pixelRatio, onChange: (e) => setPixelRatio(parseFloat(e.target.value)), style: { width: '100%' } })] })] }));
220
+ color: 'var(--nice-color-white, #fff)',
221
+ }, children: isWebGPU ? 'WebGPU' : 'WebGL' })] }), !isReady && jsx("div", { style: { opacity: 0.6, fontSize: 11 }, children: "Initializing\u2026" }), capabilities && (jsxs("div", { style: { fontSize: 10, opacity: 0.7, lineHeight: 1.5 }, children: ["GPU: ", capabilities.adapterName, jsx("br", {}), "Vendor: ", capabilities.vendor, jsx("br", {}), "Max Texture: ", capabilities.maxTextureDimension2D, "px", capabilities.features.length > 0 && (jsxs(Fragment, { children: [jsx("br", {}), "Features: ", capabilities.features.length] }))] })), jsxs("label", { children: ["Tone Mapping:", jsx("select", { value: toneMapping, onChange: (e) => setToneMapping(parseInt(e.target.value, 10)), style: { width: '100%', marginTop: 2 }, children: TONE_MAPPINGS.map((tm) => (jsx("option", { value: tm.value, children: tm.label }, tm.value))) })] }), jsxs("label", { children: ["Exposure: ", toneMappingExposure.toFixed(2), jsx("input", { type: "range", min: 0.1, max: 3, step: 0.05, value: toneMappingExposure, onChange: (e) => setToneMappingExposure(parseFloat(e.target.value)), style: { width: '100%' } })] }), jsxs("label", { children: ["Output Color Space:", jsxs("select", { value: outputColorSpace, onChange: (e) => setOutputColorSpace(e.target.value), style: { width: '100%', marginTop: 2 }, children: [jsx("option", { value: THREE.SRGBColorSpace, children: "sRGB" }), jsx("option", { value: THREE.LinearSRGBColorSpace, children: t('webGPURenderer.linearSrgb', 'Linear sRGB') })] })] }), jsxs("label", { children: ["Pixel Ratio: ", pixelRatio.toFixed(1), jsx("input", { type: "range", min: 0.5, max: 3, step: 0.5, value: pixelRatio, onChange: (e) => setPixelRatio(parseFloat(e.target.value)), style: { width: '100%' } })] })] }));
219
222
  };
220
223
 
221
224
  export { NiceWebGPURenderer, useWebGPURenderer };
@@ -1,6 +1,7 @@
1
1
  import { jsxs, jsx } from 'react/jsx-runtime';
2
2
  import { resolveCssColor, NiceIcon, NiceTutorialButton, resolveTutorial, NiceAppShell } from '@nice2dev/ui';
3
3
  import { useRef, useState, useEffect, useCallback } from 'react';
4
+ import { useNiceTranslation } from '../core/i18n.js';
4
5
  import styles from './UVEditor.module.css.js';
5
6
  import { DEFAULT_UV_STATE, DEFAULT_RELAX_OPTIONS, UV_TOOLS, UV_SELECTION_MODES, PROJECTION_TYPES, UV_CHANNELS, DEFAULT_PACKING_OPTIONS } from './uvEditorTypes.js';
6
7
  import { extractUVMeshData, uvSub, uvAdd, uvRotate, selectVerticesInRect, syncToGeometry, packIslands, applyIslandTransforms, relaxUVs, applyProjection, getSelectedVertices } from './uvEditorUtils.js';
@@ -20,6 +21,7 @@ const UV_EDITOR_TOUR = [
20
21
  Component
21
22
  ═══════════════════════════════════════════ */
22
23
  const NiceUVEditor = ({ geometry, texture, channel = 'uv', onUVChange, onSelectionChange, width = '100%', height = 600, showToolbar = true, showSidebar = true, className, readOnly = false, tutorial, }) => {
24
+ const { t } = useNiceTranslation();
23
25
  const canvasRef = useRef(null);
24
26
  const containerRef = useRef(null);
25
27
  // State
@@ -122,12 +124,14 @@ const NiceUVEditor = ({ geometry, texture, channel = 'uv', onUVChange, onSelecti
122
124
  ctx.closePath();
123
125
  ctx.stroke();
124
126
  }
125
- // Draw faces
127
+ // Draw faces — the two fills resolved once per frame, not once per face
128
+ const faceSelectedFill = resolveCssColor('var(--nice-warning-tint-30, rgba(255, 170, 0, 0.3))', ctx.canvas);
129
+ const faceFill = resolveCssColor('var(--nice-overlay-10, rgba(100, 100, 100, 0.1))', ctx.canvas);
126
130
  for (const face of meshData.faces.values()) {
127
131
  const verts = face.vertices.map((id) => meshData.vertices.get(id));
128
132
  const screenVerts = verts.map((v) => toScreen(v.position));
129
133
  // Fill
130
- ctx.fillStyle = face.selected ? 'rgba(255, 170, 0, 0.3)' : 'rgba(100, 100, 100, 0.1)';
134
+ ctx.fillStyle = face.selected ? faceSelectedFill : faceFill;
131
135
  ctx.beginPath();
132
136
  ctx.moveTo(screenVerts[0].x, screenVerts[0].y);
133
137
  screenVerts.slice(1).forEach((sv) => ctx.lineTo(sv.x, sv.y));
@@ -528,33 +532,33 @@ const NiceUVEditor = ({ geometry, texture, channel = 'uv', onUVChange, onSelecti
528
532
  setMeshData(newData);
529
533
  onUVChange === null || onUVChange === void 0 ? void 0 : onUVChange(geometry);
530
534
  }, [meshData, geometry, state.activeChannel, readOnly, pushUndo, onUVChange]);
531
- const toolbarSlot = showToolbar ? (jsxs("div", { className: styles.toolbar, children: [jsx("div", { className: styles.toolGroup, children: UV_TOOLS.slice(0, 5).map((tool) => (jsx("button", { className: `${styles.toolBtn} ${state.activeTool === tool ? styles.active : ''}`, onClick: () => setState((prev) => ({ ...prev, activeTool: tool })), title: tool, disabled: readOnly, children: tool[0].toUpperCase() }, tool))) }), jsx("div", { className: styles.toolGroup, children: UV_SELECTION_MODES.map((mode) => (jsx("button", { className: `${styles.toolBtn} ${state.selectionMode === mode ? styles.active : ''}`, onClick: () => setState((prev) => ({ ...prev, selectionMode: mode })), title: mode, children: mode[0].toUpperCase() }, mode))) }), jsxs("div", { className: styles.toolGroup, children: [jsx("button", { className: styles.toolBtn, onClick: handlePack, disabled: readOnly || !meshData, title: "Pack Islands (P)", children: "Pack" }), jsx("button", { className: styles.toolBtn, onClick: handleRelax, disabled: readOnly || !meshData, title: "Relax UVs", children: "Relax" })] }), jsx("div", { className: styles.toolGroup, children: jsxs("select", { className: styles.select, onChange: (e) => handleProject(e.target.value), disabled: readOnly || !meshData, defaultValue: "", children: [jsx("option", { value: "", disabled: true, children: "Project..." }), PROJECTION_TYPES.map((type) => (jsx("option", { value: type, children: type }, type)))] }) }), jsx("div", { className: styles.toolGroup, children: jsx("select", { className: styles.select, value: state.activeChannel, onChange: (e) => setState((prev) => ({
535
+ const toolbarSlot = showToolbar ? (jsxs("div", { className: styles.toolbar, children: [jsx("div", { className: styles.toolGroup, children: UV_TOOLS.slice(0, 5).map((tool) => (jsx("button", { className: `${styles.toolBtn} ${state.activeTool === tool ? styles.active : ''}`, onClick: () => setState((prev) => ({ ...prev, activeTool: tool })), title: tool, disabled: readOnly, children: tool[0].toUpperCase() }, tool))) }), jsx("div", { className: styles.toolGroup, children: UV_SELECTION_MODES.map((mode) => (jsx("button", { className: `${styles.toolBtn} ${state.selectionMode === mode ? styles.active : ''}`, onClick: () => setState((prev) => ({ ...prev, selectionMode: mode })), title: mode, children: mode[0].toUpperCase() }, mode))) }), jsxs("div", { className: styles.toolGroup, children: [jsx("button", { className: styles.toolBtn, onClick: handlePack, disabled: readOnly || !meshData, title: t('uvEditor.packIslandsP', 'Pack Islands (P)'), children: t('uvEditor.pack', 'Pack') }), jsx("button", { className: styles.toolBtn, onClick: handleRelax, disabled: readOnly || !meshData, title: t('uvEditor.relaxUvs', 'Relax UVs'), children: t('uvEditor.relax', 'Relax') })] }), jsx("div", { className: styles.toolGroup, children: jsxs("select", { className: styles.select, onChange: (e) => handleProject(e.target.value), disabled: readOnly || !meshData, defaultValue: "", children: [jsx("option", { value: "", disabled: true, children: t('uvEditor.project', 'Project...') }), PROJECTION_TYPES.map((type) => (jsx("option", { value: type, children: type }, type)))] }) }), jsx("div", { className: styles.toolGroup, children: jsx("select", { className: styles.select, value: state.activeChannel, onChange: (e) => setState((prev) => ({
532
536
  ...prev,
533
537
  activeChannel: e.target.value,
534
- })), children: UV_CHANNELS.map((ch) => (jsx("option", { value: ch, children: ch.toUpperCase() }, ch))) }) }), jsxs("div", { className: styles.toolGroup, children: [jsx("button", { className: styles.toolBtn, onClick: undo, disabled: undoStack.length === 0, title: "Undo (Ctrl+Z)", "aria-label": "Undo", children: jsx(NiceIcon, { name: "arrow-left", size: 16 }) }), jsx("button", { className: styles.toolBtn, onClick: redo, disabled: redoStack.length === 0, title: "Redo (Ctrl+Y)", "aria-label": "Redo", children: jsx(NiceIcon, { name: "arrow-right", size: 16 }) })] })] })) : undefined;
535
- const sidebarSlot = showSidebar && meshData ? (jsxs("div", { className: styles.sidebar, children: [jsxs("div", { className: styles.panel, children: [jsx("h4", { className: styles.panelTitle, children: "Display" }), jsxs("label", { className: styles.checkbox, children: [jsx("input", { type: "checkbox", checked: state.display.showGrid, onChange: (e) => setState((prev) => ({
538
+ })), children: UV_CHANNELS.map((ch) => (jsx("option", { value: ch, children: ch.toUpperCase() }, ch))) }) }), jsxs("div", { className: styles.toolGroup, children: [jsx("button", { className: styles.toolBtn, onClick: undo, disabled: undoStack.length === 0, title: t('uvEditor.undoCtrlZ', 'Undo (Ctrl+Z)'), "aria-label": t('uvEditor.undo', 'Undo'), children: jsx(NiceIcon, { name: "arrow-left", size: 16 }) }), jsx("button", { className: styles.toolBtn, onClick: redo, disabled: redoStack.length === 0, title: t('uvEditor.redoCtrlY', 'Redo (Ctrl+Y)'), "aria-label": t('uvEditor.redo', 'Redo'), children: jsx(NiceIcon, { name: "arrow-right", size: 16 }) })] })] })) : undefined;
539
+ const sidebarSlot = showSidebar && meshData ? (jsxs("div", { className: styles.sidebar, children: [jsxs("div", { className: styles.panel, children: [jsx("h4", { className: styles.panelTitle, children: t('uvEditor.display', 'Display') }), jsxs("label", { className: styles.checkbox, children: [jsx("input", { type: "checkbox", checked: state.display.showGrid, onChange: (e) => setState((prev) => ({
536
540
  ...prev,
537
541
  display: { ...prev.display, showGrid: e.target.checked },
538
- })) }), "Show Grid"] }), jsxs("label", { className: styles.checkbox, children: [jsx("input", { type: "checkbox", checked: state.display.showTexture, onChange: (e) => setState((prev) => ({
542
+ })) }), t('uvEditor.showGrid', 'Show Grid')] }), jsxs("label", { className: styles.checkbox, children: [jsx("input", { type: "checkbox", checked: state.display.showTexture, onChange: (e) => setState((prev) => ({
539
543
  ...prev,
540
544
  display: { ...prev.display, showTexture: e.target.checked },
541
- })) }), "Show Texture"] }), jsxs("label", { className: styles.checkbox, children: [jsx("input", { type: "checkbox", checked: state.display.showSeams, onChange: (e) => setState((prev) => ({
545
+ })) }), t('uvEditor.showTexture', 'Show Texture')] }), jsxs("label", { className: styles.checkbox, children: [jsx("input", { type: "checkbox", checked: state.display.showSeams, onChange: (e) => setState((prev) => ({
542
546
  ...prev,
543
547
  display: { ...prev.display, showSeams: e.target.checked },
544
- })) }), "Show Seams"] }), jsxs("label", { className: styles.checkbox, children: [jsx("input", { type: "checkbox", checked: state.display.showDistortion, onChange: (e) => setState((prev) => ({
548
+ })) }), t('uvEditor.showSeams', 'Show Seams')] }), jsxs("label", { className: styles.checkbox, children: [jsx("input", { type: "checkbox", checked: state.display.showDistortion, onChange: (e) => setState((prev) => ({
545
549
  ...prev,
546
550
  display: { ...prev.display, showDistortion: e.target.checked },
547
- })) }), "Show Distortion"] }), jsxs("label", { className: styles.checkbox, children: [jsx("input", { type: "checkbox", checked: state.display.showIslandBounds, onChange: (e) => setState((prev) => ({
551
+ })) }), t('uvEditor.showDistortion', 'Show Distortion')] }), jsxs("label", { className: styles.checkbox, children: [jsx("input", { type: "checkbox", checked: state.display.showIslandBounds, onChange: (e) => setState((prev) => ({
548
552
  ...prev,
549
553
  display: { ...prev.display, showIslandBounds: e.target.checked },
550
- })) }), "Show Island Bounds"] })] }), jsxs("div", { className: styles.panel, children: [jsx("h4", { className: styles.panelTitle, children: "Statistics" }), jsxs("div", { className: styles.stat, children: [jsx("span", { children: "Vertices:" }), jsx("span", { children: meshData.vertices.size })] }), jsxs("div", { className: styles.stat, children: [jsx("span", { children: "Faces:" }), jsx("span", { children: meshData.faces.size })] }), jsxs("div", { className: styles.stat, children: [jsx("span", { children: "Islands:" }), jsx("span", { children: meshData.islands.size })] }), jsxs("div", { className: styles.stat, children: [jsx("span", { children: "Selected:" }), jsx("span", { children: getSelectedVertices(meshData).length })] })] }), jsxs("div", { className: styles.panel, children: [jsx("h4", { className: styles.panelTitle, children: "Snapping" }), jsxs("label", { className: styles.checkbox, children: [jsx("input", { type: "checkbox", checked: state.snapping, onChange: (e) => setState((prev) => ({
554
+ })) }), t('uvEditor.showIslandBounds', 'Show Island Bounds')] })] }), jsxs("div", { className: styles.panel, children: [jsx("h4", { className: styles.panelTitle, children: t('uvEditor.statistics', 'Statistics') }), jsxs("div", { className: styles.stat, children: [jsx("span", { children: "Vertices:" }), jsx("span", { children: meshData.vertices.size })] }), jsxs("div", { className: styles.stat, children: [jsx("span", { children: "Faces:" }), jsx("span", { children: meshData.faces.size })] }), jsxs("div", { className: styles.stat, children: [jsx("span", { children: "Islands:" }), jsx("span", { children: meshData.islands.size })] }), jsxs("div", { className: styles.stat, children: [jsx("span", { children: "Selected:" }), jsx("span", { children: getSelectedVertices(meshData).length })] })] }), jsxs("div", { className: styles.panel, children: [jsx("h4", { className: styles.panelTitle, children: t('uvEditor.snapping', 'Snapping') }), jsxs("label", { className: styles.checkbox, children: [jsx("input", { type: "checkbox", checked: state.snapping, onChange: (e) => setState((prev) => ({
551
555
  ...prev,
552
556
  snapping: e.target.checked,
553
- })) }), "Enable Snapping"] }), jsxs("label", { className: styles.rangeLabel, children: ["Snap Size: ", state.snapSize.toFixed(4), jsx("input", { type: "range", min: "0.001", max: "0.25", step: "0.001", value: state.snapSize, onChange: (e) => setState((prev) => ({
557
+ })) }), t('uvEditor.enableSnapping', 'Enable Snapping')] }), jsxs("label", { className: styles.rangeLabel, children: ["Snap Size: ", state.snapSize.toFixed(4), jsx("input", { type: "range", min: "0.001", max: "0.25", step: "0.001", value: state.snapSize, onChange: (e) => setState((prev) => ({
554
558
  ...prev,
555
559
  snapSize: parseFloat(e.target.value),
556
560
  })) })] })] })] })) : undefined;
557
- return (jsxs("div", { style: { position: 'relative', width, height }, children: [jsx("span", { style: { position: 'absolute', top: 6, right: 6, zIndex: 30 }, children: jsx(NiceTutorialButton, { steps: resolveTutorial(tutorial, UV_EDITOR_TOUR) }) }), jsx(NiceAppShell, { className: `nice-uv-editor ${styles.uvEditor} ${className !== null && className !== void 0 ? className : ''}`, flush: true, toolbar: toolbarSlot, rightSidebar: sidebarSlot, statusBar: jsxs("div", { className: styles.statusBar, children: [jsxs("span", { children: ["Tool: ", state.activeTool] }), jsxs("span", { children: ["Mode: ", state.selectionMode] }), jsxs("span", { children: ["Zoom: ", (state.zoom * 100).toFixed(0), "%"] }), jsxs("span", { children: ["Channel: ", state.activeChannel] })] }), children: jsxs("div", { ref: containerRef, className: styles.viewport, onMouseDown: handleMouseDown, onMouseMove: handleMouseMove, onMouseUp: handleMouseUp, onMouseLeave: handleMouseUp, onWheel: handleWheel, children: [jsx("canvas", { ref: canvasRef, className: styles.canvas }), !meshData && jsx("div", { className: styles.noGeometry, children: "No geometry selected" })] }) })] }));
561
+ return (jsxs("div", { style: { position: 'relative', width, height }, children: [jsx("span", { style: { position: 'absolute', top: 6, right: 6, zIndex: 30 }, children: jsx(NiceTutorialButton, { steps: resolveTutorial(tutorial, UV_EDITOR_TOUR) }) }), jsx(NiceAppShell, { className: `nice-uv-editor ${styles.uvEditor} ${className !== null && className !== void 0 ? className : ''}`, flush: true, toolbar: toolbarSlot, rightSidebar: sidebarSlot, statusBar: jsxs("div", { className: styles.statusBar, children: [jsxs("span", { children: ["Tool: ", state.activeTool] }), jsxs("span", { children: ["Mode: ", state.selectionMode] }), jsxs("span", { children: ["Zoom: ", (state.zoom * 100).toFixed(0), "%"] }), jsxs("span", { children: ["Channel: ", state.activeChannel] })] }), children: jsxs("div", { ref: containerRef, className: styles.viewport, onMouseDown: handleMouseDown, onMouseMove: handleMouseMove, onMouseUp: handleMouseUp, onMouseLeave: handleMouseUp, onWheel: handleWheel, children: [jsx("canvas", { ref: canvasRef, className: styles.canvas }), !meshData && (jsx("div", { className: styles.noGeometry, children: t('uvEditor.noGeometrySelected', 'No geometry selected') }))] }) })] }));
558
562
  };
559
563
 
560
564
  export { NiceUVEditor };
@@ -48,6 +48,7 @@ const DEFAULT_UV_DISPLAY = {
48
48
  showDistortion: false,
49
49
  distortionGradient: { low: 'var(--nice-success, #00ff00)', high: 'var(--nice-danger, #ff0000)' },
50
50
  showSeams: true,
51
+ // audit-allow: literal-color — the seam colour setting's default
51
52
  seamColor: '#ff6600',
52
53
  highlightSelected: true,
53
54
  selectionColor: 'var(--nice-warning, #ffaa00)',
@@ -0,0 +1,26 @@
1
+ import React from 'react';
2
+ /** A question an editor hook waits on; {@link EditorAskDialog} shows it. */
3
+ export interface EditorAsk {
4
+ /** `prompt` asks for a value, `confirm` for a yes / no. */
5
+ kind: 'prompt' | 'confirm';
6
+ /** The prompt's title, or the question to confirm. */
7
+ text: string;
8
+ /** Initial value of a prompt. */
9
+ defaultValue?: string;
10
+ /** Settles it: the entered value (`''` for an accepted confirm), or `null` when cancelled. */
11
+ answer: (value: string | null) => void;
12
+ }
13
+ /**
14
+ * The library's dialogs in place of `window.prompt` / `window.confirm` for the editor hooks,
15
+ * which hold state but render nothing: `ask` resolves with the answer (`null` when cancelled)
16
+ * and `pendingAsk` is the open question, for the editor component to pass to
17
+ * {@link EditorAskDialog}. One question at a time: a new one settles the open one as cancelled.
18
+ */
19
+ export declare function useEditorAsk(): {
20
+ pendingAsk: EditorAsk | null;
21
+ ask: (kind: EditorAsk["kind"], text: string, defaultValue?: string) => Promise<string | null>;
22
+ };
23
+ /** Shows the question {@link useEditorAsk} is waiting on, if any. */
24
+ export declare const EditorAskDialog: React.FC<{
25
+ ask: EditorAsk | null;
26
+ }>;
@@ -1,26 +1,10 @@
1
- /**
2
- * i18n — Lightweight translation context for @nice2dev/ui-3d.
3
- *
4
- * Drop-in replacement for react-i18next's useTranslation, requires NO
5
- * external dependencies. Consumers can optionally wrap with NiceI18nProvider
6
- * to supply their own translation function. Falls back to the default
7
- * (English key strings) when no provider is present.
8
- *
9
- * Usage in components:
10
- * const { t } = useNiceTranslation();
11
- * <button>{t('actions.save', 'Save')}</button>
12
- *
13
- * Usage in apps:
14
- * <NiceI18nProvider t={(key, fallback) => myI18n(key) ?? fallback}>
15
- * <App />
16
- * </NiceI18nProvider>
17
- */
18
1
  import React from 'react';
19
2
  export type NiceTranslateFn = (key: string, defaultValue: string) => string;
20
3
  export interface NiceI18nProviderProps {
21
4
  t: NiceTranslateFn;
22
5
  children: React.ReactNode;
23
6
  }
7
+ /** Overrides the app's global translator for the 3D controls below it. */
24
8
  export declare const NiceI18nProvider: React.FC<NiceI18nProviderProps>;
25
9
  export declare function useNiceTranslation(): {
26
10
  t: NiceTranslateFn;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * three.js r183 deprecated `Clock` (for `Timer`) and folds `PCFSoftShadowMap` into
3
+ * `PCFShadowMap`; both log a console warning there. The peer range starts at r150, which has
4
+ * neither `Timer` in its core nor the softer `PCFShadowMap`, so the choice follows the revision.
5
+ */
6
+ import * as THREE from 'three';
7
+ /** Soft shadows without the r183 deprecation warning; older three keeps `PCFSoftShadowMap`. */
8
+ export declare const SOFT_SHADOW_MAP: THREE.ShadowMapType;
9
+ /** Seconds since the previous call — what an animation loop feeds its mixer. */
10
+ export interface FrameClock {
11
+ getDelta(): number;
12
+ }
13
+ /** `Timer` where three has deprecated `Clock`, `Clock` before. */
14
+ export declare function createFrameClock(): FrameClock;
@@ -1,9 +1,3 @@
1
- /**
2
- * DanceScoreEngine — Compares detected poses against reference choreography
3
- *
4
- * Calculates real-time dance accuracy by comparing camera-detected Pose3DFrames
5
- * against a reference sequence (from choreography DSL or recorded poses).
6
- */
7
1
  import type { Pose3DFrame, Pose3DSequenceResult } from '../model/modelEditorTypes';
8
2
  export interface DanceScoreConfig {
9
3
  /** Weight per keypoint category (0–1). Keys not listed default to 1. */
@@ -1,8 +1,17 @@
1
1
  import React from 'react';
2
- import type { Pose3DFrame } from '../model/modelEditorTypes';
2
+ import type { PosePerson3D, Pose3DFrame } from '../model/modelEditorTypes';
3
3
  export type PoseBackend = 'mediapipe' | 'tfjs';
4
+ /**
5
+ * Finds the people in one video frame; the model is yours — TF.js pose-detection (MoveNet,
6
+ * BlazePose), MediaPipe Pose Landmarker… Return keypoints in video pixels, named as in COCO-17
7
+ * (`nose`, `left_shoulder`, `right_ankle`, …): the overlay connects them by name. It may be
8
+ * async; the next detection waits for it.
9
+ */
10
+ export type NicePoseDetectorFn = (video: HTMLVideoElement, timestampMs: number) => PosePerson3D[] | Promise<PosePerson3D[]>;
4
11
  export interface NicePoseDetectorConfig {
5
- /** Detection backend */
12
+ /** Finds the poses in each frame. Without it the camera runs, but nothing is detected. */
13
+ detector?: NicePoseDetectorFn;
14
+ /** Which backend your `detector` uses — informational; the component loads no model. */
6
15
  backend?: PoseBackend;
7
16
  /** Maximum number of persons to track */
8
17
  maxPersons?: number;
@@ -17,8 +26,6 @@ export interface NicePoseDetectorConfig {
17
26
  /** Show confidence dots on keypoints */
18
27
  showConfidence?: boolean;
19
28
  }
20
- /** @deprecated Use NicePoseDetectorConfig */
21
- export type PoseDetectorConfig = NicePoseDetectorConfig;
22
29
  export interface NicePoseDetectorHandle {
23
30
  /** Start detection */
24
31
  start(): void;
@@ -29,8 +36,6 @@ export interface NicePoseDetectorHandle {
29
36
  /** Check if actively detecting */
30
37
  readonly isDetecting: boolean;
31
38
  }
32
- /** @deprecated Use NicePoseDetectorHandle */
33
- export type PoseDetectorHandle = NicePoseDetectorHandle;
34
39
  export interface NicePoseDetectorProps extends NicePoseDetectorConfig {
35
40
  /** Called each frame with detected poses */
36
41
  onFrame?: (frame: Pose3DFrame) => void;
@@ -38,7 +43,7 @@ export interface NicePoseDetectorProps extends NicePoseDetectorConfig {
38
43
  onStart?: () => void;
39
44
  /** Called when detection stops */
40
45
  onStop?: () => void;
41
- /** Called on error */
46
+ /** Called when the camera cannot start or the detector fails (detection then stops) */
42
47
  onError?: (error: Error) => void;
43
48
  /** CSS class for the container */
44
49
  className?: string;
@@ -49,14 +54,10 @@ export interface NicePoseDetectorProps extends NicePoseDetectorConfig {
49
54
  /** Height of the video feed */
50
55
  height?: number;
51
56
  }
52
- /** @deprecated Use NicePoseDetectorProps */
53
- export type PoseDetectorProps = NicePoseDetectorProps;
54
- export declare function usePoseDetection(config?: PoseDetectorConfig): {
55
- start: (video: HTMLVideoElement, onFrame?: (frame: Pose3DFrame) => void) => Promise<void>;
57
+ export declare function usePoseDetection(config?: NicePoseDetectorConfig): {
58
+ start: (video: HTMLVideoElement, onFrame?: (frame: Pose3DFrame) => void, onError?: (error: Error) => void) => Promise<void>;
56
59
  stop: () => void;
57
60
  isDetecting: boolean;
58
61
  getLatestFrame: () => Pose3DFrame | null;
59
62
  };
60
63
  export declare const NicePoseDetector: React.ForwardRefExoticComponent<NicePoseDetectorProps & React.RefAttributes<NicePoseDetectorHandle>>;
61
- /** @deprecated Use NicePoseDetector */
62
- export declare const PoseDetector: React.ForwardRefExoticComponent<NicePoseDetectorProps & React.RefAttributes<NicePoseDetectorHandle>>;