@nice2dev/ui-3d 1.2.0 → 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +142 -0
- package/dist/cjs/core/editorAsk.js +48 -0
- package/dist/cjs/core/i18n.js +7 -3
- package/dist/cjs/dance/DanceScoreEngine.js +7 -0
- package/dist/cjs/dance/PoseDetector.js +4 -2
- package/dist/cjs/material/NiceMaterialEditor.js +8 -3
- package/dist/cjs/material/materialEditorTypes.js +1 -1
- package/dist/cjs/material/materialNodeDefinitions.js +14 -11
- package/dist/cjs/model/ModelEditor.js +9 -2
- package/dist/cjs/model/ModelEditorLeftPanel.js +10 -4
- package/dist/cjs/model/ModelEditorMenuBar.js +10 -8
- package/dist/cjs/model/ModelEditorRightPanel.js +28 -22
- package/dist/cjs/model/ModelEditorSubComponents.js +17 -9
- package/dist/cjs/model/ModelEditorTimeline.js +8 -4
- package/dist/cjs/model/ModelEditorToolbar.js +6 -4
- package/dist/cjs/model/ModelEditorViewport.js +19 -6
- package/dist/cjs/model/ModelViewer.js +9 -7
- package/dist/cjs/model/NiceArmatureEditor.js +9 -7
- package/dist/cjs/model/NiceMorphTargetEditor.js +7 -3
- package/dist/cjs/model/NiceOctree.js +9 -4
- package/dist/cjs/model/NicePhysicsSimulation.js +7 -5
- package/dist/cjs/model/NiceProceduralGeometry.js +6 -4
- package/dist/cjs/model/NiceTerrainEditor.js +9 -7
- package/dist/cjs/model/NiceWeightPainter.js +6 -4
- package/dist/cjs/model/NiceXRPreview.js +9 -4
- package/dist/cjs/model/useModelEditor.js +113 -87
- package/dist/cjs/model/useModelViewer.js +13 -6
- package/dist/cjs/nice2dev-ui-3d.css +1 -1
- package/dist/cjs/particle/NiceParticleEditor.js +23 -11
- package/dist/cjs/rendering/NiceCascadedShadows.js +3 -1
- package/dist/cjs/rendering/NiceRenderExport.js +9 -3
- package/dist/cjs/rendering/NiceSSAO.js +4 -1
- package/dist/cjs/rendering/NiceSSR.js +4 -1
- package/dist/cjs/rendering/NiceWebGPURenderer.js +5 -2
- 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/dance/DanceScoreEngine.js +7 -0
- package/dist/esm/dance/PoseDetector.js +4 -2
- package/dist/esm/material/NiceMaterialEditor.js +8 -3
- package/dist/esm/material/materialEditorTypes.js +1 -1
- package/dist/esm/material/materialNodeDefinitions.js +14 -11
- package/dist/esm/model/ModelEditor.js +9 -2
- package/dist/esm/model/ModelEditorLeftPanel.js +10 -4
- package/dist/esm/model/ModelEditorMenuBar.js +10 -8
- package/dist/esm/model/ModelEditorRightPanel.js +28 -22
- package/dist/esm/model/ModelEditorSubComponents.js +17 -9
- package/dist/esm/model/ModelEditorTimeline.js +8 -4
- package/dist/esm/model/ModelEditorToolbar.js +6 -4
- package/dist/esm/model/ModelEditorViewport.js +19 -6
- package/dist/esm/model/ModelViewer.js +9 -7
- package/dist/esm/model/NiceArmatureEditor.js +9 -7
- package/dist/esm/model/NiceMorphTargetEditor.js +7 -3
- package/dist/esm/model/NiceOctree.js +9 -4
- package/dist/esm/model/NicePhysicsSimulation.js +7 -5
- package/dist/esm/model/NiceProceduralGeometry.js +6 -4
- package/dist/esm/model/NiceTerrainEditor.js +9 -7
- package/dist/esm/model/NiceWeightPainter.js +6 -4
- package/dist/esm/model/NiceXRPreview.js +9 -4
- package/dist/esm/model/useModelEditor.js +113 -87
- package/dist/esm/model/useModelViewer.js +13 -6
- package/dist/esm/nice2dev-ui-3d.css +1 -1
- package/dist/esm/particle/NiceParticleEditor.js +23 -11
- package/dist/esm/rendering/NiceCascadedShadows.js +3 -1
- package/dist/esm/rendering/NiceRenderExport.js +9 -3
- package/dist/esm/rendering/NiceSSAO.js +4 -1
- package/dist/esm/rendering/NiceSSR.js +4 -1
- package/dist/esm/rendering/NiceWebGPURenderer.js +5 -2
- 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/dance/DanceScoreEngine.d.ts +0 -6
- package/dist/types/model/useModelEditor.d.ts +3 -0
- package/package.json +3 -3
|
@@ -2,6 +2,7 @@ import { jsxs, jsx } from 'react/jsx-runtime';
|
|
|
2
2
|
import { NiceIcon } from '@nice2dev/ui';
|
|
3
3
|
import { useState, useRef, useCallback, useEffect } from 'react';
|
|
4
4
|
import * as THREE from 'three';
|
|
5
|
+
import { useNiceTranslation } from '../core/i18n.js';
|
|
5
6
|
|
|
6
7
|
/* ── SSR Shader ── */
|
|
7
8
|
const SSR_VERTEX = `
|
|
@@ -206,6 +207,7 @@ function useSSR(renderer, scene, camera, configOverride) {
|
|
|
206
207
|
material.dispose();
|
|
207
208
|
ortho.remove(quad);
|
|
208
209
|
};
|
|
210
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- the uniforms' first values; the next effect updates them without rebuilding the render targets
|
|
209
211
|
}, [renderer, camera, config.resolution]);
|
|
210
212
|
// Update uniforms when config changes
|
|
211
213
|
useEffect(() => {
|
|
@@ -253,8 +255,9 @@ function useSSR(renderer, scene, camera, configOverride) {
|
|
|
253
255
|
}
|
|
254
256
|
/* ── Component ── */
|
|
255
257
|
const NiceSSR = ({ renderer, scene, camera, config: configProp, className = '', }) => {
|
|
258
|
+
const { t } = useNiceTranslation();
|
|
256
259
|
const { config, setConfig } = useSSR(renderer, scene, camera, configProp);
|
|
257
|
-
return (jsxs("div", { className: `nice-ssr ${className}`, style: { display: 'grid', gap: 8, fontSize: 13 }, children: [jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' }, children: [jsxs("strong", { children: [jsx(NiceIcon, { name: "copy", size: 14 }),
|
|
260
|
+
return (jsxs("div", { className: `nice-ssr ${className}`, style: { display: 'grid', gap: 8, fontSize: 13 }, children: [jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' }, children: [jsxs("strong", { children: [jsx(NiceIcon, { name: "copy", size: 14 }), ' ', t('ssr.screenSpaceReflections', 'Screen-Space Reflections')] }), jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 4, fontSize: 11 }, children: [jsx("input", { type: "checkbox", checked: config.enabled, onChange: (e) => setConfig({ enabled: e.target.checked }) }), t('ssr.enabled', 'Enabled')] })] }), jsxs("label", { children: [t('ssr.maxSteps', 'Max Steps: {maxSteps}').replace('{maxSteps}', String(config.maxSteps)), jsx("input", { type: "range", min: 16, max: 256, step: 16, value: config.maxSteps, onChange: (e) => setConfig({ maxSteps: parseInt(e.target.value, 10) }), style: { width: '100%' } })] }), jsxs("label", { children: [t('ssr.maxDistance', 'Max Distance: {maxDistance}').replace('{maxDistance}', String(config.maxDistance)), jsx("input", { type: "range", min: 5, max: 200, step: 5, value: config.maxDistance, onChange: (e) => setConfig({ maxDistance: parseInt(e.target.value, 10) }), style: { width: '100%' } })] }), jsxs("label", { children: [t('ssr.intensity', 'Intensity: {intensity}').replace('{intensity}', String(config.intensity.toFixed(2))), jsx("input", { type: "range", min: 0, max: 1, step: 0.05, value: config.intensity, onChange: (e) => setConfig({ intensity: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsxs("label", { children: [t('ssr.thickness', 'Thickness: {thickness}').replace('{thickness}', String(config.thickness.toFixed(2))), jsx("input", { type: "range", min: 0.05, max: 2, step: 0.05, value: config.thickness, onChange: (e) => setConfig({ thickness: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsxs("label", { children: [t('ssr.jitter', 'Jitter: {jitter}').replace('{jitter}', String(config.jitter.toFixed(2))), jsx("input", { type: "range", min: 0, max: 1, step: 0.05, value: config.jitter, onChange: (e) => setConfig({ jitter: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsxs("label", { children: [t('ssr.resolutionScale', 'Resolution Scale: {resolution}').replace('{resolution}', String(config.resolution.toFixed(2))), jsx("input", { type: "range", min: 0.25, max: 1, step: 0.25, value: config.resolution, onChange: (e) => setConfig({ resolution: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsxs("label", { children: [t('ssr.fresnel', 'Fresnel: {fresnelExponent}').replace('{fresnelExponent}', String(config.fresnelExponent.toFixed(1))), jsx("input", { type: "range", min: 1, max: 8, step: 0.5, value: config.fresnelExponent, onChange: (e) => setConfig({ fresnelExponent: parseFloat(e.target.value) }), style: { width: '100%' } })] })] }));
|
|
258
261
|
};
|
|
259
262
|
|
|
260
263
|
export { NiceSSR, useSSR };
|
|
@@ -2,6 +2,7 @@ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
|
2
2
|
import { NiceIcon } from '@nice2dev/ui';
|
|
3
3
|
import { useState, useRef, useEffect, useCallback } from 'react';
|
|
4
4
|
import * as THREE from 'three';
|
|
5
|
+
import { useNiceTranslation } from '../core/i18n.js';
|
|
5
6
|
import { SOFT_SHADOW_MAP } from '../core/threeCompat.js';
|
|
6
7
|
|
|
7
8
|
/* ── WebGPU detection ── */
|
|
@@ -130,6 +131,7 @@ function useWebGPURenderer(canvas, scene, camera, width = 800, height = 600, onR
|
|
|
130
131
|
disposed = true;
|
|
131
132
|
(_a = rendererRef.current) === null || _a === void 0 ? void 0 : _a.dispose();
|
|
132
133
|
};
|
|
134
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- one renderer per canvas, made with the first settings; the effects below apply later changes, and onRendererReady reports that one renderer
|
|
133
135
|
}, [canvas]);
|
|
134
136
|
// Update renderer settings reactively
|
|
135
137
|
useEffect(() => {
|
|
@@ -209,14 +211,15 @@ const TONE_MAPPINGS = [
|
|
|
209
211
|
];
|
|
210
212
|
/* ── Component ── */
|
|
211
213
|
const NiceWebGPURenderer = ({ canvas, scene, camera, width, height, onRendererReady, className = '', }) => {
|
|
214
|
+
const { t } = useNiceTranslation();
|
|
212
215
|
const { isWebGPU, capabilities, isReady, toneMapping, setToneMapping, toneMappingExposure, setToneMappingExposure, outputColorSpace, setOutputColorSpace, pixelRatio, setPixelRatio, } = useWebGPURenderer(canvas, scene, camera, width, height, onRendererReady);
|
|
213
|
-
return (jsxs("div", { className: `nice-webgpu-renderer ${className}`, style: { display: 'grid', gap: 8, fontSize: 13 }, children: [jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' }, children: [jsxs("strong", { children: [jsx(NiceIcon, { name: "monitor", size: 14 }), " Renderer
|
|
216
|
+
return (jsxs("div", { className: `nice-webgpu-renderer ${className}`, style: { display: 'grid', gap: 8, fontSize: 13 }, children: [jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' }, children: [jsxs("strong", { children: [jsx(NiceIcon, { name: "monitor", size: 14 }), " ", t('webGPURenderer.renderer', 'Renderer')] }), jsx("span", { style: {
|
|
214
217
|
fontSize: 10,
|
|
215
218
|
padding: '2px 6px',
|
|
216
219
|
borderRadius: 4,
|
|
217
220
|
background: isWebGPU ? 'var(--nice-success, #22c55e)' : 'var(--nice-warning, #f59e0b)',
|
|
218
221
|
color: 'var(--nice-color-white, #fff)',
|
|
219
|
-
}, children: isWebGPU ? 'WebGPU' : 'WebGL' })] }), !isReady && jsx("div", { style: { opacity: 0.6, fontSize: 11 }, children:
|
|
222
|
+
}, children: isWebGPU ? 'WebGPU' : 'WebGL' })] }), !isReady && (jsx("div", { style: { opacity: 0.6, fontSize: 11 }, children: t('webGPURenderer.initializing', 'Initializing…') })), capabilities && (jsxs("div", { style: { fontSize: 10, opacity: 0.7, lineHeight: 1.5 }, children: [t('webGPURenderer.gpu', 'GPU: {adapterName}').replace('{adapterName}', String(capabilities.adapterName)), jsx("br", {}), t('webGPURenderer.vendor', 'Vendor: {vendor}').replace('{vendor}', String(capabilities.vendor)), jsx("br", {}), t('webGPURenderer.maxTexturePx', 'Max Texture: {maxTextureDimension2D}px').replace('{maxTextureDimension2D}', String(capabilities.maxTextureDimension2D)), capabilities.features.length > 0 && (jsxs(Fragment, { children: [jsx("br", {}), t('webGPURenderer.features', 'Features: {features}').replace('{features}', String(capabilities.features.length))] }))] })), jsxs("label", { children: [t('webGPURenderer.toneMapping', 'Tone Mapping:'), jsx("select", { value: toneMapping, onChange: (e) => setToneMapping(parseInt(e.target.value, 10)), style: { width: '100%', marginTop: 2 }, children: TONE_MAPPINGS.map((tm) => (jsx("option", { value: tm.value, children: tm.label }, tm.value))) })] }), jsxs("label", { children: [t('webGPURenderer.exposure', 'Exposure: {toneMappingExposure}').replace('{toneMappingExposure}', String(toneMappingExposure.toFixed(2))), jsx("input", { type: "range", min: 0.1, max: 3, step: 0.05, value: toneMappingExposure, onChange: (e) => setToneMappingExposure(parseFloat(e.target.value)), style: { width: '100%' } })] }), jsxs("label", { children: [t('webGPURenderer.outputColorSpace', 'Output Color Space:'), jsxs("select", { value: outputColorSpace, onChange: (e) => setOutputColorSpace(e.target.value), style: { width: '100%', marginTop: 2 }, children: [jsx("option", { value: THREE.SRGBColorSpace, children: "sRGB" }), jsx("option", { value: THREE.LinearSRGBColorSpace, children: t('webGPURenderer.linearSrgb', 'Linear sRGB') })] })] }), jsxs("label", { children: [t('webGPURenderer.pixelRatio', 'Pixel Ratio: {pixelRatio}').replace('{pixelRatio}', String(pixelRatio.toFixed(1))), jsx("input", { type: "range", min: 0.5, max: 3, step: 0.5, value: pixelRatio, onChange: (e) => setPixelRatio(parseFloat(e.target.value)), style: { width: '100%' } })] })] }));
|
|
220
223
|
};
|
|
221
224
|
|
|
222
225
|
export { NiceWebGPURenderer, useWebGPURenderer };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
2
2
|
import { resolveCssColor, NiceIcon, NiceTutorialButton, resolveTutorial, NiceAppShell } from '@nice2dev/ui';
|
|
3
3
|
import { useRef, useState, useEffect, useCallback } from 'react';
|
|
4
|
+
import { useNiceTranslation } from '../core/i18n.js';
|
|
4
5
|
import styles from './UVEditor.module.css.js';
|
|
5
6
|
import { DEFAULT_UV_STATE, DEFAULT_RELAX_OPTIONS, UV_TOOLS, UV_SELECTION_MODES, PROJECTION_TYPES, UV_CHANNELS, DEFAULT_PACKING_OPTIONS } from './uvEditorTypes.js';
|
|
6
7
|
import { extractUVMeshData, uvSub, uvAdd, uvRotate, selectVerticesInRect, syncToGeometry, packIslands, applyIslandTransforms, relaxUVs, applyProjection, getSelectedVertices } from './uvEditorUtils.js';
|
|
@@ -20,6 +21,7 @@ const UV_EDITOR_TOUR = [
|
|
|
20
21
|
Component
|
|
21
22
|
═══════════════════════════════════════════ */
|
|
22
23
|
const NiceUVEditor = ({ geometry, texture, channel = 'uv', onUVChange, onSelectionChange, width = '100%', height = 600, showToolbar = true, showSidebar = true, className, readOnly = false, tutorial, }) => {
|
|
24
|
+
const { t } = useNiceTranslation();
|
|
23
25
|
const canvasRef = useRef(null);
|
|
24
26
|
const containerRef = useRef(null);
|
|
25
27
|
// State
|
|
@@ -122,12 +124,14 @@ const NiceUVEditor = ({ geometry, texture, channel = 'uv', onUVChange, onSelecti
|
|
|
122
124
|
ctx.closePath();
|
|
123
125
|
ctx.stroke();
|
|
124
126
|
}
|
|
125
|
-
// Draw faces
|
|
127
|
+
// Draw faces — the two fills resolved once per frame, not once per face
|
|
128
|
+
const faceSelectedFill = resolveCssColor('var(--nice-warning-tint-30, rgba(255, 170, 0, 0.3))', ctx.canvas);
|
|
129
|
+
const faceFill = resolveCssColor('var(--nice-overlay-10, rgba(100, 100, 100, 0.1))', ctx.canvas);
|
|
126
130
|
for (const face of meshData.faces.values()) {
|
|
127
131
|
const verts = face.vertices.map((id) => meshData.vertices.get(id));
|
|
128
132
|
const screenVerts = verts.map((v) => toScreen(v.position));
|
|
129
133
|
// Fill
|
|
130
|
-
ctx.fillStyle = face.selected ?
|
|
134
|
+
ctx.fillStyle = face.selected ? faceSelectedFill : faceFill;
|
|
131
135
|
ctx.beginPath();
|
|
132
136
|
ctx.moveTo(screenVerts[0].x, screenVerts[0].y);
|
|
133
137
|
screenVerts.slice(1).forEach((sv) => ctx.lineTo(sv.x, sv.y));
|
|
@@ -528,33 +532,33 @@ const NiceUVEditor = ({ geometry, texture, channel = 'uv', onUVChange, onSelecti
|
|
|
528
532
|
setMeshData(newData);
|
|
529
533
|
onUVChange === null || onUVChange === void 0 ? void 0 : onUVChange(geometry);
|
|
530
534
|
}, [meshData, geometry, state.activeChannel, readOnly, pushUndo, onUVChange]);
|
|
531
|
-
const toolbarSlot = showToolbar ? (jsxs("div", { className: styles.toolbar, children: [jsx("div", { className: styles.toolGroup, children: UV_TOOLS.slice(0, 5).map((tool) => (jsx("button", { className: `${styles.toolBtn} ${state.activeTool === tool ? styles.active : ''}`, onClick: () => setState((prev) => ({ ...prev, activeTool: tool })), title: tool, disabled: readOnly, children: tool[0].toUpperCase() }, tool))) }), jsx("div", { className: styles.toolGroup, children: UV_SELECTION_MODES.map((mode) => (jsx("button", { className: `${styles.toolBtn} ${state.selectionMode === mode ? styles.active : ''}`, onClick: () => setState((prev) => ({ ...prev, selectionMode: mode })), title: mode, children: mode[0].toUpperCase() }, mode))) }), jsxs("div", { className: styles.toolGroup, children: [jsx("button", { className: styles.toolBtn, onClick: handlePack, disabled: readOnly || !meshData, title:
|
|
535
|
+
const toolbarSlot = showToolbar ? (jsxs("div", { className: styles.toolbar, children: [jsx("div", { className: styles.toolGroup, children: UV_TOOLS.slice(0, 5).map((tool) => (jsx("button", { className: `${styles.toolBtn} ${state.activeTool === tool ? styles.active : ''}`, onClick: () => setState((prev) => ({ ...prev, activeTool: tool })), title: tool, disabled: readOnly, children: tool[0].toUpperCase() }, tool))) }), jsx("div", { className: styles.toolGroup, children: UV_SELECTION_MODES.map((mode) => (jsx("button", { className: `${styles.toolBtn} ${state.selectionMode === mode ? styles.active : ''}`, onClick: () => setState((prev) => ({ ...prev, selectionMode: mode })), title: mode, children: mode[0].toUpperCase() }, mode))) }), jsxs("div", { className: styles.toolGroup, children: [jsx("button", { className: styles.toolBtn, onClick: handlePack, disabled: readOnly || !meshData, title: t('uvEditor.packIslandsP', 'Pack Islands (P)'), children: t('uvEditor.pack', 'Pack') }), jsx("button", { className: styles.toolBtn, onClick: handleRelax, disabled: readOnly || !meshData, title: t('uvEditor.relaxUvs', 'Relax UVs'), children: t('uvEditor.relax', 'Relax') })] }), jsx("div", { className: styles.toolGroup, children: jsxs("select", { className: styles.select, onChange: (e) => handleProject(e.target.value), disabled: readOnly || !meshData, defaultValue: "", children: [jsx("option", { value: "", disabled: true, children: t('uvEditor.project', 'Project...') }), PROJECTION_TYPES.map((type) => (jsx("option", { value: type, children: type }, type)))] }) }), jsx("div", { className: styles.toolGroup, children: jsx("select", { className: styles.select, value: state.activeChannel, onChange: (e) => setState((prev) => ({
|
|
532
536
|
...prev,
|
|
533
537
|
activeChannel: e.target.value,
|
|
534
|
-
})), children: UV_CHANNELS.map((ch) => (jsx("option", { value: ch, children: ch.toUpperCase() }, ch))) }) }), jsxs("div", { className: styles.toolGroup, children: [jsx("button", { className: styles.toolBtn, onClick: undo, disabled: undoStack.length === 0, title:
|
|
535
|
-
const sidebarSlot = showSidebar && meshData ? (jsxs("div", { className: styles.sidebar, children: [jsxs("div", { className: styles.panel, children: [jsx("h4", { className: styles.panelTitle, children:
|
|
538
|
+
})), children: UV_CHANNELS.map((ch) => (jsx("option", { value: ch, children: ch.toUpperCase() }, ch))) }) }), jsxs("div", { className: styles.toolGroup, children: [jsx("button", { className: styles.toolBtn, onClick: undo, disabled: undoStack.length === 0, title: t('uvEditor.undoCtrlZ', 'Undo (Ctrl+Z)'), "aria-label": t('uvEditor.undo', 'Undo'), children: jsx(NiceIcon, { name: "arrow-left", size: 16 }) }), jsx("button", { className: styles.toolBtn, onClick: redo, disabled: redoStack.length === 0, title: t('uvEditor.redoCtrlY', 'Redo (Ctrl+Y)'), "aria-label": t('uvEditor.redo', 'Redo'), children: jsx(NiceIcon, { name: "arrow-right", size: 16 }) })] })] })) : undefined;
|
|
539
|
+
const sidebarSlot = showSidebar && meshData ? (jsxs("div", { className: styles.sidebar, children: [jsxs("div", { className: styles.panel, children: [jsx("h4", { className: styles.panelTitle, children: t('uvEditor.display', 'Display') }), jsxs("label", { className: styles.checkbox, children: [jsx("input", { type: "checkbox", checked: state.display.showGrid, onChange: (e) => setState((prev) => ({
|
|
536
540
|
...prev,
|
|
537
541
|
display: { ...prev.display, showGrid: e.target.checked },
|
|
538
|
-
})) }),
|
|
542
|
+
})) }), t('uvEditor.showGrid', 'Show Grid')] }), jsxs("label", { className: styles.checkbox, children: [jsx("input", { type: "checkbox", checked: state.display.showTexture, onChange: (e) => setState((prev) => ({
|
|
539
543
|
...prev,
|
|
540
544
|
display: { ...prev.display, showTexture: e.target.checked },
|
|
541
|
-
})) }),
|
|
545
|
+
})) }), t('uvEditor.showTexture', 'Show Texture')] }), jsxs("label", { className: styles.checkbox, children: [jsx("input", { type: "checkbox", checked: state.display.showSeams, onChange: (e) => setState((prev) => ({
|
|
542
546
|
...prev,
|
|
543
547
|
display: { ...prev.display, showSeams: e.target.checked },
|
|
544
|
-
})) }),
|
|
548
|
+
})) }), t('uvEditor.showSeams', 'Show Seams')] }), jsxs("label", { className: styles.checkbox, children: [jsx("input", { type: "checkbox", checked: state.display.showDistortion, onChange: (e) => setState((prev) => ({
|
|
545
549
|
...prev,
|
|
546
550
|
display: { ...prev.display, showDistortion: e.target.checked },
|
|
547
|
-
})) }),
|
|
551
|
+
})) }), t('uvEditor.showDistortion', 'Show Distortion')] }), jsxs("label", { className: styles.checkbox, children: [jsx("input", { type: "checkbox", checked: state.display.showIslandBounds, onChange: (e) => setState((prev) => ({
|
|
548
552
|
...prev,
|
|
549
553
|
display: { ...prev.display, showIslandBounds: e.target.checked },
|
|
550
|
-
})) }),
|
|
554
|
+
})) }), t('uvEditor.showIslandBounds', 'Show Island Bounds')] })] }), jsxs("div", { className: styles.panel, children: [jsx("h4", { className: styles.panelTitle, children: t('uvEditor.statistics', 'Statistics') }), jsxs("div", { className: styles.stat, children: [jsx("span", { children: t('uvEditor.vertices', 'Vertices:') }), jsx("span", { children: meshData.vertices.size })] }), jsxs("div", { className: styles.stat, children: [jsx("span", { children: t('uvEditor.faces', 'Faces:') }), jsx("span", { children: meshData.faces.size })] }), jsxs("div", { className: styles.stat, children: [jsx("span", { children: t('uvEditor.islands', 'Islands:') }), jsx("span", { children: meshData.islands.size })] }), jsxs("div", { className: styles.stat, children: [jsx("span", { children: t('uvEditor.selected', 'Selected:') }), jsx("span", { children: getSelectedVertices(meshData).length })] })] }), jsxs("div", { className: styles.panel, children: [jsx("h4", { className: styles.panelTitle, children: t('uvEditor.snapping', 'Snapping') }), jsxs("label", { className: styles.checkbox, children: [jsx("input", { type: "checkbox", checked: state.snapping, onChange: (e) => setState((prev) => ({
|
|
551
555
|
...prev,
|
|
552
556
|
snapping: e.target.checked,
|
|
553
|
-
})) }),
|
|
557
|
+
})) }), t('uvEditor.enableSnapping', 'Enable Snapping')] }), jsxs("label", { className: styles.rangeLabel, children: [t('uvEditor.snapSize', 'Snap Size: {snapSize}').replace('{snapSize}', String(state.snapSize.toFixed(4))), jsx("input", { type: "range", min: "0.001", max: "0.25", step: "0.001", value: state.snapSize, onChange: (e) => setState((prev) => ({
|
|
554
558
|
...prev,
|
|
555
559
|
snapSize: parseFloat(e.target.value),
|
|
556
560
|
})) })] })] })] })) : undefined;
|
|
557
|
-
return (jsxs("div", { style: { position: 'relative', width, height }, children: [jsx("span", { style: { position: 'absolute', top: 6, right: 6, zIndex: 30 }, children: jsx(NiceTutorialButton, { steps: resolveTutorial(tutorial, UV_EDITOR_TOUR) }) }), jsx(NiceAppShell, { className: `nice-uv-editor ${styles.uvEditor} ${className !== null && className !== void 0 ? className : ''}`, flush: true, toolbar: toolbarSlot, rightSidebar: sidebarSlot, statusBar: jsxs("div", { className: styles.statusBar, children: [
|
|
561
|
+
return (jsxs("div", { style: { position: 'relative', width, height }, children: [jsx("span", { style: { position: 'absolute', top: 6, right: 6, zIndex: 30 }, children: jsx(NiceTutorialButton, { steps: resolveTutorial(tutorial, UV_EDITOR_TOUR) }) }), jsx(NiceAppShell, { className: `nice-uv-editor ${styles.uvEditor} ${className !== null && className !== void 0 ? className : ''}`, flush: true, toolbar: toolbarSlot, rightSidebar: sidebarSlot, statusBar: jsxs("div", { className: styles.statusBar, children: [jsx("span", { children: t('uvEditor.tool', 'Tool: {activeTool}').replace('{activeTool}', String(state.activeTool)) }), jsx("span", { children: t('uvEditor.mode', 'Mode: {selectionMode}').replace('{selectionMode}', String(state.selectionMode)) }), jsx("span", { children: t('uvEditor.zoom', 'Zoom: {zoom}%').replace('{zoom}', String((state.zoom * 100).toFixed(0))) }), jsx("span", { children: t('uvEditor.channel', 'Channel: {activeChannel}').replace('{activeChannel}', String(state.activeChannel)) })] }), children: jsxs("div", { ref: containerRef, className: styles.viewport, onMouseDown: handleMouseDown, onMouseMove: handleMouseMove, onMouseUp: handleMouseUp, onMouseLeave: handleMouseUp, onWheel: handleWheel, children: [jsx("canvas", { ref: canvasRef, className: styles.canvas }), !meshData && (jsx("div", { className: styles.noGeometry, children: t('uvEditor.noGeometrySelected', 'No geometry selected') }))] }) })] }));
|
|
558
562
|
};
|
|
559
563
|
|
|
560
564
|
export { NiceUVEditor };
|
|
@@ -48,6 +48,7 @@ const DEFAULT_UV_DISPLAY = {
|
|
|
48
48
|
showDistortion: false,
|
|
49
49
|
distortionGradient: { low: 'var(--nice-success, #00ff00)', high: 'var(--nice-danger, #ff0000)' },
|
|
50
50
|
showSeams: true,
|
|
51
|
+
// audit-allow: literal-color — the seam colour setting's default
|
|
51
52
|
seamColor: '#ff6600',
|
|
52
53
|
highlightSelected: true,
|
|
53
54
|
selectionColor: 'var(--nice-warning, #ffaa00)',
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
/** A question an editor hook waits on; {@link EditorAskDialog} shows it. */
|
|
3
|
+
export interface EditorAsk {
|
|
4
|
+
/** `prompt` asks for a value, `confirm` for a yes / no. */
|
|
5
|
+
kind: 'prompt' | 'confirm';
|
|
6
|
+
/** The prompt's title, or the question to confirm. */
|
|
7
|
+
text: string;
|
|
8
|
+
/** Initial value of a prompt. */
|
|
9
|
+
defaultValue?: string;
|
|
10
|
+
/** Settles it: the entered value (`''` for an accepted confirm), or `null` when cancelled. */
|
|
11
|
+
answer: (value: string | null) => void;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The library's dialogs in place of `window.prompt` / `window.confirm` for the editor hooks,
|
|
15
|
+
* which hold state but render nothing: `ask` resolves with the answer (`null` when cancelled)
|
|
16
|
+
* and `pendingAsk` is the open question, for the editor component to pass to
|
|
17
|
+
* {@link EditorAskDialog}. One question at a time: a new one settles the open one as cancelled.
|
|
18
|
+
*/
|
|
19
|
+
export declare function useEditorAsk(): {
|
|
20
|
+
pendingAsk: EditorAsk | null;
|
|
21
|
+
ask: (kind: EditorAsk["kind"], text: string, defaultValue?: string) => Promise<string | null>;
|
|
22
|
+
};
|
|
23
|
+
/** Shows the question {@link useEditorAsk} is waiting on, if any. */
|
|
24
|
+
export declare const EditorAskDialog: React.FC<{
|
|
25
|
+
ask: EditorAsk | null;
|
|
26
|
+
}>;
|
|
@@ -1,26 +1,10 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* i18n — Lightweight translation context for @nice2dev/ui-3d.
|
|
3
|
-
*
|
|
4
|
-
* Drop-in replacement for react-i18next's useTranslation, requires NO
|
|
5
|
-
* external dependencies. Consumers can optionally wrap with NiceI18nProvider
|
|
6
|
-
* to supply their own translation function. Falls back to the default
|
|
7
|
-
* (English key strings) when no provider is present.
|
|
8
|
-
*
|
|
9
|
-
* Usage in components:
|
|
10
|
-
* const { t } = useNiceTranslation();
|
|
11
|
-
* <button>{t('actions.save', 'Save')}</button>
|
|
12
|
-
*
|
|
13
|
-
* Usage in apps:
|
|
14
|
-
* <NiceI18nProvider t={(key, fallback) => myI18n(key) ?? fallback}>
|
|
15
|
-
* <App />
|
|
16
|
-
* </NiceI18nProvider>
|
|
17
|
-
*/
|
|
18
1
|
import React from 'react';
|
|
19
2
|
export type NiceTranslateFn = (key: string, defaultValue: string) => string;
|
|
20
3
|
export interface NiceI18nProviderProps {
|
|
21
4
|
t: NiceTranslateFn;
|
|
22
5
|
children: React.ReactNode;
|
|
23
6
|
}
|
|
7
|
+
/** Overrides the app's global translator for the 3D controls below it. */
|
|
24
8
|
export declare const NiceI18nProvider: React.FC<NiceI18nProviderProps>;
|
|
25
9
|
export declare function useNiceTranslation(): {
|
|
26
10
|
t: NiceTranslateFn;
|
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* DanceScoreEngine — Compares detected poses against reference choreography
|
|
3
|
-
*
|
|
4
|
-
* Calculates real-time dance accuracy by comparing camera-detected Pose3DFrames
|
|
5
|
-
* against a reference sequence (from choreography DSL or recorded poses).
|
|
6
|
-
*/
|
|
7
1
|
import type { Pose3DFrame, Pose3DSequenceResult } from '../model/modelEditorTypes';
|
|
8
2
|
export interface DanceScoreConfig {
|
|
9
3
|
/** Weight per keypoint category (0–1). Keys not listed default to 1. */
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* Extracted from the original ModelEditor.tsx god-component.
|
|
7
7
|
*/
|
|
8
8
|
import * as THREE from 'three';
|
|
9
|
+
import { type EditorAsk } from '../core/editorAsk';
|
|
9
10
|
import { type SceneNode, type AnimClip, type TransformMode, type ShadingMode, type EditorMode, type PropTab, type AddMenuCat, type Pose3DSequenceResult } from './modelEditorTypes';
|
|
10
11
|
import { type PBRPreset, type HDRIPreset, type SnapResult } from './modelEditorUtils';
|
|
11
12
|
export interface UseModelEditorProps {
|
|
@@ -43,6 +44,8 @@ export interface ModelEditorAPI {
|
|
|
43
44
|
bgColor: string;
|
|
44
45
|
setBgColor: React.Dispatch<React.SetStateAction<string>>;
|
|
45
46
|
statusText: string;
|
|
47
|
+
/** The question the editor waits on (clear scene, library name); render it with `EditorAskDialog`. */
|
|
48
|
+
pendingAsk: EditorAsk | null;
|
|
46
49
|
dragOver: boolean;
|
|
47
50
|
polyCount: number;
|
|
48
51
|
meshCount: number;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nice2dev/ui-3d",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.2",
|
|
4
4
|
"description": "Nice2Dev 3D Editor — Blender-style 3D model editor component for React, powered by Three.js",
|
|
5
5
|
"author": "NiceToDev <contact@nicetodev.com>",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|
|
@@ -59,13 +59,13 @@
|
|
|
59
59
|
"nicetodev"
|
|
60
60
|
],
|
|
61
61
|
"peerDependencies": {
|
|
62
|
-
"@nice2dev/ui": "^1.0
|
|
62
|
+
"@nice2dev/ui": "^1.2.0",
|
|
63
63
|
"react": ">=17.0.0",
|
|
64
64
|
"react-dom": ">=17.0.0",
|
|
65
65
|
"three": ">=0.150.0"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
|
-
"@nice2dev/ui": "^1.2.
|
|
68
|
+
"@nice2dev/ui": "^1.2.2",
|
|
69
69
|
"@rollup/plugin-commonjs": "^29.0.3",
|
|
70
70
|
"@rollup/plugin-node-resolve": "^16.0.3",
|
|
71
71
|
"@rollup/plugin-typescript": "^12.3.0",
|