@nice2dev/ui-3d 1.2.0 → 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 (74) hide show
  1. package/CHANGELOG.md +103 -0
  2. package/dist/cjs/core/editorAsk.js +48 -0
  3. package/dist/cjs/core/i18n.js +7 -3
  4. package/dist/cjs/dance/DanceScoreEngine.js +7 -0
  5. package/dist/cjs/dance/PoseDetector.js +4 -2
  6. package/dist/cjs/material/NiceMaterialEditor.js +7 -3
  7. package/dist/cjs/material/materialEditorTypes.js +1 -1
  8. package/dist/cjs/material/materialNodeDefinitions.js +14 -11
  9. package/dist/cjs/model/ModelEditor.js +3 -2
  10. package/dist/cjs/model/ModelEditorLeftPanel.js +10 -4
  11. package/dist/cjs/model/ModelEditorMenuBar.js +7 -5
  12. package/dist/cjs/model/ModelEditorRightPanel.js +18 -16
  13. package/dist/cjs/model/ModelEditorSubComponents.js +8 -6
  14. package/dist/cjs/model/ModelEditorTimeline.js +6 -4
  15. package/dist/cjs/model/ModelEditorToolbar.js +6 -4
  16. package/dist/cjs/model/ModelEditorViewport.js +16 -6
  17. package/dist/cjs/model/ModelViewer.js +6 -4
  18. package/dist/cjs/model/NiceArmatureEditor.js +6 -4
  19. package/dist/cjs/model/NiceMorphTargetEditor.js +5 -3
  20. package/dist/cjs/model/NiceOctree.js +3 -1
  21. package/dist/cjs/model/NicePhysicsSimulation.js +6 -4
  22. package/dist/cjs/model/NiceProceduralGeometry.js +5 -3
  23. package/dist/cjs/model/NiceTerrainEditor.js +8 -6
  24. package/dist/cjs/model/NiceWeightPainter.js +4 -2
  25. package/dist/cjs/model/NiceXRPreview.js +4 -2
  26. package/dist/cjs/model/useModelEditor.js +22 -15
  27. package/dist/cjs/nice2dev-ui-3d.css +1 -1
  28. package/dist/cjs/particle/NiceParticleEditor.js +14 -9
  29. package/dist/cjs/rendering/NiceCascadedShadows.js +3 -1
  30. package/dist/cjs/rendering/NiceRenderExport.js +9 -3
  31. package/dist/cjs/rendering/NiceSSAO.js +3 -1
  32. package/dist/cjs/rendering/NiceSSR.js +3 -1
  33. package/dist/cjs/rendering/NiceWebGPURenderer.js +4 -2
  34. package/dist/cjs/uv/NiceUVEditor.js +16 -12
  35. package/dist/cjs/uv/uvEditorTypes.js +1 -0
  36. package/dist/esm/core/editorAsk.js +45 -0
  37. package/dist/esm/core/i18n.js +8 -4
  38. package/dist/esm/dance/DanceScoreEngine.js +7 -0
  39. package/dist/esm/dance/PoseDetector.js +4 -2
  40. package/dist/esm/material/NiceMaterialEditor.js +7 -3
  41. package/dist/esm/material/materialEditorTypes.js +1 -1
  42. package/dist/esm/material/materialNodeDefinitions.js +14 -11
  43. package/dist/esm/model/ModelEditor.js +3 -2
  44. package/dist/esm/model/ModelEditorLeftPanel.js +10 -4
  45. package/dist/esm/model/ModelEditorMenuBar.js +7 -5
  46. package/dist/esm/model/ModelEditorRightPanel.js +18 -16
  47. package/dist/esm/model/ModelEditorSubComponents.js +8 -6
  48. package/dist/esm/model/ModelEditorTimeline.js +6 -4
  49. package/dist/esm/model/ModelEditorToolbar.js +6 -4
  50. package/dist/esm/model/ModelEditorViewport.js +16 -6
  51. package/dist/esm/model/ModelViewer.js +6 -4
  52. package/dist/esm/model/NiceArmatureEditor.js +6 -4
  53. package/dist/esm/model/NiceMorphTargetEditor.js +5 -3
  54. package/dist/esm/model/NiceOctree.js +3 -1
  55. package/dist/esm/model/NicePhysicsSimulation.js +6 -4
  56. package/dist/esm/model/NiceProceduralGeometry.js +5 -3
  57. package/dist/esm/model/NiceTerrainEditor.js +8 -6
  58. package/dist/esm/model/NiceWeightPainter.js +4 -2
  59. package/dist/esm/model/NiceXRPreview.js +4 -2
  60. package/dist/esm/model/useModelEditor.js +22 -15
  61. package/dist/esm/nice2dev-ui-3d.css +1 -1
  62. package/dist/esm/particle/NiceParticleEditor.js +14 -9
  63. package/dist/esm/rendering/NiceCascadedShadows.js +3 -1
  64. package/dist/esm/rendering/NiceRenderExport.js +9 -3
  65. package/dist/esm/rendering/NiceSSAO.js +3 -1
  66. package/dist/esm/rendering/NiceSSR.js +3 -1
  67. package/dist/esm/rendering/NiceWebGPURenderer.js +4 -2
  68. package/dist/esm/uv/NiceUVEditor.js +16 -12
  69. package/dist/esm/uv/uvEditorTypes.js +1 -0
  70. package/dist/types/core/editorAsk.d.ts +26 -0
  71. package/dist/types/core/i18n.d.ts +1 -17
  72. package/dist/types/dance/DanceScoreEngine.d.ts +0 -6
  73. package/dist/types/model/useModelEditor.d.ts +3 -0
  74. package/package.json +3 -3
