@nice2dev/ui-3d 1.0.4 → 1.0.5

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 (43) hide show
  1. package/dist/cjs/core/i18n.js +3 -3
  2. package/dist/cjs/dance/DanceBridge.js +13 -13
  3. package/dist/cjs/dance/DanceScoreEngine.js +8 -8
  4. package/dist/cjs/dance/PoseDetector.js +20 -20
  5. package/dist/cjs/material/NiceMaterialEditor.js +19 -19
  6. package/dist/cjs/model/ModelEditorLeftPanel.js +3 -3
  7. package/dist/cjs/model/ModelEditorMenuBar.js +2 -2
  8. package/dist/cjs/model/ModelEditorRightPanel.js +2 -2
  9. package/dist/cjs/model/ModelEditorSubComponents.js +3 -3
  10. package/dist/cjs/model/ModelEditorTimeline.js +3 -3
  11. package/dist/cjs/model/ModelEditorToolbar.js +2 -2
  12. package/dist/cjs/model/ModelEditorViewport.js +2 -2
  13. package/dist/cjs/model/ModelViewer.js +8 -8
  14. package/dist/cjs/model/NiceArmatureEditor.js +18 -18
  15. package/dist/cjs/model/NiceMorphTargetEditor.js +18 -18
  16. package/dist/cjs/model/NiceOctree.js +16 -16
  17. package/dist/cjs/model/NicePhysicsSimulation.js +24 -24
  18. package/dist/cjs/model/NiceProceduralGeometry.js +8 -8
  19. package/dist/cjs/model/NiceTerrainEditor.js +19 -19
  20. package/dist/cjs/model/NiceWeightPainter.js +14 -14
  21. package/dist/cjs/model/NiceXRPreview.js +18 -18
  22. package/dist/cjs/model/useModelEditor.js +127 -127
  23. package/dist/cjs/model/useModelViewer.js +61 -61
  24. package/dist/cjs/particle/NiceParticleEditor.js +11 -11
  25. package/dist/cjs/rendering/NiceCascadedShadows.js +12 -12
  26. package/dist/cjs/rendering/NiceRenderExport.js +5 -5
  27. package/dist/cjs/rendering/NiceSSAO.js +18 -18
  28. package/dist/cjs/rendering/NiceSSR.js +14 -14
  29. package/dist/cjs/rendering/NiceWebGPURenderer.js +21 -21
  30. package/dist/cjs/ui/dist/index.js +32608 -24033
  31. package/dist/cjs/ui/dist/index.js.map +1 -1
  32. package/dist/cjs/uv/NiceUVEditor.js +24 -24
  33. package/dist/esm/model/ModelEditorLeftPanel.js +5 -5
  34. package/dist/esm/model/ModelEditorMenuBar.js +5 -5
  35. package/dist/esm/model/ModelEditorRightPanel.js +17 -17
  36. package/dist/esm/model/ModelEditorSubComponents.js +6 -6
  37. package/dist/esm/model/ModelEditorTimeline.js +5 -5
  38. package/dist/esm/model/ModelEditorToolbar.js +4 -4
  39. package/dist/esm/model/ModelEditorViewport.js +2 -2
  40. package/dist/esm/model/ModelViewer.js +5 -5
  41. package/dist/esm/ui/dist/index.js +31739 -23166
  42. package/dist/esm/ui/dist/index.js.map +1 -1
  43. package/package.json +1 -1
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var jsxRuntime = require('react/jsx-runtime');
4
- var Ve = require('react');
4
+ var Ue = require('react');
5
5
  var uvEditorTypes = require('./uvEditorTypes.js');
6
6
  var uvEditorUtils = require('./uvEditorUtils.js');
7
7
  var UVEditor_module = require('./UVEditor.module.css.js');
@@ -10,31 +10,31 @@ var UVEditor_module = require('./UVEditor.module.css.js');
10
10
  Component
11
11
  ═══════════════════════════════════════════ */
