@nice2dev/ui-3d 1.0.3 → 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.
- package/dist/cjs/core/i18n.js +3 -3
- package/dist/cjs/dance/DanceBridge.js +13 -13
- package/dist/cjs/dance/DanceScoreEngine.js +8 -8
- package/dist/cjs/dance/PoseDetector.js +20 -20
- package/dist/cjs/material/NiceMaterialEditor.js +19 -19
- package/dist/cjs/model/ModelEditorLeftPanel.js +3 -3
- package/dist/cjs/model/ModelEditorMenuBar.js +2 -2
- package/dist/cjs/model/ModelEditorRightPanel.js +2 -2
- package/dist/cjs/model/ModelEditorSubComponents.js +3 -3
- package/dist/cjs/model/ModelEditorTimeline.js +3 -3
- package/dist/cjs/model/ModelEditorToolbar.js +2 -2
- package/dist/cjs/model/ModelEditorViewport.js +2 -2
- package/dist/cjs/model/ModelViewer.js +8 -8
- package/dist/cjs/model/NiceArmatureEditor.js +18 -18
- package/dist/cjs/model/NiceMorphTargetEditor.js +18 -18
- package/dist/cjs/model/NiceOctree.js +16 -16
- package/dist/cjs/model/NicePhysicsSimulation.js +24 -24
- package/dist/cjs/model/NiceProceduralGeometry.js +8 -8
- package/dist/cjs/model/NiceTerrainEditor.js +19 -19
- package/dist/cjs/model/NiceWeightPainter.js +14 -14
- package/dist/cjs/model/NiceXRPreview.js +18 -18
- package/dist/cjs/model/useModelEditor.js +127 -127
- package/dist/cjs/model/useModelViewer.js +61 -61
- package/dist/cjs/particle/NiceParticleEditor.js +11 -11
- package/dist/cjs/rendering/NiceCascadedShadows.js +12 -12
- package/dist/cjs/rendering/NiceRenderExport.js +5 -5
- package/dist/cjs/rendering/NiceSSAO.js +18 -18
- package/dist/cjs/rendering/NiceSSR.js +14 -14
- package/dist/cjs/rendering/NiceWebGPURenderer.js +21 -21
- package/dist/cjs/ui/dist/index.js +47227 -31319
- package/dist/cjs/ui/dist/index.js.map +1 -1
- package/dist/cjs/uv/NiceUVEditor.js +24 -24
- package/dist/esm/model/ModelEditorLeftPanel.js +5 -5
- package/dist/esm/model/ModelEditorMenuBar.js +5 -5
- package/dist/esm/model/ModelEditorRightPanel.js +17 -17
- package/dist/esm/model/ModelEditorSubComponents.js +6 -6
- package/dist/esm/model/ModelEditorTimeline.js +5 -5
- package/dist/esm/model/ModelEditorToolbar.js +4 -4
- package/dist/esm/model/ModelEditorViewport.js +2 -2
- package/dist/esm/model/ModelViewer.js +5 -5
- package/dist/esm/ui/dist/index.js +36901 -21016
- package/dist/esm/ui/dist/index.js.map +1 -1
- package/package.json +2 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var jsxRuntime = require('react/jsx-runtime');
|
|
4
|
-
var
|
|
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 =
|
|
14
|
-
const containerRef =
|
|
13
|
+
const canvasRef = Ue.useRef(null);
|
|
14
|
+
const containerRef = Ue.useRef(null);
|
|
15
15
|
// State
|
|
16
|
-
const [state, setState] =
|
|
16
|
+
const [state, setState] = Ue.useState({
|
|
17
17
|
...uvEditorTypes.DEFAULT_UV_STATE,
|
|
18
18
|
activeChannel: channel,
|
|
19
19
|
});
|
|
20
|
-
const [meshData, setMeshData] =
|
|
21
|
-
const [selection, setSelection] =
|
|
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] =
|
|
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] =
|
|
35
|
-
const [redoStack, setRedoStack] =
|
|
34
|
+
const [undoStack, setUndoStack] = Ue.useState([]);
|
|
35
|
+
const [redoStack, setRedoStack] = Ue.useState([]);
|
|
36
36
|
// Extract mesh data when geometry changes
|
|
37
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
2
|
+
import Ue from 'react';
|
|
3
3
|
import styles from './ModelEditor.module.css.js';
|
|
4
|
-
import { NiceButton as
|
|
4
|
+
import { NiceButton as We, NiceTextInput as Tt } from '../ui/dist/index.js';
|
|
5
5
|
|
|
6
|
-
const ModelEditorLeftPanel =
|
|
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 = Ae.memo(({ api }) => {
|
|
|
22
22
|
node.type === "light" ? "💡" :
|
|
23
23
|
node.type === "camera" ? "📷" :
|
|
24
24
|
node.type === "helper" ? "◇" : "📁";
|
|
25
|
-
return (jsxs(
|
|
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 = Ae.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(
|
|
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
|
|
2
|
+
import Ue from 'react';
|
|
3
3
|
import styles from './ModelEditor.module.css.js';
|
|
4
|
-
import { NiceSelect as
|
|
4
|
+
import { NiceSelect as fn, NiceButton as We, NiceCheckbox as Fr, NiceColorPicker as Rn } from '../ui/dist/index.js';
|
|
5
5
|
|
|
6
|
-
const ModelEditorMenuBar =
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 = Ae.memo(({ api }) => {
|
|
|
53
53
|
setStatusText("Applied Wireframe modifier");
|
|
54
54
|
rebuildTree();
|
|
55
55
|
}
|
|
56
|
-
}, children: "+ Wireframe Modifier" }), jsx(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
5
|
+
import { NiceButton as We, NiceSlider as so } from '../ui/dist/index.js';
|
|
6
6
|
|
|
7
|
-
const ModelEditorTimeline =
|
|
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(
|
|
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(
|
|
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
|
|
2
|
+
import Ue from 'react';
|
|
3
3
|
import styles from './ModelEditor.module.css.js';
|
|
4
|
-
import { NiceButton as
|
|
4
|
+
import { NiceButton as We } from '../ui/dist/index.js';
|
|
5
5
|
|
|
6
|
-
const ModelEditorToolbar =
|
|
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(
|
|
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
|
|
2
|
+
import Ue from 'react';
|
|
3
3
|
import styles from './ModelEditor.module.css.js';
|
|
4
4
|
|
|
5
|
-
const ModelEditorViewport =
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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;
|