@@ -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 ── */
@@ -209,14 +210,15 @@ const TONE_MAPPINGS = [
209
210
  ];
210
211
  /* ── Component ── */
211
212
  const NiceWebGPURenderer = ({ canvas, scene, camera, width, height, onRendererReady, className = '', }) => {
213
+ const { t } = useNiceTranslation();
212
214
  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"] }), jsx("span", { style: {
215
+ return (jsxs("div", { className: `nice-webgpu-renderer ${className}`, style: { display: 'grid', gap: 8, fontSize: 13 }, children: [jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' }, children: [jsxs("strong", { children: [jsx(NiceIcon, { name: "monitor", size: 14 }), " ", t('webGPURenderer.renderer', 'Renderer')] }), jsx("span", { style: {
214
216
  fontSize: 10,
215
217
  padding: '2px 6px',
216
218
  borderRadius: 4,
217
219
  background: isWebGPU ? 'var(--nice-success, #22c55e)' : 'var(--nice-warning, #f59e0b)',
218
220
  color: 'var(--nice-color-white, #fff)',
219
- }, children: isWebGPU ? 'WebGPU' : 'WebGL' })] }), !isReady && jsx("div", { style: { opacity: 0.6, fontSize: 11 }, children: "Initializing\u2026" }), capabilities && (jsxs("div", { style: { fontSize: 10, opacity: 0.7, lineHeight: 1.5 }, children: ["GPU: ", capabilities.adapterName, jsx("br", {}), "Vendor: ", capabilities.vendor, jsx("br", {}), "Max Texture: ", capabilities.maxTextureDimension2D, "px", capabilities.features.length > 0 && (jsxs(Fragment, { children: [jsx("br", {}), "Features: ", capabilities.features.length] }))] })), jsxs("label", { children: ["Tone Mapping:", jsx("select", { value: toneMapping, onChange: (e) => setToneMapping(parseInt(e.target.value, 10)), style: { width: '100%', marginTop: 2 }, children: TONE_MAPPINGS.map((tm) => (jsx("option", { value: tm.value, children: tm.label }, tm.value))) })] }), jsxs("label", { children: ["Exposure: ", toneMappingExposure.toFixed(2), jsx("input", { type: "range", min: 0.1, max: 3, step: 0.05, value: toneMappingExposure, onChange: (e) => setToneMappingExposure(parseFloat(e.target.value)), style: { width: '100%' } })] }), jsxs("label", { children: ["Output Color Space:", jsxs("select", { value: outputColorSpace, onChange: (e) => setOutputColorSpace(e.target.value), style: { width: '100%', marginTop: 2 }, children: [jsx("option", { value: THREE.SRGBColorSpace, children: "sRGB" }), jsx("option", { value: THREE.LinearSRGBColorSpace, children: "Linear sRGB" })] })] }), jsxs("label", { children: ["Pixel Ratio: ", pixelRatio.toFixed(1), jsx("input", { type: "range", min: 0.5, max: 3, step: 0.5, value: pixelRatio, onChange: (e) => setPixelRatio(parseFloat(e.target.value)), style: { width: '100%' } })] })] }));
221
+ }, children: isWebGPU ? 'WebGPU' : 'WebGL' })] }), !isReady && jsx("div", { style: { opacity: 0.6, fontSize: 11 }, children: "Initializing\u2026" }), capabilities && (jsxs("div", { style: { fontSize: 10, opacity: 0.7, lineHeight: 1.5 }, children: ["GPU: ", capabilities.adapterName, jsx("br", {}), "Vendor: ", capabilities.vendor, jsx("br", {}), "Max Texture: ", capabilities.maxTextureDimension2D, "px", capabilities.features.length > 0 && (jsxs(Fragment, { children: [jsx("br", {}), "Features: ", capabilities.features.length] }))] })), jsxs("label", { children: ["Tone Mapping:", jsx("select", { value: toneMapping, onChange: (e) => setToneMapping(parseInt(e.target.value, 10)), style: { width: '100%', marginTop: 2 }, children: TONE_MAPPINGS.map((tm) => (jsx("option", { value: tm.value, children: tm.label }, tm.value))) })] }), jsxs("label", { children: ["Exposure: ", toneMappingExposure.toFixed(2), jsx("input", { type: "range", min: 0.1, max: 3, step: 0.05, value: toneMappingExposure, onChange: (e) => setToneMappingExposure(parseFloat(e.target.value)), style: { width: '100%' } })] }), jsxs("label", { children: ["Output Color Space:", jsxs("select", { value: outputColorSpace, onChange: (e) => setOutputColorSpace(e.target.value), style: { width: '100%', marginTop: 2 }, children: [jsx("option", { value: THREE.SRGBColorSpace, children: "sRGB" }), jsx("option", { value: THREE.LinearSRGBColorSpace, children: t('webGPURenderer.linearSrgb', 'Linear sRGB') })] })] }), jsxs("label", { children: ["Pixel Ratio: ", pixelRatio.toFixed(1), jsx("input", { type: "range", min: 0.5, max: 3, step: 0.5, value: pixelRatio, onChange: (e) => setPixelRatio(parseFloat(e.target.value)), style: { width: '100%' } })] })] }));
220
222
  };
