@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
@@ -4,6 +4,7 @@ var jsxRuntime = require('react/jsx-runtime');
4
4
  var ui = require('@nice2dev/ui');
5
5
  var React = require('react');
6
6
  var THREE = require('three');
7
+ var i18n = require('../core/i18n.js');
7
8
 
8
9
  function _interopNamespaceDefault(e) {
9
10
  var n = Object.create(null);
@@ -274,8 +275,9 @@ function useSSR(renderer, scene, camera, configOverride) {
274
275
  }
275
276
  /* ── Component ── */
276
277
  const NiceSSR = ({ renderer, scene, camera, config: configProp, className = '', }) => {
278
+ const { t } = i18n.useNiceTranslation();
277
279
  const { config, setConfig } = useSSR(renderer, scene, camera, configProp);
278
- return (jsxRuntime.jsxs("div", { className: `nice-ssr ${className}`, style: { display: 'grid', gap: 8, fontSize: 13 }, children: [jsxRuntime.jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' }, children: [jsxRuntime.jsxs("strong", { children: [jsxRuntime.jsx(ui.NiceIcon, { name: "copy", size: 14 }), " Screen-Space Reflections"] }), jsxRuntime.jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 4, fontSize: 11 }, children: [jsxRuntime.jsx("input", { type: "checkbox", checked: config.enabled, onChange: (e) => setConfig({ enabled: e.target.checked }) }), "Enabled"] })] }), jsxRuntime.jsxs("label", { children: ["Max Steps: ", config.maxSteps, jsxRuntime.jsx("input", { type: "range", min: 16, max: 256, step: 16, value: config.maxSteps, onChange: (e) => setConfig({ maxSteps: parseInt(e.target.value, 10) }), style: { width: '100%' } })] }), jsxRuntime.jsxs("label", { children: ["Max Distance: ", config.maxDistance, jsxRuntime.jsx("input", { type: "range", min: 5, max: 200, step: 5, value: config.maxDistance, onChange: (e) => setConfig({ maxDistance: parseInt(e.target.value, 10) }), style: { width: '100%' } })] }), jsxRuntime.jsxs("label", { children: ["Intensity: ", config.intensity.toFixed(2), jsxRuntime.jsx("input", { type: "range", min: 0, max: 1, step: 0.05, value: config.intensity, onChange: (e) => setConfig({ intensity: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsxRuntime.jsxs("label", { children: ["Thickness: ", config.thickness.toFixed(2), jsxRuntime.jsx("input", { type: "range", min: 0.05, max: 2, step: 0.05, value: config.thickness, onChange: (e) => setConfig({ thickness: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsxRuntime.jsxs("label", { children: ["Jitter: ", config.jitter.toFixed(2), jsxRuntime.jsx("input", { type: "range", min: 0, max: 1, step: 0.05, value: config.jitter, onChange: (e) => setConfig({ jitter: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsxRuntime.jsxs("label", { children: ["Resolution Scale: ", config.resolution.toFixed(2), jsxRuntime.jsx("input", { type: "range", min: 0.25, max: 1, step: 0.25, value: config.resolution, onChange: (e) => setConfig({ resolution: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsxRuntime.jsxs("label", { children: ["Fresnel: ", config.fresnelExponent.toFixed(1), jsxRuntime.jsx("input", { type: "range", min: 1, max: 8, step: 0.5, value: config.fresnelExponent, onChange: (e) => setConfig({ fresnelExponent: parseFloat(e.target.value) }), style: { width: '100%' } })] })] }));
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%' } })] })] }));
279
281
  };
280
282
 
281
283
  exports.NiceSSR = NiceSSR;
@@ -4,6 +4,8 @@ var jsxRuntime = require('react/jsx-runtime');
4
4
  var ui = require('@nice2dev/ui');
5
5
  var React = require('react');
6
6
  var THREE = require('three');
7
+ var i18n = require('../core/i18n.js');
8
+ var threeCompat = require('../core/threeCompat.js');
7
9
 
8
10
  function _interopNamespaceDefault(e) {
9
11
  var n = Object.create(null);
@@ -137,7 +139,7 @@ function useWebGPURenderer(canvas, scene, camera, width = 800, height = 600, onR
137
139
  r.toneMappingExposure = toneMappingExposure;
138
140
  r.outputColorSpace = outputColorSpace;
139
141
  r.shadowMap.enabled = true;
140
- r.shadowMap.type = THREE__namespace.PCFSoftShadowMap;
142
+ r.shadowMap.type = threeCompat.SOFT_SHADOW_MAP;
141
143
  rendererRef.current = r;
142
144
  setRenderer(r);
143
145
  setIsWebGPU(usingWebGPU);
@@ -229,14 +231,15 @@ const TONE_MAPPINGS = [
229
231
  ];
230
232
  /* ── Component ── */
231
233
  const NiceWebGPURenderer = ({ canvas, scene, camera, width, height, onRendererReady, className = '', }) => {
234
+ const { t } = i18n.useNiceTranslation();
232
235
  const { isWebGPU, capabilities, isReady, toneMapping, setToneMapping, toneMappingExposure, setToneMappingExposure, outputColorSpace, setOutputColorSpace, pixelRatio, setPixelRatio, } = useWebGPURenderer(canvas, scene, camera, width, height, onRendererReady);
233
- return (jsxRuntime.jsxs("div", { className: `nice-webgpu-renderer ${className}`, style: { display: 'grid', gap: 8, fontSize: 13 }, children: [jsxRuntime.jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' }, children: [jsxRuntime.jsxs("strong", { children: [jsxRuntime.jsx(ui.NiceIcon, { name: "monitor", size: 14 }), " Renderer"] }), jsxRuntime.jsx("span", { style: {
236
+ return (jsxRuntime.jsxs("div", { className: `nice-webgpu-renderer ${className}`, style: { display: 'grid', gap: 8, fontSize: 13 }, children: [jsxRuntime.jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' }, children: [jsxRuntime.jsxs("strong", { children: [jsxRuntime.jsx(ui.NiceIcon, { name: "monitor", size: 14 }), " ", t('webGPURenderer.renderer', 'Renderer')] }), jsxRuntime.jsx("span", { style: {
234
237
  fontSize: 10,
235
238
  padding: '2px 6px',
236
239
  borderRadius: 4,
237
240
  background: isWebGPU ? 'var(--nice-success, #22c55e)' : 'var(--nice-warning, #f59e0b)',
238
- color: 'var(--nice-text-on-primary, #fff)',
239
- }, children: isWebGPU ? 'WebGPU' : 'WebGL' })] }), !isReady && jsxRuntime.jsx("div", { style: { opacity: 0.6, fontSize: 11 }, children: "Initializing\u2026" }), capabilities && (jsxRuntime.jsxs("div", { style: { fontSize: 10, opacity: 0.7, lineHeight: 1.5 }, children: ["GPU: ", capabilities.adapterName, jsxRuntime.jsx("br", {}), "Vendor: ", capabilities.vendor, jsxRuntime.jsx("br", {}), "Max Texture: ", capabilities.maxTextureDimension2D, "px", capabilities.features.length > 0 && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("br", {}), "Features: ", capabilities.features.length] }))] })), jsxRuntime.jsxs("label", { children: ["Tone Mapping:", jsxRuntime.jsx("select", { value: toneMapping, onChange: (e) => setToneMapping(parseInt(e.target.value, 10)), style: { width: '100%', marginTop: 2 }, children: TONE_MAPPINGS.map((tm) => (jsxRuntime.jsx("option", { value: tm.value, children: tm.label }, tm.value))) })] }), jsxRuntime.jsxs("label", { children: ["Exposure: ", toneMappingExposure.toFixed(2), jsxRuntime.jsx("input", { type: "range", min: 0.1, max: 3, step: 0.05, value: toneMappingExposure, onChange: (e) => setToneMappingExposure(parseFloat(e.target.value)), style: { width: '100%' } })] }), jsxRuntime.jsxs("label", { children: ["Output Color Space:", jsxRuntime.jsxs("select", { value: outputColorSpace, onChange: (e) => setOutputColorSpace(e.target.value), style: { width: '100%', marginTop: 2 }, children: [jsxRuntime.jsx("option", { value: THREE__namespace.SRGBColorSpace, children: "sRGB" }), jsxRuntime.jsx("option", { value: THREE__namespace.LinearSRGBColorSpace, children: "Linear sRGB" })] })] }), jsxRuntime.jsxs("label", { children: ["Pixel Ratio: ", pixelRatio.toFixed(1), jsxRuntime.jsx("input", { type: "range", min: 0.5, max: 3, step: 0.5, value: pixelRatio, onChange: (e) => setPixelRatio(parseFloat(e.target.value)), style: { width: '100%' } })] })] }));
241
+ 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%' } })] })] }));
240
243
  };
241
244
 
242
245
  exports.NiceWebGPURenderer = NiceWebGPURenderer;
@@ -3,6 +3,7 @@
3
3
  var jsxRuntime = require('react/jsx-runtime');
4
4
  var ui = require('@nice2dev/ui');
5
5
  var React = require('react');
6
+ var i18n = require('../core/i18n.js');
6
7
  var UVEditor_module = require('./UVEditor.module.css.js');
7
8
  var uvEditorTypes = require('./uvEditorTypes.js');
8
9
  var uvEditorUtils = require('./uvEditorUtils.js');
@@ -22,6 +23,7 @@ const UV_EDITOR_TOUR = [
22
23
  Component
23
24
  ═══════════════════════════════════════════ */
24
25
  const NiceUVEditor = ({ geometry, texture, channel = 'uv', onUVChange, onSelectionChange, width = '100%', height = 600, showToolbar = true, showSidebar = true, className, readOnly = false, tutorial, }) => {
26
+ const { t } = i18n.useNiceTranslation();
25
27
  const canvasRef = React.useRef(null);
26
28
  const containerRef = React.useRef(null);
27
29
  // State
@@ -124,12 +126,14 @@ const NiceUVEditor = ({ geometry, texture, channel = 'uv', onUVChange, onSelecti
124
126
  ctx.closePath();
125
127
  ctx.stroke();
126
128
  }
127
- // Draw faces
129
+ // Draw faces — the two fills resolved once per frame, not once per face
130
+ const faceSelectedFill = ui.resolveCssColor('var(--nice-warning-tint-30, rgba(255, 170, 0, 0.3))', ctx.canvas);
131
+ const faceFill = ui.resolveCssColor('var(--nice-overlay-10, rgba(100, 100, 100, 0.1))', ctx.canvas);
128
132
  for (const face of meshData.faces.values()) {
129
133
  const verts = face.vertices.map((id) => meshData.vertices.get(id));
130
134
  const screenVerts = verts.map((v) => toScreen(v.position));
131
135
  // Fill
132
- ctx.fillStyle = face.selected ? 'rgba(255, 170, 0, 0.3)' : 'rgba(100, 100, 100, 0.1)';
136
+ ctx.fillStyle = face.selected ? faceSelectedFill : faceFill;
133
137
  ctx.beginPath();
134
138
  ctx.moveTo(screenVerts[0].x, screenVerts[0].y);
135
139
  screenVerts.slice(1).forEach((sv) => ctx.lineTo(sv.x, sv.y));
@@ -530,33 +534,33 @@ const NiceUVEditor = ({ geometry, texture, channel = 'uv', onUVChange, onSelecti
530
534
  setMeshData(newData);
531
535
  onUVChange === null || onUVChange === void 0 ? void 0 : onUVChange(geometry);
532
536
  }, [meshData, geometry, state.activeChannel, readOnly, pushUndo, onUVChange]);
533
- const toolbarSlot = showToolbar ? (jsxRuntime.jsxs("div", { className: UVEditor_module.toolbar, children: [jsxRuntime.jsx("div", { className: UVEditor_module.toolGroup, children: uvEditorTypes.UV_TOOLS.slice(0, 5).map((tool) => (jsxRuntime.jsx("button", { className: `${UVEditor_module.toolBtn} ${state.activeTool === tool ? UVEditor_module.active : ''}`, onClick: () => setState((prev) => ({ ...prev, activeTool: tool })), title: tool, disabled: readOnly, children: tool[0].toUpperCase() }, tool))) }), jsxRuntime.jsx("div", { className: UVEditor_module.toolGroup, children: uvEditorTypes.UV_SELECTION_MODES.map((mode) => (jsxRuntime.jsx("button", { className: `${UVEditor_module.toolBtn} ${state.selectionMode === mode ? UVEditor_module.active : ''}`, onClick: () => setState((prev) => ({ ...prev, selectionMode: mode })), title: mode, children: mode[0].toUpperCase() }, mode))) }), jsxRuntime.jsxs("div", { className: UVEditor_module.toolGroup, children: [jsxRuntime.jsx("button", { className: UVEditor_module.toolBtn, onClick: handlePack, disabled: readOnly || !meshData, title: "Pack Islands (P)", children: "Pack" }), jsxRuntime.jsx("button", { className: UVEditor_module.toolBtn, onClick: handleRelax, disabled: readOnly || !meshData, title: "Relax UVs", children: "Relax" })] }), jsxRuntime.jsx("div", { className: UVEditor_module.toolGroup, children: jsxRuntime.jsxs("select", { className: UVEditor_module.select, onChange: (e) => handleProject(e.target.value), disabled: readOnly || !meshData, defaultValue: "", children: [jsxRuntime.jsx("option", { value: "", disabled: true, children: "Project..." }), uvEditorTypes.PROJECTION_TYPES.map((type) => (jsxRuntime.jsx("option", { value: type, children: type }, type)))] }) }), jsxRuntime.jsx("div", { className: UVEditor_module.toolGroup, children: jsxRuntime.jsx("select", { className: UVEditor_module.select, value: state.activeChannel, onChange: (e) => setState((prev) => ({
537
+ const toolbarSlot = showToolbar ? (jsxRuntime.jsxs("div", { className: UVEditor_module.toolbar, children: [jsxRuntime.jsx("div", { className: UVEditor_module.toolGroup, children: uvEditorTypes.UV_TOOLS.slice(0, 5).map((tool) => (jsxRuntime.jsx("button", { className: `${UVEditor_module.toolBtn} ${state.activeTool === tool ? UVEditor_module.active : ''}`, onClick: () => setState((prev) => ({ ...prev, activeTool: tool })), title: tool, disabled: readOnly, children: tool[0].toUpperCase() }, tool))) }), jsxRuntime.jsx("div", { className: UVEditor_module.toolGroup, children: uvEditorTypes.UV_SELECTION_MODES.map((mode) => (jsxRuntime.jsx("button", { className: `${UVEditor_module.toolBtn} ${state.selectionMode === mode ? UVEditor_module.active : ''}`, onClick: () => setState((prev) => ({ ...prev, selectionMode: mode })), title: mode, children: mode[0].toUpperCase() }, mode))) }), jsxRuntime.jsxs("div", { className: UVEditor_module.toolGroup, children: [jsxRuntime.jsx("button", { className: UVEditor_module.toolBtn, onClick: handlePack, disabled: readOnly || !meshData, title: t('uvEditor.packIslandsP', 'Pack Islands (P)'), children: t('uvEditor.pack', 'Pack') }), jsxRuntime.jsx("button", { className: UVEditor_module.toolBtn, onClick: handleRelax, disabled: readOnly || !meshData, title: t('uvEditor.relaxUvs', 'Relax UVs'), children: t('uvEditor.relax', 'Relax') })] }), jsxRuntime.jsx("div", { className: UVEditor_module.toolGroup, children: jsxRuntime.jsxs("select", { className: UVEditor_module.select, onChange: (e) => handleProject(e.target.value), disabled: readOnly || !meshData, defaultValue: "", children: [jsxRuntime.jsx("option", { value: "", disabled: true, children: t('uvEditor.project', 'Project...') }), uvEditorTypes.PROJECTION_TYPES.map((type) => (jsxRuntime.jsx("option", { value: type, children: type }, type)))] }) }), jsxRuntime.jsx("div", { className: UVEditor_module.toolGroup, children: jsxRuntime.jsx("select", { className: UVEditor_module.select, value: state.activeChannel, onChange: (e) => setState((prev) => ({
534
538
  ...prev,
535
539
  activeChannel: e.target.value,
536
- })), children: uvEditorTypes.UV_CHANNELS.map((ch) => (jsxRuntime.jsx("option", { value: ch, children: ch.toUpperCase() }, ch))) }) }), jsxRuntime.jsxs("div", { className: UVEditor_module.toolGroup, children: [jsxRuntime.jsx("button", { className: UVEditor_module.toolBtn, onClick: undo, disabled: undoStack.length === 0, title: "Undo (Ctrl+Z)", "aria-label": "Undo", children: jsxRuntime.jsx(ui.NiceIcon, { name: "arrow-left", size: 16 }) }), jsxRuntime.jsx("button", { className: UVEditor_module.toolBtn, onClick: redo, disabled: redoStack.length === 0, title: "Redo (Ctrl+Y)", "aria-label": "Redo", children: jsxRuntime.jsx(ui.NiceIcon, { name: "arrow-right", size: 16 }) })] })] })) : undefined;
537
- const sidebarSlot = showSidebar && meshData ? (jsxRuntime.jsxs("div", { className: UVEditor_module.sidebar, children: [jsxRuntime.jsxs("div", { className: UVEditor_module.panel, children: [jsxRuntime.jsx("h4", { className: UVEditor_module.panelTitle, children: "Display" }), jsxRuntime.jsxs("label", { className: UVEditor_module.checkbox, children: [jsxRuntime.jsx("input", { type: "checkbox", checked: state.display.showGrid, onChange: (e) => setState((prev) => ({
540
+ })), children: uvEditorTypes.UV_CHANNELS.map((ch) => (jsxRuntime.jsx("option", { value: ch, children: ch.toUpperCase() }, ch))) }) }), jsxRuntime.jsxs("div", { className: UVEditor_module.toolGroup, children: [jsxRuntime.jsx("button", { className: UVEditor_module.toolBtn, onClick: undo, disabled: undoStack.length === 0, title: t('uvEditor.undoCtrlZ', 'Undo (Ctrl+Z)'), "aria-label": t('uvEditor.undo', 'Undo'), children: jsxRuntime.jsx(ui.NiceIcon, { name: "arrow-left", size: 16 }) }), jsxRuntime.jsx("button", { className: UVEditor_module.toolBtn, onClick: redo, disabled: redoStack.length === 0, title: t('uvEditor.redoCtrlY', 'Redo (Ctrl+Y)'), "aria-label": t('uvEditor.redo', 'Redo'), children: jsxRuntime.jsx(ui.NiceIcon, { name: "arrow-right", size: 16 }) })] })] })) : undefined;
541
+ const sidebarSlot = showSidebar && meshData ? (jsxRuntime.jsxs("div", { className: UVEditor_module.sidebar, children: [jsxRuntime.jsxs("div", { className: UVEditor_module.panel, children: [jsxRuntime.jsx("h4", { className: UVEditor_module.panelTitle, children: t('uvEditor.display', 'Display') }), jsxRuntime.jsxs("label", { className: UVEditor_module.checkbox, children: [jsxRuntime.jsx("input", { type: "checkbox", checked: state.display.showGrid, onChange: (e) => setState((prev) => ({
538
542
  ...prev,
539
543
  display: { ...prev.display, showGrid: e.target.checked },
540
- })) }), "Show Grid"] }), jsxRuntime.jsxs("label", { className: UVEditor_module.checkbox, children: [jsxRuntime.jsx("input", { type: "checkbox", checked: state.display.showTexture, onChange: (e) => setState((prev) => ({
544
+ })) }), t('uvEditor.showGrid', 'Show Grid')] }), jsxRuntime.jsxs("label", { className: UVEditor_module.checkbox, children: [jsxRuntime.jsx("input", { type: "checkbox", checked: state.display.showTexture, onChange: (e) => setState((prev) => ({
541
545
  ...prev,
542
546
  display: { ...prev.display, showTexture: e.target.checked },
543
- })) }), "Show Texture"] }), jsxRuntime.jsxs("label", { className: UVEditor_module.checkbox, children: [jsxRuntime.jsx("input", { type: "checkbox", checked: state.display.showSeams, onChange: (e) => setState((prev) => ({
547
+ })) }), t('uvEditor.showTexture', 'Show Texture')] }), jsxRuntime.jsxs("label", { className: UVEditor_module.checkbox, children: [jsxRuntime.jsx("input", { type: "checkbox", checked: state.display.showSeams, onChange: (e) => setState((prev) => ({
544
548
  ...prev,
545
549
  display: { ...prev.display, showSeams: e.target.checked },
546
- })) }), "Show Seams"] }), jsxRuntime.jsxs("label", { className: UVEditor_module.checkbox, children: [jsxRuntime.jsx("input", { type: "checkbox", checked: state.display.showDistortion, onChange: (e) => setState((prev) => ({
550
+ })) }), t('uvEditor.showSeams', 'Show Seams')] }), jsxRuntime.jsxs("label", { className: UVEditor_module.checkbox, children: [jsxRuntime.jsx("input", { type: "checkbox", checked: state.display.showDistortion, onChange: (e) => setState((prev) => ({
547
551
  ...prev,
548
552
  display: { ...prev.display, showDistortion: e.target.checked },
549
- })) }), "Show Distortion"] }), jsxRuntime.jsxs("label", { className: UVEditor_module.checkbox, children: [jsxRuntime.jsx("input", { type: "checkbox", checked: state.display.showIslandBounds, onChange: (e) => setState((prev) => ({
553
+ })) }), t('uvEditor.showDistortion', 'Show Distortion')] }), jsxRuntime.jsxs("label", { className: UVEditor_module.checkbox, children: [jsxRuntime.jsx("input", { type: "checkbox", checked: state.display.showIslandBounds, onChange: (e) => setState((prev) => ({
550
554
  ...prev,
551
555
  display: { ...prev.display, showIslandBounds: e.target.checked },
552
- })) }), "Show Island Bounds"] })] }), jsxRuntime.jsxs("div", { className: UVEditor_module.panel, children: [jsxRuntime.jsx("h4", { className: UVEditor_module.panelTitle, children: "Statistics" }), jsxRuntime.jsxs("div", { className: UVEditor_module.stat, children: [jsxRuntime.jsx("span", { children: "Vertices:" }), jsxRuntime.jsx("span", { children: meshData.vertices.size })] }), jsxRuntime.jsxs("div", { className: UVEditor_module.stat, children: [jsxRuntime.jsx("span", { children: "Faces:" }), jsxRuntime.jsx("span", { children: meshData.faces.size })] }), jsxRuntime.jsxs("div", { className: UVEditor_module.stat, children: [jsxRuntime.jsx("span", { children: "Islands:" }), jsxRuntime.jsx("span", { children: meshData.islands.size })] }), jsxRuntime.jsxs("div", { className: UVEditor_module.stat, children: [jsxRuntime.jsx("span", { children: "Selected:" }), jsxRuntime.jsx("span", { children: uvEditorUtils.getSelectedVertices(meshData).length })] })] }), jsxRuntime.jsxs("div", { className: UVEditor_module.panel, children: [jsxRuntime.jsx("h4", { className: UVEditor_module.panelTitle, children: "Snapping" }), jsxRuntime.jsxs("label", { className: UVEditor_module.checkbox, children: [jsxRuntime.jsx("input", { type: "checkbox", checked: state.snapping, onChange: (e) => setState((prev) => ({
556
+ })) }), t('uvEditor.showIslandBounds', 'Show Island Bounds')] })] }), jsxRuntime.jsxs("div", { className: UVEditor_module.panel, children: [jsxRuntime.jsx("h4", { className: UVEditor_module.panelTitle, children: t('uvEditor.statistics', 'Statistics') }), jsxRuntime.jsxs("div", { className: UVEditor_module.stat, children: [jsxRuntime.jsx("span", { children: "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) => ({
553
557
  ...prev,
554
558
  snapping: e.target.checked,
555
- })) }), "Enable Snapping"] }), jsxRuntime.jsxs("label", { className: UVEditor_module.rangeLabel, children: ["Snap Size: ", state.snapSize.toFixed(4), jsxRuntime.jsx("input", { type: "range", min: "0.001", max: "0.25", step: "0.001", value: state.snapSize, onChange: (e) => setState((prev) => ({
559
+ })) }), t('uvEditor.enableSnapping', 'Enable Snapping')] }), jsxRuntime.jsxs("label", { className: UVEditor_module.rangeLabel, children: ["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) => ({
556
560
  ...prev,
557
561
  snapSize: parseFloat(e.target.value),
558
562
  })) })] })] })] })) : undefined;
559
- return (jsxRuntime.jsxs("div", { style: { position: 'relative', width, height }, children: [jsxRuntime.jsx("span", { style: { position: 'absolute', top: 6, right: 6, zIndex: 30 }, children: jsxRuntime.jsx(ui.NiceTutorialButton, { steps: ui.resolveTutorial(tutorial, UV_EDITOR_TOUR) }) }), jsxRuntime.jsx(ui.NiceAppShell, { className: `nice-uv-editor ${UVEditor_module.uvEditor} ${className !== null && className !== void 0 ? className : ''}`, flush: true, toolbar: toolbarSlot, rightSidebar: sidebarSlot, statusBar: jsxRuntime.jsxs("div", { className: UVEditor_module.statusBar, children: [jsxRuntime.jsxs("span", { children: ["Tool: ", state.activeTool] }), jsxRuntime.jsxs("span", { children: ["Mode: ", state.selectionMode] }), jsxRuntime.jsxs("span", { children: ["Zoom: ", (state.zoom * 100).toFixed(0), "%"] }), jsxRuntime.jsxs("span", { children: ["Channel: ", state.activeChannel] })] }), children: jsxRuntime.jsxs("div", { ref: containerRef, className: UVEditor_module.viewport, onMouseDown: handleMouseDown, onMouseMove: handleMouseMove, onMouseUp: handleMouseUp, onMouseLeave: handleMouseUp, onWheel: handleWheel, children: [jsxRuntime.jsx("canvas", { ref: canvasRef, className: UVEditor_module.canvas }), !meshData && jsxRuntime.jsx("div", { className: UVEditor_module.noGeometry, children: "No geometry selected" })] }) })] }));
563
+ return (jsxRuntime.jsxs("div", { style: { position: 'relative', width, height }, children: [jsxRuntime.jsx("span", { style: { position: 'absolute', top: 6, right: 6, zIndex: 30 }, children: jsxRuntime.jsx(ui.NiceTutorialButton, { steps: ui.resolveTutorial(tutorial, UV_EDITOR_TOUR) }) }), jsxRuntime.jsx(ui.NiceAppShell, { className: `nice-uv-editor ${UVEditor_module.uvEditor} ${className !== null && className !== void 0 ? className : ''}`, flush: true, toolbar: toolbarSlot, rightSidebar: sidebarSlot, statusBar: jsxRuntime.jsxs("div", { className: UVEditor_module.statusBar, children: [jsxRuntime.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') }))] }) })] }));
560
564
  };
561
565
 
562
566
  exports.NiceUVEditor = NiceUVEditor;
@@ -50,6 +50,7 @@ const DEFAULT_UV_DISPLAY = {
50
50
  showDistortion: false,
51
51
  distortionGradient: { low: 'var(--nice-success, #00ff00)', high: 'var(--nice-danger, #ff0000)' },
52
52
  showSeams: true,
53
+ // audit-allow: literal-color — the seam colour setting's default
53
54
  seamColor: '#ff6600',
54
55
  highlightSelected: true,
55
56
  selectionColor: 'var(--nice-warning, #ffaa00)',
@@ -0,0 +1,45 @@
1
+ import { jsx } from 'react/jsx-runtime';
2
+ import { NiceConfirmDialog, NicePromptDialog } from '@nice2dev/ui';
3
+ import { useState, useRef, useCallback } from 'react';
4
+
5
+ /**
6
+ * The library's dialogs in place of `window.prompt` / `window.confirm` for the editor hooks,
7
+ * which hold state but render nothing: `ask` resolves with the answer (`null` when cancelled)
8
+ * and `pendingAsk` is the open question, for the editor component to pass to
9
+ * {@link EditorAskDialog}. One question at a time: a new one settles the open one as cancelled.
10
+ */
11
+ function useEditorAsk() {
12
+ const [pendingAsk, setPendingAsk] = useState(null);
13
+ const pendingRef = useRef(null);
14
+ const ask = useCallback((kind, text, defaultValue) => new Promise((resolve) => {
15
+ var _a;
16
+ (_a = pendingRef.current) === null || _a === void 0 ? void 0 : _a.answer(null);
17
+ const next = {
18
+ kind,
19
+ text,
20
+ defaultValue,
21
+ answer: (value) => {
22
+ if (pendingRef.current === next) {
23
+ pendingRef.current = null;
24
+ setPendingAsk(null);
25
+ }
26
+ resolve(value);
27
+ },
28
+ };
29
+ pendingRef.current = next;
30
+ setPendingAsk(next);
31
+ }), []);
32
+ return { pendingAsk, ask };
33
+ }
34
+ /** Shows the question {@link useEditorAsk} is waiting on, if any. */
35
+ const EditorAskDialog = ({ ask }) => {
36
+ if (!ask) {
37
+ return null;
38
+ }
39
+ if (ask.kind === 'confirm') {
40
+ return (jsx(NiceConfirmDialog, { visible: true, message: ask.text, onAccept: () => ask.answer(''), onReject: () => ask.answer(null) }));
41
+ }
42
+ return (jsx(NicePromptDialog, { open: true, title: ask.text, defaultValue: ask.defaultValue, onSubmit: (value) => ask.answer(value), onCancel: () => ask.answer(null) }));
43
+ };
44
+
45
+ export { EditorAskDialog, useEditorAsk };
@@ -1,11 +1,15 @@
1
1
  import { jsx } from 'react/jsx-runtime';
2
- import { createContext, useContext } from 'react';
2
+ import { useNiceTranslation as useNiceTranslation$1 } from '@nice2dev/ui';
3
+ import { useContext, useMemo, createContext } from 'react';
3
4
 
4
- const fallback = (_key, defaultValue) => defaultValue;
5
- const NiceI18nContext = createContext(fallback);
5
+ /** `null` outside this package's provider: the global translator applies. */
6
+ const NiceI18nContext = /* @__PURE__ */ createContext(null);
7
+ /** Overrides the app's global translator for the 3D controls below it. */
6
8
  const NiceI18nProvider = ({ t, children }) => (jsx(NiceI18nContext.Provider, { value: t, children: children }));
7
9
  function useNiceTranslation() {
8
- const t = useContext(NiceI18nContext);
10
+ const own = useContext(NiceI18nContext);
11
+ const { t: global } = useNiceTranslation$1();
12
+ const t = useMemo(() => own !== null && own !== void 0 ? own : ((key, defaultValue) => global(key, defaultValue)), [own, global]);
9
13
  return { t };
10
14
  }
11
15
 
@@ -0,0 +1,30 @@
1
+ import * as THREE from 'three';
2
+
3
+ /**
4
+ * three.js r183 deprecated `Clock` (for `Timer`) and folds `PCFSoftShadowMap` into
5
+ * `PCFShadowMap`; both log a console warning there. The peer range starts at r150, which has
6
+ * neither `Timer` in its core nor the softer `PCFShadowMap`, so the choice follows the revision.
7
+ */
8
+ const REVISION = Number.parseInt(THREE.REVISION, 10);
9
+ const MODERN = REVISION >= 183;
10
+ /** Soft shadows without the r183 deprecation warning; older three keeps `PCFSoftShadowMap`. */
11
+ const SOFT_SHADOW_MAP = MODERN
12
+ ? THREE.PCFShadowMap
13
+ : THREE.PCFSoftShadowMap;
14
+ /** `Timer` where three has deprecated `Clock`, `Clock` before. */
15
+ function createFrameClock() {
16
+ const TimerCtor = THREE.Timer;
17
+ if (MODERN && TimerCtor) {
18
+ const timer = new TimerCtor();
19
+ return {
20
+ getDelta: () => {
21
+ timer.update();
22
+ return timer.getDelta();
23
+ },
24
+ };
25
+ }
26
+ const clock = new THREE.Clock();
27
+ return { getDelta: () => clock.getDelta() };
28
+ }
29
+
30
+ export { SOFT_SHADOW_MAP, createFrameClock };
@@ -1,5 +1,11 @@
1
1
  import { useRef, useState, useCallback } from 'react';
2
2
 
3
+ /**
4
+ * DanceScoreEngine — Compares detected poses against reference choreography
5
+ *
6
+ * Calculates real-time dance accuracy by comparing camera-detected Pose3DFrames
7
+ * against a reference sequence (from choreography DSL or recorded poses).
8
+ */
3
9
  // ── Default config ──────────────────────────────────────────────
4
10
  const DEFAULT_CONFIG = {
5
11
  keypointWeights: {},
@@ -190,6 +196,7 @@ class DanceScoreEngine {
190
196
  return 'F';
191
197
  }
192
198
  }
199
+ // ── React hook ──────────────────────────────────────────────────
193
200
  function useDanceScore(config) {
194
201
  const engineRef = useRef(new DanceScoreEngine(config));
195
202
  const [lastScore, setLastScore] = useState(null);
@@ -1,6 +1,7 @@
1
1
  import { jsxs, jsx } from 'react/jsx-runtime';
2
2
  import { resolveCssColor } from '@nice2dev/ui';
3
- import { forwardRef, useRef, useImperativeHandle, useCallback, useState, useEffect } from 'react';
3
+ import { forwardRef, useRef, useImperativeHandle, useCallback, useEffect, useState } from 'react';
4
+ import { useNiceTranslation } from '../core/i18n.js';
4
5
 
5
6
  // ── Standard keypoint definitions (COCO 17-keypoint model) ──────
6
7
  const COCO_KEYPOINTS = [
@@ -40,19 +41,15 @@ const SKELETON_CONNECTIONS = [
40
41
  [12, 14],
41
42
  [14, 16], // right leg
42
43
  ];
43
- // ── Simulated pose detection (no external dependency) ───────────
44
- function createSimulatedKeypoints() {
45
- return COCO_KEYPOINTS.map((name, _i) => ({
46
- name,
47
- x: 0,
48
- y: 0,
49
- z: 0,
50
- confidence: 0,
51
- }));
52
- }
44
+ /** A keypoint the detector reported without a score counts as found. */
45
+ const confidenceOf = (kp) => { var _a; return (_a = kp.confidence) !== null && _a !== void 0 ? _a : 1; };
53
46
  // ── Hook: usePoseDetection ──────────────────────────────────────
54
47
  function usePoseDetection(config = {}) {
55
- const { maxPersons = 1, minConfidence = 0.5, targetFps = 30 } = config;
48
+ // The detection loop outlives renders: it reads the latest settings from here.
49
+ const configRef = useRef(config);
50
+ useEffect(() => {
51
+ configRef.current = config;
52
+ });
56
53
  const [isDetecting, setIsDetecting] = useState(false);
57
54
  const latestFrameRef = useRef(null);
58
55
  const frameIndexRef = useRef(0);
@@ -60,33 +57,67 @@ function usePoseDetection(config = {}) {
60
57
  const streamRef = useRef(null);
61
58
  const videoRef = useRef(null);
62
59
  const callbackRef = useRef(null);
63
- const processFrame = useCallback(() => {
60
+ const errorRef = useRef(null);
61
+ const startedAtRef = useRef(0);
62
+ const lastRunRef = useRef(-Infinity);
63
+ const busyRef = useRef(false);
64
+ // Bumped by start() and stop(): a detection that resolves after them is dropped.
65
+ const runRef = useRef(0);
66
+ const stop = useCallback(() => {
64
67
  var _a;
65
- if (!videoRef.current || videoRef.current.readyState < 2) {
66
- rafRef.current = requestAnimationFrame(processFrame);
68
+ runRef.current += 1;
69
+ cancelAnimationFrame(rafRef.current);
70
+ (_a = streamRef.current) === null || _a === void 0 ? void 0 : _a.getTracks().forEach((t) => t.stop());
71
+ streamRef.current = null;
72
+ if (videoRef.current) {
73
+ videoRef.current.srcObject = null;
74
+ }
75
+ setIsDetecting(false);
76
+ }, []);
77
+ const processFrame = useCallback(() => {
78
+ rafRef.current = requestAnimationFrame(processFrame);
79
+ const video = videoRef.current;
80
+ const { detector, maxPersons = 1, minConfidence = 0.5, targetFps = 30 } = configRef.current;
81
+ if (!detector || !video || video.readyState < 2 || busyRef.current) {
67
82
  return;
68
83
  }
69
- const idx = frameIndexRef.current++;
70
- const timestamp = idx / targetFps;
71
- // Build frame from video analysis (placeholder for actual ML model)
72
- // In production, this calls MediaPipe/TensorFlow.js pose estimation
73
- const persons = [];
74
- for (let p = 0; p < maxPersons; p++) {
75
- const keypoints = createSimulatedKeypoints();
76
- if (keypoints.some((kp) => { var _a; return ((_a = kp.confidence) !== null && _a !== void 0 ? _a : 0) >= minConfidence; })) {
77
- persons.push({ id: p, keypoints });
78
- }
84
+ const now = performance.now();
85
+ if (now - lastRunRef.current < 1000 / targetFps) {
86
+ return;
79
87
  }
80
- const frame = {
81
- frame_index: idx,
82
- timestamp_sec: timestamp,
83
- persons,
84
- };
85
- latestFrameRef.current = frame;
86
- (_a = callbackRef.current) === null || _a === void 0 ? void 0 : _a.call(callbackRef, frame);
87
- rafRef.current = requestAnimationFrame(processFrame);
88
- }, [maxPersons, minConfidence, targetFps]);
89
- const start = useCallback(async (video, onFrame) => {
88
+ lastRunRef.current = now;
89
+ busyRef.current = true;
90
+ const run = runRef.current;
91
+ Promise.resolve()
92
+ .then(() => detector(video, now))
93
+ .then((found) => {
94
+ var _a;
95
+ if (run !== runRef.current) {
96
+ return;
97
+ }
98
+ const frame = {
99
+ frame_index: frameIndexRef.current++,
100
+ timestamp_sec: (now - startedAtRef.current) / 1000,
101
+ persons: found
102
+ .filter((person) => person.keypoints.some((kp) => confidenceOf(kp) >= minConfidence))
103
+ .slice(0, maxPersons),
104
+ };
105
+ latestFrameRef.current = frame;
106
+ (_a = callbackRef.current) === null || _a === void 0 ? void 0 : _a.call(callbackRef, frame);
107
+ })
108
+ .catch((err) => {
109
+ var _a;
110
+ if (run !== runRef.current) {
111
+ return;
112
+ }
113
+ stop();
114
+ (_a = errorRef.current) === null || _a === void 0 ? void 0 : _a.call(errorRef, err instanceof Error ? err : new Error(String(err)));
115
+ })
116
+ .finally(() => {
117
+ busyRef.current = false;
118
+ });
119
+ }, [stop]);
120
+ const start = useCallback(async (video, onFrame, onError) => {
90
121
  try {
91
122
  const stream = await navigator.mediaDevices.getUserMedia({
92
123
  video: { facingMode: 'user', width: { ideal: 640 }, height: { ideal: 480 } },
@@ -96,7 +127,14 @@ function usePoseDetection(config = {}) {
96
127
  video.srcObject = stream;
97
128
  await video.play();
98
129
  callbackRef.current = onFrame !== null && onFrame !== void 0 ? onFrame : null;
130
+ errorRef.current = onError !== null && onError !== void 0 ? onError : null;
99
131
  frameIndexRef.current = 0;
132
+ startedAtRef.current = performance.now();
133
+ lastRunRef.current = -Infinity;
134
+ runRef.current += 1;
135
+ if (!configRef.current.detector) {
136
+ console.warn('[NicePoseDetector] started without a `detector`: the camera runs, but no poses are detected.');
137
+ }
100
138
  setIsDetecting(true);
101
139
  rafRef.current = requestAnimationFrame(processFrame);
102
140
  }
@@ -104,23 +142,14 @@ function usePoseDetection(config = {}) {
104
142
  throw new Error(`Camera access failed: ${err instanceof Error ? err.message : String(err)}`);
105
143
  }
106
144
  }, [processFrame]);
107
- const stop = useCallback(() => {
108
- var _a;
109
- cancelAnimationFrame(rafRef.current);
110
- (_a = streamRef.current) === null || _a === void 0 ? void 0 : _a.getTracks().forEach((t) => t.stop());
111
- streamRef.current = null;
112
- if (videoRef.current) {
113
- videoRef.current.srcObject = null;
114
- }
115
- setIsDetecting(false);
116
- }, []);
117
145
  useEffect(() => () => {
118
146
  stop();
119
147
  }, [stop]);
120
148
  return { start, stop, isDetecting, getLatestFrame: () => latestFrameRef.current };
121
149
  }
122
150
  // ── Component: PoseDetector ─────────────────────────────────────
123
- const NicePoseDetector = forwardRef(function NicePoseDetector(props, ref) {
151
+ const NicePoseDetector = /* @__PURE__ */ forwardRef(function NicePoseDetector(props, ref) {
152
+ const { t } = useNiceTranslation();
124
153
  const { onFrame, onStart, onStop, onError, className, style, width = 640, height = 480, mirror = true, showSkeleton = true, showConfidence = false, ...config } = props;
125
154
  const videoRef = useRef(null);
126
155
  const canvasRef = useRef(null);
@@ -134,64 +163,90 @@ const NicePoseDetector = forwardRef(function NicePoseDetector(props, ref) {
134
163
  },
135
164
  }));
136
165
  const drawSkeleton = useCallback((frame) => {
137
- var _a, _b, _c, _d, _e, _f;
166
+ var _a;
138
167
  const ctx = (_a = canvasRef.current) === null || _a === void 0 ? void 0 : _a.getContext('2d');
139
168
  if (!ctx || !canvasRef.current) {
140
169
  return;
141
170
  }
142
171
  ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height);
172
+ // Keypoints are in video pixels; the video fills the box with object-fit: cover.
173
+ const video = videoRef.current;
174
+ const videoW = (video === null || video === void 0 ? void 0 : video.videoWidth) || width;
175
+ const videoH = (video === null || video === void 0 ? void 0 : video.videoHeight) || height;
176
+ const scale = Math.max(width / videoW, height / videoH);
177
+ const offsetX = (width - videoW * scale) / 2;
178
+ const offsetY = (height - videoH * scale) / 2;
179
+ const at = (kp) => [
180
+ kp.x * scale + offsetX,
181
+ kp.y * scale + offsetY,
182
+ ];
143
183
  for (const person of frame.persons) {
144
184
  const kps = person.keypoints;
185
+ const byName = new Map(kps.map((kp) => [kp.name, kp]));
145
186
  // Draw connections
146
187
  if (showSkeleton) {
147
188
  ctx.strokeStyle = resolveCssColor('var(--nice-success, #00ff88)', ctx.canvas);
148
189
  ctx.lineWidth = 2;
149
190
  for (const [a, b] of SKELETON_CONNECTIONS) {
150
- const ka = kps[a], kb = kps[b];
191
+ const ka = byName.get(COCO_KEYPOINTS[a]);
192
+ const kb = byName.get(COCO_KEYPOINTS[b]);
151
193
  if (!ka || !kb) {
152
194
  continue;
153
195
  }
154
- if (((_b = ka.confidence) !== null && _b !== void 0 ? _b : 0) < 0.3 || ((_c = kb.confidence) !== null && _c !== void 0 ? _c : 0) < 0.3) {
196
+ if (confidenceOf(ka) < 0.3 || confidenceOf(kb) < 0.3) {
155
197
  continue;
156
198
  }
157
199
  ctx.beginPath();
158
- ctx.moveTo(ka.x, ka.y);
159
- ctx.lineTo(kb.x, kb.y);
200
+ ctx.moveTo(...at(ka));
201
+ ctx.lineTo(...at(kb));
160
202
  ctx.stroke();
161
203
  }
162
204
  }
163
205
  // Draw keypoints
164
206
  for (const kp of kps) {
165
- if (((_d = kp.confidence) !== null && _d !== void 0 ? _d : 0) < 0.3) {
207
+ if (confidenceOf(kp) < 0.3) {
166
208
  continue;
167
209
  }
168
- const radius = showConfidence ? 3 + ((_e = kp.confidence) !== null && _e !== void 0 ? _e : 0) * 5 : 4;
210
+ const radius = showConfidence ? 3 + confidenceOf(kp) * 5 : 4;
169
211
  ctx.fillStyle = resolveCssColor(showConfidence
170
- ? `rgba(0, 255, 136, ${(_f = kp.confidence) !== null && _f !== void 0 ? _f : 0.5})`
212
+ ? `rgba(0, 255, 136, ${confidenceOf(kp)})`
171
213
  : 'var(--nice-success, #00ff88)', ctx.canvas);
172
214
  ctx.beginPath();
173
- ctx.arc(kp.x, kp.y, radius, 0, 2 * Math.PI);
215
+ ctx.arc(...at(kp), radius, 0, 2 * Math.PI);
174
216
  ctx.fill();
175
217
  }
176
218
  }
177
- }, [showSkeleton, showConfidence]);
219
+ }, [showSkeleton, showConfidence, width, height]);
220
+ // The detection loop is started once; it reaches this render's drawing and callbacks here.
221
+ const latestRef = useRef({ drawSkeleton, onFrame, onError });
222
+ useEffect(() => {
223
+ latestRef.current = { drawSkeleton, onFrame, onError };
224
+ });
178
225
  const handleFrame = useCallback((frame) => {
179
- drawSkeleton(frame);
180
- onFrame === null || onFrame === void 0 ? void 0 : onFrame(frame);
181
- }, [drawSkeleton, onFrame]);
226
+ var _a, _b;
227
+ latestRef.current.drawSkeleton(frame);
228
+ (_b = (_a = latestRef.current).onFrame) === null || _b === void 0 ? void 0 : _b.call(_a, frame);
229
+ }, []);
230
+ const handleDetectorError = useCallback((error) => {
231
+ var _a, _b;
232
+ (_b = (_a = latestRef.current).onError) === null || _b === void 0 ? void 0 : _b.call(_a, error);
233
+ }, []);
182
234
  const handleStart = useCallback(async () => {
183
235
  try {
184
236
  if (videoRef.current) {
185
- await detection.start(videoRef.current, handleFrame);
237
+ await detection.start(videoRef.current, handleFrame, handleDetectorError);
186
238
  onStart === null || onStart === void 0 ? void 0 : onStart();
187
239
  }
188
240
  }
189
241
  catch (err) {
190
242
  onError === null || onError === void 0 ? void 0 : onError(err instanceof Error ? err : new Error(String(err)));
191
243
  }
192
- }, [detection, handleFrame, onStart, onError]);
244
+ }, [detection, handleFrame, handleDetectorError, onStart, onError]);
193
245
  const handleStop = useCallback(() => {
246
+ var _a;
194
247
  detection.stop();
248
+ const canvas = canvasRef.current;
249
+ (_a = canvas === null || canvas === void 0 ? void 0 : canvas.getContext('2d')) === null || _a === void 0 ? void 0 : _a.clearRect(0, 0, canvas.width, canvas.height);
195
250
  onStop === null || onStop === void 0 ? void 0 : onStop();
196
251
  }, [detection, onStop]);
197
252
  const containerStyle = {
@@ -230,9 +285,7 @@ const NicePoseDetector = forwardRef(function NicePoseDetector(props, ref) {
230
285
  cursor: 'pointer',
231
286
  fontSize: 14,
232
287
  fontWeight: 600,
233
- }, children: "Start Camera" }))] }));
288
+ }, children: t('poseDetector.startCamera', 'Start Camera') }))] }));
234
289
  });
235
- /** @deprecated Use NicePoseDetector */
236
- const PoseDetector = NicePoseDetector;
237
290
 
238
- export { NicePoseDetector, PoseDetector, usePoseDetection };
291
+ export { NicePoseDetector, usePoseDetection };
package/dist/esm/index.js CHANGED
@@ -1,5 +1,5 @@
1
- export { ModelEditor, NiceModelEditor } from './model/ModelEditor.js';
2
- export { ModelViewer, NiceModelViewer } from './model/ModelViewer.js';
1
+ export { NiceModelEditor } from './model/ModelEditor.js';
2
+ export { NiceModelViewer } from './model/ModelViewer.js';
3
3
  export { DEFAULT_CAD_SETTINGS, FLOOR_MATERIALS, ROOM_COLORS, UNIT_SCALES, WALL_MATERIALS, createCADGrid, snapToCADGrid } from './model/cadModeUtils.js';
4
4
  export { FURNITURE_CATALOG, FURNITURE_CATEGORIES, ROOM_PRESETS, applyRoomLayout, calculateBudget, checkFurnitureCollision, createDragPreview, createFurnitureMesh, getFloorIntersection, placeFurnitureInScene, removeFurnitureFromScene, resizePlacedItem, rotatePlacedItem } from './model/simsModeUtils.js';
5
5
  export { useModelEditor } from './model/useModelEditor.js';
@@ -7,7 +7,7 @@ export { useModelViewer } from './model/useModelViewer.js';
7
7
  export { NiceI18nProvider, useNiceTranslation } from './core/i18n.js';
8
8
  export { HDRI_PRESETS, PBR_PRESETS, QUAD_VIEWPORT, UndoRedoStack, applyHDRIPreset, applyPBRPreset, blendAnimations, computeSceneBounds, convertToInstanced, countTriangles, createAnimationClip, createMaterialFromPreset, createViewportCamera, crossfadeAnimations, downloadBlob, evaluateTrack, exportSceneGLTF, exportSceneUSDZ, generateLOD, makeAdditiveAction, measureAngle, measureDistance, removeKeyframe, renderMultiViewport, setKeyframe, snapMulti, snapToEdge, snapToGrid, snapToVertex, solveIKChain, subclip, toThreeKeyframeTrack } from './model/modelEditorUtils.js';
9
9
  export { EditorShortcutManager } from './model/editorShortcuts.js';
10
- export { NicePoseDetector, PoseDetector, usePoseDetection } from './dance/PoseDetector.js';
10
+ export { NicePoseDetector, usePoseDetection } from './dance/PoseDetector.js';
11
11
  export { DanceScoreEngine, useDanceScore } from './dance/DanceScoreEngine.js';
12
12
  export { DanceBridge, rhythmToBeatSync, useDanceBridge } from './dance/DanceBridge.js';
13
13
  export { NiceUVEditor } from './uv/NiceUVEditor.js';