12
12
  const NiceUVEditor = ({ geometry, texture, channel = 'uv', onUVChange, onSelectionChange, width = '100%', height = 600, showToolbar = true, showSidebar = true, className, readOnly = false, }) => {
13
- const canvasRef = Ve.useRef(null);
14
- const containerRef = Ve.useRef(null);
13
+ const canvasRef = Ue.useRef(null);
14
+ const containerRef = Ue.useRef(null);
15
15
  // State
16
- const [state, setState] = Ve.useState({
16
+ const [state, setState] = Ue.useState({
17
17
  ...uvEditorTypes.DEFAULT_UV_STATE,
18
18
  activeChannel: channel,
19
19
  });
20
- const [meshData, setMeshData] = Ve.useState(null);
21
- const [selection, setSelection] = Ve.useState({
20
+ const [meshData, setMeshData] = Ue.useState(null);
21
+ const [selection, setSelection] = Ue.useState({
22
22
  vertices: new Set(),
23
23
  edges: new Set(),
24
24
  faces: new Set(),
25
25
  islands: new Set(),
26
26
  });
27
- const [dragState, setDragState] = Ve.useState({
27
+ const [dragState, setDragState] = Ue.useState({
28
28
  active: false,
29
29
  startPos: { u: 0, v: 0 },
30
30
  currentPos: { u: 0, v: 0 },
31
31
  mode: 'select',
32
32
  initialPositions: new Map(),
33
33
  });
34
- const [undoStack, setUndoStack] = Ve.useState([]);
35
- const [redoStack, setRedoStack] = Ve.useState([]);
34
+ const [undoStack, setUndoStack] = Ue.useState([]);
35
+ const [redoStack, setRedoStack] = Ue.useState([]);
36
36
  // Extract mesh data when geometry changes
37
- Ve.useEffect(() => {
37
+ Ue.useEffect(() => {
38
38
  if (geometry) {
39
39
  const data = uvEditorUtils.extractUVMeshData(geometry, state.activeChannel);
40
40
  setMeshData(data);
@@ -45,7 +45,7 @@ const NiceUVEditor = ({ geometry, texture, channel = 'uv', onUVChange, onSelecti
45
45
  }
46
46
  }, [geometry, state.activeChannel]);
47
47
  // Render UV view
48
- Ve.useEffect(() => {
48
+ Ue.useEffect(() => {
49
49
  if (!canvasRef.current || !meshData)
50
50
  return;
51
51
  const canvas = canvasRef.current;
@@ -193,7 +193,7 @@ const NiceUVEditor = ({ geometry, texture, channel = 'uv', onUVChange, onSelecti
193
193
  }
194
194
  }, [meshData, state, texture, dragState, selection]);
195
195
  // Resize canvas
196
- Ve.useEffect(() => {
196
+ Ue.useEffect(() => {
197
197
  if (!canvasRef.current || !containerRef.current)
198
198
  return;
199
199
  const observer = new ResizeObserver(() => {
@@ -205,14 +205,14 @@ const NiceUVEditor = ({ geometry, texture, channel = 'uv', onUVChange, onSelecti
205
205
  return () => observer.disconnect();
206
206
  }, []);
207
207
  // Push undo state
208
- const pushUndo = Ve.useCallback(() => {
208
+ const pushUndo = Ue.useCallback(() => {
209
209
  if (!meshData)
210
210
  return;
211
211
  setUndoStack(prev => [...prev.slice(-20), JSON.parse(JSON.stringify(meshData))]);
212
212
  setRedoStack([]);
213
213
  }, [meshData]);
214
214
  // Undo
215
- const undo = Ve.useCallback(() => {
215
+ const undo = Ue.useCallback(() => {
216
216
  if (undoStack.length === 0 || !meshData)
217
217
  return;
218
218
  setRedoStack(prev => [...prev, JSON.parse(JSON.stringify(meshData))]);
@@ -221,7 +221,7 @@ const NiceUVEditor = ({ geometry, texture, channel = 'uv', onUVChange, onSelecti
221
221
  setMeshData(prev);
222
222
  }, [undoStack, meshData]);
223
223
  // Redo
224
- const redo = Ve.useCallback(() => {
224
+ const redo = Ue.useCallback(() => {
225
225
  if (redoStack.length === 0)
226
226
  return;
227
227
  const next = redoStack[redoStack.length - 1];
@@ -232,7 +232,7 @@ const NiceUVEditor = ({ geometry, texture, channel = 'uv', onUVChange, onSelecti
232
232
  setMeshData(next);
233
233
  }, [redoStack, meshData]);
234
234
  // Screen to UV conversion
235
- const screenToUV = Ve.useCallback((clientX, clientY) => {
235
+ const screenToUV = Ue.useCallback((clientX, clientY) => {
236
236
  if (!canvasRef.current)
237
237
  return { u: 0, v: 0 };
238
238
  const rect = canvasRef.current.getBoundingClientRect();
@@ -245,7 +245,7 @@ const NiceUVEditor = ({ geometry, texture, channel = 'uv', onUVChange, onSelecti
245
245
  };
246
246
  }, [state]);
247
247
  // Mouse handlers
248
- const handleMouseDown = Ve.useCallback((e) => {
248
+ const handleMouseDown = Ue.useCallback((e) => {
249
249
  if (readOnly || !meshData)
250
250
  return;
251
251
  const uv = screenToUV(e.clientX, e.clientY);
@@ -294,7 +294,7 @@ const NiceUVEditor = ({ geometry, texture, channel = 'uv', onUVChange, onSelecti
294
294
  }
295
295
  }
296
296
  }, [readOnly, meshData, state.activeTool, screenToUV, pushUndo]);
297
- const handleMouseMove = Ve.useCallback((e) => {
297
+ const handleMouseMove = Ue.useCallback((e) => {
298
298
  if (!dragState.active || !meshData)
299
299
  return;
300
300
  const uv = screenToUV(e.clientX, e.clientY);
@@ -344,7 +344,7 @@ const NiceUVEditor = ({ geometry, texture, channel = 'uv', onUVChange, onSelecti
344
344
  }
345
345
  setDragState(prev => ({ ...prev, currentPos: uv }));
346
346
  }, [dragState, meshData, state.activeTool, screenToUV]);
347
- const handleMouseUp = Ve.useCallback((e) => {
347
+ const handleMouseUp = Ue.useCallback((e) => {
348
348
  if (!dragState.active || !meshData) {
349
349
  setDragState(prev => ({ ...prev, active: false }));
350
350
  return;
@@ -380,7 +380,7 @@ const NiceUVEditor = ({ geometry, texture, channel = 'uv', onUVChange, onSelecti
380
380
  setDragState(prev => ({ ...prev, active: false }));
381
381
  }, [dragState, meshData, geometry, state.activeChannel, onSelectionChange, onUVChange]);
382
382
  // Wheel handler (zoom)
383
- const handleWheel = Ve.useCallback((e) => {
383
+ const handleWheel = Ue.useCallback((e) => {
384
384
  e.preventDefault();
385
385
  const zoomFactor = e.deltaY > 0 ? 0.9 : 1.1;
386
386
  setState(prev => ({
@@ -389,7 +389,7 @@ const NiceUVEditor = ({ geometry, texture, channel = 'uv', onUVChange, onSelecti
389
389
  }));
390
390
  }, []);
391
391
  // Keyboard handler
392
- Ve.useEffect(() => {
392
+ Ue.useEffect(() => {
393
393
  const handler = (e) => {
394
394
  if (readOnly)
395
395
  return;
@@ -448,7 +448,7 @@ const NiceUVEditor = ({ geometry, texture, channel = 'uv', onUVChange, onSelecti
448
448
  return () => window.removeEventListener('keydown', handler);
449
449
  }, [readOnly, meshData, undo, redo]);
450
450
  // Tool actions
451
- const handlePack = Ve.useCallback(() => {
451
+ const handlePack = Ue.useCallback(() => {
452
452
  if (!meshData || readOnly)
453
453
  return;
454
454
  pushUndo();
@@ -460,7 +460,7 @@ const NiceUVEditor = ({ geometry, texture, channel = 'uv', onUVChange, onSelecti
460
460
  }
461
461
  setMeshData({ ...meshData });
462
462
  }, [meshData, geometry, state.activeChannel, readOnly, pushUndo, onUVChange]);
463
- const handleRelax = Ve.useCallback(() => {
463
+ const handleRelax = Ue.useCallback(() => {
464
464
  if (!meshData || readOnly)
465
465
  return;
466
466
  pushUndo();
@@ -474,7 +474,7 @@ const NiceUVEditor = ({ geometry, texture, channel = 'uv', onUVChange, onSelecti
474
474
  }
475
475
  setMeshData({ ...meshData });
476
476
  }, [meshData, geometry, state.activeChannel, readOnly, pushUndo, onUVChange]);
477
- const handleProject = Ve.useCallback((type) => {
477
+ const handleProject = Ue.useCallback((type) => {
478
478
  if (!meshData || !geometry || readOnly)
479
479
  return;
480
480
  pushUndo();
@@ -1,9 +1,9 @@
1
1
  import { jsx, jsxs } from 'react/jsx-runtime';
2
- import Ve from 'react';
2
+ import Ue from 'react';
3
3
  import styles from './ModelEditor.module.css.js';
4
- import { NiceButton as Oe, NiceTextInput as _t } from '../ui/dist/index.js';
4
+ import { NiceButton as We, NiceTextInput as Tt } from '../ui/dist/index.js';
5
5
 
6
- const ModelEditorLeftPanel = Ve.memo(({ api }) => {
6
+ const ModelEditorLeftPanel = Ue.memo(({ api }) => {
7
7
  const { sceneTree, selectedId, outlinerSearch, setOutlinerSearch, selectObject, rebuildTree, toggleNodeExpanded, toggleNodeVisibility, } = api;
8
8
  const renderTreeNode = (node, depth) => {
9
9
  // Outliner search filter
@@ -22,7 +22,7 @@ const ModelEditorLeftPanel = Ve.memo(({ api }) => {
22
22
  node.type === "light" ? "💡" :
23
23
  node.type === "camera" ? "📷" :
24
24
  node.type === "helper" ? "◇" : "📁";
25
- return (jsxs(Ve.Fragment, { children: [jsxs("div", { className: `${styles.treeItem} ${selectedId === node.id ? styles.treeItemActive : ""}`, style: { paddingLeft: depth * 16 + 4 }, onClick: () => selectObject(node), children: [hasChildren ? (jsx("span", { className: styles.treeToggle, onClick: (e) => {
25
+ return (jsxs(Ue.Fragment, { children: [jsxs("div", { className: `${styles.treeItem} ${selectedId === node.id ? styles.treeItemActive : ""}`, style: { paddingLeft: depth * 16 + 4 }, onClick: () => selectObject(node), children: [hasChildren ? (jsx("span", { className: styles.treeToggle, onClick: (e) => {
26
26
  e.stopPropagation();
27
27
  toggleNodeExpanded(node);
28
28
  }, children: node.expanded ? "▼" : "▶" })) : (jsx("span", { className: styles.treeToggle })), jsx("span", { className: styles.treeIcon, children: icon }), jsx("span", { style: { flex: 1, overflow: "hidden", textOverflow: "ellipsis" }, children: node.name }), jsx("span", { className: styles.treeVisibility, onClick: (e) => {
@@ -31,7 +31,7 @@ const ModelEditorLeftPanel = Ve.memo(({ api }) => {
31
31
  }, children: node.visible ? "👁" : "◌" })] }), node.expanded &&
32
32
  node.children.map((c) => renderTreeNode(c, depth + 1))] }, node.id));
33
33
  };
34
- return (jsx("div", { className: styles.leftPanel, children: jsxs("div", { className: styles.panelSection, children: [jsxs("div", { className: styles.panelTitle, children: ["\uD83D\uDCCB Outliner", jsx(Oe, { variant: "ghost", size: "sm", onClick: rebuildTree, title: "Refresh", "aria-label": "Refresh outliner", children: "\u27F3" })] }), jsx("div", { className: styles.outlinerSearch, children: jsx(_t, { className: styles.propInput, placeholder: "\uD83D\uDD0D Filter...", value: outlinerSearch, onChange: (val) => setOutlinerSearch(val), style: { width: "100%", marginBottom: 4 } }) }), jsxs("div", { className: styles.panelContent, style: { overflowY: "auto", flex: 1 }, children: [sceneTree.length === 0 && (jsx("div", { style: { color: "#666", fontSize: 10, padding: "8px 0" }, children: "Drop a 3D file to begin" })), sceneTree.map((n) => renderTreeNode(n, 0))] })] }) }));
34
+ return (jsx("div", { className: styles.leftPanel, children: jsxs("div", { className: styles.panelSection, children: [jsxs("div", { className: styles.panelTitle, children: ["\uD83D\uDCCB Outliner", jsx(We, { variant: "ghost", size: "sm", onClick: rebuildTree, title: "Refresh", "aria-label": "Refresh outliner", children: "\u27F3" })] }), jsx("div", { className: styles.outlinerSearch, children: jsx(Tt, { className: styles.propInput, placeholder: "\uD83D\uDD0D Filter...", value: outlinerSearch, onChange: (val) => setOutlinerSearch(val), style: { width: "100%", marginBottom: 4 } }) }), jsxs("div", { className: styles.panelContent, style: { overflowY: "auto", flex: 1 }, children: [sceneTree.length === 0 && (jsx("div", { style: { color: "#666", fontSize: 10, padding: "8px 0" }, children: "Drop a 3D file to begin" })), sceneTree.map((n) => renderTreeNode(n, 0))] })] }) }));
35
35
  });
36
36
  ModelEditorLeftPanel.displayName = "ModelEditorLeftPanel";
37
37
 
@@ -1,15 +1,15 @@
1
1
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
2
- import Ve from 'react';
2
+ import Ue from 'react';
3
3
  import styles from './ModelEditor.module.css.js';
4
- import { NiceSelect as nn, NiceButton as Oe, NiceCheckbox as ma, NiceColorPicker as Nn } from '../ui/dist/index.js';
4
+ import { NiceSelect as fn, NiceButton as We, NiceCheckbox as Fr, NiceColorPicker as Rn } from '../ui/dist/index.js';
5
5
 
6
- const ModelEditorMenuBar = Ve.memo(({ api, hasOnSaveToLibrary }) => {
6
+ const ModelEditorMenuBar = Ue.memo(({ api, hasOnSaveToLibrary }) => {
7
7
  const { editorMode, setEditorMode, fileInputRef, mergeInputRef, addMenuOpen, setAddMenuOpen, addPrimitive, addSceneLight, addCameraObject, addEmpty, selectedNode, duplicateSelected, deleteSelected, exportGLTF, exportOBJ, exportSTL, exportPLY, exportUSDZ, saveToLibrary, clearScene, showGrid, setShowGrid, showAxes, setShowAxes, wireframe, setWireframe, showSkeleton, setShowSkeleton, showLightHelpers, setShowLightHelpers, bgColor, setBgColor, } = api;
8
- return (jsxs("div", { className: styles.menuBar, children: [jsx(nn, { className: styles.modeSelect, value: editorMode, onChange: (val) => setEditorMode(val), options: [
8
+ return (jsxs("div", { className: styles.menuBar, children: [jsx(fn, { className: styles.modeSelect, value: editorMode, onChange: (val) => setEditorMode(val), options: [
9
9
  { value: "object", label: "Object Mode" },
10
10
  { value: "edit", label: "Edit Mode" },
11
11
  { value: "pose", label: "Pose Mode" },
12
- ] }), jsx("div", { className: styles.menuSep }), jsx(Oe, { className: styles.menuBtn, variant: "ghost", size: "sm", onClick: () => { var _a; return (_a = fileInputRef.current) === null || _a === void 0 ? void 0 : _a.click(); }, children: "\uD83D\uDCC2 Open" }), jsx(Oe, { className: styles.menuBtn, variant: "ghost", size: "sm", onClick: () => { var _a; return (_a = mergeInputRef.current) === null || _a === void 0 ? void 0 : _a.click(); }, title: "Merge animations from FBX/GLTF onto current model", children: "\u2795 Merge Anim" }), jsx("div", { className: styles.menuSep }), jsxs("div", { className: styles.menuDropdownWrap, children: [jsx(Oe, { className: styles.menuBtn, variant: "ghost", size: "sm", onClick: () => setAddMenuOpen(addMenuOpen ? null : "mesh"), children: "\uFF0B Add \u25BE" }), addMenuOpen && (jsxs("div", { className: styles.dropdownMenu, children: [jsx("div", { className: styles.dropdownTitle, children: "Mesh" }), ["cube", "sphere", "cylinder", "cone", "torus", "plane", "circle", "ring", "dodecahedron", "icosahedron", "octahedron", "tetrahedron", "torusKnot"].map((t) => (jsx("div", { className: styles.dropdownItem, onClick: () => addPrimitive(t), children: t.charAt(0).toUpperCase() + t.slice(1) }, t))), jsx("div", { className: styles.dropdownSep }), jsx("div", { className: styles.dropdownTitle, children: "Light" }), ["point", "spot", "directional", "hemisphere"].map((t) => (jsxs("div", { className: styles.dropdownItem, onClick: () => addSceneLight(t), children: ["\uD83D\uDCA1 ", t.charAt(0).toUpperCase() + t.slice(1)] }, t))), jsx("div", { className: styles.dropdownSep }), jsx("div", { className: styles.dropdownTitle, children: "Other" }), jsx("div", { className: styles.dropdownItem, onClick: addCameraObject, children: "\uD83D\uDCF7 Camera" }), jsx("div", { className: styles.dropdownItem, onClick: () => addEmpty("arrows"), children: "\u22B9 Empty (Arrows)" })] }))] }), jsx(Oe, { className: styles.menuBtn, variant: "ghost", size: "sm", onClick: duplicateSelected, disabled: !selectedNode, title: "Duplicate selected (Shift+D)", children: "\uD83D\uDCCB Duplicate" }), jsx(Oe, { className: styles.menuBtn, variant: "ghost", size: "sm", onClick: deleteSelected, disabled: !selectedNode, title: "Delete selected (Delete)", children: "\u2715 Delete" }), jsx("div", { className: styles.menuSep }), jsx(Oe, { className: styles.menuBtn, variant: "ghost", size: "sm", onClick: () => exportGLTF(true), children: "\uD83D\uDCBE Export GLB" }), jsx(Oe, { className: styles.menuBtn, variant: "ghost", size: "sm", onClick: () => exportGLTF(false), children: "\uD83D\uDCC4 Export GLTF" }), jsx(Oe, { className: styles.menuBtn, variant: "ghost", size: "sm", onClick: exportOBJ, children: "\uD83D\uDCC4 Export OBJ" }), jsx(Oe, { className: styles.menuBtn, variant: "ghost", size: "sm", onClick: () => exportSTL(true), children: "\uD83D\uDCC4 Export STL" }), jsx(Oe, { className: styles.menuBtn, variant: "ghost", size: "sm", onClick: () => exportPLY(true), children: "\uD83D\uDCC4 Export PLY" }), jsx(Oe, { className: styles.menuBtn, variant: "ghost", size: "sm", onClick: exportUSDZ, children: "\uD83D\uDCC4 Export USDZ" }), hasOnSaveToLibrary && (jsxs(Fragment, { children: [jsx("div", { className: styles.menuSep }), jsx(Oe, { className: styles.menuBtn, variant: "ghost", size: "sm", onClick: saveToLibrary, children: "\uD83D\uDCE6 Save to Library" })] })), jsx("div", { className: styles.menuSep }), jsx(Oe, { className: styles.menuBtn, variant: "ghost", size: "sm", onClick: clearScene, children: "\uD83D\uDDD1 Clear" }), jsx("div", { className: styles.menuSep }), jsx(ma, { checked: showGrid, onChange: () => setShowGrid((v) => !v), label: "Grid" }), jsx(ma, { checked: showAxes, onChange: () => setShowAxes((v) => !v), label: "Axes" }), jsx(ma, { checked: wireframe, onChange: () => setWireframe((v) => !v), label: "Wire" }), jsx(ma, { checked: showSkeleton, onChange: () => setShowSkeleton((v) => !v), label: "Skel" }), jsx(ma, { checked: showLightHelpers, onChange: () => setShowLightHelpers((v) => !v), label: "Helpers" }), jsxs("div", { className: styles.menuRight, children: [jsx("label", { className: styles.menuLabel, children: "BG" }), jsx(Nn, { value: bgColor, onChange: (c) => setBgColor(c) })] })] }));
12
+ ] }), jsx("div", { className: styles.menuSep }), jsx(We, { className: styles.menuBtn, variant: "ghost", size: "sm", onClick: () => { var _a; return (_a = fileInputRef.current) === null || _a === void 0 ? void 0 : _a.click(); }, children: "\uD83D\uDCC2 Open" }), jsx(We, { className: styles.menuBtn, variant: "ghost", size: "sm", onClick: () => { var _a; return (_a = mergeInputRef.current) === null || _a === void 0 ? void 0 : _a.click(); }, title: "Merge animations from FBX/GLTF onto current model", children: "\u2795 Merge Anim" }), jsx("div", { className: styles.menuSep }), jsxs("div", { className: styles.menuDropdownWrap, children: [jsx(We, { className: styles.menuBtn, variant: "ghost", size: "sm", onClick: () => setAddMenuOpen(addMenuOpen ? null : "mesh"), children: "\uFF0B Add \u25BE" }), addMenuOpen && (jsxs("div", { className: styles.dropdownMenu, children: [jsx("div", { className: styles.dropdownTitle, children: "Mesh" }), ["cube", "sphere", "cylinder", "cone", "torus", "plane", "circle", "ring", "dodecahedron", "icosahedron", "octahedron", "tetrahedron", "torusKnot"].map((t) => (jsx("div", { className: styles.dropdownItem, onClick: () => addPrimitive(t), children: t.charAt(0).toUpperCase() + t.slice(1) }, t))), jsx("div", { className: styles.dropdownSep }), jsx("div", { className: styles.dropdownTitle, children: "Light" }), ["point", "spot", "directional", "hemisphere"].map((t) => (jsxs("div", { className: styles.dropdownItem, onClick: () => addSceneLight(t), children: ["\uD83D\uDCA1 ", t.charAt(0).toUpperCase() + t.slice(1)] }, t))), jsx("div", { className: styles.dropdownSep }), jsx("div", { className: styles.dropdownTitle, children: "Other" }), jsx("div", { className: styles.dropdownItem, onClick: addCameraObject, children: "\uD83D\uDCF7 Camera" }), jsx("div", { className: styles.dropdownItem, onClick: () => addEmpty("arrows"), children: "\u22B9 Empty (Arrows)" })] }))] }), jsx(We, { className: styles.menuBtn, variant: "ghost", size: "sm", onClick: duplicateSelected, disabled: !selectedNode, title: "Duplicate selected (Shift+D)", children: "\uD83D\uDCCB Duplicate" }), jsx(We, { className: styles.menuBtn, variant: "ghost", size: "sm", onClick: deleteSelected, disabled: !selectedNode, title: "Delete selected (Delete)", children: "\u2715 Delete" }), jsx("div", { className: styles.menuSep }), jsx(We, { className: styles.menuBtn, variant: "ghost", size: "sm", onClick: () => exportGLTF(true), children: "\uD83D\uDCBE Export GLB" }), jsx(We, { className: styles.menuBtn, variant: "ghost", size: "sm", onClick: () => exportGLTF(false), children: "\uD83D\uDCC4 Export GLTF" }), jsx(We, { className: styles.menuBtn, variant: "ghost", size: "sm", onClick: exportOBJ, children: "\uD83D\uDCC4 Export OBJ" }), jsx(We, { className: styles.menuBtn, variant: "ghost", size: "sm", onClick: () => exportSTL(true), children: "\uD83D\uDCC4 Export STL" }), jsx(We, { className: styles.menuBtn, variant: "ghost", size: "sm", onClick: () => exportPLY(true), children: "\uD83D\uDCC4 Export PLY" }), jsx(We, { className: styles.menuBtn, variant: "ghost", size: "sm", onClick: exportUSDZ, children: "\uD83D\uDCC4 Export USDZ" }), hasOnSaveToLibrary && (jsxs(Fragment, { children: [jsx("div", { className: styles.menuSep }), jsx(We, { className: styles.menuBtn, variant: "ghost", size: "sm", onClick: saveToLibrary, children: "\uD83D\uDCE6 Save to Library" })] })), jsx("div", { className: styles.menuSep }), jsx(We, { className: styles.menuBtn, variant: "ghost", size: "sm", onClick: clearScene, children: "\uD83D\uDDD1 Clear" }), jsx("div", { className: styles.menuSep }), jsx(Fr, { checked: showGrid, onChange: () => setShowGrid((v) => !v), label: "Grid" }), jsx(Fr, { checked: showAxes, onChange: () => setShowAxes((v) => !v), label: "Axes" }), jsx(Fr, { checked: wireframe, onChange: () => setWireframe((v) => !v), label: "Wire" }), jsx(Fr, { checked: showSkeleton, onChange: () => setShowSkeleton((v) => !v), label: "Skel" }), jsx(Fr, { checked: showLightHelpers, onChange: () => setShowLightHelpers((v) => !v), label: "Helpers" }), jsxs("div", { className: styles.menuRight, children: [jsx("label", { className: styles.menuLabel, children: "BG" }), jsx(Rn, { value: bgColor, onChange: (c) => setBgColor(c) })] })] }));
13
13
  });
14
14
  ModelEditorMenuBar.displayName = "ModelEditorMenuBar";
15
15
 
@@ -1,49 +1,49 @@
1
1
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
2
- import Ve from 'react';
2
+ import Ue from 'react';
3
3
  import * as THREE from 'three';
4
4
  import styles from './ModelEditor.module.css.js';
5
5
  import { DEG } from './modelEditorTypes.js';
6
6
  import { Vec3Row, Vec3RowDeg, MaterialCard } from './ModelEditorSubComponents.js';
7
- import { NiceButton as Oe, NiceTextInput as _t, NiceColorPicker as Nn, NiceSlider as Ar, NiceCheckbox as ma, NiceNumberInput as Ri } from '../ui/dist/index.js';
7
+ import { NiceButton as We, NiceTextInput as Tt, NiceColorPicker as Rn, NiceSlider as so, NiceCheckbox as Fr, NiceNumberInput as ir } from '../ui/dist/index.js';
8
8
 
9
- const ModelEditorRightPanel = Ve.memo(({ api }) => {
9
+ const ModelEditorRightPanel = Ue.memo(({ api }) => {
10
10
  const { propTab, setPropTab, selectedNode, selectedMaterials, selMaterialIdx, setSelMaterialIdx, setMatRefresh, rebuildTree, aiBusy, aiStatus, rootObjectRef, videoInputRef, bgColor, setBgColor, fogEnabled, setFogEnabled, fogColor, setFogColor, fogNear, setFogNear, fogFar, setFogFar, showLightHelpers, setShowLightHelpers, showGrid, setShowGrid, showAxes, setShowAxes, showSkeleton, setShowSkeleton, snapEnabled, setSnapEnabled, snapGrid, setSnapGrid, setStatusText, } = api;
11
- return (jsxs("div", { className: styles.rightPanel, children: [jsx("div", { className: styles.propTabs, children: ["object", "material", "world", "modifiers", "physics"].map((t) => (jsx(Oe, { className: styles.propTabBtn, variant: propTab === t ? "primary" : "ghost", size: "sm", onClick: () => setPropTab(t), title: t.charAt(0).toUpperCase() + t.slice(1), children: t === "object" ? "🔧" : t === "material" ? "🎨" : t === "world" ? "🌍" : t === "modifiers" ? "🔩" : "⚡" }, t))) }), propTab === "object" && (jsxs(Fragment, { children: [jsxs("div", { className: styles.panelSection, children: [jsx("div", { className: styles.panelTitle, children: "\uD83D\uDD27 Transform" }), jsx("div", { className: styles.panelContent, children: selectedNode ? (jsxs(Fragment, { children: [jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: "Name" }), jsx(_t, { className: styles.propInput, value: selectedNode.name, onChange: (val) => {
11
+ return (jsxs("div", { className: styles.rightPanel, children: [jsx("div", { className: styles.propTabs, children: ["object", "material", "world", "modifiers", "physics"].map((t) => (jsx(We, { className: styles.propTabBtn, variant: propTab === t ? "primary" : "ghost", size: "sm", onClick: () => setPropTab(t), title: t.charAt(0).toUpperCase() + t.slice(1), children: t === "object" ? "🔧" : t === "material" ? "🎨" : t === "world" ? "🌍" : t === "modifiers" ? "🔩" : "⚡" }, t))) }), propTab === "object" && (jsxs(Fragment, { children: [jsxs("div", { className: styles.panelSection, children: [jsx("div", { className: styles.panelTitle, children: "\uD83D\uDD27 Transform" }), jsx("div", { className: styles.panelContent, children: selectedNode ? (jsxs(Fragment, { children: [jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: "Name" }), jsx(Tt, { className: styles.propInput, value: selectedNode.name, onChange: (val) => {
12
12
  selectedNode.object.name = val;
13
13
  rebuildTree();
14
- } })] }), jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: "Type" }), jsx("span", { style: { fontSize: 10, color: "#888" }, children: selectedNode.type })] }), jsx(Vec3Row, { label: "Position", value: selectedNode.object.position, onChange: () => setMatRefresh((n) => n + 1) }), jsx(Vec3RowDeg, { label: "Rotation", value: selectedNode.object.rotation, onChange: () => setMatRefresh((n) => n + 1) }), jsx(Vec3Row, { label: "Scale", value: selectedNode.object.scale, onChange: () => setMatRefresh((n) => n + 1) })] })) : (jsx("div", { style: { color: "#666", fontSize: 10 }, children: "Select an object" })) })] }), selectedNode && selectedNode.object instanceof THREE.Light && (jsxs("div", { className: styles.panelSection, children: [jsx("div", { className: styles.panelTitle, children: "\uD83D\uDCA1 Light" }), jsxs("div", { className: styles.panelContent, children: [jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: "Color" }), jsx(Nn, { value: `#${selectedNode.object.color.getHexString()}`, onChange: (c) => {
14
+ } })] }), jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: "Type" }), jsx("span", { style: { fontSize: 10, color: "#888" }, children: selectedNode.type })] }), jsx(Vec3Row, { label: "Position", value: selectedNode.object.position, onChange: () => setMatRefresh((n) => n + 1) }), jsx(Vec3RowDeg, { label: "Rotation", value: selectedNode.object.rotation, onChange: () => setMatRefresh((n) => n + 1) }), jsx(Vec3Row, { label: "Scale", value: selectedNode.object.scale, onChange: () => setMatRefresh((n) => n + 1) })] })) : (jsx("div", { style: { color: "#666", fontSize: 10 }, children: "Select an object" })) })] }), selectedNode && selectedNode.object instanceof THREE.Light && (jsxs("div", { className: styles.panelSection, children: [jsx("div", { className: styles.panelTitle, children: "\uD83D\uDCA1 Light" }), jsxs("div", { className: styles.panelContent, children: [jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: "Color" }), jsx(Rn, { value: `#${selectedNode.object.color.getHexString()}`, onChange: (c) => {
15
15
  selectedNode.object.color.set(c);
16
16
  setMatRefresh((n) => n + 1);
17
- } })] }), jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: "Intensity" }), jsx(Ar, { min: 0, max: 10, step: 0.1, value: selectedNode.object.intensity, onChange: (val) => {
17
+ } })] }), jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: "Intensity" }), jsx(so, { min: 0, max: 10, step: 0.1, value: selectedNode.object.intensity, onChange: (val) => {
18
18
  selectedNode.object.intensity = val;
19
19
  setMatRefresh((n) => n + 1);
20
- }, style: { flex: 1 } }), jsx("span", { style: { fontSize: 9, color: "#888", width: 28, textAlign: "right" }, children: selectedNode.object.intensity.toFixed(1) })] }), "castShadow" in selectedNode.object && (jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: "Shadow" }), jsx(ma, { checked: selectedNode.object.castShadow, onChange: (checked) => {
20
+ }, style: { flex: 1 } }), jsx("span", { style: { fontSize: 9, color: "#888", width: 28, textAlign: "right" }, children: selectedNode.object.intensity.toFixed(1) })] }), "castShadow" in selectedNode.object && (jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: "Shadow" }), jsx(Fr, { checked: selectedNode.object.castShadow, onChange: (checked) => {
21
21
  selectedNode.object.castShadow = checked;
22
22
  setMatRefresh((n) => n + 1);
23
- } })] })), selectedNode.object instanceof THREE.SpotLight && (jsxs(Fragment, { children: [jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: "Angle" }), jsx(Ar, { min: 0, max: 90, step: 1, value: (selectedNode.object.angle * DEG), onChange: (val) => {
23
+ } })] })), selectedNode.object instanceof THREE.SpotLight && (jsxs(Fragment, { children: [jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: "Angle" }), jsx(so, { min: 0, max: 90, step: 1, value: (selectedNode.object.angle * DEG), onChange: (val) => {
24
24
  selectedNode.object.angle = val / DEG;
25
25
  setMatRefresh((n) => n + 1);
26
- }, style: { flex: 1 } }), jsxs("span", { style: { fontSize: 9, color: "#888", width: 28, textAlign: "right" }, children: [(selectedNode.object.angle * DEG).toFixed(0), "\u00B0"] })] }), jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: "Penumbra" }), jsx(Ar, { min: 0, max: 1, step: 0.01, value: selectedNode.object.penumbra, onChange: (val) => {
26
+ }, style: { flex: 1 } }), jsxs("span", { style: { fontSize: 9, color: "#888", width: 28, textAlign: "right" }, children: [(selectedNode.object.angle * DEG).toFixed(0), "\u00B0"] })] }), jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: "Penumbra" }), jsx(so, { min: 0, max: 1, step: 0.01, value: selectedNode.object.penumbra, onChange: (val) => {
27
27
  selectedNode.object.penumbra = val;
28
28
  setMatRefresh((n) => n + 1);
29
- }, style: { flex: 1 } })] })] }))] })] })), selectedNode && selectedNode.object instanceof THREE.PerspectiveCamera && (jsxs("div", { className: styles.panelSection, children: [jsx("div", { className: styles.panelTitle, children: "\uD83D\uDCF7 Camera" }), jsxs("div", { className: styles.panelContent, children: [jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: "FOV" }), jsx(Ar, { min: 10, max: 120, step: 1, value: selectedNode.object.fov, onChange: (val) => {
29
+ }, style: { flex: 1 } })] })] }))] })] })), selectedNode && selectedNode.object instanceof THREE.PerspectiveCamera && (jsxs("div", { className: styles.panelSection, children: [jsx("div", { className: styles.panelTitle, children: "\uD83D\uDCF7 Camera" }), jsxs("div", { className: styles.panelContent, children: [jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: "FOV" }), jsx(so, { min: 10, max: 120, step: 1, value: selectedNode.object.fov, onChange: (val) => {
30
30
  const cam = selectedNode.object;
31
31
  cam.fov = val;
32
32
  cam.updateProjectionMatrix();
33
33
  setMatRefresh((n) => n + 1);
34
- }, style: { flex: 1 } }), jsxs("span", { style: { fontSize: 9, color: "#888", width: 28, textAlign: "right" }, children: [selectedNode.object.fov.toFixed(0), "\u00B0"] })] }), jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: "Near" }), jsx(Ri, { className: styles.propInput, step: 0.01, value: selectedNode.object.near, onChange: (val) => {
34
+ }, style: { flex: 1 } }), jsxs("span", { style: { fontSize: 9, color: "#888", width: 28, textAlign: "right" }, children: [selectedNode.object.fov.toFixed(0), "\u00B0"] })] }), jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: "Near" }), jsx(ir, { className: styles.propInput, step: 0.01, value: selectedNode.object.near, onChange: (val) => {
35
35
  const cam = selectedNode.object;
36
36
  cam.near = val !== null && val !== void 0 ? val : 0.1;
37
37
  cam.updateProjectionMatrix();
38
38
  setMatRefresh((n) => n + 1);
39
- }, style: { width: 60 } })] }), jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: "Far" }), jsx(Ri, { className: styles.propInput, step: 1, value: selectedNode.object.far, onChange: (val) => {
39
+ }, style: { width: 60 } })] }), jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: "Far" }), jsx(ir, { className: styles.propInput, step: 1, value: selectedNode.object.far, onChange: (val) => {
40
40
  const cam = selectedNode.object;
41
41
  cam.far = val !== null && val !== void 0 ? val : 1000;
42
42
  cam.updateProjectionMatrix();
43
43
  setMatRefresh((n) => n + 1);
44
- }, style: { width: 80 } })] })] })] })), jsxs("div", { className: styles.panelSection, children: [jsx("div", { className: styles.panelTitle, children: "\uD83E\uDD16 AI Animation" }), jsx("div", { className: styles.panelContent, children: jsxs("div", { className: styles.aiPanel, children: [jsx("div", { className: styles.aiHeader, children: "\u2728 Generate animation from video" }), jsx("p", { style: { fontSize: 10, color: "#888", margin: "0 0 6px" }, children: "Upload a video and AI will extract 3D pose data to create an animation clip for the current model." }), jsx("div", { className: styles.aiBtnRow, children: jsx(Oe, { className: styles.aiBtn, variant: "ghost", size: "sm", disabled: aiBusy || !rootObjectRef.current, onClick: () => { var _a; return (_a = videoInputRef.current) === null || _a === void 0 ? void 0 : _a.click(); }, children: aiBusy ? "Processing..." : "Upload Video" }) }), aiStatus && (jsx("div", { className: styles.aiStatus, children: aiStatus }))] }) })] })] })), propTab === "material" && (jsxs("div", { className: styles.panelSection, children: [jsx("div", { className: styles.panelTitle, children: "\uD83C\uDFA8 Materials" }), jsx("div", { className: styles.panelContent, children: selectedMaterials.length > 0 ? (selectedMaterials.map((mat, i) => (jsx(MaterialCard, { material: mat, active: i === selMaterialIdx, onClick: () => setSelMaterialIdx(i), onUpdate: () => setMatRefresh((n) => n + 1) }, i)))) : (jsx("div", { style: { color: "#666", fontSize: 10 }, children: selectedNode
44
+ }, style: { width: 80 } })] })] })] })), jsxs("div", { className: styles.panelSection, children: [jsx("div", { className: styles.panelTitle, children: "\uD83E\uDD16 AI Animation" }), jsx("div", { className: styles.panelContent, children: jsxs("div", { className: styles.aiPanel, children: [jsx("div", { className: styles.aiHeader, children: "\u2728 Generate animation from video" }), jsx("p", { style: { fontSize: 10, color: "#888", margin: "0 0 6px" }, children: "Upload a video and AI will extract 3D pose data to create an animation clip for the current model." }), jsx("div", { className: styles.aiBtnRow, children: jsx(We, { className: styles.aiBtn, variant: "ghost", size: "sm", disabled: aiBusy || !rootObjectRef.current, onClick: () => { var _a; return (_a = videoInputRef.current) === null || _a === void 0 ? void 0 : _a.click(); }, children: aiBusy ? "Processing..." : "Upload Video" }) }), aiStatus && (jsx("div", { className: styles.aiStatus, children: aiStatus }))] }) })] })] })), propTab === "material" && (jsxs("div", { className: styles.panelSection, children: [jsx("div", { className: styles.panelTitle, children: "\uD83C\uDFA8 Materials" }), jsx("div", { className: styles.panelContent, children: selectedMaterials.length > 0 ? (selectedMaterials.map((mat, i) => (jsx(MaterialCard, { material: mat, active: i === selMaterialIdx, onClick: () => setSelMaterialIdx(i), onUpdate: () => setMatRefresh((n) => n + 1) }, i)))) : (jsx("div", { style: { color: "#666", fontSize: 10 }, children: selectedNode
45
45
  ? "No materials on this object"
46
- : "Select a mesh to inspect" })) })] })), propTab === "world" && (jsxs(Fragment, { children: [jsxs("div", { className: styles.panelSection, children: [jsx("div", { className: styles.panelTitle, children: "\uD83C\uDF0D Environment" }), jsx("div", { className: styles.panelContent, children: jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: "Background" }), jsx(Nn, { value: bgColor, onChange: (c) => setBgColor(c) }), jsx("span", { style: { fontSize: 9, color: "#888" }, children: bgColor })] }) })] }), jsxs("div", { className: styles.panelSection, children: [jsx("div", { className: styles.panelTitle, children: "\uD83C\uDF2B Fog" }), jsxs("div", { className: styles.panelContent, children: [jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: "Enable" }), jsx(ma, { checked: fogEnabled, onChange: () => setFogEnabled((v) => !v) })] }), fogEnabled && (jsxs(Fragment, { children: [jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: "Color" }), jsx(Nn, { value: fogColor, onChange: (c) => setFogColor(c) })] }), jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: "Near" }), jsx(Ri, { className: styles.propInput, value: fogNear, onChange: (val) => setFogNear(val !== null && val !== void 0 ? val : 1), style: { width: 60 } })] }), jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: "Far" }), jsx(Ri, { className: styles.propInput, value: fogFar, onChange: (val) => setFogFar(val !== null && val !== void 0 ? val : 100), style: { width: 60 } })] })] }))] })] }), jsxs("div", { className: styles.panelSection, children: [jsx("div", { className: styles.panelTitle, children: "\uD83D\uDD06 Scene Lights" }), jsxs("div", { className: styles.panelContent, children: [jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: "Show Helpers" }), jsx(ma, { checked: showLightHelpers, onChange: () => setShowLightHelpers((v) => !v) })] }), jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: "Grid" }), jsx(ma, { checked: showGrid, onChange: () => setShowGrid((v) => !v) })] }), jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: "Axes" }), jsx(ma, { checked: showAxes, onChange: () => setShowAxes((v) => !v) })] }), jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: "Skeleton" }), jsx(ma, { checked: showSkeleton, onChange: () => setShowSkeleton((v) => !v) })] })] })] }), jsxs("div", { className: styles.panelSection, children: [jsx("div", { className: styles.panelTitle, children: "\uD83E\uDDF2 Snapping" }), jsxs("div", { className: styles.panelContent, children: [jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: "Snap" }), jsx(ma, { checked: snapEnabled, onChange: () => setSnapEnabled((v) => !v) })] }), snapEnabled && (jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: "Grid Size" }), jsx(Ri, { className: styles.propInput, step: 0.25, min: 0.1, value: snapGrid, onChange: (val) => setSnapGrid(val !== null && val !== void 0 ? val : 1), style: { width: 60 } })] }))] })] })] })), propTab === "modifiers" && (jsxs("div", { className: styles.panelSection, children: [jsx("div", { className: styles.panelTitle, children: "\uD83D\uDD29 Modifiers" }), jsx("div", { className: styles.panelContent, children: selectedNode && selectedNode.object instanceof THREE.Mesh ? (jsxs(Fragment, { children: [jsx(Oe, { className: styles.btnSecondary, variant: "ghost", size: "sm", style: { width: "100%", marginBottom: 4 }, onClick: () => {
46
+ : "Select a mesh to inspect" })) })] })), propTab === "world" && (jsxs(Fragment, { children: [jsxs("div", { className: styles.panelSection, children: [jsx("div", { className: styles.panelTitle, children: "\uD83C\uDF0D Environment" }), jsx("div", { className: styles.panelContent, children: jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: "Background" }), jsx(Rn, { value: bgColor, onChange: (c) => setBgColor(c) }), jsx("span", { style: { fontSize: 9, color: "#888" }, children: bgColor })] }) })] }), jsxs("div", { className: styles.panelSection, children: [jsx("div", { className: styles.panelTitle, children: "\uD83C\uDF2B Fog" }), jsxs("div", { className: styles.panelContent, children: [jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: "Enable" }), jsx(Fr, { checked: fogEnabled, onChange: () => setFogEnabled((v) => !v) })] }), fogEnabled && (jsxs(Fragment, { children: [jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: "Color" }), jsx(Rn, { value: fogColor, onChange: (c) => setFogColor(c) })] }), jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: "Near" }), jsx(ir, { className: styles.propInput, value: fogNear, onChange: (val) => setFogNear(val !== null && val !== void 0 ? val : 1), style: { width: 60 } })] }), jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: "Far" }), jsx(ir, { className: styles.propInput, value: fogFar, onChange: (val) => setFogFar(val !== null && val !== void 0 ? val : 100), style: { width: 60 } })] })] }))] })] }), jsxs("div", { className: styles.panelSection, children: [jsx("div", { className: styles.panelTitle, children: "\uD83D\uDD06 Scene Lights" }), jsxs("div", { className: styles.panelContent, children: [jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: "Show Helpers" }), jsx(Fr, { checked: showLightHelpers, onChange: () => setShowLightHelpers((v) => !v) })] }), jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: "Grid" }), jsx(Fr, { checked: showGrid, onChange: () => setShowGrid((v) => !v) })] }), jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: "Axes" }), jsx(Fr, { checked: showAxes, onChange: () => setShowAxes((v) => !v) })] }), jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: "Skeleton" }), jsx(Fr, { checked: showSkeleton, onChange: () => setShowSkeleton((v) => !v) })] })] })] }), jsxs("div", { className: styles.panelSection, children: [jsx("div", { className: styles.panelTitle, children: "\uD83E\uDDF2 Snapping" }), jsxs("div", { className: styles.panelContent, children: [jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: "Snap" }), jsx(Fr, { checked: snapEnabled, onChange: () => setSnapEnabled((v) => !v) })] }), snapEnabled && (jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: "Grid Size" }), jsx(ir, { className: styles.propInput, step: 0.25, min: 0.1, value: snapGrid, onChange: (val) => setSnapGrid(val !== null && val !== void 0 ? val : 1), style: { width: 60 } })] }))] })] })] })), propTab === "modifiers" && (jsxs("div", { className: styles.panelSection, children: [jsx("div", { className: styles.panelTitle, children: "\uD83D\uDD29 Modifiers" }), jsx("div", { className: styles.panelContent, children: selectedNode && selectedNode.object instanceof THREE.Mesh ? (jsxs(Fragment, { children: [jsx(We, { className: styles.btnSecondary, variant: "ghost", size: "sm", style: { width: "100%", marginBottom: 4 }, onClick: () => {
47
47
  const mesh = selectedNode.object;
48
48
  const geo = mesh.geometry;
49
49
  const pos = geo.attributes.position;
@@ -53,17 +53,17 @@ const ModelEditorRightPanel = Ve.memo(({ api }) => {
53
53
  setStatusText("Applied Wireframe modifier");
54
54
  rebuildTree();
55
55
  }
56
- }, children: "+ Wireframe Modifier" }), jsx(Oe, { className: styles.btnSecondary, variant: "ghost", size: "sm", style: { width: "100%", marginBottom: 4 }, onClick: () => {
56
+ }, children: "+ Wireframe Modifier" }), jsx(We, { className: styles.btnSecondary, variant: "ghost", size: "sm", style: { width: "100%", marginBottom: 4 }, onClick: () => {
57
57
  const mesh = selectedNode.object;
58
58
  mesh.geometry.computeVertexNormals();
59
59
  setStatusText("Recomputed normals");
60
- }, children: "Recompute Normals" }), jsx(Oe, { className: styles.btnSecondary, variant: "ghost", size: "sm", style: { width: "100%", marginBottom: 4 }, onClick: () => {
60
+ }, children: "Recompute Normals" }), jsx(We, { className: styles.btnSecondary, variant: "ghost", size: "sm", style: { width: "100%", marginBottom: 4 }, onClick: () => {
61
61
  const mesh = selectedNode.object;
62
62
  const geo = mesh.geometry;
63
63
  geo.center();
64
64
  setStatusText("Centered geometry");
65
65
  setMatRefresh((n) => n + 1);
66
- }, children: "Center Geometry" }), jsx(Oe, { className: styles.btnSecondary, variant: "ghost", size: "sm", style: { width: "100%", marginBottom: 4 }, onClick: () => {
66
+ }, children: "Center Geometry" }), jsx(We, { className: styles.btnSecondary, variant: "ghost", size: "sm", style: { width: "100%", marginBottom: 4 }, onClick: () => {
67
67
  const mesh = selectedNode.object;
68
68
  const geo = mesh.geometry;
69
69
  const idx = geo.index;
@@ -3,7 +3,7 @@ import { useState } from 'react';
3
3
  import * as THREE from 'three';
4
4
  import styles from './ModelEditor.module.css.js';
5
5
  import { DEG } from './modelEditorTypes.js';
6
- import { NiceTextInput as _t, NiceColorPicker as Nn, NiceSlider as Ar, NiceSelect as nn } from '../ui/dist/index.js';
6
+ import { NiceTextInput as Tt, NiceColorPicker as Rn, NiceSlider as so, NiceSelect as fn } from '../ui/dist/index.js';
7
7
 
8
8
  /* ═══════════════════════════════════════════
9
9
  Sub-components
@@ -19,7 +19,7 @@ const Vec3Row = ({ label, value, onChange }) => {
19
19
  onChange();
20
20
  }
21
21
  };
22
- return (jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: label }), jsxs("div", { className: styles.propVec3, children: [jsx(_t, { className: styles.propVec3X, value: value.x.toFixed(3), onChange: (val) => set("x", val) }), jsx(_t, { className: styles.propVec3Y, value: value.y.toFixed(3), onChange: (val) => set("y", val) }), jsx(_t, { className: styles.propVec3Z, value: value.z.toFixed(3), onChange: (val) => set("z", val) })] })] }));
22
+ return (jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: label }), jsxs("div", { className: styles.propVec3, children: [jsx(Tt, { className: styles.propVec3X, value: value.x.toFixed(3), onChange: (val) => set("x", val) }), jsx(Tt, { className: styles.propVec3Y, value: value.y.toFixed(3), onChange: (val) => set("y", val) }), jsx(Tt, { className: styles.propVec3Z, value: value.z.toFixed(3), onChange: (val) => set("z", val) })] })] }));
23
23
  };
24
24
  /** Vec3 input row for rotation (shows degrees) */
25
25
  const Vec3RowDeg = ({ label, value, onChange }) => {
@@ -32,7 +32,7 @@ const Vec3RowDeg = ({ label, value, onChange }) => {
32
32
  onChange();
33
33
  }
34
34
  };
35
- return (jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: label }), jsxs("div", { className: styles.propVec3, children: [jsx(_t, { className: styles.propVec3X, value: (value.x * DEG).toFixed(1), onChange: (val) => set("x", val) }), jsx(_t, { className: styles.propVec3Y, value: (value.y * DEG).toFixed(1), onChange: (val) => set("y", val) }), jsx(_t, { className: styles.propVec3Z, value: (value.z * DEG).toFixed(1), onChange: (val) => set("z", val) })] })] }));
35
+ return (jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: label }), jsxs("div", { className: styles.propVec3, children: [jsx(Tt, { className: styles.propVec3X, value: (value.x * DEG).toFixed(1), onChange: (val) => set("x", val) }), jsx(Tt, { className: styles.propVec3Y, value: (value.y * DEG).toFixed(1), onChange: (val) => set("y", val) }), jsx(Tt, { className: styles.propVec3Z, value: (value.z * DEG).toFixed(1), onChange: (val) => set("z", val) })] })] }));
36
36
  };
37
37
  /** Material card with editable properties */
38
38
  const MaterialCard = ({ material, active, onClick, onUpdate }) => {
@@ -56,16 +56,16 @@ const MaterialCard = ({ material, active, onClick, onUpdate }) => {
56
56
  mat.needsUpdate = true;
57
57
  onUpdate();
58
58
  };
59
- return (jsxs("div", { className: `${styles.materialCard} ${active ? styles.materialCardActive : ""}`, onClick: onClick, children: [jsx("div", { className: styles.materialHeader, children: jsx("span", { className: styles.materialName, children: mat.name || mat.type }) }), hasColor && (jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: "Color" }), jsx(Nn, { value: `#${mat.color.getHexString()}`, onChange: (c) => setColor(c) }), jsxs("span", { style: { fontSize: 10, color: "#888" }, children: ["#", mat.color.getHexString()] })] })), hasMetalness && (jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: "Metal" }), jsx(Ar, { min: 0, max: 1, step: 0.01, value: mat.metalness, onChange: (val) => setNum("metalness", val), style: { flex: 1 } }), jsx("span", { style: { fontSize: 9, color: "#888", width: 28, textAlign: "right" }, children: mat.metalness.toFixed(2) })] })), hasRoughness && (jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: "Rough" }), jsx(Ar, { min: 0, max: 1, step: 0.01, value: mat.roughness, onChange: (val) => setNum("roughness", val), style: { flex: 1 } }), jsx("span", { style: { fontSize: 9, color: "#888", width: 28, textAlign: "right" }, children: mat.roughness.toFixed(2) })] })), hasEmissive && mat.emissive && (jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: "Emissive" }), jsx(Nn, { value: `#${mat.emissive.getHexString()}`, onChange: (c) => {
59
+ return (jsxs("div", { className: `${styles.materialCard} ${active ? styles.materialCardActive : ""}`, onClick: onClick, children: [jsx("div", { className: styles.materialHeader, children: jsx("span", { className: styles.materialName, children: mat.name || mat.type }) }), hasColor && (jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: "Color" }), jsx(Rn, { value: `#${mat.color.getHexString()}`, onChange: (c) => setColor(c) }), jsxs("span", { style: { fontSize: 10, color: "#888" }, children: ["#", mat.color.getHexString()] })] })), hasMetalness && (jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: "Metal" }), jsx(so, { min: 0, max: 1, step: 0.01, value: mat.metalness, onChange: (val) => setNum("metalness", val), style: { flex: 1 } }), jsx("span", { style: { fontSize: 9, color: "#888", width: 28, textAlign: "right" }, children: mat.metalness.toFixed(2) })] })), hasRoughness && (jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: "Rough" }), jsx(so, { min: 0, max: 1, step: 0.01, value: mat.roughness, onChange: (val) => setNum("roughness", val), style: { flex: 1 } }), jsx("span", { style: { fontSize: 9, color: "#888", width: 28, textAlign: "right" }, children: mat.roughness.toFixed(2) })] })), hasEmissive && mat.emissive && (jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: "Emissive" }), jsx(Rn, { value: `#${mat.emissive.getHexString()}`, onChange: (c) => {
60
60
  mat.emissive.set(c);
61
61
  mat.needsUpdate = true;
62
62
  onUpdate();
63
- } })] })), jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: "Opacity" }), jsx(Ar, { min: 0, max: 1, step: 0.01, value: mat.opacity, onChange: (val) => {
63
+ } })] })), jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: "Opacity" }), jsx(so, { min: 0, max: 1, step: 0.01, value: mat.opacity, onChange: (val) => {
64
64
  mat.opacity = val;
65
65
  mat.transparent = val < 1;
66
66
  mat.needsUpdate = true;
67
67
  onUpdate();
68
- }, style: { flex: 1 } }), jsx("span", { style: { fontSize: 9, color: "#888", width: 28, textAlign: "right" }, children: mat.opacity.toFixed(2) })] }), jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: "Side" }), jsx(nn, { className: styles.propSelect, value: String(mat.side), onChange: (val) => {
68
+ }, style: { flex: 1 } }), jsx("span", { style: { fontSize: 9, color: "#888", width: 28, textAlign: "right" }, children: mat.opacity.toFixed(2) })] }), jsxs("div", { className: styles.propRow, children: [jsx("span", { className: styles.propLabel, children: "Side" }), jsx(fn, { className: styles.propSelect, value: String(mat.side), onChange: (val) => {
69
69
  mat.side = parseInt(val);
70
70
  mat.needsUpdate = true;
71
71
  onUpdate();
@@ -1,15 +1,15 @@
1
1
  import { jsxs, jsx } from 'react/jsx-runtime';
2
- import Ve from 'react';
2
+ import Ue from 'react';
3
3
  import styles from './ModelEditor.module.css.js';
4
4
  import { fmtTime } from './modelEditorTypes.js';
5
- import { NiceButton as Oe, NiceSlider as Ar } from '../ui/dist/index.js';
5
+ import { NiceButton as We, NiceSlider as so } from '../ui/dist/index.js';
6
6
 
7
- const ModelEditorTimeline = Ve.memo(({ api }) => {
7
+ const ModelEditorTimeline = Ue.memo(({ api }) => {
8
8
  const { bottomCollapsed, setBottomCollapsed, animations, activeAnimIdx, isPlaying, animTime, animDuration, animSpeed, setAnimSpeed, loopAnim, setLoopAnim, seekAnim, togglePlay, stopAnim, playClip, mergeInputRef, } = api;
9
- return (jsxs("div", { className: `${styles.bottomPanel} ${bottomCollapsed ? styles.bottomPanelCollapsed : ""}`, children: [jsxs("div", { className: styles.bottomPanelHeader, children: [jsx("span", { className: styles.bottomToggle, onClick: () => setBottomCollapsed((c) => !c), children: bottomCollapsed ? "▶" : "▼" }), jsxs("span", { className: styles.bottomPanelTitle, onClick: () => setBottomCollapsed((c) => !c), children: ["Animation (", animations.length, " clip", animations.length !== 1 ? "s" : "", ")"] }), jsx("div", { className: styles.menuSep }), jsxs("div", { className: styles.transportBar, children: [jsx(Oe, { className: styles.transportBtn, variant: "ghost", size: "sm", onClick: () => seekAnim(0), title: "Go to start", children: "\u23EE" }), jsx(Oe, { className: styles.transportBtn, variant: isPlaying ? "primary" : "ghost", size: "sm", onClick: togglePlay, title: "Play/Pause (Space)", children: isPlaying ? "⏸" : "▶" }), jsx(Oe, { className: styles.transportBtn, variant: "ghost", size: "sm", onClick: stopAnim, title: "Stop", children: "\u23F9" }), jsx(Oe, { className: styles.transportBtn, variant: loopAnim ? "primary" : "ghost", size: "sm", onClick: () => setLoopAnim((v) => !v), title: "Loop", children: "\uD83D\uDD01" })] }), jsxs("span", { className: styles.transportTime, children: [fmtTime(animTime), " / ", fmtTime(animDuration)] }), jsx("div", { className: styles.menuSep }), jsx("label", { className: styles.menuLabel, children: "Speed" }), jsx(Ar, { min: 0.1, max: 3, step: 0.1, value: animSpeed, onChange: (val) => setAnimSpeed(val), style: { width: 60 } }), jsxs("span", { className: styles.menuLabel, children: [animSpeed.toFixed(1), "\u00D7"] })] }), !bottomCollapsed && (jsxs("div", { className: styles.animListArea, children: [jsxs("div", { className: styles.animList, children: [animations.length === 0 && (jsx("div", { style: { color: "#666", fontSize: 10, padding: 8 }, children: "No animations loaded" })), animations.map((anim, i) => (jsxs("div", { className: `${styles.animItem} ${i === activeAnimIdx ? styles.animItemActive : ""}`, onClick: () => playClip(i), children: [jsx("span", { children: anim.name }), jsxs("span", { className: styles.animDuration, children: [anim.duration.toFixed(1), "s"] })] }, `${anim.name}-${i}`))), jsx("div", { style: { padding: "4px 8px" }, children: jsx(Oe, { className: styles.btnSecondary, variant: "ghost", size: "sm", style: { width: "100%", fontSize: 10 }, onClick: () => { var _a; return (_a = mergeInputRef.current) === null || _a === void 0 ? void 0 : _a.click(); }, children: "+ Add Animation File" }) })] }), jsxs("div", { className: styles.timelineArea, children: [jsxs("div", { className: styles.timelineRuler, children: [animDuration > 0 &&
9
+ return (jsxs("div", { className: `${styles.bottomPanel} ${bottomCollapsed ? styles.bottomPanelCollapsed : ""}`, children: [jsxs("div", { className: styles.bottomPanelHeader, children: [jsx("span", { className: styles.bottomToggle, onClick: () => setBottomCollapsed((c) => !c), children: bottomCollapsed ? "▶" : "▼" }), jsxs("span", { className: styles.bottomPanelTitle, onClick: () => setBottomCollapsed((c) => !c), children: ["Animation (", animations.length, " clip", animations.length !== 1 ? "s" : "", ")"] }), jsx("div", { className: styles.menuSep }), jsxs("div", { className: styles.transportBar, children: [jsx(We, { className: styles.transportBtn, variant: "ghost", size: "sm", onClick: () => seekAnim(0), title: "Go to start", children: "\u23EE" }), jsx(We, { className: styles.transportBtn, variant: isPlaying ? "primary" : "ghost", size: "sm", onClick: togglePlay, title: "Play/Pause (Space)", children: isPlaying ? "⏸" : "▶" }), jsx(We, { className: styles.transportBtn, variant: "ghost", size: "sm", onClick: stopAnim, title: "Stop", children: "\u23F9" }), jsx(We, { className: styles.transportBtn, variant: loopAnim ? "primary" : "ghost", size: "sm", onClick: () => setLoopAnim((v) => !v), title: "Loop", children: "\uD83D\uDD01" })] }), jsxs("span", { className: styles.transportTime, children: [fmtTime(animTime), " / ", fmtTime(animDuration)] }), jsx("div", { className: styles.menuSep }), jsx("label", { className: styles.menuLabel, children: "Speed" }), jsx(so, { min: 0.1, max: 3, step: 0.1, value: animSpeed, onChange: (val) => setAnimSpeed(val), style: { width: 60 } }), jsxs("span", { className: styles.menuLabel, children: [animSpeed.toFixed(1), "\u00D7"] })] }), !bottomCollapsed && (jsxs("div", { className: styles.animListArea, children: [jsxs("div", { className: styles.animList, children: [animations.length === 0 && (jsx("div", { style: { color: "#666", fontSize: 10, padding: 8 }, children: "No animations loaded" })), animations.map((anim, i) => (jsxs("div", { className: `${styles.animItem} ${i === activeAnimIdx ? styles.animItemActive : ""}`, onClick: () => playClip(i), children: [jsx("span", { children: anim.name }), jsxs("span", { className: styles.animDuration, children: [anim.duration.toFixed(1), "s"] })] }, `${anim.name}-${i}`))), jsx("div", { style: { padding: "4px 8px" }, children: jsx(We, { className: styles.btnSecondary, variant: "ghost", size: "sm", style: { width: "100%", fontSize: 10 }, onClick: () => { var _a; return (_a = mergeInputRef.current) === null || _a === void 0 ? void 0 : _a.click(); }, children: "+ Add Animation File" }) })] }), jsxs("div", { className: styles.timelineArea, children: [jsxs("div", { className: styles.timelineRuler, children: [animDuration > 0 &&
10
10
  Array.from({ length: Math.ceil(animDuration) + 1 }, (_, i) => {
11
11
  const pct = (i / animDuration) * 100;
12
- return (jsxs(Ve.Fragment, { children: [jsx("div", { className: styles.rulerTick, style: { left: `${pct}%` } }), jsxs("span", { className: styles.rulerLabel, style: { left: `${pct}%` }, children: [i, "s"] })] }, i));
12
+ return (jsxs(Ue.Fragment, { children: [jsx("div", { className: styles.rulerTick, style: { left: `${pct}%` } }), jsxs("span", { className: styles.rulerLabel, style: { left: `${pct}%` }, children: [i, "s"] })] }, i));
13
13
  }), animDuration > 0 && (jsx("div", { className: styles.playhead, style: {
14
14
  left: `${(animTime / animDuration) * 100}%`,
15
15
  height: "100%",
@@ -1,11 +1,11 @@
1
1
  import { jsxs, jsx } from 'react/jsx-runtime';
2
- import Ve from 'react';
2
+ import Ue from 'react';
3
3
  import styles from './ModelEditor.module.css.js';
4
- import { NiceButton as Oe } from '../ui/dist/index.js';
4
+ import { NiceButton as We } from '../ui/dist/index.js';
5
5
 
6
- const ModelEditorToolbar = Ve.memo(({ api }) => {
6
+ const ModelEditorToolbar = Ue.memo(({ api }) => {
7
7
  const { transformMode, setTransformMode, snapEnabled, setSnapEnabled, gizmoSpace, setGizmoSpace, rootObjectRef, focusOnObject, setCameraPreset, deleteSelected, duplicateSelected, selectedNode, } = api;
8
- return (jsxs("div", { className: styles.toolbar, children: [jsx(Oe, { className: styles.toolBtn, variant: transformMode === "translate" ? "primary" : "ghost", size: "sm", onClick: () => setTransformMode("translate"), title: "Translate (G)", children: "\u2725" }), jsx(Oe, { className: styles.toolBtn, variant: transformMode === "rotate" ? "primary" : "ghost", size: "sm", onClick: () => setTransformMode("rotate"), title: "Rotate (R)", children: "\u21BB" }), jsx(Oe, { className: styles.toolBtn, variant: transformMode === "scale" ? "primary" : "ghost", size: "sm", onClick: () => setTransformMode("scale"), title: "Scale (S)", children: "\u2B21" }), jsx("div", { className: styles.toolSep }), jsx(Oe, { className: styles.toolBtn, variant: snapEnabled ? "primary" : "ghost", size: "sm", onClick: () => setSnapEnabled((v) => !v), title: `Snap to Grid (${snapEnabled ? "ON" : "OFF"})`, children: "\uD83E\uDDF2" }), jsx(Oe, { className: styles.toolBtn, variant: "ghost", size: "sm", onClick: () => setGizmoSpace((s) => (s === "local" ? "world" : "local")), title: `Orientation: ${gizmoSpace}`, children: gizmoSpace === "local" ? "🔶" : "🌐" }), jsx("div", { className: styles.toolSep }), jsx(Oe, { className: styles.toolBtn, variant: "ghost", size: "sm", onClick: () => rootObjectRef.current && focusOnObject(rootObjectRef.current), title: "Focus (F)", children: "\u25CE" }), jsx(Oe, { className: styles.toolBtn, variant: "ghost", size: "sm", onClick: () => setCameraPreset("front"), title: "Front view (Numpad 1)", children: "1" }), jsx(Oe, { className: styles.toolBtn, variant: "ghost", size: "sm", onClick: () => setCameraPreset("right"), title: "Right view (Numpad 3)", children: "3" }), jsx(Oe, { className: styles.toolBtn, variant: "ghost", size: "sm", onClick: () => setCameraPreset("top"), title: "Top view (Numpad 7)", children: "7" }), jsx("div", { className: styles.toolSep }), jsx(Oe, { className: styles.toolBtn, variant: "ghost", size: "sm", onClick: deleteSelected, title: "Delete selected (Del)", children: "\uD83D\uDDD1" }), jsx(Oe, { className: styles.toolBtn, variant: "ghost", size: "sm", onClick: duplicateSelected, title: "Duplicate (Shift+D)", disabled: !selectedNode, children: "\uD83D\uDCCB" })] }));
8
+ return (jsxs("div", { className: styles.toolbar, children: [jsx(We, { className: styles.toolBtn, variant: transformMode === "translate" ? "primary" : "ghost", size: "sm", onClick: () => setTransformMode("translate"), title: "Translate (G)", children: "\u2725" }), jsx(We, { className: styles.toolBtn, variant: transformMode === "rotate" ? "primary" : "ghost", size: "sm", onClick: () => setTransformMode("rotate"), title: "Rotate (R)", children: "\u21BB" }), jsx(We, { className: styles.toolBtn, variant: transformMode === "scale" ? "primary" : "ghost", size: "sm", onClick: () => setTransformMode("scale"), title: "Scale (S)", children: "\u2B21" }), jsx("div", { className: styles.toolSep }), jsx(We, { className: styles.toolBtn, variant: snapEnabled ? "primary" : "ghost", size: "sm", onClick: () => setSnapEnabled((v) => !v), title: `Snap to Grid (${snapEnabled ? "ON" : "OFF"})`, children: "\uD83E\uDDF2" }), jsx(We, { className: styles.toolBtn, variant: "ghost", size: "sm", onClick: () => setGizmoSpace((s) => (s === "local" ? "world" : "local")), title: `Orientation: ${gizmoSpace}`, children: gizmoSpace === "local" ? "🔶" : "🌐" }), jsx("div", { className: styles.toolSep }), jsx(We, { className: styles.toolBtn, variant: "ghost", size: "sm", onClick: () => rootObjectRef.current && focusOnObject(rootObjectRef.current), title: "Focus (F)", children: "\u25CE" }), jsx(We, { className: styles.toolBtn, variant: "ghost", size: "sm", onClick: () => setCameraPreset("front"), title: "Front view (Numpad 1)", children: "1" }), jsx(We, { className: styles.toolBtn, variant: "ghost", size: "sm", onClick: () => setCameraPreset("right"), title: "Right view (Numpad 3)", children: "3" }), jsx(We, { className: styles.toolBtn, variant: "ghost", size: "sm", onClick: () => setCameraPreset("top"), title: "Top view (Numpad 7)", children: "7" }), jsx("div", { className: styles.toolSep }), jsx(We, { className: styles.toolBtn, variant: "ghost", size: "sm", onClick: deleteSelected, title: "Delete selected (Del)", children: "\uD83D\uDDD1" }), jsx(We, { className: styles.toolBtn, variant: "ghost", size: "sm", onClick: duplicateSelected, title: "Duplicate (Shift+D)", disabled: !selectedNode, children: "\uD83D\uDCCB" })] }));
9
9
  });
10
10
  ModelEditorToolbar.displayName = "ModelEditorToolbar";
11
11
 
@@ -1,8 +1,8 @@
1
1
  import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
2
- import Ve from 'react';
2
+ import Ue from 'react';
3
3
  import styles from './ModelEditor.module.css.js';
4
4
 
5
- const ModelEditorViewport = Ve.memo(({ api }) => {
5
+ const ModelEditorViewport = Ue.memo(({ api }) => {
6
6
  const { mountRef, dragOver, handleDragOver, handleDragLeave, handleDrop, handleViewportClick, handleContextMenu, editorMode, polyCount, meshCount, boneCount, shadingMode, setShadingMode, gizmoSpace, setGizmoSpace, contextMenu, closeContextMenu, addPrimitive, addSceneLight, duplicateSelected, deleteSelected, selectedNode, rootObjectRef, focusOnObject, snapEnabled, setSnapEnabled, } = api;
7
7
  return (jsxs(Fragment, { children: [jsxs("div", { ref: mountRef, className: `${styles.viewportContainer} ${dragOver ? styles.dragOver : ""}`, onDragOver: handleDragOver, onDragLeave: handleDragLeave, onDrop: handleDrop, onClick: handleViewportClick, onContextMenu: handleContextMenu, children: [jsxs("div", { className: styles.viewportOverlay, children: [jsxs("span", { className: styles.viewportBadge, children: [editorMode.toUpperCase(), " MODE"] }), jsxs("span", { className: styles.viewportBadge, children: [polyCount.toLocaleString(), " tris \u00B7 ", meshCount, " meshes \u00B7 ", boneCount, " bones"] })] }), jsx("div", { className: styles.viewportShadingBar, children: ["wireframe", "solid", "material", "rendered"].map((m) => (jsx("button", { className: `${styles.shadingBtn} ${shadingMode === m ? styles.shadingBtnActive : ""}`, onClick: (e) => { e.stopPropagation(); setShadingMode(m); }, title: m.charAt(0).toUpperCase() + m.slice(1), children: m === "wireframe" ? "◇" : m === "solid" ? "◆" : m === "material" ? "🎨" : "☀" }, m))) }), jsx("div", { className: styles.viewportGizmoInfo, children: jsx("button", { className: styles.shadingBtn, onClick: (e) => {
8
8
  e.stopPropagation();
@@ -2,7 +2,7 @@ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
2
2
  import { useState, useRef, useCallback } from 'react';
3
3
  import styles from './ModelViewer.module.css.js';
4
4
  import { useModelViewer } from './useModelViewer.js';
5
- import { NiceButton as Oe, NiceSelect as nn, NiceSlider as Ar } from '../ui/dist/index.js';
5
+ import { NiceButton as We, NiceSelect as fn, NiceSlider as so } from '../ui/dist/index.js';
6
6
 
7
7
  const NAV_LABELS = {
8
8
  orbit: '🌍 Orbit',
@@ -40,23 +40,23 @@ const NiceModelViewer = ({ className, showToolbar = true, showStatusBar = true,
40
40
  const idx = navModes.indexOf(api.navigationMode);
41
41
  api.setNavigationMode(navModes[(idx + 1) % navModes.length]);
42
42
  }, [api]);
43
- return (jsxs("div", { className: `${styles.root} ${className !== null && className !== void 0 ? className : ''}`, children: [showToolbar && (jsxs("div", { className: styles.toolbar, children: [jsx(Oe, { className: styles.toolBtn, variant: "ghost", size: "sm", title: "Open file\u2026", onClick: () => { var _a; return (_a = fileInputRef.current) === null || _a === void 0 ? void 0 : _a.click(); }, children: "\uD83D\uDCC2 Open" }), jsx("input", { ref: fileInputRef, type: "file", accept: ".fbx,.glb,.gltf,.obj,.dae,.stl,.ply", style: { display: 'none' }, onChange: (e) => {
43
+ return (jsxs("div", { className: `${styles.root} ${className !== null && className !== void 0 ? className : ''}`, children: [showToolbar && (jsxs("div", { className: styles.toolbar, children: [jsx(We, { className: styles.toolBtn, variant: "ghost", size: "sm", title: "Open file\u2026", onClick: () => { var _a; return (_a = fileInputRef.current) === null || _a === void 0 ? void 0 : _a.click(); }, children: "\uD83D\uDCC2 Open" }), jsx("input", { ref: fileInputRef, type: "file", accept: ".fbx,.glb,.gltf,.obj,.dae,.stl,.ply", style: { display: 'none' }, onChange: (e) => {
44
44
  var _a;
45
45
  const f = (_a = e.target.files) === null || _a === void 0 ? void 0 : _a[0];
46
46
  if (f)
47
47
  api.loadFile(f);
48
48
  e.target.value = '';
49
- } }), jsx("div", { className: styles.toolSep }), jsx(Oe, { className: styles.toolBtn, variant: "ghost", size: "sm", title: "Navigation mode", onClick: cycleNav, children: NAV_LABELS[api.navigationMode] }), jsx(Oe, { className: styles.toolBtn, variant: api.wireframe ? 'primary' : 'ghost', size: "sm", onClick: () => api.setWireframe(!api.wireframe), title: "Wireframe", children: "\uD83D\uDD32" }), jsx(Oe, { className: styles.toolBtn, variant: api.showGrid ? 'primary' : 'ghost', size: "sm", onClick: () => api.setShowGrid(!api.showGrid), title: "Grid", children: "\u229E" }), jsx(Oe, { className: styles.toolBtn, variant: api.clippingEnabled ? 'primary' : 'ghost', size: "sm", onClick: () => api.setClippingEnabled(!api.clippingEnabled), title: "Clipping plane", children: "\u2702\uFE0F" }), api.clippingEnabled && (jsxs(Fragment, { children: [jsx(nn, { className: styles.animSelect, value: api.clippingAxis, onChange: (val) => api.setClippingAxis(val), options: [
49
+ } }), jsx("div", { className: styles.toolSep }), jsx(We, { className: styles.toolBtn, variant: "ghost", size: "sm", title: "Navigation mode", onClick: cycleNav, children: NAV_LABELS[api.navigationMode] }), jsx(We, { className: styles.toolBtn, variant: api.wireframe ? 'primary' : 'ghost', size: "sm", onClick: () => api.setWireframe(!api.wireframe), title: "Wireframe", children: "\uD83D\uDD32" }), jsx(We, { className: styles.toolBtn, variant: api.showGrid ? 'primary' : 'ghost', size: "sm", onClick: () => api.setShowGrid(!api.showGrid), title: "Grid", children: "\u229E" }), jsx(We, { className: styles.toolBtn, variant: api.clippingEnabled ? 'primary' : 'ghost', size: "sm", onClick: () => api.setClippingEnabled(!api.clippingEnabled), title: "Clipping plane", children: "\u2702\uFE0F" }), api.clippingEnabled && (jsxs(Fragment, { children: [jsx(fn, { className: styles.animSelect, value: api.clippingAxis, onChange: (val) => api.setClippingAxis(val), options: [
50
50
  { value: 'x', label: 'X' },
51
51
  { value: 'y', label: 'Y' },
52
52
  { value: 'z', label: 'Z' },
53
- ] }), jsx(Ar, { min: -10, max: 10, step: 0.1, value: api.clippingPosition, onChange: (val) => api.setClippingPosition(val), style: { width: 60 } })] })), jsx("div", { className: styles.toolSep }), jsx(Oe, { className: styles.toolBtn, variant: "ghost", size: "sm", onClick: api.zoomToFit, title: "Zoom to fit", children: "\uD83D\uDD0D" }), jsx(Oe, { className: styles.toolBtn, variant: "ghost", size: "sm", onClick: api.resetCamera, title: "Reset camera", children: "\uD83C\uDFE0" }), jsx("div", { className: styles.toolSep }), jsx(Oe, { className: styles.toolBtn, variant: "ghost", size: "sm", onClick: () => {
53
+ ] }), jsx(so, { min: -10, max: 10, step: 0.1, value: api.clippingPosition, onChange: (val) => api.setClippingPosition(val), style: { width: 60 } })] })), jsx("div", { className: styles.toolSep }), jsx(We, { className: styles.toolBtn, variant: "ghost", size: "sm", onClick: api.zoomToFit, title: "Zoom to fit", children: "\uD83D\uDD0D" }), jsx(We, { className: styles.toolBtn, variant: "ghost", size: "sm", onClick: api.resetCamera, title: "Reset camera", children: "\uD83C\uDFE0" }), jsx("div", { className: styles.toolSep }), jsx(We, { className: styles.toolBtn, variant: "ghost", size: "sm", onClick: () => {
54
54
  const url = api.screenshot();
55
55
  const a = document.createElement('a');
56
56
  a.href = url;
57
57
  a.download = 'screenshot.png';
58
58
  a.click();
59
- }, title: "Screenshot", children: "\uD83D\uDCF7" }), jsx(Oe, { className: styles.toolBtn, variant: "ghost", size: "sm", onClick: api.toggleFullscreen, title: "Fullscreen", children: api.isFullscreen ? '⊡' : '⛶' }), toolbarExtra, showInfoPanel && (jsx("div", { className: styles.toolRight, children: jsx(Oe, { className: `${styles.toolBtn} ${panelOpen ? styles.active : ''}`, variant: panelOpen ? 'primary' : 'ghost', size: "sm", onClick: () => setPanelOpen(!panelOpen), title: "Info panel", children: "\u2139\uFE0F" }) }))] })), jsxs("div", { className: styles.viewport, ref: api.mountRef, tabIndex: 0, onDragOver: handleDragOver, onDragLeave: handleDragLeave, onDrop: handleDrop, children: [api.loading && (jsxs("div", { className: styles.loadingOverlay, children: [jsx("span", { className: styles.loadingText, children: "Loading model\u2026" }), jsx("div", { className: styles.loadingBar, children: jsx("div", { className: styles.loadingBarFill, style: { width: `${(api.loadProgress * 100).toFixed(0)}%` } }) })] })), api.error && (jsxs("div", { className: styles.errorOverlay, children: [jsx("span", { style: { fontSize: 28 }, children: "\u26A0\uFE0F" }), jsx("span", { style: { marginTop: 8 }, children: api.error })] })), dragOver && (jsx("div", { className: styles.dropZone, children: "Drop 3D model file here" }))] }), jsxs("div", { className: `${styles.sidePanel} ${!panelOpen ? styles.hidden : ''}`, children: [jsxs("div", { className: styles.panelSection, children: [jsx("div", { className: styles.panelTitle, children: "Model Info" }), jsxs("div", { className: styles.panelRow, children: [jsx("label", { children: "Triangles" }), jsx("span", { children: api.polyCount.toLocaleString() })] }), jsxs("div", { className: styles.panelRow, children: [jsx("label", { children: "Meshes" }), jsx("span", { children: api.meshCount })] }), api.dimensions && (jsxs(Fragment, { children: [jsxs("div", { className: styles.panelRow, children: [jsx("label", { children: "Width" }), jsx("span", { children: api.dimensions.width })] }), jsxs("div", { className: styles.panelRow, children: [jsx("label", { children: "Height" }), jsx("span", { children: api.dimensions.height })] }), jsxs("div", { className: styles.panelRow, children: [jsx("label", { children: "Depth" }), jsx("span", { children: api.dimensions.depth })] })] }))] }), jsxs("div", { className: styles.panelSection, children: [jsx("div", { className: styles.panelTitle, children: "Navigation" }), navModes.map((m) => (jsx(Oe, { className: styles.toolBtn, variant: api.navigationMode === m ? 'primary' : 'ghost', size: "sm", onClick: () => api.setNavigationMode(m), children: NAV_LABELS[m] }, m)))] }), jsxs("div", { className: styles.panelSection, children: [jsx("div", { className: styles.panelTitle, children: "Auto Rotate" }), jsx(Ar, { min: 0, max: 10, step: 0.5, value: api.autoRotateSpeed, onChange: (val) => api.setAutoRotateSpeed(val), style: { width: '100%' } })] }), api.animations.length > 0 && (jsxs("div", { className: styles.panelSection, children: [jsx("div", { className: styles.panelTitle, children: "Animations" }), api.animations.map((name, i) => (jsxs(Oe, { className: styles.toolBtn, variant: api.activeAnimIdx === i ? 'primary' : 'ghost', size: "sm", onClick: () => api.playAnimation(i), children: ["\u25B6 ", name] }, i)))] }))] }), jsxs("div", { className: `${styles.animBar} ${api.animations.length === 0 ? styles.hidden : ''}`, children: [jsx(Oe, { className: styles.toolBtn, variant: "ghost", size: "sm", onClick: api.togglePlay, children: api.isPlaying ? '⏸' : '▶' }), jsx(nn, { className: styles.animSelect, value: String(api.activeAnimIdx), onChange: (val) => api.playAnimation(parseInt(val)), options: api.animations.map((n, i) => ({ value: String(i), label: n })) }), jsx(Ar, { className: styles.animSlider, min: 0, max: api.animDuration || 1, step: 0.01, value: api.animTime, onChange: (val) => api.seekAnim(val) }), jsxs("span", { className: styles.toolLabel, children: [api.animTime.toFixed(1), "s / ", api.animDuration.toFixed(1), "s"] })] }), showStatusBar && (jsxs("div", { className: styles.statusBar, children: [jsx("span", { children: api.statusText }), jsxs("div", { className: styles.statusRight, children: [jsx("span", { children: NAV_LABELS[api.navigationMode] }), api.dimensions && (jsxs("span", { children: [api.dimensions.width, "\u00D7", api.dimensions.height, "\u00D7", api.dimensions.depth] }))] })] })), children === null || children === void 0 ? void 0 : children(api)] }));
59
+ }, title: "Screenshot", children: "\uD83D\uDCF7" }), jsx(We, { className: styles.toolBtn, variant: "ghost", size: "sm", onClick: api.toggleFullscreen, title: "Fullscreen", children: api.isFullscreen ? '⊡' : '⛶' }), toolbarExtra, showInfoPanel && (jsx("div", { className: styles.toolRight, children: jsx(We, { className: `${styles.toolBtn} ${panelOpen ? styles.active : ''}`, variant: panelOpen ? 'primary' : 'ghost', size: "sm", onClick: () => setPanelOpen(!panelOpen), title: "Info panel", children: "\u2139\uFE0F" }) }))] })), jsxs("div", { className: styles.viewport, ref: api.mountRef, tabIndex: 0, onDragOver: handleDragOver, onDragLeave: handleDragLeave, onDrop: handleDrop, children: [api.loading && (jsxs("div", { className: styles.loadingOverlay, children: [jsx("span", { className: styles.loadingText, children: "Loading model\u2026" }), jsx("div", { className: styles.loadingBar, children: jsx("div", { className: styles.loadingBarFill, style: { width: `${(api.loadProgress * 100).toFixed(0)}%` } }) })] })), api.error && (jsxs("div", { className: styles.errorOverlay, children: [jsx("span", { style: { fontSize: 28 }, children: "\u26A0\uFE0F" }), jsx("span", { style: { marginTop: 8 }, children: api.error })] })), dragOver && (jsx("div", { className: styles.dropZone, children: "Drop 3D model file here" }))] }), jsxs("div", { className: `${styles.sidePanel} ${!panelOpen ? styles.hidden : ''}`, children: [jsxs("div", { className: styles.panelSection, children: [jsx("div", { className: styles.panelTitle, children: "Model Info" }), jsxs("div", { className: styles.panelRow, children: [jsx("label", { children: "Triangles" }), jsx("span", { children: api.polyCount.toLocaleString() })] }), jsxs("div", { className: styles.panelRow, children: [jsx("label", { children: "Meshes" }), jsx("span", { children: api.meshCount })] }), api.dimensions && (jsxs(Fragment, { children: [jsxs("div", { className: styles.panelRow, children: [jsx("label", { children: "Width" }), jsx("span", { children: api.dimensions.width })] }), jsxs("div", { className: styles.panelRow, children: [jsx("label", { children: "Height" }), jsx("span", { children: api.dimensions.height })] }), jsxs("div", { className: styles.panelRow, children: [jsx("label", { children: "Depth" }), jsx("span", { children: api.dimensions.depth })] })] }))] }), jsxs("div", { className: styles.panelSection, children: [jsx("div", { className: styles.panelTitle, children: "Navigation" }), navModes.map((m) => (jsx(We, { className: styles.toolBtn, variant: api.navigationMode === m ? 'primary' : 'ghost', size: "sm", onClick: () => api.setNavigationMode(m), children: NAV_LABELS[m] }, m)))] }), jsxs("div", { className: styles.panelSection, children: [jsx("div", { className: styles.panelTitle, children: "Auto Rotate" }), jsx(so, { min: 0, max: 10, step: 0.5, value: api.autoRotateSpeed, onChange: (val) => api.setAutoRotateSpeed(val), style: { width: '100%' } })] }), api.animations.length > 0 && (jsxs("div", { className: styles.panelSection, children: [jsx("div", { className: styles.panelTitle, children: "Animations" }), api.animations.map((name, i) => (jsxs(We, { className: styles.toolBtn, variant: api.activeAnimIdx === i ? 'primary' : 'ghost', size: "sm", onClick: () => api.playAnimation(i), children: ["\u25B6 ", name] }, i)))] }))] }), jsxs("div", { className: `${styles.animBar} ${api.animations.length === 0 ? styles.hidden : ''}`, children: [jsx(We, { className: styles.toolBtn, variant: "ghost", size: "sm", onClick: api.togglePlay, children: api.isPlaying ? '⏸' : '▶' }), jsx(fn, { className: styles.animSelect, value: String(api.activeAnimIdx), onChange: (val) => api.playAnimation(parseInt(val)), options: api.animations.map((n, i) => ({ value: String(i), label: n })) }), jsx(so, { className: styles.animSlider, min: 0, max: api.animDuration || 1, step: 0.01, value: api.animTime, onChange: (val) => api.seekAnim(val) }), jsxs("span", { className: styles.toolLabel, children: [api.animTime.toFixed(1), "s / ", api.animDuration.toFixed(1), "s"] })] }), showStatusBar && (jsxs("div", { className: styles.statusBar, children: [jsx("span", { children: api.statusText }), jsxs("div", { className: styles.statusRight, children: [jsx("span", { children: NAV_LABELS[api.navigationMode] }), api.dimensions && (jsxs("span", { children: [api.dimensions.width, "\u00D7", api.dimensions.height, "\u00D7", api.dimensions.depth] }))] })] })), children === null || children === void 0 ? void 0 : children(api)] }));
60
60
  };
61
61
  /** @deprecated Use NiceModelViewer */
62
62
  const ModelViewer = NiceModelViewer;