221
223
 
222
224
  export { NiceWebGPURenderer, useWebGPURenderer };
@@ -1,6 +1,7 @@
1
1
  import { jsxs, jsx } from 'react/jsx-runtime';
2
2
  import { resolveCssColor, NiceIcon, NiceTutorialButton, resolveTutorial, NiceAppShell } from '@nice2dev/ui';
3
3
  import { useRef, useState, useEffect, useCallback } from 'react';
4
+ import { useNiceTranslation } from '../core/i18n.js';
4
5
  import styles from './UVEditor.module.css.js';
5
6
  import { DEFAULT_UV_STATE, DEFAULT_RELAX_OPTIONS, UV_TOOLS, UV_SELECTION_MODES, PROJECTION_TYPES, UV_CHANNELS, DEFAULT_PACKING_OPTIONS } from './uvEditorTypes.js';
6
7
  import { extractUVMeshData, uvSub, uvAdd, uvRotate, selectVerticesInRect, syncToGeometry, packIslands, applyIslandTransforms, relaxUVs, applyProjection, getSelectedVertices } from './uvEditorUtils.js';
@@ -20,6 +21,7 @@ const UV_EDITOR_TOUR = [
20
21
  Component
21
22
  ═══════════════════════════════════════════ */
22
23
  const NiceUVEditor = ({ geometry, texture, channel = 'uv', onUVChange, onSelectionChange, width = '100%', height = 600, showToolbar = true, showSidebar = true, className, readOnly = false, tutorial, }) => {
24
+ const { t } = useNiceTranslation();
23
25
  const canvasRef = useRef(null);
24
26
  const containerRef = useRef(null);
25
27
  // State
@@ -122,12 +124,14 @@ const NiceUVEditor = ({ geometry, texture, channel = 'uv', onUVChange, onSelecti
122
124
  ctx.closePath();
123
125
  ctx.stroke();
124
126
  }
125
- // Draw faces
127
+ // Draw faces — the two fills resolved once per frame, not once per face
128
+ const faceSelectedFill = resolveCssColor('var(--nice-warning-tint-30, rgba(255, 170, 0, 0.3))', ctx.canvas);
129
+ const faceFill = resolveCssColor('var(--nice-overlay-10, rgba(100, 100, 100, 0.1))', ctx.canvas);
126
130
  for (const face of meshData.faces.values()) {
127
131
  const verts = face.vertices.map((id) => meshData.vertices.get(id));
128
132
  const screenVerts = verts.map((v) => toScreen(v.position));
129
133
  // Fill
130
- ctx.fillStyle = face.selected ? 'rgba(255, 170, 0, 0.3)' : 'rgba(100, 100, 100, 0.1)';
134
+ ctx.fillStyle = face.selected ? faceSelectedFill : faceFill;
131
135
  ctx.beginPath();
132
136
  ctx.moveTo(screenVerts[0].x, screenVerts[0].y);
133
137
  screenVerts.slice(1).forEach((sv) => ctx.lineTo(sv.x, sv.y));
@@ -528,33 +532,33 @@ const NiceUVEditor = ({ geometry, texture, channel = 'uv', onUVChange, onSelecti
528
532
  setMeshData(newData);
529
533
  onUVChange === null || onUVChange === void 0 ? void 0 : onUVChange(geometry);
530
534
  }, [meshData, geometry, state.activeChannel, readOnly, pushUndo, onUVChange]);
531
- const toolbarSlot = showToolbar ? (jsxs("div", { className: styles.toolbar, children: [jsx("div", { className: styles.toolGroup, children: UV_TOOLS.slice(0, 5).map((tool) => (jsx("button", { className: `${styles.toolBtn} ${state.activeTool === tool ? styles.active : ''}`, onClick: () => setState((prev) => ({ ...prev, activeTool: tool })), title: tool, disabled: readOnly, children: tool[0].toUpperCase() }, tool))) }), jsx("div", { className: styles.toolGroup, children: UV_SELECTION_MODES.map((mode) => (jsx("button", { className: `${styles.toolBtn} ${state.selectionMode === mode ? styles.active : ''}`, onClick: () => setState((prev) => ({ ...prev, selectionMode: mode })), title: mode, children: mode[0].toUpperCase() }, mode))) }), jsxs("div", { className: styles.toolGroup, children: [jsx("button", { className: styles.toolBtn, onClick: handlePack, disabled: readOnly || !meshData, title: "Pack Islands (P)", children: "Pack" }), jsx("button", { className: styles.toolBtn, onClick: handleRelax, disabled: readOnly || !meshData, title: "Relax UVs", children: "Relax" })] }), jsx("div", { className: styles.toolGroup, children: jsxs("select", { className: styles.select, onChange: (e) => handleProject(e.target.value), disabled: readOnly || !meshData, defaultValue: "", children: [jsx("option", { value: "", disabled: true, children: "Project..." }), PROJECTION_TYPES.map((type) => (jsx("option", { value: type, children: type }, type)))] }) }), jsx("div", { className: styles.toolGroup, children: jsx("select", { className: styles.select, value: state.activeChannel, onChange: (e) => setState((prev) => ({
535
+ const toolbarSlot = showToolbar ? (jsxs("div", { className: styles.toolbar, children: [jsx("div", { className: styles.toolGroup, children: UV_TOOLS.slice(0, 5).map((tool) => (jsx("button", { className: `${styles.toolBtn} ${state.activeTool === tool ? styles.active : ''}`, onClick: () => setState((prev) => ({ ...prev, activeTool: tool })), title: tool, disabled: readOnly, children: tool[0].toUpperCase() }, tool))) }), jsx("div", { className: styles.toolGroup, children: UV_SELECTION_MODES.map((mode) => (jsx("button", { className: `${styles.toolBtn} ${state.selectionMode === mode ? styles.active : ''}`, onClick: () => setState((prev) => ({ ...prev, selectionMode: mode })), title: mode, children: mode[0].toUpperCase() }, mode))) }), jsxs("div", { className: styles.toolGroup, children: [jsx("button", { className: styles.toolBtn, onClick: handlePack, disabled: readOnly || !meshData, title: t('uvEditor.packIslandsP', 'Pack Islands (P)'), children: t('uvEditor.pack', 'Pack') }), jsx("button", { className: styles.toolBtn, onClick: handleRelax, disabled: readOnly || !meshData, title: t('uvEditor.relaxUvs', 'Relax UVs'), children: t('uvEditor.relax', 'Relax') })] }), jsx("div", { className: styles.toolGroup, children: jsxs("select", { className: styles.select, onChange: (e) => handleProject(e.target.value), disabled: readOnly || !meshData, defaultValue: "", children: [jsx("option", { value: "", disabled: true, children: t('uvEditor.project', 'Project...') }), PROJECTION_TYPES.map((type) => (jsx("option", { value: type, children: type }, type)))] }) }), jsx("div", { className: styles.toolGroup, children: jsx("select", { className: styles.select, value: state.activeChannel, onChange: (e) => setState((prev) => ({
532
536
  ...prev,
533
537
  activeChannel: e.target.value,
534
- })), children: UV_CHANNELS.map((ch) => (jsx("option", { value: ch, children: ch.toUpperCase() }, ch))) }) }), jsxs("div", { className: styles.toolGroup, children: [jsx("button", { className: styles.toolBtn, onClick: undo, disabled: undoStack.length === 0, title: "Undo (Ctrl+Z)", "aria-label": "Undo", children: jsx(NiceIcon, { name: "arrow-left", size: 16 }) }), jsx("button", { className: styles.toolBtn, onClick: redo, disabled: redoStack.length === 0, title: "Redo (Ctrl+Y)", "aria-label": "Redo", children: jsx(NiceIcon, { name: "arrow-right", size: 16 }) })] })] })) : undefined;
535
- const sidebarSlot = showSidebar && meshData ? (jsxs("div", { className: styles.sidebar, children: [jsxs("div", { className: styles.panel, children: [jsx("h4", { className: styles.panelTitle, children: "Display" }), jsxs("label", { className: styles.checkbox, children: [jsx("input", { type: "checkbox", checked: state.display.showGrid, onChange: (e) => setState((prev) => ({
538
+ })), children: UV_CHANNELS.map((ch) => (jsx("option", { value: ch, children: ch.toUpperCase() }, ch))) }) }), jsxs("div", { className: styles.toolGroup, children: [jsx("button", { className: styles.toolBtn, onClick: undo, disabled: undoStack.length === 0, title: t('uvEditor.undoCtrlZ', 'Undo (Ctrl+Z)'), "aria-label": t('uvEditor.undo', 'Undo'), children: jsx(NiceIcon, { name: "arrow-left", size: 16 }) }), jsx("button", { className: styles.toolBtn, onClick: redo, disabled: redoStack.length === 0, title: t('uvEditor.redoCtrlY', 'Redo (Ctrl+Y)'), "aria-label": t('uvEditor.redo', 'Redo'), children: jsx(NiceIcon, { name: "arrow-right", size: 16 }) })] })] })) : undefined;
539
+ const sidebarSlot = showSidebar && meshData ? (jsxs("div", { className: styles.sidebar, children: [jsxs("div", { className: styles.panel, children: [jsx("h4", { className: styles.panelTitle, children: t('uvEditor.display', 'Display') }), jsxs("label", { className: styles.checkbox, children: [jsx("input", { type: "checkbox", checked: state.display.showGrid, onChange: (e) => setState((prev) => ({
536
540
  ...prev,
537
541
  display: { ...prev.display, showGrid: e.target.checked },
538
- })) }), "Show Grid"] }), jsxs("label", { className: styles.checkbox, children: [jsx("input", { type: "checkbox", checked: state.display.showTexture, onChange: (e) => setState((prev) => ({
542
+ })) }), t('uvEditor.showGrid', 'Show Grid')] }), jsxs("label", { className: styles.checkbox, children: [jsx("input", { type: "checkbox", checked: state.display.showTexture, onChange: (e) => setState((prev) => ({
539
543
  ...prev,
540
544
  display: { ...prev.display, showTexture: e.target.checked },
541
- })) }), "Show Texture"] }), jsxs("label", { className: styles.checkbox, children: [jsx("input", { type: "checkbox", checked: state.display.showSeams, onChange: (e) => setState((prev) => ({
545
+ })) }), t('uvEditor.showTexture', 'Show Texture')] }), jsxs("label", { className: styles.checkbox, children: [jsx("input", { type: "checkbox", checked: state.display.showSeams, onChange: (e) => setState((prev) => ({
542
546
  ...prev,
543
547
  display: { ...prev.display, showSeams: e.target.checked },
544
- })) }), "Show Seams"] }), jsxs("label", { className: styles.checkbox, children: [jsx("input", { type: "checkbox", checked: state.display.showDistortion, onChange: (e) => setState((prev) => ({
548
+ })) }), t('uvEditor.showSeams', 'Show Seams')] }), jsxs("label", { className: styles.checkbox, children: [jsx("input", { type: "checkbox", checked: state.display.showDistortion, onChange: (e) => setState((prev) => ({
545
549
  ...prev,
546
550
  display: { ...prev.display, showDistortion: e.target.checked },
547
- })) }), "Show Distortion"] }), jsxs("label", { className: styles.checkbox, children: [jsx("input", { type: "checkbox", checked: state.display.showIslandBounds, onChange: (e) => setState((prev) => ({
551
+ })) }), t('uvEditor.showDistortion', 'Show Distortion')] }), jsxs("label", { className: styles.checkbox, children: [jsx("input", { type: "checkbox", checked: state.display.showIslandBounds, onChange: (e) => setState((prev) => ({
548
552
  ...prev,
549
553
  display: { ...prev.display, showIslandBounds: e.target.checked },
550
- })) }), "Show Island Bounds"] })] }), jsxs("div", { className: styles.panel, children: [jsx("h4", { className: styles.panelTitle, children: "Statistics" }), jsxs("div", { className: styles.stat, children: [jsx("span", { children: "Vertices:" }), jsx("span", { children: meshData.vertices.size })] }), jsxs("div", { className: styles.stat, children: [jsx("span", { children: "Faces:" }), jsx("span", { children: meshData.faces.size })] }), jsxs("div", { className: styles.stat, children: [jsx("span", { children: "Islands:" }), jsx("span", { children: meshData.islands.size })] }), jsxs("div", { className: styles.stat, children: [jsx("span", { children: "Selected:" }), jsx("span", { children: getSelectedVertices(meshData).length })] })] }), jsxs("div", { className: styles.panel, children: [jsx("h4", { className: styles.panelTitle, children: "Snapping" }), jsxs("label", { className: styles.checkbox, children: [jsx("input", { type: "checkbox", checked: state.snapping, onChange: (e) => setState((prev) => ({
554
+ })) }), t('uvEditor.showIslandBounds', 'Show Island Bounds')] })] }), jsxs("div", { className: styles.panel, children: [jsx("h4", { className: styles.panelTitle, children: t('uvEditor.statistics', 'Statistics') }), jsxs("div", { className: styles.stat, children: [jsx("span", { children: "Vertices:" }), jsx("span", { children: meshData.vertices.size })] }), jsxs("div", { className: styles.stat, children: [jsx("span", { children: "Faces:" }), jsx("span", { children: meshData.faces.size })] }), jsxs("div", { className: styles.stat, children: [jsx("span", { children: "Islands:" }), jsx("span", { children: meshData.islands.size })] }), jsxs("div", { className: styles.stat, children: [jsx("span", { children: "Selected:" }), jsx("span", { children: getSelectedVertices(meshData).length })] })] }), jsxs("div", { className: styles.panel, children: [jsx("h4", { className: styles.panelTitle, children: t('uvEditor.snapping', 'Snapping') }), jsxs("label", { className: styles.checkbox, children: [jsx("input", { type: "checkbox", checked: state.snapping, onChange: (e) => setState((prev) => ({
551
555
  ...prev,
552
556
  snapping: e.target.checked,
553
- })) }), "Enable Snapping"] }), jsxs("label", { className: styles.rangeLabel, children: ["Snap Size: ", state.snapSize.toFixed(4), jsx("input", { type: "range", min: "0.001", max: "0.25", step: "0.001", value: state.snapSize, onChange: (e) => setState((prev) => ({
557
+ })) }), t('uvEditor.enableSnapping', 'Enable Snapping')] }), jsxs("label", { className: styles.rangeLabel, children: ["Snap Size: ", state.snapSize.toFixed(4), jsx("input", { type: "range", min: "0.001", max: "0.25", step: "0.001", value: state.snapSize, onChange: (e) => setState((prev) => ({
554
558
  ...prev,
555
559
  snapSize: parseFloat(e.target.value),
556
560
  })) })] })] })] })) : undefined;
557
- return (jsxs("div", { style: { position: 'relative', width, height }, children: [jsx("span", { style: { position: 'absolute', top: 6, right: 6, zIndex: 30 }, children: jsx(NiceTutorialButton, { steps: resolveTutorial(tutorial, UV_EDITOR_TOUR) }) }), jsx(NiceAppShell, { className: `nice-uv-editor ${styles.uvEditor} ${className !== null && className !== void 0 ? className : ''}`, flush: true, toolbar: toolbarSlot, rightSidebar: sidebarSlot, statusBar: jsxs("div", { className: styles.statusBar, children: [jsxs("span", { children: ["Tool: ", state.activeTool] }), jsxs("span", { children: ["Mode: ", state.selectionMode] }), jsxs("span", { children: ["Zoom: ", (state.zoom * 100).toFixed(0), "%"] }), jsxs("span", { children: ["Channel: ", state.activeChannel] })] }), children: jsxs("div", { ref: containerRef, className: styles.viewport, onMouseDown: handleMouseDown, onMouseMove: handleMouseMove, onMouseUp: handleMouseUp, onMouseLeave: handleMouseUp, onWheel: handleWheel, children: [jsx("canvas", { ref: canvasRef, className: styles.canvas }), !meshData && jsx("div", { className: styles.noGeometry, children: "No geometry selected" })] }) })] }));
561
+ return (jsxs("div", { style: { position: 'relative', width, height }, children: [jsx("span", { style: { position: 'absolute', top: 6, right: 6, zIndex: 30 }, children: jsx(NiceTutorialButton, { steps: resolveTutorial(tutorial, UV_EDITOR_TOUR) }) }), jsx(NiceAppShell, { className: `nice-uv-editor ${styles.uvEditor} ${className !== null && className !== void 0 ? className : ''}`, flush: true, toolbar: toolbarSlot, rightSidebar: sidebarSlot, statusBar: jsxs("div", { className: styles.statusBar, children: [jsxs("span", { children: ["Tool: ", state.activeTool] }), jsxs("span", { children: ["Mode: ", state.selectionMode] }), jsxs("span", { children: ["Zoom: ", (state.zoom * 100).toFixed(0), "%"] }), jsxs("span", { children: ["Channel: ", state.activeChannel] })] }), children: jsxs("div", { ref: containerRef, className: styles.viewport, onMouseDown: handleMouseDown, onMouseMove: handleMouseMove, onMouseUp: handleMouseUp, onMouseLeave: handleMouseUp, onWheel: handleWheel, children: [jsx("canvas", { ref: canvasRef, className: styles.canvas }), !meshData && (jsx("div", { className: styles.noGeometry, children: t('uvEditor.noGeometrySelected', 'No geometry selected') }))] }) })] }));
558
562
  };
559
563
 
560
564
  export { NiceUVEditor };
@@ -48,6 +48,7 @@ const DEFAULT_UV_DISPLAY = {
48
48
  showDistortion: false,
49
49
  distortionGradient: { low: 'var(--nice-success, #00ff00)', high: 'var(--nice-danger, #ff0000)' },
50
50
  showSeams: true,
51
+ // audit-allow: literal-color — the seam colour setting's default
51
52
  seamColor: '#ff6600',
52
53
  highlightSelected: true,
53
54
  selectionColor: 'var(--nice-warning, #ffaa00)',
@@ -0,0 +1,26 @@
1
+ import React from 'react';
2
+ /** A question an editor hook waits on; {@link EditorAskDialog} shows it. */
3
+ export interface EditorAsk {
4
+ /** `prompt` asks for a value, `confirm` for a yes / no. */
5
+ kind: 'prompt' | 'confirm';
6
+ /** The prompt's title, or the question to confirm. */
7
+ text: string;
8
+ /** Initial value of a prompt. */
9
+ defaultValue?: string;
10
+ /** Settles it: the entered value (`''` for an accepted confirm), or `null` when cancelled. */
11
+ answer: (value: string | null) => void;
12
+ }
13
+ /**
14
+ * The library's dialogs in place of `window.prompt` / `window.confirm` for the editor hooks,
15
+ * which hold state but render nothing: `ask` resolves with the answer (`null` when cancelled)
16
+ * and `pendingAsk` is the open question, for the editor component to pass to
17
+ * {@link EditorAskDialog}. One question at a time: a new one settles the open one as cancelled.
18
+ */
19
+ export declare function useEditorAsk(): {
20
+ pendingAsk: EditorAsk | null;
21
+ ask: (kind: EditorAsk["kind"], text: string, defaultValue?: string) => Promise<string | null>;
22
+ };
23
+ /** Shows the question {@link useEditorAsk} is waiting on, if any. */
24
+ export declare const EditorAskDialog: React.FC<{
25
+ ask: EditorAsk | null;
26
+ }>;
@@ -1,26 +1,10 @@
1
- /**
2
- * i18n — Lightweight translation context for @nice2dev/ui-3d.
3
- *
4
- * Drop-in replacement for react-i18next's useTranslation, requires NO
5
- * external dependencies. Consumers can optionally wrap with NiceI18nProvider
6
- * to supply their own translation function. Falls back to the default
7
- * (English key strings) when no provider is present.
8
- *
9
- * Usage in components:
10
- * const { t } = useNiceTranslation();
11
- * <button>{t('actions.save', 'Save')}</button>
12
- *
13
- * Usage in apps:
14
- * <NiceI18nProvider t={(key, fallback) => myI18n(key) ?? fallback}>
15
- * <App />
16
- * </NiceI18nProvider>
17
- */
18
1
  import React from 'react';
19
2
  export type NiceTranslateFn = (key: string, defaultValue: string) => string;
20
3
  export interface NiceI18nProviderProps {
21
4
  t: NiceTranslateFn;
22
5
  children: React.ReactNode;
23
6
  }
7
+ /** Overrides the app's global translator for the 3D controls below it. */
24
8
  export declare const NiceI18nProvider: React.FC<NiceI18nProviderProps>;
25
9
  export declare function useNiceTranslation(): {
26
10
  t: NiceTranslateFn;
@@ -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.0",
3
+ "version": "1.2.1",
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.20",
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.0",
68
+ "@nice2dev/ui": "^1.2.1",
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",