@nice2dev/ui-3d 1.1.0 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/collaborative/collaborativeScene.js +16 -8
- package/dist/cjs/collaborative/collaborativeScene.js.map +1 -1
- package/dist/cjs/core/i18n.js +3 -3
- package/dist/cjs/dance/DanceBridge.js +65 -30
- package/dist/cjs/dance/DanceBridge.js.map +1 -1
- package/dist/cjs/dance/DanceScoreEngine.js +37 -23
- package/dist/cjs/dance/DanceScoreEngine.js.map +1 -1
- package/dist/cjs/dance/PoseDetector.js +92 -49
- package/dist/cjs/dance/PoseDetector.js.map +1 -1
- package/dist/cjs/material/NiceMaterialEditor.js +134 -91
- package/dist/cjs/material/NiceMaterialEditor.js.map +1 -1
- package/dist/cjs/material/materialEditorUtils.js +86 -55
- package/dist/cjs/material/materialEditorUtils.js.map +1 -1
- package/dist/cjs/model/ModelEditor.js +16 -14
- package/dist/cjs/model/ModelEditor.js.map +1 -1
- package/dist/cjs/model/ModelEditorLeftPanel.js +21 -15
- package/dist/cjs/model/ModelEditorLeftPanel.js.map +1 -1
- package/dist/cjs/model/ModelEditorMenuBar.js +23 -10
- package/dist/cjs/model/ModelEditorMenuBar.js.map +1 -1
- package/dist/cjs/model/ModelEditorRightPanel.js +32 -27
- package/dist/cjs/model/ModelEditorRightPanel.js.map +1 -1
- package/dist/cjs/model/ModelEditorSubComponents.js +20 -20
- package/dist/cjs/model/ModelEditorSubComponents.js.map +1 -1
- package/dist/cjs/model/ModelEditorTimeline.js +12 -11
- package/dist/cjs/model/ModelEditorTimeline.js.map +1 -1
- package/dist/cjs/model/ModelEditorToolbar.js +5 -5
- package/dist/cjs/model/ModelEditorToolbar.js.map +1 -1
- package/dist/cjs/model/ModelEditorViewport.js +12 -8
- package/dist/cjs/model/ModelEditorViewport.js.map +1 -1
- package/dist/cjs/model/ModelViewer.js +16 -15
- package/dist/cjs/model/ModelViewer.js.map +1 -1
- package/dist/cjs/model/NiceArmatureEditor.js +53 -30
- package/dist/cjs/model/NiceArmatureEditor.js.map +1 -1
- package/dist/cjs/model/NiceMorphTargetEditor.js +51 -30
- package/dist/cjs/model/NiceMorphTargetEditor.js.map +1 -1
- package/dist/cjs/model/NiceOctree.js +62 -37
- package/dist/cjs/model/NiceOctree.js.map +1 -1
- package/dist/cjs/model/NicePhysicsSimulation.js +74 -36
- package/dist/cjs/model/NicePhysicsSimulation.js.map +1 -1
- package/dist/cjs/model/NiceProceduralGeometry.js +101 -31
- package/dist/cjs/model/NiceProceduralGeometry.js.map +1 -1
- package/dist/cjs/model/NiceTerrainEditor.js +88 -38
- package/dist/cjs/model/NiceTerrainEditor.js.map +1 -1
- package/dist/cjs/model/NiceWeightPainter.js +46 -24
- package/dist/cjs/model/NiceWeightPainter.js.map +1 -1
- package/dist/cjs/model/NiceXRPreview.js +89 -43
- package/dist/cjs/model/NiceXRPreview.js.map +1 -1
- package/dist/cjs/model/cadModeUtils.js +2 -2
- package/dist/cjs/model/cadModeUtils.js.map +1 -1
- package/dist/cjs/model/editorShortcuts.js +69 -35
- package/dist/cjs/model/editorShortcuts.js.map +1 -1
- package/dist/cjs/model/modelEditorTypes.js +56 -52
- package/dist/cjs/model/modelEditorTypes.js.map +1 -1
- package/dist/cjs/model/modelEditorUtils.js +356 -131
- package/dist/cjs/model/modelEditorUtils.js.map +1 -1
- package/dist/cjs/model/simsModeUtils.js +819 -74
- package/dist/cjs/model/simsModeUtils.js.map +1 -1
- package/dist/cjs/model/useModelEditor.js +501 -394
- package/dist/cjs/model/useModelEditor.js.map +1 -1
- package/dist/cjs/model/useModelViewer.js +117 -86
- package/dist/cjs/model/useModelViewer.js.map +1 -1
- package/dist/cjs/particle/NiceParticleEditor.js +126 -69
- package/dist/cjs/particle/NiceParticleEditor.js.map +1 -1
- package/dist/cjs/particle/particleEditorUtils.js +64 -27
- package/dist/cjs/particle/particleEditorUtils.js.map +1 -1
- package/dist/cjs/rendering/NiceCascadedShadows.js +35 -23
- package/dist/cjs/rendering/NiceCascadedShadows.js.map +1 -1
- package/dist/cjs/rendering/NiceRenderExport.js +26 -12
- package/dist/cjs/rendering/NiceRenderExport.js.map +1 -1
- package/dist/cjs/rendering/NiceSSAO.js +28 -23
- package/dist/cjs/rendering/NiceSSAO.js.map +1 -1
- package/dist/cjs/rendering/NiceSSR.js +22 -18
- package/dist/cjs/rendering/NiceSSR.js.map +1 -1
- package/dist/cjs/rendering/NiceWebGPURenderer.js +60 -37
- package/dist/cjs/rendering/NiceWebGPURenderer.js.map +1 -1
- package/dist/cjs/ui/dist/NiceErrorBoundary-D75yr_WE.js +572 -0
- package/dist/cjs/ui/dist/NiceErrorBoundary-D75yr_WE.js.map +1 -0
- package/dist/cjs/ui/dist/NiceForm-CKoJA6NG.js +4979 -0
- package/dist/cjs/ui/dist/NiceForm-CKoJA6NG.js.map +1 -0
- package/dist/cjs/ui/dist/NiceModal-1QX7NhyH.js +98 -0
- package/dist/cjs/ui/dist/NiceModal-1QX7NhyH.js.map +1 -0
- package/dist/cjs/ui/dist/NiceModuleLifecyclePanel-CMrPbSw9.js +5244 -0
- package/dist/cjs/ui/dist/NiceModuleLifecyclePanel-CMrPbSw9.js.map +1 -0
- package/dist/cjs/ui/dist/NicePagination-DbE-uOkl.js +174 -0
- package/dist/cjs/ui/dist/NicePagination-DbE-uOkl.js.map +1 -0
- package/dist/cjs/ui/dist/NicePinCodeInput-B8cI8zu8.js +10459 -0
- package/dist/cjs/ui/dist/NicePinCodeInput-B8cI8zu8.js.map +1 -0
- package/dist/cjs/ui/dist/NiceSavedQueryPanel-CZ5bYOe_.js +6313 -0
- package/dist/cjs/ui/dist/NiceSavedQueryPanel-CZ5bYOe_.js.map +1 -0
- package/dist/cjs/ui/dist/NiceTabs-DzLHzwuA.js +1639 -0
- package/dist/cjs/ui/dist/NiceTabs-DzLHzwuA.js.map +1 -0
- package/dist/cjs/ui/dist/NiceWindow-DeC83yfE.js +807 -0
- package/dist/cjs/ui/dist/NiceWindow-DeC83yfE.js.map +1 -0
- package/dist/cjs/ui/dist/charts-BAS21jkC.js +3833 -0
- package/dist/cjs/ui/dist/charts-BAS21jkC.js.map +1 -0
- package/dist/cjs/ui/dist/core-Da43q-pO.js +11683 -0
- package/dist/cjs/ui/dist/core-Da43q-pO.js.map +1 -0
- package/dist/cjs/ui/dist/index-pAuGTbuB.js +28876 -0
- package/dist/cjs/ui/dist/index-pAuGTbuB.js.map +1 -0
- package/dist/cjs/uv/NiceUVEditor.js +115 -86
- package/dist/cjs/uv/NiceUVEditor.js.map +1 -1
- package/dist/cjs/uv/uvEditorUtils.js +41 -23
- package/dist/cjs/uv/uvEditorUtils.js.map +1 -1
- package/dist/esm/collaborative/collaborativeScene.js +16 -8
- package/dist/esm/collaborative/collaborativeScene.js.map +1 -1
- package/dist/esm/dance/DanceBridge.js +55 -20
- package/dist/esm/dance/DanceBridge.js.map +1 -1
- package/dist/esm/dance/DanceScoreEngine.js +29 -15
- package/dist/esm/dance/DanceScoreEngine.js.map +1 -1
- package/dist/esm/dance/PoseDetector.js +73 -30
- package/dist/esm/dance/PoseDetector.js.map +1 -1
- package/dist/esm/material/NiceMaterialEditor.js +115 -72
- package/dist/esm/material/NiceMaterialEditor.js.map +1 -1
- package/dist/esm/material/materialEditorUtils.js +86 -55
- package/dist/esm/material/materialEditorUtils.js.map +1 -1
- package/dist/esm/model/ModelEditor.js +16 -14
- package/dist/esm/model/ModelEditor.js.map +1 -1
- package/dist/esm/model/ModelEditorLeftPanel.js +21 -15
- package/dist/esm/model/ModelEditorLeftPanel.js.map +1 -1
- package/dist/esm/model/ModelEditorMenuBar.js +23 -10
- package/dist/esm/model/ModelEditorMenuBar.js.map +1 -1
- package/dist/esm/model/ModelEditorRightPanel.js +32 -27
- package/dist/esm/model/ModelEditorRightPanel.js.map +1 -1
- package/dist/esm/model/ModelEditorSubComponents.js +17 -17
- package/dist/esm/model/ModelEditorSubComponents.js.map +1 -1
- package/dist/esm/model/ModelEditorTimeline.js +12 -11
- package/dist/esm/model/ModelEditorTimeline.js.map +1 -1
- package/dist/esm/model/ModelEditorToolbar.js +5 -5
- package/dist/esm/model/ModelEditorToolbar.js.map +1 -1
- package/dist/esm/model/ModelEditorViewport.js +12 -8
- package/dist/esm/model/ModelEditorViewport.js.map +1 -1
- package/dist/esm/model/ModelViewer.js +8 -7
- package/dist/esm/model/ModelViewer.js.map +1 -1
- package/dist/esm/model/NiceArmatureEditor.js +36 -13
- package/dist/esm/model/NiceArmatureEditor.js.map +1 -1
- package/dist/esm/model/NiceMorphTargetEditor.js +34 -13
- package/dist/esm/model/NiceMorphTargetEditor.js.map +1 -1
- package/dist/esm/model/NiceOctree.js +47 -22
- package/dist/esm/model/NiceOctree.js.map +1 -1
- package/dist/esm/model/NicePhysicsSimulation.js +51 -13
- package/dist/esm/model/NicePhysicsSimulation.js.map +1 -1
- package/dist/esm/model/NiceProceduralGeometry.js +94 -24
- package/dist/esm/model/NiceProceduralGeometry.js.map +1 -1
- package/dist/esm/model/NiceTerrainEditor.js +70 -20
- package/dist/esm/model/NiceTerrainEditor.js.map +1 -1
- package/dist/esm/model/NiceWeightPainter.js +33 -11
- package/dist/esm/model/NiceWeightPainter.js.map +1 -1
- package/dist/esm/model/NiceXRPreview.js +72 -26
- package/dist/esm/model/NiceXRPreview.js.map +1 -1
- package/dist/esm/model/cadModeUtils.js +2 -2
- package/dist/esm/model/cadModeUtils.js.map +1 -1
- package/dist/esm/model/editorShortcuts.js +69 -35
- package/dist/esm/model/editorShortcuts.js.map +1 -1
- package/dist/esm/model/modelEditorTypes.js +56 -52
- package/dist/esm/model/modelEditorTypes.js.map +1 -1
- package/dist/esm/model/modelEditorUtils.js +356 -131
- package/dist/esm/model/modelEditorUtils.js.map +1 -1
- package/dist/esm/model/simsModeUtils.js +819 -74
- package/dist/esm/model/simsModeUtils.js.map +1 -1
- package/dist/esm/model/useModelEditor.js +384 -277
- package/dist/esm/model/useModelEditor.js.map +1 -1
- package/dist/esm/model/useModelViewer.js +56 -25
- package/dist/esm/model/useModelViewer.js.map +1 -1
- package/dist/esm/particle/NiceParticleEditor.js +116 -59
- package/dist/esm/particle/NiceParticleEditor.js.map +1 -1
- package/dist/esm/particle/particleEditorUtils.js +64 -27
- package/dist/esm/particle/particleEditorUtils.js.map +1 -1
- package/dist/esm/rendering/NiceCascadedShadows.js +24 -12
- package/dist/esm/rendering/NiceCascadedShadows.js.map +1 -1
- package/dist/esm/rendering/NiceRenderExport.js +22 -8
- package/dist/esm/rendering/NiceRenderExport.js.map +1 -1
- package/dist/esm/rendering/NiceSSAO.js +11 -6
- package/dist/esm/rendering/NiceSSAO.js.map +1 -1
- package/dist/esm/rendering/NiceSSR.js +9 -5
- package/dist/esm/rendering/NiceSSR.js.map +1 -1
- package/dist/esm/rendering/NiceWebGPURenderer.js +40 -17
- package/dist/esm/rendering/NiceWebGPURenderer.js.map +1 -1
- package/dist/esm/ui/dist/NiceErrorBoundary-D75yr_WE.js +563 -0
- package/dist/esm/ui/dist/NiceErrorBoundary-D75yr_WE.js.map +1 -0
- package/dist/esm/ui/dist/NiceForm-CKoJA6NG.js +4960 -0
- package/dist/esm/ui/dist/NiceForm-CKoJA6NG.js.map +1 -0
- package/dist/esm/ui/dist/NiceModal-1QX7NhyH.js +96 -0
- package/dist/esm/ui/dist/NiceModal-1QX7NhyH.js.map +1 -0
- package/dist/esm/ui/dist/NiceModuleLifecyclePanel-CMrPbSw9.js +5201 -0
- package/dist/esm/ui/dist/NiceModuleLifecyclePanel-CMrPbSw9.js.map +1 -0
- package/dist/esm/ui/dist/NicePagination-DbE-uOkl.js +172 -0
- package/dist/esm/ui/dist/NicePagination-DbE-uOkl.js.map +1 -0
- package/dist/esm/ui/dist/NicePinCodeInput-B8cI8zu8.js +10408 -0
- package/dist/esm/ui/dist/NicePinCodeInput-B8cI8zu8.js.map +1 -0
- package/dist/esm/ui/dist/NiceSavedQueryPanel-CZ5bYOe_.js +6296 -0
- package/dist/esm/ui/dist/NiceSavedQueryPanel-CZ5bYOe_.js.map +1 -0
- package/dist/esm/ui/dist/NiceTabs-DzLHzwuA.js +1630 -0
- package/dist/esm/ui/dist/NiceTabs-DzLHzwuA.js.map +1 -0
- package/dist/esm/ui/dist/NiceWindow-DeC83yfE.js +796 -0
- package/dist/esm/ui/dist/NiceWindow-DeC83yfE.js.map +1 -0
- package/dist/esm/ui/dist/charts-BAS21jkC.js +3809 -0
- package/dist/esm/ui/dist/charts-BAS21jkC.js.map +1 -0
- package/dist/esm/ui/dist/core-Da43q-pO.js +11502 -0
- package/dist/esm/ui/dist/core-Da43q-pO.js.map +1 -0
- package/dist/esm/ui/dist/index-pAuGTbuB.js +28792 -0
- package/dist/esm/ui/dist/index-pAuGTbuB.js.map +1 -0
- package/dist/esm/uv/NiceUVEditor.js +91 -62
- package/dist/esm/uv/NiceUVEditor.js.map +1 -1
- package/dist/esm/uv/uvEditorUtils.js +41 -23
- package/dist/esm/uv/uvEditorUtils.js.map +1 -1
- package/package.json +2 -2
|
@@ -2,25 +2,25 @@ import { useRef, useState, useEffect, useCallback, useMemo } from 'react';
|
|
|
2
2
|
import * as THREE from 'three';
|
|
3
3
|
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
|
|
4
4
|
import { TransformControls } from 'three/examples/jsm/controls/TransformControls.js';
|
|
5
|
-
import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader.js';
|
|
6
|
-
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
|
|
7
5
|
import { GLTFExporter } from 'three/examples/jsm/exporters/GLTFExporter.js';
|
|
8
6
|
import { OBJExporter } from 'three/examples/jsm/exporters/OBJExporter.js';
|
|
9
|
-
import { STLExporter } from 'three/examples/jsm/exporters/STLExporter.js';
|
|
10
7
|
import { PLYExporter } from 'three/examples/jsm/exporters/PLYExporter.js';
|
|
8
|
+
import { STLExporter } from 'three/examples/jsm/exporters/STLExporter.js';
|
|
11
9
|
import { USDZExporter } from 'three/examples/jsm/exporters/USDZExporter.js';
|
|
12
|
-
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js';
|
|
13
|
-
import { ColladaLoader } from 'three/examples/jsm/loaders/ColladaLoader.js';
|
|
14
|
-
import { STLLoader } from 'three/examples/jsm/loaders/STLLoader.js';
|
|
15
|
-
import { PLYLoader } from 'three/examples/jsm/loaders/PLYLoader.js';
|
|
16
|
-
import { TDSLoader } from 'three/examples/jsm/loaders/TDSLoader.js';
|
|
17
10
|
import { ThreeMFLoader } from 'three/examples/jsm/loaders/3MFLoader.js';
|
|
18
11
|
import { AMFLoader } from 'three/examples/jsm/loaders/AMFLoader.js';
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import { VRMLLoader } from 'three/examples/jsm/loaders/VRMLLoader.js';
|
|
12
|
+
import { ColladaLoader } from 'three/examples/jsm/loaders/ColladaLoader.js';
|
|
13
|
+
import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader.js';
|
|
22
14
|
import { GCodeLoader } from 'three/examples/jsm/loaders/GCodeLoader.js';
|
|
15
|
+
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
|
|
16
|
+
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js';
|
|
17
|
+
import { PCDLoader } from 'three/examples/jsm/loaders/PCDLoader.js';
|
|
18
|
+
import { PLYLoader } from 'three/examples/jsm/loaders/PLYLoader.js';
|
|
19
|
+
import { STLLoader } from 'three/examples/jsm/loaders/STLLoader.js';
|
|
23
20
|
import { SVGLoader } from 'three/examples/jsm/loaders/SVGLoader.js';
|
|
21
|
+
import { TDSLoader } from 'three/examples/jsm/loaders/TDSLoader.js';
|
|
22
|
+
import { VRMLLoader } from 'three/examples/jsm/loaders/VRMLLoader.js';
|
|
23
|
+
import { VTKLoader } from 'three/examples/jsm/loaders/VTKLoader.js';
|
|
24
24
|
import { SUPPORTED_EXTENSIONS, pose3dToClip } from './modelEditorTypes.js';
|
|
25
25
|
import { UndoRedoStack, createMaterialFromPreset, generateLOD, convertToInstanced, applyHDRIPreset, snapMulti, HDRI_PRESETS, PBR_PRESETS } from './modelEditorUtils.js';
|
|
26
26
|
|
|
@@ -38,11 +38,13 @@ const nid = () => `n${++_nid}`;
|
|
|
38
38
|
/* ── Pure helper — extracted outside the hook for stable identity ── */
|
|
39
39
|
function findNodeById(nodes, id) {
|
|
40
40
|
for (const n of nodes) {
|
|
41
|
-
if (n.id === id)
|
|
41
|
+
if (n.id === id) {
|
|
42
42
|
return n;
|
|
43
|
+
}
|
|
43
44
|
const found = findNodeById(n.children, id);
|
|
44
|
-
if (found)
|
|
45
|
+
if (found) {
|
|
45
46
|
return found;
|
|
47
|
+
}
|
|
46
48
|
}
|
|
47
49
|
return null;
|
|
48
50
|
}
|
|
@@ -68,7 +70,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
68
70
|
/* ── state ── */
|
|
69
71
|
const [sceneTree, setSceneTree] = useState([]);
|
|
70
72
|
const [selectedId, setSelectedId] = useState(null);
|
|
71
|
-
const [transformMode, setTransformMode] = useState(
|
|
73
|
+
const [transformMode, setTransformMode] = useState('translate');
|
|
72
74
|
const [animations, setAnimations] = useState([]);
|
|
73
75
|
const [activeAnimIdx, setActiveAnimIdx] = useState(-1);
|
|
74
76
|
const [isPlaying, setIsPlaying] = useState(false);
|
|
@@ -80,8 +82,8 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
80
82
|
const [showGrid, setShowGrid] = useState(true);
|
|
81
83
|
const [showAxes, setShowAxes] = useState(true);
|
|
82
84
|
const [wireframe, setWireframe] = useState(false);
|
|
83
|
-
const [bgColor, setBgColor] = useState(
|
|
84
|
-
const [statusText, setStatusText] = useState(
|
|
85
|
+
const [bgColor, setBgColor] = useState('#111122');
|
|
86
|
+
const [statusText, setStatusText] = useState('Ready');
|
|
85
87
|
const [dragOver, setDragOver] = useState(false);
|
|
86
88
|
const [polyCount, setPolyCount] = useState(0);
|
|
87
89
|
const [meshCount, setMeshCount] = useState(0);
|
|
@@ -90,21 +92,21 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
90
92
|
const [selMaterialIdx, setSelMaterialIdx] = useState(0);
|
|
91
93
|
const [matRefresh, setMatRefresh] = useState(0);
|
|
92
94
|
// AI panel
|
|
93
|
-
const [aiStatus, setAiStatus] = useState(
|
|
95
|
+
const [aiStatus, setAiStatus] = useState('');
|
|
94
96
|
const [aiBusy, setAiBusy] = useState(false);
|
|
95
97
|
// Blender 2.0 — viewport & editor
|
|
96
|
-
const [shadingMode, setShadingMode] = useState(
|
|
97
|
-
const [editorMode, setEditorMode] = useState(
|
|
98
|
+
const [shadingMode, setShadingMode] = useState('solid');
|
|
99
|
+
const [editorMode, setEditorMode] = useState('object');
|
|
98
100
|
const [contextMenu, setContextMenu] = useState(null);
|
|
99
|
-
const [outlinerSearch, setOutlinerSearch] = useState(
|
|
101
|
+
const [outlinerSearch, setOutlinerSearch] = useState('');
|
|
100
102
|
const [snapEnabled, setSnapEnabled] = useState(false);
|
|
101
103
|
const [snapGrid, setSnapGrid] = useState(1);
|
|
102
104
|
const [showSkeleton, setShowSkeleton] = useState(true);
|
|
103
105
|
const [addMenuOpen, setAddMenuOpen] = useState(null);
|
|
104
|
-
const [gizmoSpace, setGizmoSpace] = useState(
|
|
105
|
-
const [propTab, setPropTab] = useState(
|
|
106
|
+
const [gizmoSpace, setGizmoSpace] = useState('world');
|
|
107
|
+
const [propTab, setPropTab] = useState('object');
|
|
106
108
|
const [fogEnabled, setFogEnabled] = useState(false);
|
|
107
|
-
const [fogColor, setFogColor] = useState(
|
|
109
|
+
const [fogColor, setFogColor] = useState('#111122');
|
|
108
110
|
const [fogNear, setFogNear] = useState(10);
|
|
109
111
|
const [fogFar, setFogFar] = useState(100);
|
|
110
112
|
const [showLightHelpers, setShowLightHelpers] = useState(true);
|
|
@@ -128,8 +130,9 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
128
130
|
───────────────────────────────────────── */
|
|
129
131
|
useEffect(() => {
|
|
130
132
|
const container = mountRef.current;
|
|
131
|
-
if (!container)
|
|
133
|
+
if (!container) {
|
|
132
134
|
return;
|
|
135
|
+
}
|
|
133
136
|
const scene = sceneRef.current;
|
|
134
137
|
const camera = cameraRef.current;
|
|
135
138
|
// Renderer
|
|
@@ -146,13 +149,13 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
146
149
|
camera.position.set(3, 2, 5);
|
|
147
150
|
camera.lookAt(0, 0, 0);
|
|
148
151
|
// Scene background
|
|
149
|
-
scene.background = new THREE.Color(
|
|
152
|
+
scene.background = new THREE.Color('#111122');
|
|
150
153
|
// Lights
|
|
151
154
|
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
|
|
152
|
-
ambientLight.name =
|
|
155
|
+
ambientLight.name = '__ambient';
|
|
153
156
|
scene.add(ambientLight);
|
|
154
157
|
const dirLight = new THREE.DirectionalLight(0xffffff, 1.2);
|
|
155
|
-
dirLight.name =
|
|
158
|
+
dirLight.name = '__dirLight';
|
|
156
159
|
dirLight.position.set(5, 8, 5);
|
|
157
160
|
dirLight.castShadow = true;
|
|
158
161
|
dirLight.shadow.mapSize.set(2048, 2048);
|
|
@@ -164,21 +167,21 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
164
167
|
dirLight.shadow.camera.bottom = -10;
|
|
165
168
|
scene.add(dirLight);
|
|
166
169
|
const hemiLight = new THREE.HemisphereLight(0x8888ff, 0x444422, 0.4);
|
|
167
|
-
hemiLight.name =
|
|
170
|
+
hemiLight.name = '__hemiLight';
|
|
168
171
|
scene.add(hemiLight);
|
|
169
172
|
// Ground plane
|
|
170
173
|
const ground = new THREE.Mesh(new THREE.PlaneGeometry(100, 100), new THREE.ShadowMaterial({ opacity: 0.15 }));
|
|
171
|
-
ground.name =
|
|
174
|
+
ground.name = '__ground';
|
|
172
175
|
ground.rotation.x = -Math.PI / 2;
|
|
173
176
|
ground.receiveShadow = true;
|
|
174
177
|
scene.add(ground);
|
|
175
178
|
// Grid
|
|
176
179
|
const grid = new THREE.GridHelper(20, 20, 0x333355, 0x22223a);
|
|
177
|
-
grid.name =
|
|
180
|
+
grid.name = '__grid';
|
|
178
181
|
scene.add(grid);
|
|
179
182
|
// Axes
|
|
180
183
|
const axes = new THREE.AxesHelper(2);
|
|
181
|
-
axes.name =
|
|
184
|
+
axes.name = '__axes';
|
|
182
185
|
scene.add(axes);
|
|
183
186
|
// Orbit controls
|
|
184
187
|
const orbit = new OrbitControls(camera, renderer.domElement);
|
|
@@ -190,9 +193,9 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
190
193
|
orbitRef.current = orbit;
|
|
191
194
|
// Transform controls
|
|
192
195
|
const tc = new TransformControls(camera, renderer.domElement);
|
|
193
|
-
tc.name =
|
|
196
|
+
tc.name = '__transformControls';
|
|
194
197
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
195
|
-
tc.addEventListener(
|
|
198
|
+
tc.addEventListener('dragging-changed', ((e) => {
|
|
196
199
|
orbit.enabled = !e.value;
|
|
197
200
|
}));
|
|
198
201
|
scene.add(tc);
|
|
@@ -201,8 +204,9 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
201
204
|
const onResize = () => {
|
|
202
205
|
const w = container.clientWidth;
|
|
203
206
|
const h = container.clientHeight;
|
|
204
|
-
if (w === 0 || h === 0)
|
|
207
|
+
if (w === 0 || h === 0) {
|
|
205
208
|
return;
|
|
209
|
+
}
|
|
206
210
|
camera.aspect = w / h;
|
|
207
211
|
camera.updateProjectionMatrix();
|
|
208
212
|
renderer.setSize(w, h);
|
|
@@ -235,8 +239,9 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
235
239
|
ro.disconnect();
|
|
236
240
|
const scn = sceneRef.current;
|
|
237
241
|
scn.traverse((obj) => {
|
|
238
|
-
if (obj.geometry)
|
|
242
|
+
if (obj.geometry) {
|
|
239
243
|
obj.geometry.dispose();
|
|
244
|
+
}
|
|
240
245
|
if (obj.material) {
|
|
241
246
|
const mats = Array.isArray(obj.material)
|
|
242
247
|
? obj.material
|
|
@@ -254,7 +259,6 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
254
259
|
container.removeChild(renderer.domElement);
|
|
255
260
|
}
|
|
256
261
|
};
|
|
257
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
258
262
|
}, []);
|
|
259
263
|
/* ─────────────────────────────────────────
|
|
260
264
|
Rebuild scene tree from Three.js scene
|
|
@@ -266,15 +270,18 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
266
270
|
let bones = 0;
|
|
267
271
|
const walk = (obj, depth) => {
|
|
268
272
|
var _a, _b;
|
|
269
|
-
if (obj.name.startsWith(
|
|
273
|
+
if (obj.name.startsWith('__')) {
|
|
270
274
|
return null;
|
|
271
|
-
|
|
275
|
+
}
|
|
276
|
+
if (obj.type === 'TransformControlsPlane' || obj.type === 'TransformControlsGizmo') {
|
|
272
277
|
return null;
|
|
273
|
-
|
|
278
|
+
}
|
|
279
|
+
if ('isTransformControls' in obj) {
|
|
274
280
|
return null;
|
|
275
|
-
|
|
281
|
+
}
|
|
282
|
+
let nodeType = 'group';
|
|
276
283
|
if (obj instanceof THREE.Mesh) {
|
|
277
|
-
nodeType =
|
|
284
|
+
nodeType = 'mesh';
|
|
278
285
|
meshes++;
|
|
279
286
|
const geo = obj.geometry;
|
|
280
287
|
if (geo) {
|
|
@@ -283,27 +290,28 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
283
290
|
}
|
|
284
291
|
}
|
|
285
292
|
else if (obj instanceof THREE.Bone) {
|
|
286
|
-
nodeType =
|
|
293
|
+
nodeType = 'bone';
|
|
287
294
|
bones++;
|
|
288
295
|
}
|
|
289
296
|
else if (obj instanceof THREE.Light) {
|
|
290
|
-
nodeType =
|
|
297
|
+
nodeType = 'light';
|
|
291
298
|
}
|
|
292
299
|
else if (obj instanceof THREE.Camera) {
|
|
293
|
-
nodeType =
|
|
300
|
+
nodeType = 'camera';
|
|
294
301
|
}
|
|
295
|
-
else if (obj instanceof THREE.GridHelper ||
|
|
296
|
-
|
|
297
|
-
nodeType = "helper";
|
|
302
|
+
else if (obj instanceof THREE.GridHelper || obj instanceof THREE.AxesHelper) {
|
|
303
|
+
nodeType = 'helper';
|
|
298
304
|
}
|
|
299
305
|
const children = [];
|
|
300
306
|
for (const c of obj.children) {
|
|
301
307
|
const cn = walk(c, depth + 1);
|
|
302
|
-
if (cn)
|
|
308
|
+
if (cn) {
|
|
303
309
|
children.push(cn);
|
|
310
|
+
}
|
|
304
311
|
}
|
|
305
312
|
return {
|
|
306
|
-
id: obj.userData.__nodeId ||
|
|
313
|
+
id: obj.userData.__nodeId ||
|
|
314
|
+
((obj.userData.__nodeId = nid()), obj.userData.__nodeId),
|
|
307
315
|
name: obj.name || obj.type,
|
|
308
316
|
type: nodeType,
|
|
309
317
|
object: obj,
|
|
@@ -315,8 +323,9 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
315
323
|
const roots = [];
|
|
316
324
|
for (const c of scene.children) {
|
|
317
325
|
const n = walk(c, 0);
|
|
318
|
-
if (n)
|
|
326
|
+
if (n) {
|
|
319
327
|
roots.push(n);
|
|
328
|
+
}
|
|
320
329
|
}
|
|
321
330
|
setSceneTree(roots);
|
|
322
331
|
setPolyCount(Math.round(polys));
|
|
@@ -357,7 +366,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
357
366
|
const mixer = new THREE.AnimationMixer(obj);
|
|
358
367
|
mixerRef.current = mixer;
|
|
359
368
|
const newAnims = clips.map((c) => ({
|
|
360
|
-
name: c.name ||
|
|
369
|
+
name: c.name || 'Animation',
|
|
361
370
|
clip: c,
|
|
362
371
|
duration: c.duration,
|
|
363
372
|
source: filename,
|
|
@@ -378,70 +387,78 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
378
387
|
const loadFile = useCallback(async (file) => {
|
|
379
388
|
var _a, _b, _c, _d, _e, _f;
|
|
380
389
|
const name = file.name.toLowerCase();
|
|
381
|
-
const ext =
|
|
390
|
+
const ext = '.' + name.split('.').pop();
|
|
382
391
|
const url = URL.createObjectURL(file);
|
|
383
392
|
setStatusText(`Loading ${file.name}...`);
|
|
384
393
|
try {
|
|
385
|
-
if (ext ===
|
|
394
|
+
if (ext === '.fbx') {
|
|
386
395
|
const loader = new FBXLoader();
|
|
387
396
|
const obj = await new Promise((res, rej) => loader.load(url, res, undefined, rej));
|
|
388
397
|
addObjectToScene(obj, (_a = obj.animations) !== null && _a !== void 0 ? _a : [], file.name);
|
|
389
398
|
}
|
|
390
|
-
else if (ext ===
|
|
399
|
+
else if (ext === '.glb' || ext === '.gltf') {
|
|
391
400
|
const loader = new GLTFLoader();
|
|
392
401
|
const gltf = await new Promise((res, rej) => loader.load(url, res, undefined, rej));
|
|
393
402
|
addObjectToScene(gltf.scene, (_b = gltf.animations) !== null && _b !== void 0 ? _b : [], file.name);
|
|
394
403
|
}
|
|
395
|
-
else if (ext ===
|
|
404
|
+
else if (ext === '.obj') {
|
|
396
405
|
const loader = new OBJLoader();
|
|
397
406
|
const obj = await new Promise((res, rej) => loader.load(url, res, undefined, rej));
|
|
398
407
|
addObjectToScene(obj, [], file.name);
|
|
399
408
|
}
|
|
400
|
-
else if (ext ===
|
|
409
|
+
else if (ext === '.dae') {
|
|
401
410
|
const loader = new ColladaLoader();
|
|
402
|
-
|
|
403
|
-
|
|
411
|
+
const collada = await new Promise((res, rej) =>
|
|
412
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- ColladaLoader callback type widens beyond { scene: Group }
|
|
413
|
+
loader.load(url, res, undefined, rej));
|
|
404
414
|
addObjectToScene(collada.scene, (_d = (_c = collada.scene) === null || _c === void 0 ? void 0 : _c.animations) !== null && _d !== void 0 ? _d : [], file.name);
|
|
405
415
|
}
|
|
406
|
-
else if (ext ===
|
|
416
|
+
else if (ext === '.stl') {
|
|
407
417
|
const loader = new STLLoader();
|
|
408
418
|
const geo = await new Promise((res, rej) => loader.load(url, res, undefined, rej));
|
|
409
|
-
const mat = new THREE.MeshStandardMaterial({
|
|
419
|
+
const mat = new THREE.MeshStandardMaterial({
|
|
420
|
+
color: 0x888888,
|
|
421
|
+
metalness: 0.3,
|
|
422
|
+
roughness: 0.6,
|
|
423
|
+
});
|
|
410
424
|
const mesh = new THREE.Mesh(geo, mat);
|
|
411
425
|
mesh.name = file.name;
|
|
412
426
|
addObjectToScene(mesh, [], file.name);
|
|
413
427
|
}
|
|
414
|
-
else if (ext ===
|
|
428
|
+
else if (ext === '.ply') {
|
|
415
429
|
const loader = new PLYLoader();
|
|
416
430
|
const geo = await new Promise((res, rej) => loader.load(url, res, undefined, rej));
|
|
417
431
|
geo.computeVertexNormals();
|
|
418
|
-
const mat = new THREE.MeshStandardMaterial({
|
|
432
|
+
const mat = new THREE.MeshStandardMaterial({
|
|
433
|
+
color: 0x888888,
|
|
434
|
+
vertexColors: geo.hasAttribute('color'),
|
|
435
|
+
});
|
|
419
436
|
const mesh = new THREE.Mesh(geo, mat);
|
|
420
437
|
mesh.name = file.name;
|
|
421
438
|
addObjectToScene(mesh, [], file.name);
|
|
422
439
|
}
|
|
423
|
-
else if (ext ===
|
|
440
|
+
else if (ext === '.3ds') {
|
|
424
441
|
const loader = new TDSLoader();
|
|
425
442
|
const obj = await new Promise((res, rej) => loader.load(url, res, undefined, rej));
|
|
426
443
|
addObjectToScene(obj, [], file.name);
|
|
427
444
|
}
|
|
428
|
-
else if (ext ===
|
|
445
|
+
else if (ext === '.3mf') {
|
|
429
446
|
const loader = new ThreeMFLoader();
|
|
430
447
|
const obj = await new Promise((res, rej) => loader.load(url, res, undefined, rej));
|
|
431
448
|
addObjectToScene(obj, [], file.name);
|
|
432
449
|
}
|
|
433
|
-
else if (ext ===
|
|
450
|
+
else if (ext === '.amf') {
|
|
434
451
|
const loader = new AMFLoader();
|
|
435
452
|
const obj = await new Promise((res, rej) => loader.load(url, res, undefined, rej));
|
|
436
453
|
addObjectToScene(obj, [], file.name);
|
|
437
454
|
}
|
|
438
|
-
else if (ext ===
|
|
455
|
+
else if (ext === '.pcd') {
|
|
439
456
|
const loader = new PCDLoader();
|
|
440
457
|
const points = await new Promise((res, rej) => loader.load(url, res, undefined, rej));
|
|
441
458
|
points.name = file.name;
|
|
442
459
|
addObjectToScene(points, [], file.name);
|
|
443
460
|
}
|
|
444
|
-
else if (ext ===
|
|
461
|
+
else if (ext === '.vtk' || ext === '.vtp') {
|
|
445
462
|
const loader = new VTKLoader();
|
|
446
463
|
const geo = await new Promise((res, rej) => loader.load(url, res, undefined, rej));
|
|
447
464
|
geo.computeVertexNormals();
|
|
@@ -450,31 +467,36 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
450
467
|
mesh.name = file.name;
|
|
451
468
|
addObjectToScene(mesh, [], file.name);
|
|
452
469
|
}
|
|
453
|
-
else if (ext ===
|
|
470
|
+
else if (ext === '.wrl' || ext === '.vrml') {
|
|
454
471
|
const loader = new VRMLLoader();
|
|
455
472
|
const scn = await new Promise((res, rej) => loader.load(url, res, undefined, rej));
|
|
456
473
|
const group = new THREE.Group();
|
|
457
474
|
group.name = file.name;
|
|
458
|
-
while (scn.children.length)
|
|
475
|
+
while (scn.children.length) {
|
|
459
476
|
group.add(scn.children[0]);
|
|
477
|
+
}
|
|
460
478
|
addObjectToScene(group, [], file.name);
|
|
461
479
|
}
|
|
462
|
-
else if (ext ===
|
|
480
|
+
else if (ext === '.gcode') {
|
|
463
481
|
const loader = new GCodeLoader();
|
|
464
482
|
const obj = await new Promise((res, rej) => loader.load(url, res, undefined, rej));
|
|
465
483
|
addObjectToScene(obj, [], file.name);
|
|
466
484
|
}
|
|
467
|
-
else if (ext ===
|
|
485
|
+
else if (ext === '.svg') {
|
|
468
486
|
const loader = new SVGLoader();
|
|
469
|
-
|
|
470
|
-
|
|
487
|
+
const data = await new Promise((res, rej) =>
|
|
488
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- SVGLoader callback type widens beyond { paths: SVGResultPaths[] }
|
|
489
|
+
loader.load(url, res, undefined, rej));
|
|
471
490
|
const group = new THREE.Group();
|
|
472
491
|
group.name = file.name;
|
|
473
492
|
for (const path of data.paths) {
|
|
474
493
|
const shapes2 = SVGLoader.createShapes(path);
|
|
475
494
|
for (const shape of shapes2) {
|
|
476
495
|
const geo = new THREE.ExtrudeGeometry(shape, { depth: 2, bevelEnabled: false });
|
|
477
|
-
const mat = new THREE.MeshStandardMaterial({
|
|
496
|
+
const mat = new THREE.MeshStandardMaterial({
|
|
497
|
+
color: ((_f = (_e = path.color) === null || _e === void 0 ? void 0 : _e.getHex) === null || _f === void 0 ? void 0 : _f.call(_e)) || 0x888888,
|
|
498
|
+
side: THREE.DoubleSide,
|
|
499
|
+
});
|
|
478
500
|
const mesh = new THREE.Mesh(geo, mat);
|
|
479
501
|
group.add(mesh);
|
|
480
502
|
}
|
|
@@ -487,7 +509,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
487
509
|
}
|
|
488
510
|
}
|
|
489
511
|
catch (err) {
|
|
490
|
-
log.error(
|
|
512
|
+
log.error('Load error:', err);
|
|
491
513
|
setStatusText(`Error loading ${file.name}: ${String(err)}`);
|
|
492
514
|
}
|
|
493
515
|
finally {
|
|
@@ -508,14 +530,14 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
508
530
|
}
|
|
509
531
|
const root = rootObjectRef.current;
|
|
510
532
|
if (!root) {
|
|
511
|
-
setStatusText(
|
|
533
|
+
setStatusText('No model loaded — load a base model first');
|
|
512
534
|
return;
|
|
513
535
|
}
|
|
514
536
|
if (!mixerRef.current) {
|
|
515
537
|
mixerRef.current = new THREE.AnimationMixer(root);
|
|
516
538
|
}
|
|
517
539
|
const newAnims = clips.map((c) => ({
|
|
518
|
-
name: c.name || file.name.replace(/\.[^.]+$/,
|
|
540
|
+
name: c.name || file.name.replace(/\.[^.]+$/, ''),
|
|
519
541
|
clip: c,
|
|
520
542
|
duration: c.duration,
|
|
521
543
|
source: file.name,
|
|
@@ -525,7 +547,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
525
547
|
setStatusText(`Merged ${clips.length} animation(s) from ${file.name}`);
|
|
526
548
|
}
|
|
527
549
|
catch (err) {
|
|
528
|
-
log.error(
|
|
550
|
+
log.error('Merge error:', err);
|
|
529
551
|
setStatusText(`Error merging ${file.name}: ${String(err)}`);
|
|
530
552
|
}
|
|
531
553
|
finally {
|
|
@@ -537,8 +559,9 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
537
559
|
───────────────────────────────────────── */
|
|
538
560
|
const playClip = useCallback((index) => {
|
|
539
561
|
const mixer = mixerRef.current;
|
|
540
|
-
if (!mixer || index < 0 || index >= allClipsRef.current.length)
|
|
562
|
+
if (!mixer || index < 0 || index >= allClipsRef.current.length) {
|
|
541
563
|
return;
|
|
564
|
+
}
|
|
542
565
|
if (activeActionRef.current) {
|
|
543
566
|
activeActionRef.current.stop();
|
|
544
567
|
}
|
|
@@ -561,8 +584,9 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
561
584
|
return;
|
|
562
585
|
}
|
|
563
586
|
const action = activeActionRef.current;
|
|
564
|
-
if (!action)
|
|
587
|
+
if (!action) {
|
|
565
588
|
return;
|
|
589
|
+
}
|
|
566
590
|
if (isPlayingRef.current) {
|
|
567
591
|
action.paused = true;
|
|
568
592
|
setIsPlaying(false);
|
|
@@ -584,8 +608,9 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
584
608
|
const seekAnim = useCallback((t) => {
|
|
585
609
|
var _a;
|
|
586
610
|
const action = activeActionRef.current;
|
|
587
|
-
if (!action)
|
|
611
|
+
if (!action) {
|
|
588
612
|
return;
|
|
613
|
+
}
|
|
589
614
|
action.time = t;
|
|
590
615
|
action.paused = true;
|
|
591
616
|
setAnimTime(t);
|
|
@@ -611,31 +636,33 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
611
636
|
focusOnObjectRef.current = focusOnObject;
|
|
612
637
|
const setCameraPreset = useCallback((preset) => {
|
|
613
638
|
const orbit = orbitRef.current;
|
|
614
|
-
if (!orbit)
|
|
639
|
+
if (!orbit) {
|
|
615
640
|
return;
|
|
641
|
+
}
|
|
616
642
|
const target = orbit.target.clone();
|
|
617
643
|
const d = cameraRef.current.position.distanceTo(target);
|
|
618
644
|
const pos = new THREE.Vector3();
|
|
619
645
|
switch (preset) {
|
|
620
|
-
case
|
|
646
|
+
case 'front':
|
|
621
647
|
pos.set(0, 0, d);
|
|
622
648
|
break;
|
|
623
|
-
case
|
|
649
|
+
case 'back':
|
|
624
650
|
pos.set(0, 0, -d);
|
|
625
651
|
break;
|
|
626
|
-
case
|
|
652
|
+
case 'left':
|
|
627
653
|
pos.set(-d, 0, 0);
|
|
628
654
|
break;
|
|
629
|
-
case
|
|
655
|
+
case 'right':
|
|
630
656
|
pos.set(d, 0, 0);
|
|
631
657
|
break;
|
|
632
|
-
case
|
|
658
|
+
case 'top':
|
|
633
659
|
pos.set(0, d, 0.001);
|
|
634
660
|
break;
|
|
635
|
-
case
|
|
661
|
+
case 'bottom':
|
|
636
662
|
pos.set(0, -d, 0.001);
|
|
637
663
|
break;
|
|
638
|
-
default:
|
|
664
|
+
default:
|
|
665
|
+
return;
|
|
639
666
|
}
|
|
640
667
|
cameraRef.current.position.copy(target.clone().add(pos));
|
|
641
668
|
orbit.update();
|
|
@@ -647,9 +674,10 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
647
674
|
var _a;
|
|
648
675
|
setSelectedId((_a = node === null || node === void 0 ? void 0 : node.id) !== null && _a !== void 0 ? _a : null);
|
|
649
676
|
const tc = transformRef.current;
|
|
650
|
-
if (!tc)
|
|
677
|
+
if (!tc) {
|
|
651
678
|
return;
|
|
652
|
-
|
|
679
|
+
}
|
|
680
|
+
if (node && node.type !== 'helper') {
|
|
653
681
|
tc.attach(node.object);
|
|
654
682
|
tc.setMode(transformMode);
|
|
655
683
|
}
|
|
@@ -666,31 +694,33 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
666
694
|
───────────────────────────────────────── */
|
|
667
695
|
useEffect(() => {
|
|
668
696
|
const scene = sceneRef.current;
|
|
669
|
-
const grid = scene.getObjectByName(
|
|
670
|
-
if (grid)
|
|
697
|
+
const grid = scene.getObjectByName('__grid');
|
|
698
|
+
if (grid) {
|
|
671
699
|
grid.visible = showGrid;
|
|
700
|
+
}
|
|
672
701
|
}, [showGrid]);
|
|
673
702
|
useEffect(() => {
|
|
674
703
|
const scene = sceneRef.current;
|
|
675
|
-
const axes = scene.getObjectByName(
|
|
676
|
-
if (axes)
|
|
704
|
+
const axes = scene.getObjectByName('__axes');
|
|
705
|
+
if (axes) {
|
|
677
706
|
axes.visible = showAxes;
|
|
707
|
+
}
|
|
678
708
|
}, [showAxes]);
|
|
679
709
|
useEffect(() => {
|
|
680
710
|
sceneRef.current.background = new THREE.Color(bgColor);
|
|
681
711
|
}, [bgColor]);
|
|
682
712
|
useEffect(() => {
|
|
683
713
|
const root = rootObjectRef.current;
|
|
684
|
-
if (!root)
|
|
714
|
+
if (!root) {
|
|
685
715
|
return;
|
|
716
|
+
}
|
|
686
717
|
root.traverse((child) => {
|
|
687
718
|
if (child instanceof THREE.Mesh) {
|
|
688
|
-
const mats = Array.isArray(child.material)
|
|
689
|
-
? child.material
|
|
690
|
-
: [child.material];
|
|
719
|
+
const mats = Array.isArray(child.material) ? child.material : [child.material];
|
|
691
720
|
mats.forEach((m) => {
|
|
692
|
-
if (
|
|
721
|
+
if ('wireframe' in m) {
|
|
693
722
|
m.wireframe = wireframe;
|
|
723
|
+
}
|
|
694
724
|
});
|
|
695
725
|
}
|
|
696
726
|
});
|
|
@@ -700,49 +730,62 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
700
730
|
───────────────────────────────────────── */
|
|
701
731
|
useEffect(() => {
|
|
702
732
|
const renderer = rendererRef.current;
|
|
703
|
-
if (!renderer)
|
|
733
|
+
if (!renderer) {
|
|
704
734
|
return;
|
|
735
|
+
}
|
|
705
736
|
const scene = sceneRef.current;
|
|
706
737
|
switch (shadingMode) {
|
|
707
|
-
case
|
|
738
|
+
case 'wireframe':
|
|
708
739
|
scene.traverse((obj) => {
|
|
709
|
-
if (obj instanceof THREE.Mesh && !obj.name.startsWith(
|
|
740
|
+
if (obj instanceof THREE.Mesh && !obj.name.startsWith('__')) {
|
|
710
741
|
const mats = Array.isArray(obj.material) ? obj.material : [obj.material];
|
|
711
|
-
mats.forEach((m) => {
|
|
712
|
-
|
|
742
|
+
mats.forEach((m) => {
|
|
743
|
+
if ('wireframe' in m) {
|
|
744
|
+
m.wireframe = true;
|
|
745
|
+
}
|
|
746
|
+
});
|
|
713
747
|
}
|
|
714
748
|
});
|
|
715
749
|
renderer.toneMapping = THREE.NoToneMapping;
|
|
716
750
|
renderer.toneMappingExposure = 1;
|
|
717
751
|
break;
|
|
718
|
-
case
|
|
752
|
+
case 'solid':
|
|
719
753
|
scene.traverse((obj) => {
|
|
720
|
-
if (obj instanceof THREE.Mesh && !obj.name.startsWith(
|
|
754
|
+
if (obj instanceof THREE.Mesh && !obj.name.startsWith('__')) {
|
|
721
755
|
const mats = Array.isArray(obj.material) ? obj.material : [obj.material];
|
|
722
|
-
mats.forEach((m) => {
|
|
723
|
-
|
|
756
|
+
mats.forEach((m) => {
|
|
757
|
+
if ('wireframe' in m) {
|
|
758
|
+
m.wireframe = false;
|
|
759
|
+
}
|
|
760
|
+
});
|
|
724
761
|
}
|
|
725
762
|
});
|
|
726
763
|
renderer.toneMapping = THREE.NoToneMapping;
|
|
727
764
|
renderer.toneMappingExposure = 1;
|
|
728
765
|
break;
|
|
729
|
-
case
|
|
766
|
+
case 'material':
|
|
730
767
|
scene.traverse((obj) => {
|
|
731
|
-
if (obj instanceof THREE.Mesh && !obj.name.startsWith(
|
|
768
|
+
if (obj instanceof THREE.Mesh && !obj.name.startsWith('__')) {
|
|
732
769
|
const mats = Array.isArray(obj.material) ? obj.material : [obj.material];
|
|
733
|
-
mats.forEach((m) => {
|
|
734
|
-
|
|
770
|
+
mats.forEach((m) => {
|
|
771
|
+
if ('wireframe' in m) {
|
|
772
|
+
m.wireframe = false;
|
|
773
|
+
}
|
|
774
|
+
});
|
|
735
775
|
}
|
|
736
776
|
});
|
|
737
777
|
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
|
738
778
|
renderer.toneMappingExposure = 1.2;
|
|
739
779
|
break;
|
|
740
|
-
case
|
|
780
|
+
case 'rendered':
|
|
741
781
|
scene.traverse((obj) => {
|
|
742
|
-
if (obj instanceof THREE.Mesh && !obj.name.startsWith(
|
|
782
|
+
if (obj instanceof THREE.Mesh && !obj.name.startsWith('__')) {
|
|
743
783
|
const mats = Array.isArray(obj.material) ? obj.material : [obj.material];
|
|
744
|
-
mats.forEach((m) => {
|
|
745
|
-
|
|
784
|
+
mats.forEach((m) => {
|
|
785
|
+
if ('wireframe' in m) {
|
|
786
|
+
m.wireframe = false;
|
|
787
|
+
}
|
|
788
|
+
});
|
|
746
789
|
}
|
|
747
790
|
});
|
|
748
791
|
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
|
@@ -758,8 +801,9 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
758
801
|
/* Snap settings */
|
|
759
802
|
useEffect(() => {
|
|
760
803
|
const tc = transformRef.current;
|
|
761
|
-
if (!tc)
|
|
804
|
+
if (!tc) {
|
|
762
805
|
return;
|
|
806
|
+
}
|
|
763
807
|
if (snapEnabled) {
|
|
764
808
|
tc.setTranslationSnap(snapGrid);
|
|
765
809
|
tc.setRotationSnap(THREE.MathUtils.degToRad(15));
|
|
@@ -778,16 +822,18 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
778
822
|
scene.remove(skeletonHelperRef.current);
|
|
779
823
|
skeletonHelperRef.current = null;
|
|
780
824
|
}
|
|
781
|
-
if (!showSkeleton || !rootObjectRef.current)
|
|
825
|
+
if (!showSkeleton || !rootObjectRef.current) {
|
|
782
826
|
return;
|
|
827
|
+
}
|
|
783
828
|
let hasRig = false;
|
|
784
829
|
rootObjectRef.current.traverse((obj) => {
|
|
785
|
-
if (obj instanceof THREE.SkinnedMesh)
|
|
830
|
+
if (obj instanceof THREE.SkinnedMesh) {
|
|
786
831
|
hasRig = true;
|
|
832
|
+
}
|
|
787
833
|
});
|
|
788
834
|
if (hasRig) {
|
|
789
835
|
const helper = new THREE.SkeletonHelper(rootObjectRef.current);
|
|
790
|
-
helper.name =
|
|
836
|
+
helper.name = '__skeletonHelper';
|
|
791
837
|
scene.add(helper);
|
|
792
838
|
skeletonHelperRef.current = helper;
|
|
793
839
|
}
|
|
@@ -804,7 +850,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
804
850
|
/* Light helpers visibility */
|
|
805
851
|
useEffect(() => {
|
|
806
852
|
sceneRef.current.traverse((obj) => {
|
|
807
|
-
if (obj.name.startsWith(
|
|
853
|
+
if (obj.name.startsWith('__helper_')) {
|
|
808
854
|
obj.visible = showLightHelpers;
|
|
809
855
|
}
|
|
810
856
|
});
|
|
@@ -827,7 +873,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
827
873
|
setDragOver(false);
|
|
828
874
|
const files = Array.from(e.dataTransfer.files);
|
|
829
875
|
files.forEach((f) => {
|
|
830
|
-
const ext =
|
|
876
|
+
const ext = '.' + f.name.toLowerCase().split('.').pop();
|
|
831
877
|
if (SUPPORTED_EXTENSIONS.includes(ext)) {
|
|
832
878
|
loadFile(f);
|
|
833
879
|
}
|
|
@@ -838,25 +884,28 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
838
884
|
───────────────────────────────────────── */
|
|
839
885
|
const selectedNode = useMemo(() => (selectedId ? findNodeById(sceneTree, selectedId) : null), [selectedId, sceneTree]);
|
|
840
886
|
const selectedMaterials = useMemo(() => {
|
|
841
|
-
if (!selectedNode || !(selectedNode.object instanceof THREE.Mesh))
|
|
887
|
+
if (!selectedNode || !(selectedNode.object instanceof THREE.Mesh)) {
|
|
842
888
|
return [];
|
|
889
|
+
}
|
|
843
890
|
const m = selectedNode.object.material;
|
|
844
891
|
return Array.isArray(m) ? m : [m];
|
|
845
892
|
}, [selectedNode, matRefresh]);
|
|
846
893
|
const deleteSelected = useCallback(() => {
|
|
847
894
|
var _a;
|
|
848
|
-
if (!selectedNode)
|
|
895
|
+
if (!selectedNode) {
|
|
849
896
|
return;
|
|
897
|
+
}
|
|
850
898
|
selectedNode.object.removeFromParent();
|
|
851
899
|
(_a = transformRef.current) === null || _a === void 0 ? void 0 : _a.detach();
|
|
852
900
|
setSelectedId(null);
|
|
853
901
|
rebuildTree();
|
|
854
902
|
}, [selectedNode, rebuildTree]);
|
|
855
903
|
const duplicateSelected = useCallback(() => {
|
|
856
|
-
if (!selectedNode)
|
|
904
|
+
if (!selectedNode) {
|
|
857
905
|
return;
|
|
906
|
+
}
|
|
858
907
|
const clone = selectedNode.object.clone(true);
|
|
859
|
-
clone.name = selectedNode.object.name +
|
|
908
|
+
clone.name = selectedNode.object.name + '.001';
|
|
860
909
|
clone.position.x += 1;
|
|
861
910
|
sceneRef.current.add(clone);
|
|
862
911
|
rebuildTree();
|
|
@@ -869,28 +918,31 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
869
918
|
const handleKey = (e) => {
|
|
870
919
|
var _a, _b, _c;
|
|
871
920
|
const tag = (_a = e.target) === null || _a === void 0 ? void 0 : _a.tagName;
|
|
872
|
-
if (tag ===
|
|
921
|
+
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') {
|
|
873
922
|
return;
|
|
923
|
+
}
|
|
874
924
|
switch (e.key.toLowerCase()) {
|
|
875
|
-
case
|
|
876
|
-
setTransformMode(
|
|
925
|
+
case 'g':
|
|
926
|
+
setTransformMode('translate');
|
|
877
927
|
break;
|
|
878
|
-
case
|
|
879
|
-
setTransformMode(
|
|
928
|
+
case 'r':
|
|
929
|
+
setTransformMode('rotate');
|
|
880
930
|
break;
|
|
881
|
-
case
|
|
882
|
-
if (!e.ctrlKey)
|
|
883
|
-
setTransformMode(
|
|
931
|
+
case 's':
|
|
932
|
+
if (!e.ctrlKey) {
|
|
933
|
+
setTransformMode('scale');
|
|
934
|
+
}
|
|
884
935
|
break;
|
|
885
|
-
case
|
|
936
|
+
case ' ':
|
|
886
937
|
e.preventDefault();
|
|
887
938
|
togglePlay();
|
|
888
939
|
break;
|
|
889
|
-
case
|
|
890
|
-
if (rootObjectRef.current)
|
|
940
|
+
case 'f':
|
|
941
|
+
if (rootObjectRef.current) {
|
|
891
942
|
focusOnObject(rootObjectRef.current);
|
|
943
|
+
}
|
|
892
944
|
break;
|
|
893
|
-
case
|
|
945
|
+
case 'delete':
|
|
894
946
|
if (selectedId) {
|
|
895
947
|
const sel = findNodeById(sceneTree, selectedId);
|
|
896
948
|
if (sel) {
|
|
@@ -901,7 +953,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
901
953
|
}
|
|
902
954
|
}
|
|
903
955
|
break;
|
|
904
|
-
case
|
|
956
|
+
case 'x':
|
|
905
957
|
if (e.ctrlKey && selectedId) {
|
|
906
958
|
const sel = findNodeById(sceneTree, selectedId);
|
|
907
959
|
if (sel) {
|
|
@@ -912,55 +964,62 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
912
964
|
}
|
|
913
965
|
}
|
|
914
966
|
break;
|
|
915
|
-
case
|
|
967
|
+
case 'd':
|
|
916
968
|
if (e.shiftKey) {
|
|
917
969
|
e.preventDefault();
|
|
918
970
|
duplicateSelected();
|
|
919
971
|
}
|
|
920
972
|
break;
|
|
921
|
-
case
|
|
973
|
+
case 'tab':
|
|
922
974
|
e.preventDefault();
|
|
923
|
-
setEditorMode((m) => m ===
|
|
975
|
+
setEditorMode((m) => (m === 'object' ? 'edit' : m === 'edit' ? 'pose' : 'object'));
|
|
924
976
|
break;
|
|
925
|
-
case
|
|
977
|
+
case 'n':
|
|
926
978
|
if (!e.ctrlKey) ;
|
|
927
979
|
break;
|
|
928
|
-
case
|
|
929
|
-
if (e.code.startsWith(
|
|
930
|
-
setCameraPreset(
|
|
980
|
+
case '1':
|
|
981
|
+
if (e.code.startsWith('Numpad')) {
|
|
982
|
+
setCameraPreset('front');
|
|
983
|
+
}
|
|
931
984
|
break;
|
|
932
|
-
case
|
|
933
|
-
if (e.code.startsWith(
|
|
934
|
-
setCameraPreset(
|
|
985
|
+
case '3':
|
|
986
|
+
if (e.code.startsWith('Numpad')) {
|
|
987
|
+
setCameraPreset('right');
|
|
988
|
+
}
|
|
935
989
|
break;
|
|
936
|
-
case
|
|
937
|
-
if (e.code.startsWith(
|
|
938
|
-
setCameraPreset(
|
|
990
|
+
case '7':
|
|
991
|
+
if (e.code.startsWith('Numpad')) {
|
|
992
|
+
setCameraPreset('top');
|
|
993
|
+
}
|
|
939
994
|
break;
|
|
940
|
-
case
|
|
941
|
-
if (e.code.startsWith(
|
|
942
|
-
setCameraPreset(
|
|
995
|
+
case '4':
|
|
996
|
+
if (e.code.startsWith('Numpad')) {
|
|
997
|
+
setCameraPreset('left');
|
|
998
|
+
}
|
|
943
999
|
break;
|
|
944
|
-
case
|
|
945
|
-
if (e.code.startsWith(
|
|
946
|
-
setCameraPreset(
|
|
1000
|
+
case '6':
|
|
1001
|
+
if (e.code.startsWith('Numpad')) {
|
|
1002
|
+
setCameraPreset('right');
|
|
1003
|
+
}
|
|
947
1004
|
break;
|
|
948
|
-
case
|
|
949
|
-
if (e.code.startsWith(
|
|
950
|
-
setCameraPreset(
|
|
1005
|
+
case '9':
|
|
1006
|
+
if (e.code.startsWith('Numpad')) {
|
|
1007
|
+
setCameraPreset('back');
|
|
1008
|
+
}
|
|
951
1009
|
break;
|
|
952
|
-
case
|
|
953
|
-
if (e.code.startsWith(
|
|
954
|
-
setCameraPreset(
|
|
1010
|
+
case '2':
|
|
1011
|
+
if (e.code.startsWith('Numpad')) {
|
|
1012
|
+
setCameraPreset('bottom');
|
|
1013
|
+
}
|
|
955
1014
|
break;
|
|
956
1015
|
}
|
|
957
|
-
if (e.key ===
|
|
1016
|
+
if (e.key === 'Escape') {
|
|
958
1017
|
setAddMenuOpen(null);
|
|
959
1018
|
setContextMenu(null);
|
|
960
1019
|
}
|
|
961
1020
|
};
|
|
962
|
-
window.addEventListener(
|
|
963
|
-
return () => window.removeEventListener(
|
|
1021
|
+
window.addEventListener('keydown', handleKey);
|
|
1022
|
+
return () => window.removeEventListener('keydown', handleKey);
|
|
964
1023
|
}, [
|
|
965
1024
|
togglePlay,
|
|
966
1025
|
focusOnObject,
|
|
@@ -975,17 +1034,19 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
975
1034
|
───────────────────────────────────────── */
|
|
976
1035
|
const handleFileInput = useCallback((e) => {
|
|
977
1036
|
const files = e.target.files;
|
|
978
|
-
if (!files)
|
|
1037
|
+
if (!files) {
|
|
979
1038
|
return;
|
|
1039
|
+
}
|
|
980
1040
|
Array.from(files).forEach(loadFile);
|
|
981
|
-
e.target.value =
|
|
1041
|
+
e.target.value = '';
|
|
982
1042
|
}, [loadFile]);
|
|
983
1043
|
const handleMergeInput = useCallback((e) => {
|
|
984
1044
|
const files = e.target.files;
|
|
985
|
-
if (!files)
|
|
1045
|
+
if (!files) {
|
|
986
1046
|
return;
|
|
1047
|
+
}
|
|
987
1048
|
Array.from(files).forEach(mergeAnimationFBX);
|
|
988
|
-
e.target.value =
|
|
1049
|
+
e.target.value = '';
|
|
989
1050
|
}, [mergeAnimationFBX]);
|
|
990
1051
|
/* ─────────────────────────────────────────
|
|
991
1052
|
Export functions
|
|
@@ -993,72 +1054,75 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
993
1054
|
const exportGLTF = useCallback((binary) => {
|
|
994
1055
|
const root = rootObjectRef.current;
|
|
995
1056
|
if (!root) {
|
|
996
|
-
setStatusText(
|
|
1057
|
+
setStatusText('Nothing to export');
|
|
997
1058
|
return;
|
|
998
1059
|
}
|
|
999
1060
|
const exporter = new GLTFExporter();
|
|
1000
|
-
const options = {
|
|
1061
|
+
const options = {
|
|
1062
|
+
binary,
|
|
1063
|
+
animations: allClipsRef.current.map((a) => a.clip),
|
|
1064
|
+
};
|
|
1001
1065
|
exporter.parse(root, (result) => {
|
|
1002
1066
|
let blob;
|
|
1003
1067
|
if (binary) {
|
|
1004
|
-
blob = new Blob([result], { type:
|
|
1068
|
+
blob = new Blob([result], { type: 'application/octet-stream' });
|
|
1005
1069
|
}
|
|
1006
1070
|
else {
|
|
1007
|
-
blob = new Blob([JSON.stringify(result, null, 2)], { type:
|
|
1071
|
+
blob = new Blob([JSON.stringify(result, null, 2)], { type: 'application/json' });
|
|
1008
1072
|
}
|
|
1009
|
-
const a = document.createElement(
|
|
1073
|
+
const a = document.createElement('a');
|
|
1010
1074
|
a.href = URL.createObjectURL(blob);
|
|
1011
|
-
a.download = `export.${binary ?
|
|
1075
|
+
a.download = `export.${binary ? 'glb' : 'gltf'}`;
|
|
1012
1076
|
a.click();
|
|
1013
1077
|
URL.revokeObjectURL(a.href);
|
|
1014
|
-
setStatusText(`Exported ${binary ?
|
|
1078
|
+
setStatusText(`Exported ${binary ? 'GLB' : 'GLTF'} successfully`);
|
|
1015
1079
|
}, (err) => {
|
|
1016
|
-
log.error(
|
|
1080
|
+
log.error('Export error:', err);
|
|
1017
1081
|
setStatusText(`Export error: ${String(err)}`);
|
|
1018
1082
|
}, options);
|
|
1019
1083
|
}, []);
|
|
1020
1084
|
const exportOBJ = useCallback(() => {
|
|
1021
1085
|
const root = rootObjectRef.current;
|
|
1022
1086
|
if (!root) {
|
|
1023
|
-
setStatusText(
|
|
1087
|
+
setStatusText('Nothing to export');
|
|
1024
1088
|
return;
|
|
1025
1089
|
}
|
|
1026
1090
|
const exporter = new OBJExporter();
|
|
1027
1091
|
const result = exporter.parse(root);
|
|
1028
|
-
const blob = new Blob([result], { type:
|
|
1029
|
-
const a = document.createElement(
|
|
1092
|
+
const blob = new Blob([result], { type: 'text/plain' });
|
|
1093
|
+
const a = document.createElement('a');
|
|
1030
1094
|
a.href = URL.createObjectURL(blob);
|
|
1031
|
-
a.download =
|
|
1095
|
+
a.download = 'export.obj';
|
|
1032
1096
|
a.click();
|
|
1033
1097
|
URL.revokeObjectURL(a.href);
|
|
1034
|
-
setStatusText(
|
|
1098
|
+
setStatusText('Exported OBJ successfully');
|
|
1035
1099
|
}, []);
|
|
1036
1100
|
const exportSTL = useCallback((binary) => {
|
|
1037
1101
|
const root = rootObjectRef.current;
|
|
1038
1102
|
if (!root) {
|
|
1039
|
-
setStatusText(
|
|
1103
|
+
setStatusText('Nothing to export');
|
|
1040
1104
|
return;
|
|
1041
1105
|
}
|
|
1042
1106
|
const exporter = new STLExporter();
|
|
1043
1107
|
const result = exporter.parse(root, { binary });
|
|
1044
1108
|
let blob;
|
|
1045
1109
|
if (binary) {
|
|
1046
|
-
blob = new Blob([result], { type:
|
|
1110
|
+
blob = new Blob([result], { type: 'application/octet-stream' });
|
|
1047
1111
|
}
|
|
1048
1112
|
else {
|
|
1049
|
-
blob = new Blob([result], { type:
|
|
1113
|
+
blob = new Blob([result], { type: 'text/plain' });
|
|
1050
1114
|
}
|
|
1051
|
-
const a = document.createElement(
|
|
1115
|
+
const a = document.createElement('a');
|
|
1052
1116
|
a.href = URL.createObjectURL(blob);
|
|
1053
|
-
a.download =
|
|
1117
|
+
a.download = 'export.stl';
|
|
1054
1118
|
a.click();
|
|
1055
1119
|
URL.revokeObjectURL(a.href);
|
|
1056
|
-
setStatusText(`Exported STL${binary ?
|
|
1120
|
+
setStatusText(`Exported STL${binary ? ' (binary)' : ''} successfully`);
|
|
1057
1121
|
}, []);
|
|
1058
1122
|
const exportPLY = useCallback((binary) => {
|
|
1059
1123
|
const root = rootObjectRef.current;
|
|
1060
1124
|
if (!root) {
|
|
1061
|
-
setStatusText(
|
|
1125
|
+
setStatusText('Nothing to export');
|
|
1062
1126
|
return;
|
|
1063
1127
|
}
|
|
1064
1128
|
const exporter = new PLYExporter();
|
|
@@ -1066,41 +1130,41 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1066
1130
|
exporter.parse(root, (result) => {
|
|
1067
1131
|
let blob;
|
|
1068
1132
|
if (binary) {
|
|
1069
|
-
blob = new Blob([result], { type:
|
|
1133
|
+
blob = new Blob([result], { type: 'application/octet-stream' });
|
|
1070
1134
|
}
|
|
1071
1135
|
else {
|
|
1072
|
-
blob = new Blob([result], { type:
|
|
1136
|
+
blob = new Blob([result], { type: 'text/plain' });
|
|
1073
1137
|
}
|
|
1074
|
-
const a = document.createElement(
|
|
1138
|
+
const a = document.createElement('a');
|
|
1075
1139
|
a.href = URL.createObjectURL(blob);
|
|
1076
|
-
a.download =
|
|
1140
|
+
a.download = 'export.ply';
|
|
1077
1141
|
a.click();
|
|
1078
1142
|
URL.revokeObjectURL(a.href);
|
|
1079
|
-
setStatusText(`Exported PLY${binary ?
|
|
1143
|
+
setStatusText(`Exported PLY${binary ? ' (binary)' : ''} successfully`);
|
|
1080
1144
|
}, { binary });
|
|
1081
1145
|
}, []);
|
|
1082
1146
|
const exportUSDZ = useCallback(async () => {
|
|
1083
1147
|
const root = rootObjectRef.current;
|
|
1084
1148
|
if (!root) {
|
|
1085
|
-
setStatusText(
|
|
1149
|
+
setStatusText('Nothing to export');
|
|
1086
1150
|
return;
|
|
1087
1151
|
}
|
|
1088
1152
|
try {
|
|
1089
1153
|
const exporter = new USDZExporter();
|
|
1090
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1091
1154
|
const result = await new Promise((resolve) => {
|
|
1155
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- USDZExporter.parse callback signature varies between three.js minor versions
|
|
1092
1156
|
exporter.parse(root, resolve);
|
|
1093
1157
|
});
|
|
1094
|
-
const blob = new Blob([result.buffer], { type:
|
|
1095
|
-
const a = document.createElement(
|
|
1158
|
+
const blob = new Blob([result.buffer], { type: 'application/octet-stream' });
|
|
1159
|
+
const a = document.createElement('a');
|
|
1096
1160
|
a.href = URL.createObjectURL(blob);
|
|
1097
|
-
a.download =
|
|
1161
|
+
a.download = 'export.usdz';
|
|
1098
1162
|
a.click();
|
|
1099
1163
|
URL.revokeObjectURL(a.href);
|
|
1100
|
-
setStatusText(
|
|
1164
|
+
setStatusText('Exported USDZ successfully');
|
|
1101
1165
|
}
|
|
1102
1166
|
catch (err) {
|
|
1103
|
-
log.error(
|
|
1167
|
+
log.error('USDZ export error:', err);
|
|
1104
1168
|
setStatusText(`USDZ export error: ${String(err)}`);
|
|
1105
1169
|
}
|
|
1106
1170
|
}, []);
|
|
@@ -1118,9 +1182,23 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1118
1182
|
: [child.material];
|
|
1119
1183
|
mats.forEach((mat) => {
|
|
1120
1184
|
const m = mat;
|
|
1121
|
-
[
|
|
1122
|
-
|
|
1185
|
+
[
|
|
1186
|
+
'map',
|
|
1187
|
+
'normalMap',
|
|
1188
|
+
'roughnessMap',
|
|
1189
|
+
'metalnessMap',
|
|
1190
|
+
'emissiveMap',
|
|
1191
|
+
'aoMap',
|
|
1192
|
+
'alphaMap',
|
|
1193
|
+
'envMap',
|
|
1194
|
+
'lightMap',
|
|
1195
|
+
'bumpMap',
|
|
1196
|
+
'displacementMap',
|
|
1197
|
+
'specularMap',
|
|
1198
|
+
].forEach((prop) => {
|
|
1199
|
+
if (m[prop] && typeof m[prop].dispose === 'function') {
|
|
1123
1200
|
m[prop].dispose();
|
|
1201
|
+
}
|
|
1124
1202
|
});
|
|
1125
1203
|
mat.dispose();
|
|
1126
1204
|
});
|
|
@@ -1129,12 +1207,13 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1129
1207
|
}, []);
|
|
1130
1208
|
const clearScene = useCallback(() => {
|
|
1131
1209
|
var _a;
|
|
1132
|
-
if (!window.confirm(
|
|
1210
|
+
if (!window.confirm('Clear the entire scene? This cannot be undone.')) {
|
|
1133
1211
|
return;
|
|
1212
|
+
}
|
|
1134
1213
|
const scene = sceneRef.current;
|
|
1135
1214
|
const toRemove = [];
|
|
1136
1215
|
scene.traverse((obj) => {
|
|
1137
|
-
if (!obj.name.startsWith(
|
|
1216
|
+
if (!obj.name.startsWith('__') && obj !== scene && obj.parent === scene) {
|
|
1138
1217
|
toRemove.push(obj);
|
|
1139
1218
|
}
|
|
1140
1219
|
});
|
|
@@ -1160,33 +1239,35 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1160
1239
|
setAnimDuration(0);
|
|
1161
1240
|
setSelectedId(null);
|
|
1162
1241
|
rebuildTree();
|
|
1163
|
-
setStatusText(
|
|
1242
|
+
setStatusText('Scene cleared');
|
|
1164
1243
|
}, [rebuildTree, disposeObject]);
|
|
1165
1244
|
/* ─────────────────────────────────────────
|
|
1166
1245
|
Save to Library
|
|
1167
1246
|
───────────────────────────────────────── */
|
|
1168
1247
|
const saveToLibrary = useCallback(() => {
|
|
1169
|
-
if (!onSaveToLibrary)
|
|
1248
|
+
if (!onSaveToLibrary) {
|
|
1170
1249
|
return;
|
|
1250
|
+
}
|
|
1171
1251
|
const root = rootObjectRef.current;
|
|
1172
1252
|
if (!root) {
|
|
1173
|
-
setStatusText(
|
|
1253
|
+
setStatusText('Nothing to save');
|
|
1174
1254
|
return;
|
|
1175
1255
|
}
|
|
1176
1256
|
const exporter = new GLTFExporter();
|
|
1177
1257
|
exporter.parse(root, (result) => {
|
|
1178
|
-
const blob = new Blob([result], { type:
|
|
1258
|
+
const blob = new Blob([result], { type: 'model/gltf-binary' });
|
|
1179
1259
|
const reader = new FileReader();
|
|
1180
1260
|
reader.onload = () => {
|
|
1181
|
-
const defaultName =
|
|
1182
|
-
const name = window.prompt(
|
|
1183
|
-
if (!name || !name.trim())
|
|
1261
|
+
const defaultName = 'model.glb';
|
|
1262
|
+
const name = window.prompt('Save to library as:', defaultName);
|
|
1263
|
+
if (!name || !name.trim()) {
|
|
1184
1264
|
return;
|
|
1185
|
-
|
|
1265
|
+
}
|
|
1266
|
+
onSaveToLibrary(reader.result, name.trim(), 'model/gltf-binary');
|
|
1186
1267
|
};
|
|
1187
1268
|
reader.readAsDataURL(blob);
|
|
1188
1269
|
}, (err) => {
|
|
1189
|
-
log.error(
|
|
1270
|
+
log.error('Save to library error:', err);
|
|
1190
1271
|
setStatusText(`Save error: ${String(err)}`);
|
|
1191
1272
|
}, { binary: true, animations: allClipsRef.current.map((a) => a.clip) });
|
|
1192
1273
|
}, [onSaveToLibrary]);
|
|
@@ -1196,53 +1277,58 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1196
1277
|
const addPrimitive = useCallback((type) => {
|
|
1197
1278
|
let geo;
|
|
1198
1279
|
switch (type) {
|
|
1199
|
-
case
|
|
1280
|
+
case 'cube':
|
|
1200
1281
|
geo = new THREE.BoxGeometry(1, 1, 1);
|
|
1201
1282
|
break;
|
|
1202
|
-
case
|
|
1283
|
+
case 'sphere':
|
|
1203
1284
|
geo = new THREE.SphereGeometry(0.5, 32, 32);
|
|
1204
1285
|
break;
|
|
1205
|
-
case
|
|
1286
|
+
case 'cylinder':
|
|
1206
1287
|
geo = new THREE.CylinderGeometry(0.5, 0.5, 1, 32);
|
|
1207
1288
|
break;
|
|
1208
|
-
case
|
|
1289
|
+
case 'cone':
|
|
1209
1290
|
geo = new THREE.ConeGeometry(0.5, 1, 32);
|
|
1210
1291
|
break;
|
|
1211
|
-
case
|
|
1292
|
+
case 'torus':
|
|
1212
1293
|
geo = new THREE.TorusGeometry(0.5, 0.2, 16, 32);
|
|
1213
1294
|
break;
|
|
1214
|
-
case
|
|
1295
|
+
case 'plane':
|
|
1215
1296
|
geo = new THREE.PlaneGeometry(2, 2);
|
|
1216
1297
|
break;
|
|
1217
|
-
case
|
|
1298
|
+
case 'circle':
|
|
1218
1299
|
geo = new THREE.CircleGeometry(0.5, 32);
|
|
1219
1300
|
break;
|
|
1220
|
-
case
|
|
1301
|
+
case 'ring':
|
|
1221
1302
|
geo = new THREE.RingGeometry(0.3, 0.5, 32);
|
|
1222
1303
|
break;
|
|
1223
|
-
case
|
|
1304
|
+
case 'dodecahedron':
|
|
1224
1305
|
geo = new THREE.DodecahedronGeometry(0.5);
|
|
1225
1306
|
break;
|
|
1226
|
-
case
|
|
1307
|
+
case 'icosahedron':
|
|
1227
1308
|
geo = new THREE.IcosahedronGeometry(0.5);
|
|
1228
1309
|
break;
|
|
1229
|
-
case
|
|
1310
|
+
case 'octahedron':
|
|
1230
1311
|
geo = new THREE.OctahedronGeometry(0.5);
|
|
1231
1312
|
break;
|
|
1232
|
-
case
|
|
1313
|
+
case 'tetrahedron':
|
|
1233
1314
|
geo = new THREE.TetrahedronGeometry(0.5);
|
|
1234
1315
|
break;
|
|
1235
|
-
case
|
|
1316
|
+
case 'torusKnot':
|
|
1236
1317
|
geo = new THREE.TorusKnotGeometry(0.4, 0.15, 64, 16);
|
|
1237
1318
|
break;
|
|
1238
|
-
default:
|
|
1319
|
+
default:
|
|
1320
|
+
return;
|
|
1239
1321
|
}
|
|
1240
|
-
const mat = new THREE.MeshStandardMaterial({
|
|
1322
|
+
const mat = new THREE.MeshStandardMaterial({
|
|
1323
|
+
color: 0x888888,
|
|
1324
|
+
metalness: 0.1,
|
|
1325
|
+
roughness: 0.7,
|
|
1326
|
+
});
|
|
1241
1327
|
const mesh = new THREE.Mesh(geo, mat);
|
|
1242
1328
|
mesh.name = type.charAt(0).toUpperCase() + type.slice(1);
|
|
1243
1329
|
mesh.castShadow = true;
|
|
1244
1330
|
mesh.receiveShadow = true;
|
|
1245
|
-
if (type ===
|
|
1331
|
+
if (type === 'plane' || type === 'circle') {
|
|
1246
1332
|
mesh.rotation.x = -Math.PI / 2;
|
|
1247
1333
|
mesh.position.y = 0.001;
|
|
1248
1334
|
}
|
|
@@ -1257,34 +1343,35 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1257
1343
|
const addSceneLight = useCallback((type) => {
|
|
1258
1344
|
let light;
|
|
1259
1345
|
switch (type) {
|
|
1260
|
-
case
|
|
1346
|
+
case 'point': {
|
|
1261
1347
|
const l = new THREE.PointLight(0xffffff, 1, 20);
|
|
1262
1348
|
l.position.set(0, 3, 0);
|
|
1263
1349
|
l.castShadow = true;
|
|
1264
1350
|
light = l;
|
|
1265
1351
|
break;
|
|
1266
1352
|
}
|
|
1267
|
-
case
|
|
1353
|
+
case 'spot': {
|
|
1268
1354
|
const l = new THREE.SpotLight(0xffffff, 1, 20, Math.PI / 6, 0.3);
|
|
1269
1355
|
l.position.set(0, 4, 2);
|
|
1270
1356
|
l.castShadow = true;
|
|
1271
1357
|
light = l;
|
|
1272
1358
|
break;
|
|
1273
1359
|
}
|
|
1274
|
-
case
|
|
1360
|
+
case 'directional': {
|
|
1275
1361
|
const l = new THREE.DirectionalLight(0xffffff, 1);
|
|
1276
1362
|
l.position.set(3, 5, 3);
|
|
1277
1363
|
l.castShadow = true;
|
|
1278
1364
|
light = l;
|
|
1279
1365
|
break;
|
|
1280
1366
|
}
|
|
1281
|
-
case
|
|
1367
|
+
case 'hemisphere': {
|
|
1282
1368
|
light = new THREE.HemisphereLight(0x8888ff, 0x442200, 0.5);
|
|
1283
1369
|
break;
|
|
1284
1370
|
}
|
|
1285
|
-
default:
|
|
1371
|
+
default:
|
|
1372
|
+
return;
|
|
1286
1373
|
}
|
|
1287
|
-
light.name = type.charAt(0).toUpperCase() + type.slice(1) +
|
|
1374
|
+
light.name = type.charAt(0).toUpperCase() + type.slice(1) + 'Light';
|
|
1288
1375
|
sceneRef.current.add(light);
|
|
1289
1376
|
let helper = null;
|
|
1290
1377
|
if (light instanceof THREE.PointLight) {
|
|
@@ -1309,24 +1396,25 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1309
1396
|
}, [rebuildTree]);
|
|
1310
1397
|
const addCameraObject = useCallback(() => {
|
|
1311
1398
|
const cam = new THREE.PerspectiveCamera(50, 16 / 9, 0.1, 1000);
|
|
1312
|
-
cam.name =
|
|
1399
|
+
cam.name = 'Camera';
|
|
1313
1400
|
cam.position.set(5, 3, 5);
|
|
1314
1401
|
cam.lookAt(0, 0, 0);
|
|
1315
1402
|
sceneRef.current.add(cam);
|
|
1316
1403
|
const helper = new THREE.CameraHelper(cam);
|
|
1317
|
-
helper.name =
|
|
1404
|
+
helper.name = '__helper_Camera';
|
|
1318
1405
|
sceneRef.current.add(helper);
|
|
1319
1406
|
rebuildTree();
|
|
1320
|
-
setStatusText(
|
|
1407
|
+
setStatusText('Added Camera');
|
|
1321
1408
|
setAddMenuOpen(null);
|
|
1322
1409
|
}, [rebuildTree]);
|
|
1323
1410
|
const addEmpty = useCallback((type) => {
|
|
1324
1411
|
const obj = new THREE.Object3D();
|
|
1325
|
-
obj.name =
|
|
1412
|
+
obj.name =
|
|
1413
|
+
type === 'arrows' ? 'Empty_Arrows' : type === 'cube_empty' ? 'Empty_Cube' : 'Empty';
|
|
1326
1414
|
sceneRef.current.add(obj);
|
|
1327
|
-
if (type ===
|
|
1415
|
+
if (type === 'arrows') {
|
|
1328
1416
|
const h = new THREE.AxesHelper(0.5);
|
|
1329
|
-
h.name =
|
|
1417
|
+
h.name = '__emptyAxes';
|
|
1330
1418
|
obj.add(h);
|
|
1331
1419
|
}
|
|
1332
1420
|
rebuildTree();
|
|
@@ -1337,20 +1425,24 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1337
1425
|
Raycaster click-to-select
|
|
1338
1426
|
───────────────────────────────────────── */
|
|
1339
1427
|
const handleViewportClick = useCallback((e) => {
|
|
1340
|
-
if (editorMode !==
|
|
1428
|
+
if (editorMode !== 'object') {
|
|
1341
1429
|
return;
|
|
1342
|
-
|
|
1430
|
+
}
|
|
1431
|
+
if (!mountRef.current || !rendererRef.current || !cameraRef.current) {
|
|
1343
1432
|
return;
|
|
1344
|
-
|
|
1433
|
+
}
|
|
1434
|
+
if (e.target !== rendererRef.current.domElement) {
|
|
1345
1435
|
return;
|
|
1436
|
+
}
|
|
1346
1437
|
const rect = mountRef.current.getBoundingClientRect();
|
|
1347
1438
|
const x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
|
|
1348
1439
|
const y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
|
|
1349
1440
|
raycasterRef.current.setFromCamera(new THREE.Vector2(x, y), cameraRef.current);
|
|
1350
1441
|
const meshes = [];
|
|
1351
1442
|
sceneRef.current.traverse((obj) => {
|
|
1352
|
-
if (obj instanceof THREE.Mesh && !obj.name.startsWith(
|
|
1443
|
+
if (obj instanceof THREE.Mesh && !obj.name.startsWith('__')) {
|
|
1353
1444
|
meshes.push(obj);
|
|
1445
|
+
}
|
|
1354
1446
|
});
|
|
1355
1447
|
const intersects = raycasterRef.current.intersectObjects(meshes, false);
|
|
1356
1448
|
if (intersects.length > 0) {
|
|
@@ -1378,11 +1470,12 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1378
1470
|
}, []);
|
|
1379
1471
|
const closeContextMenu = useCallback(() => setContextMenu(null), []);
|
|
1380
1472
|
useEffect(() => {
|
|
1381
|
-
if (!contextMenu)
|
|
1473
|
+
if (!contextMenu) {
|
|
1382
1474
|
return;
|
|
1475
|
+
}
|
|
1383
1476
|
const close = () => setContextMenu(null);
|
|
1384
|
-
window.addEventListener(
|
|
1385
|
-
return () => window.removeEventListener(
|
|
1477
|
+
window.addEventListener('click', close);
|
|
1478
|
+
return () => window.removeEventListener('click', close);
|
|
1386
1479
|
}, [contextMenu]);
|
|
1387
1480
|
/* ─────────────────────────────────────────
|
|
1388
1481
|
AI animation from video
|
|
@@ -1390,15 +1483,16 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1390
1483
|
const handleAIVideo = useCallback(async (e) => {
|
|
1391
1484
|
var _a;
|
|
1392
1485
|
const file = (_a = e.target.files) === null || _a === void 0 ? void 0 : _a[0];
|
|
1393
|
-
if (!file)
|
|
1486
|
+
if (!file) {
|
|
1394
1487
|
return;
|
|
1395
|
-
|
|
1488
|
+
}
|
|
1489
|
+
e.target.value = '';
|
|
1396
1490
|
if (!onAIPoseVideo) {
|
|
1397
|
-
setAiStatus(
|
|
1491
|
+
setAiStatus('AI pose callback not configured.');
|
|
1398
1492
|
return;
|
|
1399
1493
|
}
|
|
1400
1494
|
setAiBusy(true);
|
|
1401
|
-
setAiStatus(
|
|
1495
|
+
setAiStatus('Uploading video for AI pose estimation...');
|
|
1402
1496
|
try {
|
|
1403
1497
|
const result = await onAIPoseVideo(file);
|
|
1404
1498
|
setAiStatus(`Got ${result.frame_count} frames at ${result.fps} fps. Creating animation clip...`);
|
|
@@ -1416,14 +1510,14 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1416
1510
|
allClipsRef.current = [...allClipsRef.current, newAnim];
|
|
1417
1511
|
setAnimations([...allClipsRef.current]);
|
|
1418
1512
|
playClip(allClipsRef.current.length - 1);
|
|
1419
|
-
setAiStatus(
|
|
1513
|
+
setAiStatus('Animation created successfully!');
|
|
1420
1514
|
}
|
|
1421
1515
|
else {
|
|
1422
|
-
setAiStatus(
|
|
1516
|
+
setAiStatus('Could not create animation — load a rigged model first.');
|
|
1423
1517
|
}
|
|
1424
1518
|
}
|
|
1425
1519
|
catch (err) {
|
|
1426
|
-
log.error(
|
|
1520
|
+
log.error('AI pose error:', err);
|
|
1427
1521
|
setAiStatus(`Error: ${String(err)}`);
|
|
1428
1522
|
}
|
|
1429
1523
|
finally {
|
|
@@ -1473,28 +1567,28 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1473
1567
|
// Keyboard shortcuts for undo/redo
|
|
1474
1568
|
useEffect(() => {
|
|
1475
1569
|
const handleKeyDown = (e) => {
|
|
1476
|
-
if ((e.ctrlKey || e.metaKey) && e.key ===
|
|
1570
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 'z' && !e.shiftKey) {
|
|
1477
1571
|
e.preventDefault();
|
|
1478
1572
|
undo();
|
|
1479
1573
|
}
|
|
1480
|
-
else if ((e.ctrlKey || e.metaKey) && (e.key ===
|
|
1574
|
+
else if ((e.ctrlKey || e.metaKey) && (e.key === 'y' || (e.key === 'z' && e.shiftKey))) {
|
|
1481
1575
|
e.preventDefault();
|
|
1482
1576
|
redo();
|
|
1483
1577
|
}
|
|
1484
1578
|
};
|
|
1485
|
-
window.addEventListener(
|
|
1486
|
-
return () => window.removeEventListener(
|
|
1579
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
1580
|
+
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
1487
1581
|
}, [undo, redo]);
|
|
1488
1582
|
/* ─────────────────────────────────────────
|
|
1489
1583
|
Batch Import
|
|
1490
1584
|
───────────────────────────────────────── */
|
|
1491
1585
|
const loadFiles = useCallback(async (files) => {
|
|
1492
1586
|
const fileArray = Array.from(files).filter((f) => {
|
|
1493
|
-
const ext = f.name.substring(f.name.lastIndexOf(
|
|
1587
|
+
const ext = f.name.substring(f.name.lastIndexOf('.')).toLowerCase();
|
|
1494
1588
|
return SUPPORTED_EXTENSIONS.includes(ext);
|
|
1495
1589
|
});
|
|
1496
1590
|
if (fileArray.length === 0) {
|
|
1497
|
-
setStatusText(
|
|
1591
|
+
setStatusText('No supported files found');
|
|
1498
1592
|
return;
|
|
1499
1593
|
}
|
|
1500
1594
|
setStatusText(`Importing ${fileArray.length} file(s)...`);
|
|
@@ -1517,25 +1611,31 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1517
1611
|
const applyPreset = useCallback((preset) => {
|
|
1518
1612
|
const node = selectedNode;
|
|
1519
1613
|
if (!node || !(node.object instanceof THREE.Mesh)) {
|
|
1520
|
-
setStatusText(
|
|
1614
|
+
setStatusText('Select a mesh to apply material preset');
|
|
1521
1615
|
return;
|
|
1522
1616
|
}
|
|
1523
1617
|
const mesh = node.object;
|
|
1524
|
-
const oldMaterial = Array.isArray(mesh.material)
|
|
1525
|
-
|
|
1618
|
+
const oldMaterial = Array.isArray(mesh.material)
|
|
1619
|
+
? mesh.material[selMaterialIdx]
|
|
1620
|
+
: mesh.material;
|
|
1621
|
+
if (!oldMaterial) {
|
|
1526
1622
|
return;
|
|
1623
|
+
}
|
|
1527
1624
|
// Save for undo
|
|
1528
1625
|
const oldClone = oldMaterial.clone();
|
|
1529
1626
|
const materialIdx = selMaterialIdx;
|
|
1530
1627
|
const newMaterial = createMaterialFromPreset(preset);
|
|
1531
1628
|
// Copy existing textures if present
|
|
1532
1629
|
if (oldMaterial instanceof THREE.MeshStandardMaterial) {
|
|
1533
|
-
if (oldMaterial.map)
|
|
1630
|
+
if (oldMaterial.map) {
|
|
1534
1631
|
newMaterial.map = oldMaterial.map;
|
|
1535
|
-
|
|
1632
|
+
}
|
|
1633
|
+
if (oldMaterial.normalMap) {
|
|
1536
1634
|
newMaterial.normalMap = oldMaterial.normalMap;
|
|
1537
|
-
|
|
1635
|
+
}
|
|
1636
|
+
if (oldMaterial.aoMap) {
|
|
1538
1637
|
newMaterial.aoMap = oldMaterial.aoMap;
|
|
1638
|
+
}
|
|
1539
1639
|
}
|
|
1540
1640
|
if (Array.isArray(mesh.material)) {
|
|
1541
1641
|
mesh.material[materialIdx] = newMaterial;
|
|
@@ -1571,7 +1671,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1571
1671
|
const generateSelectedLOD = useCallback(() => {
|
|
1572
1672
|
const node = selectedNode;
|
|
1573
1673
|
if (!node || !(node.object instanceof THREE.Mesh)) {
|
|
1574
|
-
setStatusText(
|
|
1674
|
+
setStatusText('Select a mesh to generate LOD');
|
|
1575
1675
|
return;
|
|
1576
1676
|
}
|
|
1577
1677
|
const mesh = node.object;
|
|
@@ -1581,8 +1681,14 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1581
1681
|
parent.add(lod);
|
|
1582
1682
|
pushUndo({
|
|
1583
1683
|
label: `Generate LOD for ${mesh.name}`,
|
|
1584
|
-
undo: () => {
|
|
1585
|
-
|
|
1684
|
+
undo: () => {
|
|
1685
|
+
parent.remove(lod);
|
|
1686
|
+
parent.add(mesh);
|
|
1687
|
+
},
|
|
1688
|
+
redo: () => {
|
|
1689
|
+
parent.remove(mesh);
|
|
1690
|
+
parent.add(lod);
|
|
1691
|
+
},
|
|
1586
1692
|
});
|
|
1587
1693
|
rebuildTree();
|
|
1588
1694
|
setStatusText(`Generated LOD for ${mesh.name} (4 levels)`);
|
|
@@ -1594,7 +1700,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1594
1700
|
const root = rootObjectRef.current || sceneRef.current;
|
|
1595
1701
|
const result = convertToInstanced(root);
|
|
1596
1702
|
if (result.created === 0) {
|
|
1597
|
-
setStatusText(
|
|
1703
|
+
setStatusText('No duplicate meshes found to instance');
|
|
1598
1704
|
return;
|
|
1599
1705
|
}
|
|
1600
1706
|
rebuildTree();
|
|
@@ -1603,8 +1709,9 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1603
1709
|
/* ── HDRI Environment ── */
|
|
1604
1710
|
const applyHDRI = useCallback((preset) => {
|
|
1605
1711
|
const renderer = rendererRef.current;
|
|
1606
|
-
if (!renderer)
|
|
1712
|
+
if (!renderer) {
|
|
1607
1713
|
return;
|
|
1714
|
+
}
|
|
1608
1715
|
applyHDRIPreset(sceneRef.current, renderer, preset);
|
|
1609
1716
|
setBgColor(`#${new THREE.Color(preset.bgColor).getHexString()}`);
|
|
1610
1717
|
if (preset.fog) {
|
|
@@ -1621,7 +1728,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1621
1728
|
/* ── Snap ── */
|
|
1622
1729
|
const snapPoint = useCallback((point) => {
|
|
1623
1730
|
const root = rootObjectRef.current || sceneRef.current;
|
|
1624
|
-
const modes = snapEnabled ? [
|
|
1731
|
+
const modes = snapEnabled ? ['vertex', 'edge', 'grid'] : ['grid'];
|
|
1625
1732
|
return snapMulti(point, root, snapGrid, modes, 0.5);
|
|
1626
1733
|
}, [snapEnabled, snapGrid]);
|
|
1627
1734
|
/* ═══════════════════════════════════════════
|