@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 } from 'react/jsx-runtime';
2
2
  import { NiceIcon } from '@nice2dev/ui';
3
3
  import { useEffect, useMemo, useState, useRef, useCallback } from 'react';
4
4
  import * as THREE from 'three';
5
+ import { useNiceTranslation } from '../core/i18n.js';
5
6
 
6
7
  /* ── Octree implementation ── */
7
8
  const DEFAULT_CONFIG = {
@@ -324,6 +325,7 @@ function useOctree(scene, configOverride) {
324
325
  }
325
326
  /* ── Component ── */
326
327
  const NiceOctree = ({ scene, config, showDebug: initialShowDebug, className = '', }) => {
328
+ const { t } = useNiceTranslation();
327
329
  const octree = useOctree(scene, config);
328
330
  useEffect(() => {
329
331
  if (initialShowDebug !== undefined) {
@@ -337,7 +339,7 @@ const NiceOctree = ({ scene, config, showDebug: initialShowDebug, className = ''
337
339
  borderRadius: 4,
338
340
  padding: '6px 12px',
339
341
  cursor: 'pointer',
340
- }, children: [jsx(NiceIcon, { name: "zap", size: 14 }), " Build Octree"] }), jsxs("div", { style: { fontSize: 11, opacity: 0.7 }, children: ["Nodes: ", octree.nodeCount, " | Objects: ", octree.objectCount] }), jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 6 }, children: [jsx("input", { type: "checkbox", checked: octree.showDebug, onChange: (e) => octree.setShowDebug(e.target.checked) }), "Show debug wireframes"] })] }));
342
+ }, children: [jsx(NiceIcon, { name: "zap", size: 14 }), " ", t('octree.buildOctree', 'Build Octree')] }), jsxs("div", { style: { fontSize: 11, opacity: 0.7 }, children: ["Nodes: ", octree.nodeCount, " | Objects: ", octree.objectCount] }), jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 6 }, children: [jsx("input", { type: "checkbox", checked: octree.showDebug, onChange: (e) => octree.setShowDebug(e.target.checked) }), t('octree.showDebugWireframes', 'Show debug wireframes')] })] }));
341
343
  };
342
344
 
343
345
  export { NiceOctree, useOctree };
@@ -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
 
