@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.
- package/CHANGELOG.md +173 -0
- package/README.md +11 -15
- package/dist/cjs/core/editorAsk.js +48 -0
- package/dist/cjs/core/i18n.js +7 -3
- package/dist/cjs/core/threeCompat.js +52 -0
- package/dist/cjs/dance/DanceScoreEngine.js +7 -0
- package/dist/cjs/dance/PoseDetector.js +116 -64
- package/dist/cjs/index.js +0 -3
- package/dist/cjs/material/NiceMaterialEditor.js +7 -3
- package/dist/cjs/material/materialEditorTypes.js +1 -1
- package/dist/cjs/material/materialNodeDefinitions.js +14 -11
- package/dist/cjs/model/ModelEditor.js +3 -5
- package/dist/cjs/model/ModelEditorLeftPanel.js +10 -4
- package/dist/cjs/model/ModelEditorMenuBar.js +7 -5
- package/dist/cjs/model/ModelEditorRightPanel.js +18 -16
- package/dist/cjs/model/ModelEditorSubComponents.js +8 -6
- package/dist/cjs/model/ModelEditorTimeline.js +6 -4
- package/dist/cjs/model/ModelEditorToolbar.js +6 -4
- package/dist/cjs/model/ModelEditorViewport.js +16 -6
- package/dist/cjs/model/ModelViewer.js +6 -7
- package/dist/cjs/model/NiceArmatureEditor.js +8 -6
- package/dist/cjs/model/NiceMorphTargetEditor.js +5 -3
- package/dist/cjs/model/NiceOctree.js +3 -1
- package/dist/cjs/model/NicePhysicsSimulation.js +7 -5
- package/dist/cjs/model/NiceProceduralGeometry.js +5 -3
- package/dist/cjs/model/NiceTerrainEditor.js +10 -8
- package/dist/cjs/model/NiceWeightPainter.js +6 -4
- package/dist/cjs/model/NiceXRPreview.js +7 -5
- package/dist/cjs/model/useModelEditor.js +30 -18
- package/dist/cjs/model/useModelViewer.js +13 -9
- package/dist/cjs/nice2dev-ui-3d.css +1 -1
- package/dist/cjs/particle/NiceParticleEditor.js +14 -9
- package/dist/cjs/rendering/NiceCascadedShadows.js +3 -1
- package/dist/cjs/rendering/NiceRenderExport.js +11 -5
- package/dist/cjs/rendering/NiceSSAO.js +3 -1
- package/dist/cjs/rendering/NiceSSR.js +3 -1
- package/dist/cjs/rendering/NiceWebGPURenderer.js +7 -4
- package/dist/cjs/uv/NiceUVEditor.js +16 -12
- package/dist/cjs/uv/uvEditorTypes.js +1 -0
- package/dist/esm/core/editorAsk.js +45 -0
- package/dist/esm/core/i18n.js +8 -4
- package/dist/esm/core/threeCompat.js +30 -0
- package/dist/esm/dance/DanceScoreEngine.js +7 -0
- package/dist/esm/dance/PoseDetector.js +118 -65
- package/dist/esm/index.js +3 -3
- package/dist/esm/material/NiceMaterialEditor.js +7 -3
- package/dist/esm/material/materialEditorTypes.js +1 -1
- package/dist/esm/material/materialNodeDefinitions.js +14 -11
- package/dist/esm/model/ModelEditor.js +4 -5
- package/dist/esm/model/ModelEditorLeftPanel.js +10 -4
- package/dist/esm/model/ModelEditorMenuBar.js +7 -5
- package/dist/esm/model/ModelEditorRightPanel.js +18 -16
- package/dist/esm/model/ModelEditorSubComponents.js +8 -6
- package/dist/esm/model/ModelEditorTimeline.js +6 -4
- package/dist/esm/model/ModelEditorToolbar.js +6 -4
- package/dist/esm/model/ModelEditorViewport.js +16 -6
- package/dist/esm/model/ModelViewer.js +7 -7
- package/dist/esm/model/NiceArmatureEditor.js +9 -7
- package/dist/esm/model/NiceMorphTargetEditor.js +5 -3
- package/dist/esm/model/NiceOctree.js +3 -1
- package/dist/esm/model/NicePhysicsSimulation.js +7 -5
- package/dist/esm/model/NiceProceduralGeometry.js +5 -3
- package/dist/esm/model/NiceTerrainEditor.js +10 -8
- package/dist/esm/model/NiceWeightPainter.js +6 -4
- package/dist/esm/model/NiceXRPreview.js +7 -5
- package/dist/esm/model/useModelEditor.js +30 -18
- package/dist/esm/model/useModelViewer.js +13 -9
- package/dist/esm/nice2dev-ui-3d.css +1 -1
- package/dist/esm/particle/NiceParticleEditor.js +14 -9
- package/dist/esm/rendering/NiceCascadedShadows.js +3 -1
- package/dist/esm/rendering/NiceRenderExport.js +11 -5
- package/dist/esm/rendering/NiceSSAO.js +3 -1
- package/dist/esm/rendering/NiceSSR.js +3 -1
- package/dist/esm/rendering/NiceWebGPURenderer.js +7 -4
- package/dist/esm/uv/NiceUVEditor.js +16 -12
- package/dist/esm/uv/uvEditorTypes.js +1 -0
- package/dist/types/core/editorAsk.d.ts +26 -0
- package/dist/types/core/i18n.d.ts +1 -17
- package/dist/types/core/threeCompat.d.ts +14 -0
- package/dist/types/dance/DanceScoreEngine.d.ts +0 -6
- package/dist/types/dance/PoseDetector.d.ts +14 -13
- package/dist/types/index.d.ts +4 -4
- package/dist/types/model/ModelEditor.d.ts +0 -4
- package/dist/types/model/ModelViewer.d.ts +0 -4
- package/dist/types/model/useModelEditor.d.ts +3 -0
- 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 }),
|
|
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 =
|
|
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
|
|
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-
|
|
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:
|
|
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 ?
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
})) }),
|
|
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
|
-
})) }),
|
|
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
|
-
})) }),
|
|
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
|
-
})) }),
|
|
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
|
-
})) }),
|
|
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
|
-
})) }),
|
|
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:
|
|
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 };
|
package/dist/esm/core/i18n.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { jsx } from 'react/jsx-runtime';
|
|
2
|
-
import {
|
|
2
|
+
import { useNiceTranslation as useNiceTranslation$1 } from '@nice2dev/ui';
|
|
3
|
+
import { useContext, useMemo, createContext } from 'react';
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
const NiceI18nContext = createContext(
|
|
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
|
|
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,
|
|
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
|
-
|
|
44
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
66
|
-
|
|
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
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
|
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 =
|
|
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 ((
|
|
196
|
+
if (confidenceOf(ka) < 0.3 || confidenceOf(kb) < 0.3) {
|
|
155
197
|
continue;
|
|
156
198
|
}
|
|
157
199
|
ctx.beginPath();
|
|
158
|
-
ctx.moveTo(ka
|
|
159
|
-
ctx.lineTo(kb
|
|
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 ((
|
|
207
|
+
if (confidenceOf(kp) < 0.3) {
|
|
166
208
|
continue;
|
|
167
209
|
}
|
|
168
|
-
const radius = showConfidence ? 3 + (
|
|
210
|
+
const radius = showConfidence ? 3 + confidenceOf(kp) * 5 : 4;
|
|
169
211
|
ctx.fillStyle = resolveCssColor(showConfidence
|
|
170
|
-
? `rgba(0, 255, 136, ${(
|
|
212
|
+
? `rgba(0, 255, 136, ${confidenceOf(kp)})`
|
|
171
213
|
: 'var(--nice-success, #00ff88)', ctx.canvas);
|
|
172
214
|
ctx.beginPath();
|
|
173
|
-
ctx.arc(kp
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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:
|
|
288
|
+
}, children: t('poseDetector.startCamera', 'Start Camera') }))] }));
|
|
234
289
|
});
|
|
235
|
-
/** @deprecated Use NicePoseDetector */
|
|
236
|
-
const PoseDetector = NicePoseDetector;
|
|
237
290
|
|
|
238
|
-
export { NicePoseDetector,
|
|
291
|
+
export { NicePoseDetector, usePoseDetection };
|
package/dist/esm/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export {
|
|
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,
|
|
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';
|