6
7
  function inferColliderShape(object) {
7
8
  if (object instanceof THREE.Mesh) {
@@ -261,11 +262,12 @@ function usePhysicsSimulation(scene, onSimulationStep) {
261
262
  }
262
263
  /* ── Component ── */
263
264
  const NicePhysicsSimulation = ({ scene, selectedObject, onSimulationStep, className = '', }) => {
265
+ const { t } = useNiceTranslation();
264
266
  const { worldConfig, setWorldConfig, bodies, addBody, removeBody, updateBody, getBodyForObject, isRunning, play, pause, step, reset, debugDraw, setDebugDraw, } = usePhysicsSimulation(scene, onSimulationStep);
265
267
  const selectedBody = selectedObject
266
268
  ? getBodyForObject(selectedObject.name || selectedObject.uuid)
267
269
  : undefined;
268
- return (jsxs("div", { className: `nice-physics-simulation ${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: "zap", size: 14 }), " Physics"] }), jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 4, fontSize: 11 }, children: [jsx("input", { type: "checkbox", checked: debugDraw, onChange: (e) => setDebugDraw(e.target.checked) }), "Debug"] })] }), jsxs("div", { style: { display: 'flex', gap: 4 }, children: [jsx("button", { type: "button", onClick: isRunning ? pause : play, style: {
270
+ return (jsxs("div", { className: `nice-physics-simulation ${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: "zap", size: 14 }), " ", t('physicsSimulation.physics', 'Physics')] }), jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 4, fontSize: 11 }, children: [jsx("input", { type: "checkbox", checked: debugDraw, onChange: (e) => setDebugDraw(e.target.checked) }), t('physicsSimulation.debug', 'Debug')] })] }), jsxs("div", { style: { display: 'flex', gap: 4 }, children: [jsx("button", { type: "button", onClick: isRunning ? pause : play, style: {
269
271
  flex: 1,
270
272
  background: isRunning ? 'var(--nice-danger, #ef4444)' : 'var(--nice-success, #22c55e)',
271
273
  color: 'var(--nice-color-white, #fff)',
@@ -273,7 +275,7 @@ const NicePhysicsSimulation = ({ scene, selectedObject, onSimulationStep, classN
273
275
  borderRadius: 4,
274
276
  padding: '5px 8px',
275
277
  cursor: 'pointer',
276
- }, children: isRunning ? (jsxs(Fragment, { children: [jsx(NiceIcon, { name: "pause", size: 14 }), " Pause"] })) : (jsxs(Fragment, { children: [jsx(NiceIcon, { name: "play", size: 14 }), " Play"] })) }), jsxs("button", { type: "button", onClick: step, disabled: isRunning, style: { padding: '5px 8px', cursor: 'pointer' }, children: [jsx(NiceIcon, { name: "skip-forward", size: 14 }), " Step"] }), jsxs("button", { type: "button", onClick: reset, style: { padding: '5px 8px', cursor: 'pointer' }, children: [jsx(NiceIcon, { name: "refresh", size: 14 }), " Reset"] })] }), jsxs("label", { children: ["Gravity Y: ", worldConfig.gravity.y.toFixed(1), jsx("input", { type: "range", min: -20, max: 0, step: 0.1, value: worldConfig.gravity.y, onChange: (e) => setWorldConfig({ gravity: new THREE.Vector3(0, parseFloat(e.target.value), 0) }), style: { width: '100%' } })] }), selectedObject && (jsxs("fieldset", { style: {
278
+ }, children: isRunning ? (jsxs(Fragment, { children: [jsx(NiceIcon, { name: "pause", size: 14 }), " ", t('physicsSimulation.pause', 'Pause')] })) : (jsxs(Fragment, { children: [jsx(NiceIcon, { name: "play", size: 14 }), " ", t('physicsSimulation.play', 'Play')] })) }), jsxs("button", { type: "button", onClick: step, disabled: isRunning, style: { padding: '5px 8px', cursor: 'pointer' }, children: [jsx(NiceIcon, { name: "skip-forward", size: 14 }), " ", t('physicsSimulation.step', 'Step')] }), jsxs("button", { type: "button", onClick: reset, style: { padding: '5px 8px', cursor: 'pointer' }, children: [jsx(NiceIcon, { name: "refresh", size: 14 }), " ", t('physicsSimulation.reset', 'Reset')] })] }), jsxs("label", { children: ["Gravity Y: ", worldConfig.gravity.y.toFixed(1), jsx("input", { type: "range", min: -20, max: 0, step: 0.1, value: worldConfig.gravity.y, onChange: (e) => setWorldConfig({ gravity: new THREE.Vector3(0, parseFloat(e.target.value), 0) }), style: { width: '100%' } })] }), selectedObject && (jsxs("fieldset", { style: {
277
279
  border: '1px solid var(--nice-border, #e2e8f0)',
278
280
  borderRadius: 6,
279
281
  padding: 6,
@@ -286,7 +288,7 @@ const NicePhysicsSimulation = ({ scene, selectedObject, onSimulationStep, classN
286
288
  borderRadius: 4,
287
289
  padding: '5px 8px',
288
290
  cursor: 'pointer',
289
- }, children: "+ Add Rigid Body" })) : (jsxs(Fragment, { children: [jsxs("label", { children: ["Type:", jsxs("select", { value: selectedBody.bodyType, onChange: (e) => updateBody(selectedBody.id, { bodyType: e.target.value }), style: { marginLeft: 8, width: '100%' }, children: [jsx("option", { value: "dynamic", children: "Dynamic" }), jsx("option", { value: "static", children: "Static" }), jsx("option", { value: "kinematic", children: "Kinematic" })] })] }), jsxs("label", { children: ["Shape:", jsxs("select", { value: selectedBody.colliderShape, onChange: (e) => updateBody(selectedBody.id, { colliderShape: e.target.value }), style: { marginLeft: 8, width: '100%' }, children: [jsx("option", { value: "box", children: "Box" }), jsx("option", { value: "sphere", children: "Sphere" }), jsx("option", { value: "capsule", children: "Capsule" }), jsx("option", { value: "convexHull", children: "Convex Hull" }), jsx("option", { value: "trimesh", children: "Trimesh" })] })] }), jsxs("label", { children: ["Mass: ", selectedBody.mass.toFixed(1), jsx("input", { type: "range", min: 0.1, max: 100, step: 0.1, value: selectedBody.mass, onChange: (e) => updateBody(selectedBody.id, { mass: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Restitution: ", selectedBody.restitution.toFixed(2), jsx("input", { type: "range", min: 0, max: 1, step: 0.01, value: selectedBody.restitution, onChange: (e) => updateBody(selectedBody.id, { restitution: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Friction: ", selectedBody.friction.toFixed(2), jsx("input", { type: "range", min: 0, max: 1, step: 0.01, value: selectedBody.friction, onChange: (e) => updateBody(selectedBody.id, { friction: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 6 }, children: [jsx("input", { type: "checkbox", checked: selectedBody.isSensor, onChange: (e) => updateBody(selectedBody.id, { isSensor: e.target.checked }) }), "Sensor (trigger only)"] }), jsx("button", { type: "button", onClick: () => removeBody(selectedBody.id), style: {
291
+ }, children: "+ Add Rigid Body" })) : (jsxs(Fragment, { children: [jsxs("label", { children: ["Type:", jsxs("select", { value: selectedBody.bodyType, onChange: (e) => updateBody(selectedBody.id, { bodyType: e.target.value }), style: { marginLeft: 8, width: '100%' }, children: [jsx("option", { value: "dynamic", children: t('physicsSimulation.dynamic', 'Dynamic') }), jsx("option", { value: "static", children: t('physicsSimulation.static', 'Static') }), jsx("option", { value: "kinematic", children: t('physicsSimulation.kinematic', 'Kinematic') })] })] }), jsxs("label", { children: ["Shape:", jsxs("select", { value: selectedBody.colliderShape, onChange: (e) => updateBody(selectedBody.id, { colliderShape: e.target.value }), style: { marginLeft: 8, width: '100%' }, children: [jsx("option", { value: "box", children: "Box" }), jsx("option", { value: "sphere", children: t('physicsSimulation.sphere', 'Sphere') }), jsx("option", { value: "capsule", children: t('physicsSimulation.capsule', 'Capsule') }), jsx("option", { value: "convexHull", children: t('physicsSimulation.convexHull', 'Convex Hull') }), jsx("option", { value: "trimesh", children: t('physicsSimulation.trimesh', 'Trimesh') })] })] }), jsxs("label", { children: ["Mass: ", selectedBody.mass.toFixed(1), jsx("input", { type: "range", min: 0.1, max: 100, step: 0.1, value: selectedBody.mass, onChange: (e) => updateBody(selectedBody.id, { mass: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Restitution: ", selectedBody.restitution.toFixed(2), jsx("input", { type: "range", min: 0, max: 1, step: 0.01, value: selectedBody.restitution, onChange: (e) => updateBody(selectedBody.id, { restitution: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Friction: ", selectedBody.friction.toFixed(2), jsx("input", { type: "range", min: 0, max: 1, step: 0.01, value: selectedBody.friction, onChange: (e) => updateBody(selectedBody.id, { friction: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 6 }, children: [jsx("input", { type: "checkbox", checked: selectedBody.isSensor, onChange: (e) => updateBody(selectedBody.id, { isSensor: e.target.checked }) }), "Sensor (trigger only)"] }), jsx("button", { type: "button", onClick: () => removeBody(selectedBody.id), style: {
290
292
  background: 'var(--nice-danger, #ef4444)',
291
293
  color: 'var(--nice-text-on-primary, #fff)',
292
294
  border: 'none',
@@ -294,7 +296,7 @@ const NicePhysicsSimulation = ({ scene, selectedObject, onSimulationStep, classN
294
296
  padding: '4px 8px',
295
297
  cursor: 'pointer',
296
298
  fontSize: 11,
297
- }, children: "Remove Rigid Body" })] }))] })), jsxs("div", { style: { fontSize: 11, opacity: 0.7 }, children: ["Total bodies: ", bodies.length] })] }));
299
+ }, children: t('physicsSimulation.removeRigidBody', 'Remove Rigid Body') })] }))] })), jsxs("div", { style: { fontSize: 11, opacity: 0.7 }, children: ["Total bodies: ", bodies.length] })] }));
298
300
  };
299
301
 
300
302
  export { NicePhysicsSimulation, usePhysicsSimulation };
@@ -2,6 +2,7 @@ import { jsxs, jsx } from 'react/jsx-runtime';
2
2
  import { NiceIcon } from '@nice2dev/ui';
3
3
  import { useState, useCallback } from 'react';
4
4
  import * as THREE from 'three';
5
+ import { useNiceTranslation } from '../core/i18n.js';
5
6
 
6
7
  /* ── Generators ── */
7
8
  function seededRng(seed) {
@@ -292,21 +293,22 @@ function useProceduralGeometry(scene, onGenerated) {
292
293
  }
293
294
  /* ── Component ── */
294
295
  const NiceProceduralGeometry = ({ scene, onGenerated, className = '', }) => {
296
+ const { t } = useNiceTranslation();
295
297
  const { type, setType, treeParams, setTreeParams, rockParams, setRockParams, buildingParams, setBuildingParams, gearParams, setGearParams, generate, } = useProceduralGeometry(scene, onGenerated);
296
- return (jsxs("div", { className: `nice-procedural-geometry ${className}`, style: { display: 'grid', gap: 8, fontSize: 13 }, children: [jsx("strong", { children: "Procedural Geometry" }), jsxs("select", { value: type, onChange: (e) => setType(e.target.value), style: { width: '100%', padding: '4px 8px' }, children: [jsx("option", { value: "tree", children: "Tree" }), jsx("option", { value: "rock", children: "Rock" }), jsx("option", { value: "building", children: "Building" }), jsx("option", { value: "pipe", children: "Pipe" }), jsx("option", { value: "gear", children: "Gear" })] }), type === 'tree' && (jsxs("div", { style: { display: 'grid', gap: 4 }, children: [jsxs("label", { children: ["Height: ", treeParams.trunkHeight.toFixed(1), jsx("input", { type: "range", min: 1, max: 15, step: 0.5, value: treeParams.trunkHeight, onChange: (e) => setTreeParams({ ...treeParams, trunkHeight: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Branches: ", treeParams.branches, jsx("input", { type: "range", min: 1, max: 6, value: treeParams.branches, onChange: (e) => setTreeParams({ ...treeParams, branches: parseInt(e.target.value, 10) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Depth: ", treeParams.depth, jsx("input", { type: "range", min: 1, max: 5, value: treeParams.depth, onChange: (e) => setTreeParams({ ...treeParams, depth: parseInt(e.target.value, 10) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Angle: ", treeParams.branchAngle, "\u00B0", jsx("input", { type: "range", min: 10, max: 80, value: treeParams.branchAngle, onChange: (e) => setTreeParams({ ...treeParams, branchAngle: parseInt(e.target.value, 10) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Seed: ", treeParams.seed, jsx("input", { type: "range", min: 1, max: 999, value: treeParams.seed, onChange: (e) => setTreeParams({ ...treeParams, seed: parseInt(e.target.value, 10) }), style: { width: '100%' } })] })] })), type === 'rock' && (jsxs("div", { style: { display: 'grid', gap: 4 }, children: [jsxs("label", { children: ["Radius: ", rockParams.radius.toFixed(1), jsx("input", { type: "range", min: 0.2, max: 5, step: 0.1, value: rockParams.radius, onChange: (e) => setRockParams({ ...rockParams, radius: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Detail: ", rockParams.detail, jsx("input", { type: "range", min: 1, max: 5, value: rockParams.detail, onChange: (e) => setRockParams({ ...rockParams, detail: parseInt(e.target.value, 10) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Noise: ", rockParams.noiseAmount.toFixed(2), jsx("input", { type: "range", min: 0, max: 0.5, step: 0.01, value: rockParams.noiseAmount, onChange: (e) => setRockParams({ ...rockParams, noiseAmount: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Seed: ", rockParams.seed, jsx("input", { type: "range", min: 1, max: 999, value: rockParams.seed, onChange: (e) => setRockParams({ ...rockParams, seed: parseInt(e.target.value, 10) }), style: { width: '100%' } })] })] })), type === 'building' && (jsxs("div", { style: { display: 'grid', gap: 4 }, children: [jsxs("label", { children: ["Width: ", buildingParams.width, jsx("input", { type: "range", min: 4, max: 20, step: 1, value: buildingParams.width, onChange: (e) => setBuildingParams({ ...buildingParams, width: parseInt(e.target.value, 10) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Floors: ", buildingParams.floors, jsx("input", { type: "range", min: 1, max: 20, value: buildingParams.floors, onChange: (e) => setBuildingParams({ ...buildingParams, floors: parseInt(e.target.value, 10) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Windows/Floor: ", buildingParams.windowsPerFloor, jsx("input", { type: "range", min: 1, max: 10, value: buildingParams.windowsPerFloor, onChange: (e) => setBuildingParams({
298
+ return (jsxs("div", { className: `nice-procedural-geometry ${className}`, style: { display: 'grid', gap: 8, fontSize: 13 }, children: [jsx("strong", { children: t('proceduralGeometry.proceduralGeometry', 'Procedural Geometry') }), jsxs("select", { value: type, onChange: (e) => setType(e.target.value), style: { width: '100%', padding: '4px 8px' }, children: [jsx("option", { value: "tree", children: t('proceduralGeometry.tree', 'Tree') }), jsx("option", { value: "rock", children: t('proceduralGeometry.rock', 'Rock') }), jsx("option", { value: "building", children: t('proceduralGeometry.building', 'Building') }), jsx("option", { value: "pipe", children: t('proceduralGeometry.pipe', 'Pipe') }), jsx("option", { value: "gear", children: t('proceduralGeometry.gear', 'Gear') })] }), type === 'tree' && (jsxs("div", { style: { display: 'grid', gap: 4 }, children: [jsxs("label", { children: ["Height: ", treeParams.trunkHeight.toFixed(1), jsx("input", { type: "range", min: 1, max: 15, step: 0.5, value: treeParams.trunkHeight, onChange: (e) => setTreeParams({ ...treeParams, trunkHeight: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Branches: ", treeParams.branches, jsx("input", { type: "range", min: 1, max: 6, value: treeParams.branches, onChange: (e) => setTreeParams({ ...treeParams, branches: parseInt(e.target.value, 10) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Depth: ", treeParams.depth, jsx("input", { type: "range", min: 1, max: 5, value: treeParams.depth, onChange: (e) => setTreeParams({ ...treeParams, depth: parseInt(e.target.value, 10) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Angle: ", treeParams.branchAngle, "\u00B0", jsx("input", { type: "range", min: 10, max: 80, value: treeParams.branchAngle, onChange: (e) => setTreeParams({ ...treeParams, branchAngle: parseInt(e.target.value, 10) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Seed: ", treeParams.seed, jsx("input", { type: "range", min: 1, max: 999, value: treeParams.seed, onChange: (e) => setTreeParams({ ...treeParams, seed: parseInt(e.target.value, 10) }), style: { width: '100%' } })] })] })), type === 'rock' && (jsxs("div", { style: { display: 'grid', gap: 4 }, children: [jsxs("label", { children: ["Radius: ", rockParams.radius.toFixed(1), jsx("input", { type: "range", min: 0.2, max: 5, step: 0.1, value: rockParams.radius, onChange: (e) => setRockParams({ ...rockParams, radius: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Detail: ", rockParams.detail, jsx("input", { type: "range", min: 1, max: 5, value: rockParams.detail, onChange: (e) => setRockParams({ ...rockParams, detail: parseInt(e.target.value, 10) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Noise: ", rockParams.noiseAmount.toFixed(2), jsx("input", { type: "range", min: 0, max: 0.5, step: 0.01, value: rockParams.noiseAmount, onChange: (e) => setRockParams({ ...rockParams, noiseAmount: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Seed: ", rockParams.seed, jsx("input", { type: "range", min: 1, max: 999, value: rockParams.seed, onChange: (e) => setRockParams({ ...rockParams, seed: parseInt(e.target.value, 10) }), style: { width: '100%' } })] })] })), type === 'building' && (jsxs("div", { style: { display: 'grid', gap: 4 }, children: [jsxs("label", { children: ["Width: ", buildingParams.width, jsx("input", { type: "range", min: 4, max: 20, step: 1, value: buildingParams.width, onChange: (e) => setBuildingParams({ ...buildingParams, width: parseInt(e.target.value, 10) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Floors: ", buildingParams.floors, jsx("input", { type: "range", min: 1, max: 20, value: buildingParams.floors, onChange: (e) => setBuildingParams({ ...buildingParams, floors: parseInt(e.target.value, 10) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Windows/Floor: ", buildingParams.windowsPerFloor, jsx("input", { type: "range", min: 1, max: 10, value: buildingParams.windowsPerFloor, onChange: (e) => setBuildingParams({
297
299
  ...buildingParams,
298
300
  windowsPerFloor: parseInt(e.target.value, 10),
299
301
  }), style: { width: '100%' } })] }), jsxs("label", { children: ["Roof:", jsxs("select", { value: buildingParams.roofStyle, onChange: (e) => setBuildingParams({
300
302
  ...buildingParams,
301
303
  roofStyle: e.target.value,
302
- }), style: { marginLeft: 8 }, children: [jsx("option", { value: "flat", children: "Flat" }), jsx("option", { value: "gable", children: "Gable" }), jsx("option", { value: "hip", children: "Hip" })] })] })] })), type === 'gear' && (jsxs("div", { style: { display: 'grid', gap: 4 }, children: [jsxs("label", { children: ["Outer Radius: ", gearParams.outerRadius.toFixed(1), jsx("input", { type: "range", min: 0.5, max: 5, step: 0.1, value: gearParams.outerRadius, onChange: (e) => setGearParams({ ...gearParams, outerRadius: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Teeth: ", gearParams.teeth, jsx("input", { type: "range", min: 4, max: 30, value: gearParams.teeth, onChange: (e) => setGearParams({ ...gearParams, teeth: parseInt(e.target.value, 10) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Tooth Depth: ", gearParams.toothDepth.toFixed(2), jsx("input", { type: "range", min: 0.05, max: 1, step: 0.05, value: gearParams.toothDepth, onChange: (e) => setGearParams({ ...gearParams, toothDepth: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Thickness: ", gearParams.thickness.toFixed(2), jsx("input", { type: "range", min: 0.1, max: 2, step: 0.1, value: gearParams.thickness, onChange: (e) => setGearParams({ ...gearParams, thickness: parseFloat(e.target.value) }), style: { width: '100%' } })] })] })), jsxs("button", { type: "button", onClick: generate, style: {
304
+ }), style: { marginLeft: 8 }, children: [jsx("option", { value: "flat", children: t('proceduralGeometry.flat', 'Flat') }), jsx("option", { value: "gable", children: t('proceduralGeometry.gable', 'Gable') }), jsx("option", { value: "hip", children: "Hip" })] })] })] })), type === 'gear' && (jsxs("div", { style: { display: 'grid', gap: 4 }, children: [jsxs("label", { children: ["Outer Radius: ", gearParams.outerRadius.toFixed(1), jsx("input", { type: "range", min: 0.5, max: 5, step: 0.1, value: gearParams.outerRadius, onChange: (e) => setGearParams({ ...gearParams, outerRadius: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Teeth: ", gearParams.teeth, jsx("input", { type: "range", min: 4, max: 30, value: gearParams.teeth, onChange: (e) => setGearParams({ ...gearParams, teeth: parseInt(e.target.value, 10) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Tooth Depth: ", gearParams.toothDepth.toFixed(2), jsx("input", { type: "range", min: 0.05, max: 1, step: 0.05, value: gearParams.toothDepth, onChange: (e) => setGearParams({ ...gearParams, toothDepth: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Thickness: ", gearParams.thickness.toFixed(2), jsx("input", { type: "range", min: 0.1, max: 2, step: 0.1, value: gearParams.thickness, onChange: (e) => setGearParams({ ...gearParams, thickness: parseFloat(e.target.value) }), style: { width: '100%' } })] })] })), jsxs("button", { type: "button", onClick: generate, style: {
303
305
  background: 'var(--nice-color-accent, #6366f1)',
304
306
  color: 'var(--nice-text-on-primary, #fff)',
305
307
  border: 'none',
306
308
  borderRadius: 4,
307
309
  padding: '6px 12px',
308
310
  cursor: 'pointer',
309
- }, children: [jsx(NiceIcon, { name: "wand", size: 14 }), " Generate"] })] }));
311
+ }, children: [jsx(NiceIcon, { name: "wand", size: 14 }), " ", t('proceduralGeometry.generate', 'Generate')] })] }));
310
312
  };
311
313
 
312
314
  export { NiceProceduralGeometry, generateBuilding, generateGear, generatePipe, generateRock, generateTree, useProceduralGeometry };
@@ -2,6 +2,7 @@ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
2
2
  import { NiceTutorialButton, resolveTutorial, 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
 
6
7
  /** Built-in guided tour (EN inline + i18n keys → PL and other locales). */
7
8
  const TERRAIN_EDITOR_TOUR = [
@@ -319,6 +320,7 @@ function useTerrainEditor(scene, camera, canvas, config = { width: 100, depth: 1
319
320
  }
320
321
  /* ── Component ── */
321
322
  const NiceTerrainEditor = ({ scene, camera, canvas, config: configProp, onTerrainChange, className = '', tutorial, }) => {
323
+ const { t } = useNiceTranslation();
322
324
  const config = {
323
325
  width: 100,
324
326
  depth: 100,
@@ -329,14 +331,14 @@ const NiceTerrainEditor = ({ scene, camera, canvas, config: configProp, onTerrai
329
331
  const { brush, setBrush, isPainting, startPainting, stopPainting, generateFlat, generateNoise, exportHeightmap, textureLayers, setTextureLayer, activeTextureLayer, setActiveTextureLayer, } = useTerrainEditor(scene, camera, canvas, config, onTerrainChange);
330
332
  const [noiseOctaves, setNoiseOctaves] = useState(6);
331
333
  const [noisePersistence, setNoisePersistence] = useState(0.5);
332
- return (jsxs("div", { className: `nice-terrain-editor ${className}`, style: { position: 'relative', display: 'grid', gap: 8, fontSize: 13 }, children: [jsx("span", { style: { position: 'absolute', top: 0, right: 0, zIndex: 20 }, children: jsx(NiceTutorialButton, { steps: resolveTutorial(tutorial, TERRAIN_EDITOR_TOUR) }) }), jsx("strong", { children: "Terrain Editor" }), jsxs("fieldset", { style: {
334
+ return (jsxs("div", { className: `nice-terrain-editor ${className}`, style: { position: 'relative', display: 'grid', gap: 8, fontSize: 13 }, children: [jsx("span", { style: { position: 'absolute', top: 0, right: 0, zIndex: 20 }, children: jsx(NiceTutorialButton, { steps: resolveTutorial(tutorial, TERRAIN_EDITOR_TOUR) }) }), jsx("strong", { children: t('terrainEditor.terrainEditor', 'Terrain Editor') }), jsxs("fieldset", { style: {
333
335
  border: '1px solid var(--nice-border, #e2e8f0)',
334
336
  borderRadius: 6,
335
337
  padding: 4,
336
338
  display: 'flex',
337
339
  flexWrap: 'wrap',
338
340
  gap: 2,
339
- }, children: [jsx("legend", { style: { fontSize: 11 }, children: "Brush" }), ['raise', 'lower', 'flatten', 'smooth', 'noise', 'paint'].map((m) => (jsx("button", { type: "button", onClick: () => setBrush({ mode: m }), style: {
341
+ }, children: [jsx("legend", { style: { fontSize: 11 }, children: t('terrainEditor.brush', 'Brush') }), ['raise', 'lower', 'flatten', 'smooth', 'noise', 'paint'].map((m) => (jsx("button", { type: "button", onClick: () => setBrush({ mode: m }), style: {
340
342
  padding: '3px 6px',
341
343
  fontSize: 11,
342
344
  cursor: 'pointer',
@@ -353,19 +355,19 @@ const NiceTerrainEditor = ({ scene, camera, canvas, config: configProp, onTerrai
353
355
  borderRadius: 4,
354
356
  padding: '6px 12px',
355
357
  cursor: 'pointer',
356
- }, children: isPainting ? (jsxs(Fragment, { children: [jsx(NiceIcon, { name: "stop", size: 14 }), " Stop Painting"] })) : (jsxs(Fragment, { children: [jsx(NiceIcon, { name: "brush", size: 14 }), " Start Painting"] })) }), jsxs("fieldset", { style: {
358
+ }, children: isPainting ? (jsxs(Fragment, { children: [jsx(NiceIcon, { name: "stop", size: 14 }), " ", t('terrainEditor.stopPainting', 'Stop Painting')] })) : (jsxs(Fragment, { children: [jsx(NiceIcon, { name: "brush", size: 14 }), " ", t('terrainEditor.startPainting', 'Start Painting')] })) }), jsxs("fieldset", { style: {
357
359
  border: '1px solid var(--nice-border, #e2e8f0)',
358
360
  borderRadius: 6,
359
361
  padding: 6,
360
362
  display: 'grid',
361
363
  gap: 4,
362
- }, children: [jsx("legend", { style: { fontSize: 11 }, children: "Procedural" }), jsxs("label", { children: ["Octaves: ", noiseOctaves, jsx("input", { type: "range", min: 1, max: 10, value: noiseOctaves, onChange: (e) => setNoiseOctaves(parseInt(e.target.value, 10)), style: { width: '100%' } })] }), jsxs("label", { children: ["Persistence: ", noisePersistence.toFixed(2), jsx("input", { type: "range", min: 0.1, max: 1, step: 0.05, value: noisePersistence, onChange: (e) => setNoisePersistence(parseFloat(e.target.value)), style: { width: '100%' } })] }), jsxs("div", { style: { display: 'flex', gap: 4 }, children: [jsxs("button", { type: "button", onClick: () => generateNoise(noiseOctaves, noisePersistence), style: { flex: 1 }, children: [jsx(NiceIcon, { name: "mountain", size: 14 }), " Generate Noise"] }), jsxs("button", { type: "button", onClick: generateFlat, style: { flex: 1 }, children: [jsx(NiceIcon, { name: "square", size: 14 }), " Flatten"] })] })] }), brush.mode === 'paint' && (jsxs("fieldset", { style: {
364
+ }, children: [jsx("legend", { style: { fontSize: 11 }, children: t('terrainEditor.procedural', 'Procedural') }), jsxs("label", { children: ["Octaves: ", noiseOctaves, jsx("input", { type: "range", min: 1, max: 10, value: noiseOctaves, onChange: (e) => setNoiseOctaves(parseInt(e.target.value, 10)), style: { width: '100%' } })] }), jsxs("label", { children: ["Persistence: ", noisePersistence.toFixed(2), jsx("input", { type: "range", min: 0.1, max: 1, step: 0.05, value: noisePersistence, onChange: (e) => setNoisePersistence(parseFloat(e.target.value)), style: { width: '100%' } })] }), jsxs("div", { style: { display: 'flex', gap: 4 }, children: [jsxs("button", { type: "button", onClick: () => generateNoise(noiseOctaves, noisePersistence), style: { flex: 1 }, children: [jsx(NiceIcon, { name: "mountain", size: 14 }), ' ', t('terrainEditor.generateNoise', 'Generate Noise')] }), jsxs("button", { type: "button", onClick: generateFlat, style: { flex: 1 }, children: [jsx(NiceIcon, { name: "square", size: 14 }), " ", t('terrainEditor.flatten', 'Flatten')] })] })] }), brush.mode === 'paint' && (jsxs("fieldset", { style: {
363
365
  border: '1px solid var(--nice-border, #e2e8f0)',
364
366
  borderRadius: 6,
365
367
  padding: 6,
366
368
  display: 'grid',
367
369
  gap: 4,
368
- }, children: [jsx("legend", { style: { fontSize: 11 }, children: "Texture Layers" }), textureLayers.map((layer, i) => (jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 6 }, children: [jsx("input", { type: "radio", name: "texLayer", checked: activeTextureLayer === i, onChange: () => setActiveTextureLayer(i) }), jsx("input", { type: "text", value: layer.name, onChange: (e) => setTextureLayer(i, { name: e.target.value }), style: {
370
+ }, children: [jsx("legend", { style: { fontSize: 11 }, children: t('terrainEditor.textureLayers', 'Texture Layers') }), textureLayers.map((layer, i) => (jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 6 }, children: [jsx("input", { type: "radio", name: "texLayer", checked: activeTextureLayer === i, onChange: () => setActiveTextureLayer(i) }), jsx("input", { type: "text", value: layer.name, onChange: (e) => setTextureLayer(i, { name: e.target.value }), style: {
369
371
  flex: 1,
370
372
  padding: '2px 6px',
371
373
  border: '1px solid var(--nice-border, #e2e8f0)',
@@ -378,7 +380,7 @@ const NiceTerrainEditor = ({ scene, camera, canvas, config: configProp, onTerrai
378
380
  link.href = c.toDataURL('image/png');
379
381
  link.click();
380
382
  }
381
- }, style: { fontSize: 11 }, children: [jsx(NiceIcon, { name: "download", size: 14 }), " Export Heightmap PNG"] })] }));
383
+ }, style: { fontSize: 11 }, children: [jsx(NiceIcon, { name: "download", size: 14 }), ' ', t('terrainEditor.exportHeightmapPng', 'Export Heightmap PNG')] })] }));
382
384
  };
383
385
 
384
386
  export { NiceTerrainEditor, useTerrainEditor };
@@ -2,6 +2,7 @@ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
2
2
  import { NiceIcon } from '@nice2dev/ui';
3
3
  import { useEffect, useState, useRef, useCallback } from 'react';
4
4
  import * as THREE from 'three';
5
+ import { useNiceTranslation } from '../core/i18n.js';
5
6
 
6
7
  /* ── Weight painting utilities ── */
7
8
  function getFalloff(distance, radius, type) {
@@ -225,6 +226,7 @@ function updateHeatMapColors(mesh, boneIndex, colors) {
225
226
  }
226
227
  /* ── Component ── */
227
228
  const NiceWeightPainter = ({ skinnedMesh, activeBoneIndex: initialBoneIndex, scene, camera, canvas, onWeightsChange, className = '', }) => {
229
+ const { t } = useNiceTranslation();
228
230
  const { brush, setBrush, isPainting, activeBoneIndex, setActiveBoneIndex, boneNames, startPainting, stopPainting, normalizeWeights, showHeatMap, setShowHeatMap, } = useWeightPainter(skinnedMesh, scene, camera, canvas, onWeightsChange);
229
231
  useEffect(() => {
230
232
  setActiveBoneIndex(initialBoneIndex);
@@ -235,7 +237,7 @@ const NiceWeightPainter = ({ skinnedMesh, activeBoneIndex: initialBoneIndex, sce
235
237
  border: '1px solid var(--nice-border, #e2e8f0)',
236
238
  borderRadius: 6,
237
239
  padding: 4,
238
- }, children: [jsx("legend", { style: { fontSize: 11 }, children: "Mode" }), ['add', 'subtract', 'smooth', 'blur'].map((mode) => (jsx("button", { type: "button", onClick: () => setBrush({ mode }), style: {
240
+ }, children: [jsx("legend", { style: { fontSize: 11 }, children: t('weightPainter.mode', 'Mode') }), ['add', 'subtract', 'smooth', 'blur'].map((mode) => (jsx("button", { type: "button", onClick: () => setBrush({ mode }), style: {
239
241
  flex: 1,
240
242
  padding: '4px 6px',
241
243
  background: brush.mode === mode ? 'var(--nice-color-accent, #6366f1)' : 'transparent',
@@ -254,7 +256,7 @@ const NiceWeightPainter = ({ skinnedMesh, activeBoneIndex: initialBoneIndex, sce
254
256
  borderRadius: 4,
255
257
  padding: '6px 12px',
256
258
  cursor: 'pointer',
257
- }, children: isPainting ? (jsxs(Fragment, { children: [jsx(NiceIcon, { name: "stop", size: 14 }), " Stop Painting"] })) : (jsxs(Fragment, { children: [jsx(NiceIcon, { name: "brush", size: 14 }), " Start Painting"] })) }), jsxs("button", { type: "button", onClick: normalizeWeights, title: "Normalize all weights", children: [jsx(NiceIcon, { name: "scale", size: 14 }), " Normalize"] })] }), jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 6 }, children: [jsx("input", { type: "checkbox", checked: showHeatMap, onChange: (e) => setShowHeatMap(e.target.checked) }), "Show weight heat map"] })] }));
259
+ }, children: isPainting ? (jsxs(Fragment, { children: [jsx(NiceIcon, { name: "stop", size: 14 }), " ", t('weightPainter.stopPainting', 'Stop Painting')] })) : (jsxs(Fragment, { children: [jsx(NiceIcon, { name: "brush", size: 14 }), ' ', t('weightPainter.startPainting', 'Start Painting')] })) }), jsxs("button", { type: "button", onClick: normalizeWeights, title: t('weightPainter.normalizeAllWeights', 'Normalize all weights'), children: [jsx(NiceIcon, { name: "scale", size: 14 }), " ", t('weightPainter.normalize', 'Normalize')] })] }), jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 6 }, children: [jsx("input", { type: "checkbox", checked: showHeatMap, onChange: (e) => setShowHeatMap(e.target.checked) }), t('weightPainter.showWeightHeatMap', 'Show weight heat map')] })] }));
258
260
  };
259
261
 
260
262
  export { NiceWeightPainter, useWeightPainter };
@@ -2,6 +2,7 @@ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
2
2
  import { NiceIcon } from '@nice2dev/ui';
3
3
  import { useEffect, useState, useRef, useCallback } from 'react';
4
4
  import * as THREE from 'three';
5
+ import { useNiceTranslation } from '../core/i18n.js';
5
6
 
6
7
  /* ── Hook ── */
7
8
  function useXRPreview(renderer, scene, camera, onSessionStart, onSessionEnd) {
@@ -259,13 +260,14 @@ function useXRPreview(renderer, scene, camera, onSessionStart, onSessionEnd) {
259
260
  }
260
261
  /* ── Component ── */
261
262
  const NiceXRPreview = ({ renderer, scene, camera, mode: initialMode, onSessionStart, onSessionEnd, className = '', }) => {
263
+ const { t } = useNiceTranslation();
262
264
  const { isSupported, isSessionActive, mode, setMode, startSession, endSession, controllers, teleportEnabled, setTeleportEnabled, hitTestResult, } = useXRPreview(renderer, scene, camera, onSessionStart, onSessionEnd);
263
265
  useEffect(() => {
264
266
  if (initialMode) {
265
267
  setMode(initialMode);
266
268
  }
267
269
  }, [initialMode, setMode]);
268
- return (jsxs("div", { className: `nice-xr-preview ${className}`, style: { display: 'grid', gap: 8, fontSize: 13 }, children: [jsxs("strong", { children: [jsx(NiceIcon, { name: "eye", size: 14 }), " XR Preview"] }), !isSupported && (jsx("div", { style: { color: 'var(--nice-danger, #ef4444)', fontSize: 11 }, children: "WebXR is not supported in this browser." })), isSupported && (jsxs(Fragment, { children: [jsxs("div", { style: { display: 'flex', gap: 4 }, children: [jsxs("button", { type: "button", onClick: () => setMode('vr'), style: {
270
+ return (jsxs("div", { className: `nice-xr-preview ${className}`, style: { display: 'grid', gap: 8, fontSize: 13 }, children: [jsxs("strong", { children: [jsx(NiceIcon, { name: "eye", size: 14 }), " ", t('xrPreview.xrPreview', 'XR Preview')] }), !isSupported && (jsx("div", { style: { color: 'var(--nice-danger, #ef4444)', fontSize: 11 }, children: t('xrPreview.webxrIsNotSupportedIn', 'WebXR is not supported in this browser.') })), isSupported && (jsxs(Fragment, { children: [jsxs("div", { style: { display: 'flex', gap: 4 }, children: [jsxs("button", { type: "button", onClick: () => setMode('vr'), style: {
269
271
  flex: 1,
270
272
  padding: '5px 8px',
271
273
  borderRadius: 4,
@@ -290,7 +292,7 @@ const NiceXRPreview = ({ renderer, scene, camera, mode: initialMode, onSessionSt
290
292
  borderRadius: 4,
291
293
  padding: '6px 12px',
292
294
  cursor: 'pointer',
293
- }, children: isSessionActive ? (jsxs(Fragment, { children: [jsx(NiceIcon, { name: "stop", size: 14 }), " Exit XR"] })) : (jsxs(Fragment, { children: [jsx(NiceIcon, { name: "play", size: 14 }), " Enter XR"] })) }), mode === 'vr' && (jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 6 }, children: [jsx("input", { type: "checkbox", checked: teleportEnabled, onChange: (e) => setTeleportEnabled(e.target.checked) }), "Teleportation"] })), isSessionActive && (jsxs("div", { style: { fontSize: 11, opacity: 0.7 }, children: ["Controllers: ", controllers.length, jsx("br", {}), mode === 'ar' && hitTestResult && (jsxs(Fragment, { children: ["Hit: (", hitTestResult.x.toFixed(2), ", ", hitTestResult.y.toFixed(2), ",", ' ', hitTestResult.z.toFixed(2), ")"] }))] }))] }))] }));
295
+ }, children: isSessionActive ? (jsxs(Fragment, { children: [jsx(NiceIcon, { name: "stop", size: 14 }), " ", t('xrPreview.exitXr', 'Exit XR')] })) : (jsxs(Fragment, { children: [jsx(NiceIcon, { name: "play", size: 14 }), " ", t('xrPreview.enterXr', 'Enter XR')] })) }), mode === 'vr' && (jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 6 }, children: [jsx("input", { type: "checkbox", checked: teleportEnabled, onChange: (e) => setTeleportEnabled(e.target.checked) }), t('xrPreview.teleportation', 'Teleportation')] })), isSessionActive && (jsxs("div", { style: { fontSize: 11, opacity: 0.7 }, children: ["Controllers: ", controllers.length, jsx("br", {}), mode === 'ar' && hitTestResult && (jsxs(Fragment, { children: ["Hit: (", hitTestResult.x.toFixed(2), ", ", hitTestResult.y.toFixed(2), ",", ' ', hitTestResult.z.toFixed(2), ")"] }))] }))] }))] }));
294
296
  };
295
297
 
296
298
  export { NiceXRPreview, useXRPreview };
@@ -21,6 +21,7 @@ import { STLLoader } from 'three/examples/jsm/loaders/STLLoader.js';
21
21
  import { SVGLoader } from 'three/examples/jsm/loaders/SVGLoader.js';
22
22
  import { TDSLoader } from 'three/examples/jsm/loaders/TDSLoader.js';
23
23
  import { VTKLoader } from 'three/examples/jsm/loaders/VTKLoader.js';
24
+ import { useEditorAsk } from '../core/editorAsk.js';
24
25
  import { createFrameClock, SOFT_SHADOW_MAP } from '../core/threeCompat.js';
25
26
  import { SUPPORTED_EXTENSIONS, pose3dToClip } from './modelEditorTypes.js';
26
27
  import { UndoRedoStack, createMaterialFromPreset, generateLOD, convertToInstanced, applyHDRIPreset, snapMulti, HDRI_PRESETS, PBR_PRESETS } from './modelEditorUtils.js';
@@ -32,7 +33,7 @@ import { UndoRedoStack, createMaterialFromPreset, generateLOD, convertToInstance
32
33
  *
33
34
  * Extracted from the original ModelEditor.tsx god-component.
34
35
  */
35
- const log = { error: console.error, warn: console.warn, info: console.info };
36
+ const log = { error: console.error, warn: console.warn };
36
37
  /* helpers */
37
38
  let _nid = 0;
38
39
  const nid = () => `n${++_nid}`;
@@ -54,6 +55,7 @@ function findNodeById(nodes, id) {
54
55
  ═══════════════════════════════════════════ */
55
56
  function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
56
57
  var _a;
58
+ const { pendingAsk, ask } = useEditorAsk();
57
59
  /* ── refs ── */
58
60
  const mountRef = useRef(null);
59
61
  const rendererRef = useRef(null);
@@ -204,10 +206,9 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
204
206
  // goes into the scene is its helper.
205
207
  const tcHelper = tc.getHelper();
206
208
  tcHelper.name = '__transformControls';
207
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
208
- tc.addEventListener('dragging-changed', ((e) => {
209
+ tc.addEventListener('dragging-changed', (e) => {
209
210
  orbit.enabled = !e.value;
210
- }));
211
+ });
211
212
  scene.add(tcHelper);
212
213
  transformRef.current = tc;
213
214
  // Resize handler
@@ -1226,11 +1227,8 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1226
1227
  }
1227
1228
  });
1228
1229
  }, []);
1229
- const clearScene = useCallback(() => {
1230
+ const clearSceneNow = useCallback(() => {
1230
1231
  var _a;
1231
- if (!window.confirm('Clear the entire scene? This cannot be undone.')) {
1232
- return;
1233
- }
1234
1232
  const scene = sceneRef.current;
1235
1233
  const toRemove = [];
1236
1234
  scene.traverse((obj) => {
@@ -1262,6 +1260,14 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1262
1260
  rebuildTree();
1263
1261
  setStatusText('Scene cleared');
1264
1262
  }, [rebuildTree, disposeObject]);
1263
+ // asks first: clearing cannot be undone
1264
+ const clearScene = useCallback(() => {
1265
+ void ask('confirm', 'Clear the entire scene? This cannot be undone.').then((answer) => {
1266
+ if (answer !== null) {
1267
+ clearSceneNow();
1268
+ }
1269
+ });
1270
+ }, [ask, clearSceneNow]);
1265
1271
  /* ─────────────────────────────────────────
1266
1272
  Save to Library
1267
1273
  ───────────────────────────────────────── */
@@ -1279,19 +1285,19 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1279
1285
  const blob = new Blob([result], { type: 'model/gltf-binary' });
1280
1286
  const reader = new FileReader();
1281
1287
  reader.onload = () => {
1282
- const defaultName = 'model.glb';
1283
- const name = window.prompt('Save to library as:', defaultName);
1284
- if (!name || !name.trim()) {
1285
- return;
1286
- }
1287
- onSaveToLibrary(reader.result, name.trim(), 'model/gltf-binary');
1288
+ const dataUrl = reader.result;
1289
+ void ask('prompt', 'Save to library as:', 'model.glb').then((name) => {
1290
+ if (name && name.trim()) {
1291
+ onSaveToLibrary(dataUrl, name.trim(), 'model/gltf-binary');
1292
+ }
1293
+ });
1288
1294
  };
1289
1295
  reader.readAsDataURL(blob);
1290
1296
  }, (err) => {
1291
1297
  log.error('Save to library error:', err);
1292
1298
  setStatusText(`Save error: ${String(err)}`);
1293
1299
  }, { binary: true, animations: allClipsRef.current.map((a) => a.clip) });
1294
- }, [onSaveToLibrary]);
1300
+ }, [onSaveToLibrary, ask]);
1295
1301
  /* ─────────────────────────────────────────
1296
1302
  Add Mesh / Light / Camera / Empty
1297
1303
  ───────────────────────────────────────── */
@@ -1787,6 +1793,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1787
1793
  bgColor,
1788
1794
  setBgColor,
1789
1795
  statusText,
1796
+ pendingAsk,
1790
1797
  dragOver,
1791
1798
  polyCount,
1792
1799
  meshCount,