@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
|
@@ -1,28 +1,28 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var _e = require('react');
|
|
4
4
|
var THREE = require('three');
|
|
5
5
|
var OrbitControls_js = require('three/examples/jsm/controls/OrbitControls.js');
|
|
6
6
|
var TransformControls_js = require('three/examples/jsm/controls/TransformControls.js');
|
|
7
|
-
var FBXLoader_js = require('three/examples/jsm/loaders/FBXLoader.js');
|
|
8
|
-
var GLTFLoader_js = require('three/examples/jsm/loaders/GLTFLoader.js');
|
|
9
7
|
var GLTFExporter_js = require('three/examples/jsm/exporters/GLTFExporter.js');
|
|
10
8
|
var OBJExporter_js = require('three/examples/jsm/exporters/OBJExporter.js');
|
|
11
|
-
var STLExporter_js = require('three/examples/jsm/exporters/STLExporter.js');
|
|
12
9
|
var PLYExporter_js = require('three/examples/jsm/exporters/PLYExporter.js');
|
|
10
|
+
var STLExporter_js = require('three/examples/jsm/exporters/STLExporter.js');
|
|
13
11
|
var USDZExporter_js = require('three/examples/jsm/exporters/USDZExporter.js');
|
|
14
|
-
var OBJLoader_js = require('three/examples/jsm/loaders/OBJLoader.js');
|
|
15
|
-
var ColladaLoader_js = require('three/examples/jsm/loaders/ColladaLoader.js');
|
|
16
|
-
var STLLoader_js = require('three/examples/jsm/loaders/STLLoader.js');
|
|
17
|
-
var PLYLoader_js = require('three/examples/jsm/loaders/PLYLoader.js');
|
|
18
|
-
var TDSLoader_js = require('three/examples/jsm/loaders/TDSLoader.js');
|
|
19
12
|
var _3MFLoader_js = require('three/examples/jsm/loaders/3MFLoader.js');
|
|
20
13
|
var AMFLoader_js = require('three/examples/jsm/loaders/AMFLoader.js');
|
|
21
|
-
var
|
|
22
|
-
var
|
|
23
|
-
var VRMLLoader_js = require('three/examples/jsm/loaders/VRMLLoader.js');
|
|
14
|
+
var ColladaLoader_js = require('three/examples/jsm/loaders/ColladaLoader.js');
|
|
15
|
+
var FBXLoader_js = require('three/examples/jsm/loaders/FBXLoader.js');
|
|
24
16
|
var GCodeLoader_js = require('three/examples/jsm/loaders/GCodeLoader.js');
|
|
17
|
+
var GLTFLoader_js = require('three/examples/jsm/loaders/GLTFLoader.js');
|
|
18
|
+
var OBJLoader_js = require('three/examples/jsm/loaders/OBJLoader.js');
|
|
19
|
+
var PCDLoader_js = require('three/examples/jsm/loaders/PCDLoader.js');
|
|
20
|
+
var PLYLoader_js = require('three/examples/jsm/loaders/PLYLoader.js');
|
|
21
|
+
var STLLoader_js = require('three/examples/jsm/loaders/STLLoader.js');
|
|
25
22
|
var SVGLoader_js = require('three/examples/jsm/loaders/SVGLoader.js');
|
|
23
|
+
var TDSLoader_js = require('three/examples/jsm/loaders/TDSLoader.js');
|
|
24
|
+
var VRMLLoader_js = require('three/examples/jsm/loaders/VRMLLoader.js');
|
|
25
|
+
var VTKLoader_js = require('three/examples/jsm/loaders/VTKLoader.js');
|
|
26
26
|
var modelEditorTypes = require('./modelEditorTypes.js');
|
|
27
27
|
var modelEditorUtils = require('./modelEditorUtils.js');
|
|
28
28
|
|
|
@@ -59,11 +59,13 @@ const nid = () => `n${++_nid}`;
|
|
|
59
59
|
/* ── Pure helper — extracted outside the hook for stable identity ── */
|
|
60
60
|
function findNodeById(nodes, id) {
|
|
61
61
|
for (const n of nodes) {
|
|
62
|
-
if (n.id === id)
|
|
62
|
+
if (n.id === id) {
|
|
63
63
|
return n;
|
|
64
|
+
}
|
|
64
65
|
const found = findNodeById(n.children, id);
|
|
65
|
-
if (found)
|
|
66
|
+
if (found) {
|
|
66
67
|
return found;
|
|
68
|
+
}
|
|
67
69
|
}
|
|
68
70
|
return null;
|
|
69
71
|
}
|
|
@@ -72,85 +74,86 @@ function findNodeById(nodes, id) {
|
|
|
72
74
|
═══════════════════════════════════════════ */
|
|
73
75
|
function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
74
76
|
/* ── refs ── */
|
|
75
|
-
const mountRef =
|
|
76
|
-
const rendererRef =
|
|
77
|
-
const sceneRef =
|
|
78
|
-
const cameraRef =
|
|
79
|
-
const orbitRef =
|
|
80
|
-
const transformRef =
|
|
81
|
-
const clockRef =
|
|
82
|
-
const mixerRef =
|
|
83
|
-
const rafRef =
|
|
84
|
-
const fileInputRef =
|
|
85
|
-
const mergeInputRef =
|
|
86
|
-
const videoInputRef =
|
|
87
|
-
const raycasterRef =
|
|
88
|
-
const skeletonHelperRef =
|
|
77
|
+
const mountRef = _e.useRef(null);
|
|
78
|
+
const rendererRef = _e.useRef(null);
|
|
79
|
+
const sceneRef = _e.useRef(new THREE__namespace.Scene());
|
|
80
|
+
const cameraRef = _e.useRef(new THREE__namespace.PerspectiveCamera(50, 1, 0.01, 10000));
|
|
81
|
+
const orbitRef = _e.useRef(null);
|
|
82
|
+
const transformRef = _e.useRef(null);
|
|
83
|
+
const clockRef = _e.useRef(new THREE__namespace.Clock());
|
|
84
|
+
const mixerRef = _e.useRef(null);
|
|
85
|
+
const rafRef = _e.useRef(0);
|
|
86
|
+
const fileInputRef = _e.useRef(null);
|
|
87
|
+
const mergeInputRef = _e.useRef(null);
|
|
88
|
+
const videoInputRef = _e.useRef(null);
|
|
89
|
+
const raycasterRef = _e.useRef(new THREE__namespace.Raycaster());
|
|
90
|
+
const skeletonHelperRef = _e.useRef(null);
|
|
89
91
|
/* ── state ── */
|
|
90
|
-
const [sceneTree, setSceneTree] =
|
|
91
|
-
const [selectedId, setSelectedId] =
|
|
92
|
-
const [transformMode, setTransformMode] =
|
|
93
|
-
const [animations, setAnimations] =
|
|
94
|
-
const [activeAnimIdx, setActiveAnimIdx] =
|
|
95
|
-
const [isPlaying, setIsPlaying] =
|
|
96
|
-
const [animTime, setAnimTime] =
|
|
97
|
-
const [animDuration, setAnimDuration] =
|
|
98
|
-
const [animSpeed, setAnimSpeed] =
|
|
99
|
-
const [loopAnim, setLoopAnim] =
|
|
100
|
-
const [bottomCollapsed, setBottomCollapsed] =
|
|
101
|
-
const [showGrid, setShowGrid] =
|
|
102
|
-
const [showAxes, setShowAxes] =
|
|
103
|
-
const [wireframe, setWireframe] =
|
|
104
|
-
const [bgColor, setBgColor] =
|
|
105
|
-
const [statusText, setStatusText] =
|
|
106
|
-
const [dragOver, setDragOver] =
|
|
107
|
-
const [polyCount, setPolyCount] =
|
|
108
|
-
const [meshCount, setMeshCount] =
|
|
109
|
-
const [boneCount, setBoneCount] =
|
|
92
|
+
const [sceneTree, setSceneTree] = _e.useState([]);
|
|
93
|
+
const [selectedId, setSelectedId] = _e.useState(null);
|
|
94
|
+
const [transformMode, setTransformMode] = _e.useState('translate');
|
|
95
|
+
const [animations, setAnimations] = _e.useState([]);
|
|
96
|
+
const [activeAnimIdx, setActiveAnimIdx] = _e.useState(-1);
|
|
97
|
+
const [isPlaying, setIsPlaying] = _e.useState(false);
|
|
98
|
+
const [animTime, setAnimTime] = _e.useState(0);
|
|
99
|
+
const [animDuration, setAnimDuration] = _e.useState(0);
|
|
100
|
+
const [animSpeed, setAnimSpeed] = _e.useState(1);
|
|
101
|
+
const [loopAnim, setLoopAnim] = _e.useState(true);
|
|
102
|
+
const [bottomCollapsed, setBottomCollapsed] = _e.useState(false);
|
|
103
|
+
const [showGrid, setShowGrid] = _e.useState(true);
|
|
104
|
+
const [showAxes, setShowAxes] = _e.useState(true);
|
|
105
|
+
const [wireframe, setWireframe] = _e.useState(false);
|
|
106
|
+
const [bgColor, setBgColor] = _e.useState('#111122');
|
|
107
|
+
const [statusText, setStatusText] = _e.useState('Ready');
|
|
108
|
+
const [dragOver, setDragOver] = _e.useState(false);
|
|
109
|
+
const [polyCount, setPolyCount] = _e.useState(0);
|
|
110
|
+
const [meshCount, setMeshCount] = _e.useState(0);
|
|
111
|
+
const [boneCount, setBoneCount] = _e.useState(0);
|
|
110
112
|
// Material editor
|
|
111
|
-
const [selMaterialIdx, setSelMaterialIdx] =
|
|
112
|
-
const [matRefresh, setMatRefresh] =
|
|
113
|
+
const [selMaterialIdx, setSelMaterialIdx] = _e.useState(0);
|
|
114
|
+
const [matRefresh, setMatRefresh] = _e.useState(0);
|
|
113
115
|
// AI panel
|
|
114
|
-
const [aiStatus, setAiStatus] =
|
|
115
|
-
const [aiBusy, setAiBusy] =
|
|
116
|
+
const [aiStatus, setAiStatus] = _e.useState('');
|
|
117
|
+
const [aiBusy, setAiBusy] = _e.useState(false);
|
|
116
118
|
// Blender 2.0 — viewport & editor
|
|
117
|
-
const [shadingMode, setShadingMode] =
|
|
118
|
-
const [editorMode, setEditorMode] =
|
|
119
|
-
const [contextMenu, setContextMenu] =
|
|
120
|
-
const [outlinerSearch, setOutlinerSearch] =
|
|
121
|
-
const [snapEnabled, setSnapEnabled] =
|
|
122
|
-
const [snapGrid, setSnapGrid] =
|
|
123
|
-
const [showSkeleton, setShowSkeleton] =
|
|
124
|
-
const [addMenuOpen, setAddMenuOpen] =
|
|
125
|
-
const [gizmoSpace, setGizmoSpace] =
|
|
126
|
-
const [propTab, setPropTab] =
|
|
127
|
-
const [fogEnabled, setFogEnabled] =
|
|
128
|
-
const [fogColor, setFogColor] =
|
|
129
|
-
const [fogNear, setFogNear] =
|
|
130
|
-
const [fogFar, setFogFar] =
|
|
131
|
-
const [showLightHelpers, setShowLightHelpers] =
|
|
119
|
+
const [shadingMode, setShadingMode] = _e.useState('solid');
|
|
120
|
+
const [editorMode, setEditorMode] = _e.useState('object');
|
|
121
|
+
const [contextMenu, setContextMenu] = _e.useState(null);
|
|
122
|
+
const [outlinerSearch, setOutlinerSearch] = _e.useState('');
|
|
123
|
+
const [snapEnabled, setSnapEnabled] = _e.useState(false);
|
|
124
|
+
const [snapGrid, setSnapGrid] = _e.useState(1);
|
|
125
|
+
const [showSkeleton, setShowSkeleton] = _e.useState(true);
|
|
126
|
+
const [addMenuOpen, setAddMenuOpen] = _e.useState(null);
|
|
127
|
+
const [gizmoSpace, setGizmoSpace] = _e.useState('world');
|
|
128
|
+
const [propTab, setPropTab] = _e.useState('object');
|
|
129
|
+
const [fogEnabled, setFogEnabled] = _e.useState(false);
|
|
130
|
+
const [fogColor, setFogColor] = _e.useState('#111122');
|
|
131
|
+
const [fogNear, setFogNear] = _e.useState(10);
|
|
132
|
+
const [fogFar, setFogFar] = _e.useState(100);
|
|
133
|
+
const [showLightHelpers, setShowLightHelpers] = _e.useState(true);
|
|
132
134
|
// The root object that was loaded (mesh/character)
|
|
133
|
-
const rootObjectRef =
|
|
135
|
+
const rootObjectRef = _e.useRef(null);
|
|
134
136
|
// Active animation action
|
|
135
|
-
const activeActionRef =
|
|
137
|
+
const activeActionRef = _e.useRef(null);
|
|
136
138
|
// All loaded animation clips (for the current model)
|
|
137
|
-
const allClipsRef =
|
|
139
|
+
const allClipsRef = _e.useRef([]);
|
|
138
140
|
/* keep refs current */
|
|
139
|
-
const animTimeRef =
|
|
141
|
+
const animTimeRef = _e.useRef(animTime);
|
|
140
142
|
animTimeRef.current = animTime;
|
|
141
|
-
const isPlayingRef =
|
|
143
|
+
const isPlayingRef = _e.useRef(isPlaying);
|
|
142
144
|
isPlayingRef.current = isPlaying;
|
|
143
|
-
const animSpeedRef =
|
|
145
|
+
const animSpeedRef = _e.useRef(animSpeed);
|
|
144
146
|
animSpeedRef.current = animSpeed;
|
|
145
|
-
const loopAnimRef =
|
|
147
|
+
const loopAnimRef = _e.useRef(loopAnim);
|
|
146
148
|
loopAnimRef.current = loopAnim;
|
|
147
149
|
/* ─────────────────────────────────────────
|
|
148
150
|
Scene setup
|
|
149
151
|
───────────────────────────────────────── */
|
|
150
|
-
|
|
152
|
+
_e.useEffect(() => {
|
|
151
153
|
const container = mountRef.current;
|
|
152
|
-
if (!container)
|
|
154
|
+
if (!container) {
|
|
153
155
|
return;
|
|
156
|
+
}
|
|
154
157
|
const scene = sceneRef.current;
|
|
155
158
|
const camera = cameraRef.current;
|
|
156
159
|
// Renderer
|
|
@@ -167,13 +170,13 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
167
170
|
camera.position.set(3, 2, 5);
|
|
168
171
|
camera.lookAt(0, 0, 0);
|
|
169
172
|
// Scene background
|
|
170
|
-
scene.background = new THREE__namespace.Color(
|
|
173
|
+
scene.background = new THREE__namespace.Color('#111122');
|
|
171
174
|
// Lights
|
|
172
175
|
const ambientLight = new THREE__namespace.AmbientLight(0xffffff, 0.5);
|
|
173
|
-
ambientLight.name =
|
|
176
|
+
ambientLight.name = '__ambient';
|
|
174
177
|
scene.add(ambientLight);
|
|
175
178
|
const dirLight = new THREE__namespace.DirectionalLight(0xffffff, 1.2);
|
|
176
|
-
dirLight.name =
|
|
179
|
+
dirLight.name = '__dirLight';
|
|
177
180
|
dirLight.position.set(5, 8, 5);
|
|
178
181
|
dirLight.castShadow = true;
|
|
179
182
|
dirLight.shadow.mapSize.set(2048, 2048);
|
|
@@ -185,21 +188,21 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
185
188
|
dirLight.shadow.camera.bottom = -10;
|
|
186
189
|
scene.add(dirLight);
|
|
187
190
|
const hemiLight = new THREE__namespace.HemisphereLight(0x8888ff, 0x444422, 0.4);
|
|
188
|
-
hemiLight.name =
|
|
191
|
+
hemiLight.name = '__hemiLight';
|
|
189
192
|
scene.add(hemiLight);
|
|
190
193
|
// Ground plane
|
|
191
194
|
const ground = new THREE__namespace.Mesh(new THREE__namespace.PlaneGeometry(100, 100), new THREE__namespace.ShadowMaterial({ opacity: 0.15 }));
|
|
192
|
-
ground.name =
|
|
195
|
+
ground.name = '__ground';
|
|
193
196
|
ground.rotation.x = -Math.PI / 2;
|
|
194
197
|
ground.receiveShadow = true;
|
|
195
198
|
scene.add(ground);
|
|
196
199
|
// Grid
|
|
197
200
|
const grid = new THREE__namespace.GridHelper(20, 20, 0x333355, 0x22223a);
|
|
198
|
-
grid.name =
|
|
201
|
+
grid.name = '__grid';
|
|
199
202
|
scene.add(grid);
|
|
200
203
|
// Axes
|
|
201
204
|
const axes = new THREE__namespace.AxesHelper(2);
|
|
202
|
-
axes.name =
|
|
205
|
+
axes.name = '__axes';
|
|
203
206
|
scene.add(axes);
|
|
204
207
|
// Orbit controls
|
|
205
208
|
const orbit = new OrbitControls_js.OrbitControls(camera, renderer.domElement);
|
|
@@ -211,9 +214,9 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
211
214
|
orbitRef.current = orbit;
|
|
212
215
|
// Transform controls
|
|
213
216
|
const tc = new TransformControls_js.TransformControls(camera, renderer.domElement);
|
|
214
|
-
tc.name =
|
|
217
|
+
tc.name = '__transformControls';
|
|
215
218
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
216
|
-
tc.addEventListener(
|
|
219
|
+
tc.addEventListener('dragging-changed', ((e) => {
|
|
217
220
|
orbit.enabled = !e.value;
|
|
218
221
|
}));
|
|
219
222
|
scene.add(tc);
|
|
@@ -222,8 +225,9 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
222
225
|
const onResize = () => {
|
|
223
226
|
const w = container.clientWidth;
|
|
224
227
|
const h = container.clientHeight;
|
|
225
|
-
if (w === 0 || h === 0)
|
|
228
|
+
if (w === 0 || h === 0) {
|
|
226
229
|
return;
|
|
230
|
+
}
|
|
227
231
|
camera.aspect = w / h;
|
|
228
232
|
camera.updateProjectionMatrix();
|
|
229
233
|
renderer.setSize(w, h);
|
|
@@ -256,8 +260,9 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
256
260
|
ro.disconnect();
|
|
257
261
|
const scn = sceneRef.current;
|
|
258
262
|
scn.traverse((obj) => {
|
|
259
|
-
if (obj.geometry)
|
|
263
|
+
if (obj.geometry) {
|
|
260
264
|
obj.geometry.dispose();
|
|
265
|
+
}
|
|
261
266
|
if (obj.material) {
|
|
262
267
|
const mats = Array.isArray(obj.material)
|
|
263
268
|
? obj.material
|
|
@@ -275,27 +280,29 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
275
280
|
container.removeChild(renderer.domElement);
|
|
276
281
|
}
|
|
277
282
|
};
|
|
278
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
279
283
|
}, []);
|
|
280
284
|
/* ─────────────────────────────────────────
|
|
281
285
|
Rebuild scene tree from Three.js scene
|
|
282
286
|
───────────────────────────────────────── */
|
|
283
|
-
const rebuildTree =
|
|
287
|
+
const rebuildTree = _e.useCallback(() => {
|
|
284
288
|
const scene = sceneRef.current;
|
|
285
289
|
let polys = 0;
|
|
286
290
|
let meshes = 0;
|
|
287
291
|
let bones = 0;
|
|
288
292
|
const walk = (obj, depth) => {
|
|
289
293
|
var _a, _b;
|
|
290
|
-
if (obj.name.startsWith(
|
|
294
|
+
if (obj.name.startsWith('__')) {
|
|
291
295
|
return null;
|
|
292
|
-
|
|
296
|
+
}
|
|
297
|
+
if (obj.type === 'TransformControlsPlane' || obj.type === 'TransformControlsGizmo') {
|
|
293
298
|
return null;
|
|
294
|
-
|
|
299
|
+
}
|
|
300
|
+
if ('isTransformControls' in obj) {
|
|
295
301
|
return null;
|
|
296
|
-
|
|
302
|
+
}
|
|
303
|
+
let nodeType = 'group';
|
|
297
304
|
if (obj instanceof THREE__namespace.Mesh) {
|
|
298
|
-
nodeType =
|
|
305
|
+
nodeType = 'mesh';
|
|
299
306
|
meshes++;
|
|
300
307
|
const geo = obj.geometry;
|
|
301
308
|
if (geo) {
|
|
@@ -304,27 +311,28 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
304
311
|
}
|
|
305
312
|
}
|
|
306
313
|
else if (obj instanceof THREE__namespace.Bone) {
|
|
307
|
-
nodeType =
|
|
314
|
+
nodeType = 'bone';
|
|
308
315
|
bones++;
|
|
309
316
|
}
|
|
310
317
|
else if (obj instanceof THREE__namespace.Light) {
|
|
311
|
-
nodeType =
|
|
318
|
+
nodeType = 'light';
|
|
312
319
|
}
|
|
313
320
|
else if (obj instanceof THREE__namespace.Camera) {
|
|
314
|
-
nodeType =
|
|
321
|
+
nodeType = 'camera';
|
|
315
322
|
}
|
|
316
|
-
else if (obj instanceof THREE__namespace.GridHelper ||
|
|
317
|
-
|
|
318
|
-
nodeType = "helper";
|
|
323
|
+
else if (obj instanceof THREE__namespace.GridHelper || obj instanceof THREE__namespace.AxesHelper) {
|
|
324
|
+
nodeType = 'helper';
|
|
319
325
|
}
|
|
320
326
|
const children = [];
|
|
321
327
|
for (const c of obj.children) {
|
|
322
328
|
const cn = walk(c, depth + 1);
|
|
323
|
-
if (cn)
|
|
329
|
+
if (cn) {
|
|
324
330
|
children.push(cn);
|
|
331
|
+
}
|
|
325
332
|
}
|
|
326
333
|
return {
|
|
327
|
-
id: obj.userData.__nodeId ||
|
|
334
|
+
id: obj.userData.__nodeId ||
|
|
335
|
+
((obj.userData.__nodeId = nid()), obj.userData.__nodeId),
|
|
328
336
|
name: obj.name || obj.type,
|
|
329
337
|
type: nodeType,
|
|
330
338
|
object: obj,
|
|
@@ -336,8 +344,9 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
336
344
|
const roots = [];
|
|
337
345
|
for (const c of scene.children) {
|
|
338
346
|
const n = walk(c, 0);
|
|
339
|
-
if (n)
|
|
347
|
+
if (n) {
|
|
340
348
|
roots.push(n);
|
|
349
|
+
}
|
|
341
350
|
}
|
|
342
351
|
setSceneTree(roots);
|
|
343
352
|
setPolyCount(Math.round(polys));
|
|
@@ -347,9 +356,9 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
347
356
|
/* ─────────────────────────────────────────
|
|
348
357
|
File loading
|
|
349
358
|
───────────────────────────────────────── */
|
|
350
|
-
const playClipRef =
|
|
351
|
-
const focusOnObjectRef =
|
|
352
|
-
const addObjectToScene =
|
|
359
|
+
const playClipRef = _e.useRef(() => { });
|
|
360
|
+
const focusOnObjectRef = _e.useRef(() => { });
|
|
361
|
+
const addObjectToScene = _e.useCallback((obj, clips, filename) => {
|
|
353
362
|
const scene = sceneRef.current;
|
|
354
363
|
obj.traverse((child) => {
|
|
355
364
|
if (child instanceof THREE__namespace.Mesh) {
|
|
@@ -378,7 +387,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
378
387
|
const mixer = new THREE__namespace.AnimationMixer(obj);
|
|
379
388
|
mixerRef.current = mixer;
|
|
380
389
|
const newAnims = clips.map((c) => ({
|
|
381
|
-
name: c.name ||
|
|
390
|
+
name: c.name || 'Animation',
|
|
382
391
|
clip: c,
|
|
383
392
|
duration: c.duration,
|
|
384
393
|
source: filename,
|
|
@@ -396,73 +405,81 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
396
405
|
rebuildTree();
|
|
397
406
|
setStatusText(`Loaded: ${filename} (${clips.length} animation(s))`);
|
|
398
407
|
}, [rebuildTree]);
|
|
399
|
-
const loadFile =
|
|
408
|
+
const loadFile = _e.useCallback(async (file) => {
|
|
400
409
|
var _a, _b, _c, _d, _e, _f;
|
|
401
410
|
const name = file.name.toLowerCase();
|
|
402
|
-
const ext =
|
|
411
|
+
const ext = '.' + name.split('.').pop();
|
|
403
412
|
const url = URL.createObjectURL(file);
|
|
404
413
|
setStatusText(`Loading ${file.name}...`);
|
|
405
414
|
try {
|
|
406
|
-
if (ext ===
|
|
415
|
+
if (ext === '.fbx') {
|
|
407
416
|
const loader = new FBXLoader_js.FBXLoader();
|
|
408
417
|
const obj = await new Promise((res, rej) => loader.load(url, res, undefined, rej));
|
|
409
418
|
addObjectToScene(obj, (_a = obj.animations) !== null && _a !== void 0 ? _a : [], file.name);
|
|
410
419
|
}
|
|
411
|
-
else if (ext ===
|
|
420
|
+
else if (ext === '.glb' || ext === '.gltf') {
|
|
412
421
|
const loader = new GLTFLoader_js.GLTFLoader();
|
|
413
422
|
const gltf = await new Promise((res, rej) => loader.load(url, res, undefined, rej));
|
|
414
423
|
addObjectToScene(gltf.scene, (_b = gltf.animations) !== null && _b !== void 0 ? _b : [], file.name);
|
|
415
424
|
}
|
|
416
|
-
else if (ext ===
|
|
425
|
+
else if (ext === '.obj') {
|
|
417
426
|
const loader = new OBJLoader_js.OBJLoader();
|
|
418
427
|
const obj = await new Promise((res, rej) => loader.load(url, res, undefined, rej));
|
|
419
428
|
addObjectToScene(obj, [], file.name);
|
|
420
429
|
}
|
|
421
|
-
else if (ext ===
|
|
430
|
+
else if (ext === '.dae') {
|
|
422
431
|
const loader = new ColladaLoader_js.ColladaLoader();
|
|
423
|
-
|
|
424
|
-
|
|
432
|
+
const collada = await new Promise((res, rej) =>
|
|
433
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- ColladaLoader callback type widens beyond { scene: Group }
|
|
434
|
+
loader.load(url, res, undefined, rej));
|
|
425
435
|
addObjectToScene(collada.scene, (_d = (_c = collada.scene) === null || _c === void 0 ? void 0 : _c.animations) !== null && _d !== void 0 ? _d : [], file.name);
|
|
426
436
|
}
|
|
427
|
-
else if (ext ===
|
|
437
|
+
else if (ext === '.stl') {
|
|
428
438
|
const loader = new STLLoader_js.STLLoader();
|
|
429
439
|
const geo = await new Promise((res, rej) => loader.load(url, res, undefined, rej));
|
|
430
|
-
const mat = new THREE__namespace.MeshStandardMaterial({
|
|
440
|
+
const mat = new THREE__namespace.MeshStandardMaterial({
|
|
441
|
+
color: 0x888888,
|
|
442
|
+
metalness: 0.3,
|
|
443
|
+
roughness: 0.6,
|
|
444
|
+
});
|
|
431
445
|
const mesh = new THREE__namespace.Mesh(geo, mat);
|
|
432
446
|
mesh.name = file.name;
|
|
433
447
|
addObjectToScene(mesh, [], file.name);
|
|
434
448
|
}
|
|
435
|
-
else if (ext ===
|
|
449
|
+
else if (ext === '.ply') {
|
|
436
450
|
const loader = new PLYLoader_js.PLYLoader();
|
|
437
451
|
const geo = await new Promise((res, rej) => loader.load(url, res, undefined, rej));
|
|
438
452
|
geo.computeVertexNormals();
|
|
439
|
-
const mat = new THREE__namespace.MeshStandardMaterial({
|
|
453
|
+
const mat = new THREE__namespace.MeshStandardMaterial({
|
|
454
|
+
color: 0x888888,
|
|
455
|
+
vertexColors: geo.hasAttribute('color'),
|
|
456
|
+
});
|
|
440
457
|
const mesh = new THREE__namespace.Mesh(geo, mat);
|
|
441
458
|
mesh.name = file.name;
|
|
442
459
|
addObjectToScene(mesh, [], file.name);
|
|
443
460
|
}
|
|
444
|
-
else if (ext ===
|
|
461
|
+
else if (ext === '.3ds') {
|
|
445
462
|
const loader = new TDSLoader_js.TDSLoader();
|
|
446
463
|
const obj = await new Promise((res, rej) => loader.load(url, res, undefined, rej));
|
|
447
464
|
addObjectToScene(obj, [], file.name);
|
|
448
465
|
}
|
|
449
|
-
else if (ext ===
|
|
466
|
+
else if (ext === '.3mf') {
|
|
450
467
|
const loader = new _3MFLoader_js.ThreeMFLoader();
|
|
451
468
|
const obj = await new Promise((res, rej) => loader.load(url, res, undefined, rej));
|
|
452
469
|
addObjectToScene(obj, [], file.name);
|
|
453
470
|
}
|
|
454
|
-
else if (ext ===
|
|
471
|
+
else if (ext === '.amf') {
|
|
455
472
|
const loader = new AMFLoader_js.AMFLoader();
|
|
456
473
|
const obj = await new Promise((res, rej) => loader.load(url, res, undefined, rej));
|
|
457
474
|
addObjectToScene(obj, [], file.name);
|
|
458
475
|
}
|
|
459
|
-
else if (ext ===
|
|
476
|
+
else if (ext === '.pcd') {
|
|
460
477
|
const loader = new PCDLoader_js.PCDLoader();
|
|
461
478
|
const points = await new Promise((res, rej) => loader.load(url, res, undefined, rej));
|
|
462
479
|
points.name = file.name;
|
|
463
480
|
addObjectToScene(points, [], file.name);
|
|
464
481
|
}
|
|
465
|
-
else if (ext ===
|
|
482
|
+
else if (ext === '.vtk' || ext === '.vtp') {
|
|
466
483
|
const loader = new VTKLoader_js.VTKLoader();
|
|
467
484
|
const geo = await new Promise((res, rej) => loader.load(url, res, undefined, rej));
|
|
468
485
|
geo.computeVertexNormals();
|
|
@@ -471,31 +488,36 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
471
488
|
mesh.name = file.name;
|
|
472
489
|
addObjectToScene(mesh, [], file.name);
|
|
473
490
|
}
|
|
474
|
-
else if (ext ===
|
|
491
|
+
else if (ext === '.wrl' || ext === '.vrml') {
|
|
475
492
|
const loader = new VRMLLoader_js.VRMLLoader();
|
|
476
493
|
const scn = await new Promise((res, rej) => loader.load(url, res, undefined, rej));
|
|
477
494
|
const group = new THREE__namespace.Group();
|
|
478
495
|
group.name = file.name;
|
|
479
|
-
while (scn.children.length)
|
|
496
|
+
while (scn.children.length) {
|
|
480
497
|
group.add(scn.children[0]);
|
|
498
|
+
}
|
|
481
499
|
addObjectToScene(group, [], file.name);
|
|
482
500
|
}
|
|
483
|
-
else if (ext ===
|
|
501
|
+
else if (ext === '.gcode') {
|
|
484
502
|
const loader = new GCodeLoader_js.GCodeLoader();
|
|
485
503
|
const obj = await new Promise((res, rej) => loader.load(url, res, undefined, rej));
|
|
486
504
|
addObjectToScene(obj, [], file.name);
|
|
487
505
|
}
|
|
488
|
-
else if (ext ===
|
|
506
|
+
else if (ext === '.svg') {
|
|
489
507
|
const loader = new SVGLoader_js.SVGLoader();
|
|
490
|
-
|
|
491
|
-
|
|
508
|
+
const data = await new Promise((res, rej) =>
|
|
509
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- SVGLoader callback type widens beyond { paths: SVGResultPaths[] }
|
|
510
|
+
loader.load(url, res, undefined, rej));
|
|
492
511
|
const group = new THREE__namespace.Group();
|
|
493
512
|
group.name = file.name;
|
|
494
513
|
for (const path of data.paths) {
|
|
495
514
|
const shapes2 = SVGLoader_js.SVGLoader.createShapes(path);
|
|
496
515
|
for (const shape of shapes2) {
|
|
497
516
|
const geo = new THREE__namespace.ExtrudeGeometry(shape, { depth: 2, bevelEnabled: false });
|
|
498
|
-
const mat = new THREE__namespace.MeshStandardMaterial({
|
|
517
|
+
const mat = new THREE__namespace.MeshStandardMaterial({
|
|
518
|
+
color: ((_f = (_e = path.color) === null || _e === void 0 ? void 0 : _e.getHex) === null || _f === void 0 ? void 0 : _f.call(_e)) || 0x888888,
|
|
519
|
+
side: THREE__namespace.DoubleSide,
|
|
520
|
+
});
|
|
499
521
|
const mesh = new THREE__namespace.Mesh(geo, mat);
|
|
500
522
|
group.add(mesh);
|
|
501
523
|
}
|
|
@@ -508,14 +530,14 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
508
530
|
}
|
|
509
531
|
}
|
|
510
532
|
catch (err) {
|
|
511
|
-
log.error(
|
|
533
|
+
log.error('Load error:', err);
|
|
512
534
|
setStatusText(`Error loading ${file.name}: ${String(err)}`);
|
|
513
535
|
}
|
|
514
536
|
finally {
|
|
515
537
|
URL.revokeObjectURL(url);
|
|
516
538
|
}
|
|
517
539
|
}, [addObjectToScene]);
|
|
518
|
-
const mergeAnimationFBX =
|
|
540
|
+
const mergeAnimationFBX = _e.useCallback(async (file) => {
|
|
519
541
|
var _a;
|
|
520
542
|
const url = URL.createObjectURL(file);
|
|
521
543
|
setStatusText(`Merging animations from ${file.name}...`);
|
|
@@ -529,14 +551,14 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
529
551
|
}
|
|
530
552
|
const root = rootObjectRef.current;
|
|
531
553
|
if (!root) {
|
|
532
|
-
setStatusText(
|
|
554
|
+
setStatusText('No model loaded — load a base model first');
|
|
533
555
|
return;
|
|
534
556
|
}
|
|
535
557
|
if (!mixerRef.current) {
|
|
536
558
|
mixerRef.current = new THREE__namespace.AnimationMixer(root);
|
|
537
559
|
}
|
|
538
560
|
const newAnims = clips.map((c) => ({
|
|
539
|
-
name: c.name || file.name.replace(/\.[^.]+$/,
|
|
561
|
+
name: c.name || file.name.replace(/\.[^.]+$/, ''),
|
|
540
562
|
clip: c,
|
|
541
563
|
duration: c.duration,
|
|
542
564
|
source: file.name,
|
|
@@ -546,7 +568,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
546
568
|
setStatusText(`Merged ${clips.length} animation(s) from ${file.name}`);
|
|
547
569
|
}
|
|
548
570
|
catch (err) {
|
|
549
|
-
log.error(
|
|
571
|
+
log.error('Merge error:', err);
|
|
550
572
|
setStatusText(`Error merging ${file.name}: ${String(err)}`);
|
|
551
573
|
}
|
|
552
574
|
finally {
|
|
@@ -556,10 +578,11 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
556
578
|
/* ─────────────────────────────────────────
|
|
557
579
|
Animation controls
|
|
558
580
|
───────────────────────────────────────── */
|
|
559
|
-
const playClip =
|
|
581
|
+
const playClip = _e.useCallback((index) => {
|
|
560
582
|
const mixer = mixerRef.current;
|
|
561
|
-
if (!mixer || index < 0 || index >= allClipsRef.current.length)
|
|
583
|
+
if (!mixer || index < 0 || index >= allClipsRef.current.length) {
|
|
562
584
|
return;
|
|
585
|
+
}
|
|
563
586
|
if (activeActionRef.current) {
|
|
564
587
|
activeActionRef.current.stop();
|
|
565
588
|
}
|
|
@@ -576,14 +599,15 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
576
599
|
setIsPlaying(true);
|
|
577
600
|
}, []);
|
|
578
601
|
playClipRef.current = playClip;
|
|
579
|
-
const togglePlay =
|
|
602
|
+
const togglePlay = _e.useCallback(() => {
|
|
580
603
|
if (activeAnimIdx < 0 && allClipsRef.current.length > 0) {
|
|
581
604
|
playClip(0);
|
|
582
605
|
return;
|
|
583
606
|
}
|
|
584
607
|
const action = activeActionRef.current;
|
|
585
|
-
if (!action)
|
|
608
|
+
if (!action) {
|
|
586
609
|
return;
|
|
610
|
+
}
|
|
587
611
|
if (isPlayingRef.current) {
|
|
588
612
|
action.paused = true;
|
|
589
613
|
setIsPlaying(false);
|
|
@@ -593,7 +617,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
593
617
|
setIsPlaying(true);
|
|
594
618
|
}
|
|
595
619
|
}, [activeAnimIdx, playClip]);
|
|
596
|
-
const stopAnim =
|
|
620
|
+
const stopAnim = _e.useCallback(() => {
|
|
597
621
|
const action = activeActionRef.current;
|
|
598
622
|
if (action) {
|
|
599
623
|
action.stop();
|
|
@@ -602,11 +626,12 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
602
626
|
setIsPlaying(false);
|
|
603
627
|
setAnimTime(0);
|
|
604
628
|
}, []);
|
|
605
|
-
const seekAnim =
|
|
629
|
+
const seekAnim = _e.useCallback((t) => {
|
|
606
630
|
var _a;
|
|
607
631
|
const action = activeActionRef.current;
|
|
608
|
-
if (!action)
|
|
632
|
+
if (!action) {
|
|
609
633
|
return;
|
|
634
|
+
}
|
|
610
635
|
action.time = t;
|
|
611
636
|
action.paused = true;
|
|
612
637
|
setAnimTime(t);
|
|
@@ -616,7 +641,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
616
641
|
/* ─────────────────────────────────────────
|
|
617
642
|
Camera helpers
|
|
618
643
|
───────────────────────────────────────── */
|
|
619
|
-
const focusOnObject =
|
|
644
|
+
const focusOnObject = _e.useCallback((obj) => {
|
|
620
645
|
const box = new THREE__namespace.Box3().setFromObject(obj);
|
|
621
646
|
const center = box.getCenter(new THREE__namespace.Vector3());
|
|
622
647
|
const size = box.getSize(new THREE__namespace.Vector3());
|
|
@@ -630,33 +655,35 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
630
655
|
}
|
|
631
656
|
}, []);
|
|
632
657
|
focusOnObjectRef.current = focusOnObject;
|
|
633
|
-
const setCameraPreset =
|
|
658
|
+
const setCameraPreset = _e.useCallback((preset) => {
|
|
634
659
|
const orbit = orbitRef.current;
|
|
635
|
-
if (!orbit)
|
|
660
|
+
if (!orbit) {
|
|
636
661
|
return;
|
|
662
|
+
}
|
|
637
663
|
const target = orbit.target.clone();
|
|
638
664
|
const d = cameraRef.current.position.distanceTo(target);
|
|
639
665
|
const pos = new THREE__namespace.Vector3();
|
|
640
666
|
switch (preset) {
|
|
641
|
-
case
|
|
667
|
+
case 'front':
|
|
642
668
|
pos.set(0, 0, d);
|
|
643
669
|
break;
|
|
644
|
-
case
|
|
670
|
+
case 'back':
|
|
645
671
|
pos.set(0, 0, -d);
|
|
646
672
|
break;
|
|
647
|
-
case
|
|
673
|
+
case 'left':
|
|
648
674
|
pos.set(-d, 0, 0);
|
|
649
675
|
break;
|
|
650
|
-
case
|
|
676
|
+
case 'right':
|
|
651
677
|
pos.set(d, 0, 0);
|
|
652
678
|
break;
|
|
653
|
-
case
|
|
679
|
+
case 'top':
|
|
654
680
|
pos.set(0, d, 0.001);
|
|
655
681
|
break;
|
|
656
|
-
case
|
|
682
|
+
case 'bottom':
|
|
657
683
|
pos.set(0, -d, 0.001);
|
|
658
684
|
break;
|
|
659
|
-
default:
|
|
685
|
+
default:
|
|
686
|
+
return;
|
|
660
687
|
}
|
|
661
688
|
cameraRef.current.position.copy(target.clone().add(pos));
|
|
662
689
|
orbit.update();
|
|
@@ -664,13 +691,14 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
664
691
|
/* ─────────────────────────────────────────
|
|
665
692
|
Selection & transform
|
|
666
693
|
───────────────────────────────────────── */
|
|
667
|
-
const selectObject =
|
|
694
|
+
const selectObject = _e.useCallback((node) => {
|
|
668
695
|
var _a;
|
|
669
696
|
setSelectedId((_a = node === null || node === void 0 ? void 0 : node.id) !== null && _a !== void 0 ? _a : null);
|
|
670
697
|
const tc = transformRef.current;
|
|
671
|
-
if (!tc)
|
|
698
|
+
if (!tc) {
|
|
672
699
|
return;
|
|
673
|
-
|
|
700
|
+
}
|
|
701
|
+
if (node && node.type !== 'helper') {
|
|
674
702
|
tc.attach(node.object);
|
|
675
703
|
tc.setMode(transformMode);
|
|
676
704
|
}
|
|
@@ -678,40 +706,42 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
678
706
|
tc.detach();
|
|
679
707
|
}
|
|
680
708
|
}, [transformMode]);
|
|
681
|
-
|
|
709
|
+
_e.useEffect(() => {
|
|
682
710
|
var _a;
|
|
683
711
|
(_a = transformRef.current) === null || _a === void 0 ? void 0 : _a.setMode(transformMode);
|
|
684
712
|
}, [transformMode]);
|
|
685
713
|
/* ─────────────────────────────────────────
|
|
686
714
|
Toggle helpers
|
|
687
715
|
───────────────────────────────────────── */
|
|
688
|
-
|
|
716
|
+
_e.useEffect(() => {
|
|
689
717
|
const scene = sceneRef.current;
|
|
690
|
-
const grid = scene.getObjectByName(
|
|
691
|
-
if (grid)
|
|
718
|
+
const grid = scene.getObjectByName('__grid');
|
|
719
|
+
if (grid) {
|
|
692
720
|
grid.visible = showGrid;
|
|
721
|
+
}
|
|
693
722
|
}, [showGrid]);
|
|
694
|
-
|
|
723
|
+
_e.useEffect(() => {
|
|
695
724
|
const scene = sceneRef.current;
|
|
696
|
-
const axes = scene.getObjectByName(
|
|
697
|
-
if (axes)
|
|
725
|
+
const axes = scene.getObjectByName('__axes');
|
|
726
|
+
if (axes) {
|
|
698
727
|
axes.visible = showAxes;
|
|
728
|
+
}
|
|
699
729
|
}, [showAxes]);
|
|
700
|
-
|
|
730
|
+
_e.useEffect(() => {
|
|
701
731
|
sceneRef.current.background = new THREE__namespace.Color(bgColor);
|
|
702
732
|
}, [bgColor]);
|
|
703
|
-
|
|
733
|
+
_e.useEffect(() => {
|
|
704
734
|
const root = rootObjectRef.current;
|
|
705
|
-
if (!root)
|
|
735
|
+
if (!root) {
|
|
706
736
|
return;
|
|
737
|
+
}
|
|
707
738
|
root.traverse((child) => {
|
|
708
739
|
if (child instanceof THREE__namespace.Mesh) {
|
|
709
|
-
const mats = Array.isArray(child.material)
|
|
710
|
-
? child.material
|
|
711
|
-
: [child.material];
|
|
740
|
+
const mats = Array.isArray(child.material) ? child.material : [child.material];
|
|
712
741
|
mats.forEach((m) => {
|
|
713
|
-
if (
|
|
742
|
+
if ('wireframe' in m) {
|
|
714
743
|
m.wireframe = wireframe;
|
|
744
|
+
}
|
|
715
745
|
});
|
|
716
746
|
}
|
|
717
747
|
});
|
|
@@ -719,51 +749,64 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
719
749
|
/* ─────────────────────────────────────────
|
|
720
750
|
Viewport shading mode
|
|
721
751
|
───────────────────────────────────────── */
|
|
722
|
-
|
|
752
|
+
_e.useEffect(() => {
|
|
723
753
|
const renderer = rendererRef.current;
|
|
724
|
-
if (!renderer)
|
|
754
|
+
if (!renderer) {
|
|
725
755
|
return;
|
|
756
|
+
}
|
|
726
757
|
const scene = sceneRef.current;
|
|
727
758
|
switch (shadingMode) {
|
|
728
|
-
case
|
|
759
|
+
case 'wireframe':
|
|
729
760
|
scene.traverse((obj) => {
|
|
730
|
-
if (obj instanceof THREE__namespace.Mesh && !obj.name.startsWith(
|
|
761
|
+
if (obj instanceof THREE__namespace.Mesh && !obj.name.startsWith('__')) {
|
|
731
762
|
const mats = Array.isArray(obj.material) ? obj.material : [obj.material];
|
|
732
|
-
mats.forEach((m) => {
|
|
733
|
-
|
|
763
|
+
mats.forEach((m) => {
|
|
764
|
+
if ('wireframe' in m) {
|
|
765
|
+
m.wireframe = true;
|
|
766
|
+
}
|
|
767
|
+
});
|
|
734
768
|
}
|
|
735
769
|
});
|
|
736
770
|
renderer.toneMapping = THREE__namespace.NoToneMapping;
|
|
737
771
|
renderer.toneMappingExposure = 1;
|
|
738
772
|
break;
|
|
739
|
-
case
|
|
773
|
+
case 'solid':
|
|
740
774
|
scene.traverse((obj) => {
|
|
741
|
-
if (obj instanceof THREE__namespace.Mesh && !obj.name.startsWith(
|
|
775
|
+
if (obj instanceof THREE__namespace.Mesh && !obj.name.startsWith('__')) {
|
|
742
776
|
const mats = Array.isArray(obj.material) ? obj.material : [obj.material];
|
|
743
|
-
mats.forEach((m) => {
|
|
744
|
-
|
|
777
|
+
mats.forEach((m) => {
|
|
778
|
+
if ('wireframe' in m) {
|
|
779
|
+
m.wireframe = false;
|
|
780
|
+
}
|
|
781
|
+
});
|
|
745
782
|
}
|
|
746
783
|
});
|
|
747
784
|
renderer.toneMapping = THREE__namespace.NoToneMapping;
|
|
748
785
|
renderer.toneMappingExposure = 1;
|
|
749
786
|
break;
|
|
750
|
-
case
|
|
787
|
+
case 'material':
|
|
751
788
|
scene.traverse((obj) => {
|
|
752
|
-
if (obj instanceof THREE__namespace.Mesh && !obj.name.startsWith(
|
|
789
|
+
if (obj instanceof THREE__namespace.Mesh && !obj.name.startsWith('__')) {
|
|
753
790
|
const mats = Array.isArray(obj.material) ? obj.material : [obj.material];
|
|
754
|
-
mats.forEach((m) => {
|
|
755
|
-
|
|
791
|
+
mats.forEach((m) => {
|
|
792
|
+
if ('wireframe' in m) {
|
|
793
|
+
m.wireframe = false;
|
|
794
|
+
}
|
|
795
|
+
});
|
|
756
796
|
}
|
|
757
797
|
});
|
|
758
798
|
renderer.toneMapping = THREE__namespace.ACESFilmicToneMapping;
|
|
759
799
|
renderer.toneMappingExposure = 1.2;
|
|
760
800
|
break;
|
|
761
|
-
case
|
|
801
|
+
case 'rendered':
|
|
762
802
|
scene.traverse((obj) => {
|
|
763
|
-
if (obj instanceof THREE__namespace.Mesh && !obj.name.startsWith(
|
|
803
|
+
if (obj instanceof THREE__namespace.Mesh && !obj.name.startsWith('__')) {
|
|
764
804
|
const mats = Array.isArray(obj.material) ? obj.material : [obj.material];
|
|
765
|
-
mats.forEach((m) => {
|
|
766
|
-
|
|
805
|
+
mats.forEach((m) => {
|
|
806
|
+
if ('wireframe' in m) {
|
|
807
|
+
m.wireframe = false;
|
|
808
|
+
}
|
|
809
|
+
});
|
|
767
810
|
}
|
|
768
811
|
});
|
|
769
812
|
renderer.toneMapping = THREE__namespace.ACESFilmicToneMapping;
|
|
@@ -772,15 +815,16 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
772
815
|
}
|
|
773
816
|
}, [shadingMode]);
|
|
774
817
|
/* Gizmo space */
|
|
775
|
-
|
|
818
|
+
_e.useEffect(() => {
|
|
776
819
|
var _a;
|
|
777
820
|
(_a = transformRef.current) === null || _a === void 0 ? void 0 : _a.setSpace(gizmoSpace);
|
|
778
821
|
}, [gizmoSpace]);
|
|
779
822
|
/* Snap settings */
|
|
780
|
-
|
|
823
|
+
_e.useEffect(() => {
|
|
781
824
|
const tc = transformRef.current;
|
|
782
|
-
if (!tc)
|
|
825
|
+
if (!tc) {
|
|
783
826
|
return;
|
|
827
|
+
}
|
|
784
828
|
if (snapEnabled) {
|
|
785
829
|
tc.setTranslationSnap(snapGrid);
|
|
786
830
|
tc.setRotationSnap(THREE__namespace.MathUtils.degToRad(15));
|
|
@@ -793,28 +837,30 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
793
837
|
}
|
|
794
838
|
}, [snapEnabled, snapGrid]);
|
|
795
839
|
/* Skeleton helper */
|
|
796
|
-
|
|
840
|
+
_e.useEffect(() => {
|
|
797
841
|
const scene = sceneRef.current;
|
|
798
842
|
if (skeletonHelperRef.current) {
|
|
799
843
|
scene.remove(skeletonHelperRef.current);
|
|
800
844
|
skeletonHelperRef.current = null;
|
|
801
845
|
}
|
|
802
|
-
if (!showSkeleton || !rootObjectRef.current)
|
|
846
|
+
if (!showSkeleton || !rootObjectRef.current) {
|
|
803
847
|
return;
|
|
848
|
+
}
|
|
804
849
|
let hasRig = false;
|
|
805
850
|
rootObjectRef.current.traverse((obj) => {
|
|
806
|
-
if (obj instanceof THREE__namespace.SkinnedMesh)
|
|
851
|
+
if (obj instanceof THREE__namespace.SkinnedMesh) {
|
|
807
852
|
hasRig = true;
|
|
853
|
+
}
|
|
808
854
|
});
|
|
809
855
|
if (hasRig) {
|
|
810
856
|
const helper = new THREE__namespace.SkeletonHelper(rootObjectRef.current);
|
|
811
|
-
helper.name =
|
|
857
|
+
helper.name = '__skeletonHelper';
|
|
812
858
|
scene.add(helper);
|
|
813
859
|
skeletonHelperRef.current = helper;
|
|
814
860
|
}
|
|
815
861
|
}, [showSkeleton, sceneTree]);
|
|
816
862
|
/* Fog */
|
|
817
|
-
|
|
863
|
+
_e.useEffect(() => {
|
|
818
864
|
if (fogEnabled) {
|
|
819
865
|
sceneRef.current.fog = new THREE__namespace.Fog(fogColor, fogNear, fogFar);
|
|
820
866
|
}
|
|
@@ -823,9 +869,9 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
823
869
|
}
|
|
824
870
|
}, [fogEnabled, fogColor, fogNear, fogFar]);
|
|
825
871
|
/* Light helpers visibility */
|
|
826
|
-
|
|
872
|
+
_e.useEffect(() => {
|
|
827
873
|
sceneRef.current.traverse((obj) => {
|
|
828
|
-
if (obj.name.startsWith(
|
|
874
|
+
if (obj.name.startsWith('__helper_')) {
|
|
829
875
|
obj.visible = showLightHelpers;
|
|
830
876
|
}
|
|
831
877
|
});
|
|
@@ -833,22 +879,22 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
833
879
|
/* ─────────────────────────────────────────
|
|
834
880
|
Drag-and-drop
|
|
835
881
|
───────────────────────────────────────── */
|
|
836
|
-
const handleDragOver =
|
|
882
|
+
const handleDragOver = _e.useCallback((e) => {
|
|
837
883
|
e.preventDefault();
|
|
838
884
|
e.stopPropagation();
|
|
839
885
|
setDragOver(true);
|
|
840
886
|
}, []);
|
|
841
|
-
const handleDragLeave =
|
|
887
|
+
const handleDragLeave = _e.useCallback((e) => {
|
|
842
888
|
e.preventDefault();
|
|
843
889
|
setDragOver(false);
|
|
844
890
|
}, []);
|
|
845
|
-
const handleDrop =
|
|
891
|
+
const handleDrop = _e.useCallback((e) => {
|
|
846
892
|
e.preventDefault();
|
|
847
893
|
e.stopPropagation();
|
|
848
894
|
setDragOver(false);
|
|
849
895
|
const files = Array.from(e.dataTransfer.files);
|
|
850
896
|
files.forEach((f) => {
|
|
851
|
-
const ext =
|
|
897
|
+
const ext = '.' + f.name.toLowerCase().split('.').pop();
|
|
852
898
|
if (modelEditorTypes.SUPPORTED_EXTENSIONS.includes(ext)) {
|
|
853
899
|
loadFile(f);
|
|
854
900
|
}
|
|
@@ -857,27 +903,30 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
857
903
|
/* ─────────────────────────────────────────
|
|
858
904
|
Helpers
|
|
859
905
|
───────────────────────────────────────── */
|
|
860
|
-
const selectedNode =
|
|
861
|
-
const selectedMaterials =
|
|
862
|
-
if (!selectedNode || !(selectedNode.object instanceof THREE__namespace.Mesh))
|
|
906
|
+
const selectedNode = _e.useMemo(() => (selectedId ? findNodeById(sceneTree, selectedId) : null), [selectedId, sceneTree]);
|
|
907
|
+
const selectedMaterials = _e.useMemo(() => {
|
|
908
|
+
if (!selectedNode || !(selectedNode.object instanceof THREE__namespace.Mesh)) {
|
|
863
909
|
return [];
|
|
910
|
+
}
|
|
864
911
|
const m = selectedNode.object.material;
|
|
865
912
|
return Array.isArray(m) ? m : [m];
|
|
866
913
|
}, [selectedNode, matRefresh]);
|
|
867
|
-
const deleteSelected =
|
|
914
|
+
const deleteSelected = _e.useCallback(() => {
|
|
868
915
|
var _a;
|
|
869
|
-
if (!selectedNode)
|
|
916
|
+
if (!selectedNode) {
|
|
870
917
|
return;
|
|
918
|
+
}
|
|
871
919
|
selectedNode.object.removeFromParent();
|
|
872
920
|
(_a = transformRef.current) === null || _a === void 0 ? void 0 : _a.detach();
|
|
873
921
|
setSelectedId(null);
|
|
874
922
|
rebuildTree();
|
|
875
923
|
}, [selectedNode, rebuildTree]);
|
|
876
|
-
const duplicateSelected =
|
|
877
|
-
if (!selectedNode)
|
|
924
|
+
const duplicateSelected = _e.useCallback(() => {
|
|
925
|
+
if (!selectedNode) {
|
|
878
926
|
return;
|
|
927
|
+
}
|
|
879
928
|
const clone = selectedNode.object.clone(true);
|
|
880
|
-
clone.name = selectedNode.object.name +
|
|
929
|
+
clone.name = selectedNode.object.name + '.001';
|
|
881
930
|
clone.position.x += 1;
|
|
882
931
|
sceneRef.current.add(clone);
|
|
883
932
|
rebuildTree();
|
|
@@ -886,32 +935,35 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
886
935
|
/* ─────────────────────────────────────────
|
|
887
936
|
Keyboard shortcuts
|
|
888
937
|
───────────────────────────────────────── */
|
|
889
|
-
|
|
938
|
+
_e.useEffect(() => {
|
|
890
939
|
const handleKey = (e) => {
|
|
891
940
|
var _a, _b, _c;
|
|
892
941
|
const tag = (_a = e.target) === null || _a === void 0 ? void 0 : _a.tagName;
|
|
893
|
-
if (tag ===
|
|
942
|
+
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') {
|
|
894
943
|
return;
|
|
944
|
+
}
|
|
895
945
|
switch (e.key.toLowerCase()) {
|
|
896
|
-
case
|
|
897
|
-
setTransformMode(
|
|
946
|
+
case 'g':
|
|
947
|
+
setTransformMode('translate');
|
|
898
948
|
break;
|
|
899
|
-
case
|
|
900
|
-
setTransformMode(
|
|
949
|
+
case 'r':
|
|
950
|
+
setTransformMode('rotate');
|
|
901
951
|
break;
|
|
902
|
-
case
|
|
903
|
-
if (!e.ctrlKey)
|
|
904
|
-
setTransformMode(
|
|
952
|
+
case 's':
|
|
953
|
+
if (!e.ctrlKey) {
|
|
954
|
+
setTransformMode('scale');
|
|
955
|
+
}
|
|
905
956
|
break;
|
|
906
|
-
case
|
|
957
|
+
case ' ':
|
|
907
958
|
e.preventDefault();
|
|
908
959
|
togglePlay();
|
|
909
960
|
break;
|
|
910
|
-
case
|
|
911
|
-
if (rootObjectRef.current)
|
|
961
|
+
case 'f':
|
|
962
|
+
if (rootObjectRef.current) {
|
|
912
963
|
focusOnObject(rootObjectRef.current);
|
|
964
|
+
}
|
|
913
965
|
break;
|
|
914
|
-
case
|
|
966
|
+
case 'delete':
|
|
915
967
|
if (selectedId) {
|
|
916
968
|
const sel = findNodeById(sceneTree, selectedId);
|
|
917
969
|
if (sel) {
|
|
@@ -922,7 +974,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
922
974
|
}
|
|
923
975
|
}
|
|
924
976
|
break;
|
|
925
|
-
case
|
|
977
|
+
case 'x':
|
|
926
978
|
if (e.ctrlKey && selectedId) {
|
|
927
979
|
const sel = findNodeById(sceneTree, selectedId);
|
|
928
980
|
if (sel) {
|
|
@@ -933,55 +985,62 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
933
985
|
}
|
|
934
986
|
}
|
|
935
987
|
break;
|
|
936
|
-
case
|
|
988
|
+
case 'd':
|
|
937
989
|
if (e.shiftKey) {
|
|
938
990
|
e.preventDefault();
|
|
939
991
|
duplicateSelected();
|
|
940
992
|
}
|
|
941
993
|
break;
|
|
942
|
-
case
|
|
994
|
+
case 'tab':
|
|
943
995
|
e.preventDefault();
|
|
944
|
-
setEditorMode((m) => m ===
|
|
996
|
+
setEditorMode((m) => (m === 'object' ? 'edit' : m === 'edit' ? 'pose' : 'object'));
|
|
945
997
|
break;
|
|
946
|
-
case
|
|
998
|
+
case 'n':
|
|
947
999
|
if (!e.ctrlKey) ;
|
|
948
1000
|
break;
|
|
949
|
-
case
|
|
950
|
-
if (e.code.startsWith(
|
|
951
|
-
setCameraPreset(
|
|
1001
|
+
case '1':
|
|
1002
|
+
if (e.code.startsWith('Numpad')) {
|
|
1003
|
+
setCameraPreset('front');
|
|
1004
|
+
}
|
|
952
1005
|
break;
|
|
953
|
-
case
|
|
954
|
-
if (e.code.startsWith(
|
|
955
|
-
setCameraPreset(
|
|
1006
|
+
case '3':
|
|
1007
|
+
if (e.code.startsWith('Numpad')) {
|
|
1008
|
+
setCameraPreset('right');
|
|
1009
|
+
}
|
|
956
1010
|
break;
|
|
957
|
-
case
|
|
958
|
-
if (e.code.startsWith(
|
|
959
|
-
setCameraPreset(
|
|
1011
|
+
case '7':
|
|
1012
|
+
if (e.code.startsWith('Numpad')) {
|
|
1013
|
+
setCameraPreset('top');
|
|
1014
|
+
}
|
|
960
1015
|
break;
|
|
961
|
-
case
|
|
962
|
-
if (e.code.startsWith(
|
|
963
|
-
setCameraPreset(
|
|
1016
|
+
case '4':
|
|
1017
|
+
if (e.code.startsWith('Numpad')) {
|
|
1018
|
+
setCameraPreset('left');
|
|
1019
|
+
}
|
|
964
1020
|
break;
|
|
965
|
-
case
|
|
966
|
-
if (e.code.startsWith(
|
|
967
|
-
setCameraPreset(
|
|
1021
|
+
case '6':
|
|
1022
|
+
if (e.code.startsWith('Numpad')) {
|
|
1023
|
+
setCameraPreset('right');
|
|
1024
|
+
}
|
|
968
1025
|
break;
|
|
969
|
-
case
|
|
970
|
-
if (e.code.startsWith(
|
|
971
|
-
setCameraPreset(
|
|
1026
|
+
case '9':
|
|
1027
|
+
if (e.code.startsWith('Numpad')) {
|
|
1028
|
+
setCameraPreset('back');
|
|
1029
|
+
}
|
|
972
1030
|
break;
|
|
973
|
-
case
|
|
974
|
-
if (e.code.startsWith(
|
|
975
|
-
setCameraPreset(
|
|
1031
|
+
case '2':
|
|
1032
|
+
if (e.code.startsWith('Numpad')) {
|
|
1033
|
+
setCameraPreset('bottom');
|
|
1034
|
+
}
|
|
976
1035
|
break;
|
|
977
1036
|
}
|
|
978
|
-
if (e.key ===
|
|
1037
|
+
if (e.key === 'Escape') {
|
|
979
1038
|
setAddMenuOpen(null);
|
|
980
1039
|
setContextMenu(null);
|
|
981
1040
|
}
|
|
982
1041
|
};
|
|
983
|
-
window.addEventListener(
|
|
984
|
-
return () => window.removeEventListener(
|
|
1042
|
+
window.addEventListener('keydown', handleKey);
|
|
1043
|
+
return () => window.removeEventListener('keydown', handleKey);
|
|
985
1044
|
}, [
|
|
986
1045
|
togglePlay,
|
|
987
1046
|
focusOnObject,
|
|
@@ -994,92 +1053,97 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
994
1053
|
/* ─────────────────────────────────────────
|
|
995
1054
|
File input handlers
|
|
996
1055
|
───────────────────────────────────────── */
|
|
997
|
-
const handleFileInput =
|
|
1056
|
+
const handleFileInput = _e.useCallback((e) => {
|
|
998
1057
|
const files = e.target.files;
|
|
999
|
-
if (!files)
|
|
1058
|
+
if (!files) {
|
|
1000
1059
|
return;
|
|
1060
|
+
}
|
|
1001
1061
|
Array.from(files).forEach(loadFile);
|
|
1002
|
-
e.target.value =
|
|
1062
|
+
e.target.value = '';
|
|
1003
1063
|
}, [loadFile]);
|
|
1004
|
-
const handleMergeInput =
|
|
1064
|
+
const handleMergeInput = _e.useCallback((e) => {
|
|
1005
1065
|
const files = e.target.files;
|
|
1006
|
-
if (!files)
|
|
1066
|
+
if (!files) {
|
|
1007
1067
|
return;
|
|
1068
|
+
}
|
|
1008
1069
|
Array.from(files).forEach(mergeAnimationFBX);
|
|
1009
|
-
e.target.value =
|
|
1070
|
+
e.target.value = '';
|
|
1010
1071
|
}, [mergeAnimationFBX]);
|
|
1011
1072
|
/* ─────────────────────────────────────────
|
|
1012
1073
|
Export functions
|
|
1013
1074
|
───────────────────────────────────────── */
|
|
1014
|
-
const exportGLTF =
|
|
1075
|
+
const exportGLTF = _e.useCallback((binary) => {
|
|
1015
1076
|
const root = rootObjectRef.current;
|
|
1016
1077
|
if (!root) {
|
|
1017
|
-
setStatusText(
|
|
1078
|
+
setStatusText('Nothing to export');
|
|
1018
1079
|
return;
|
|
1019
1080
|
}
|
|
1020
1081
|
const exporter = new GLTFExporter_js.GLTFExporter();
|
|
1021
|
-
const options = {
|
|
1082
|
+
const options = {
|
|
1083
|
+
binary,
|
|
1084
|
+
animations: allClipsRef.current.map((a) => a.clip),
|
|
1085
|
+
};
|
|
1022
1086
|
exporter.parse(root, (result) => {
|
|
1023
1087
|
let blob;
|
|
1024
1088
|
if (binary) {
|
|
1025
|
-
blob = new Blob([result], { type:
|
|
1089
|
+
blob = new Blob([result], { type: 'application/octet-stream' });
|
|
1026
1090
|
}
|
|
1027
1091
|
else {
|
|
1028
|
-
blob = new Blob([JSON.stringify(result, null, 2)], { type:
|
|
1092
|
+
blob = new Blob([JSON.stringify(result, null, 2)], { type: 'application/json' });
|
|
1029
1093
|
}
|
|
1030
|
-
const a = document.createElement(
|
|
1094
|
+
const a = document.createElement('a');
|
|
1031
1095
|
a.href = URL.createObjectURL(blob);
|
|
1032
|
-
a.download = `export.${binary ?
|
|
1096
|
+
a.download = `export.${binary ? 'glb' : 'gltf'}`;
|
|
1033
1097
|
a.click();
|
|
1034
1098
|
URL.revokeObjectURL(a.href);
|
|
1035
|
-
setStatusText(`Exported ${binary ?
|
|
1099
|
+
setStatusText(`Exported ${binary ? 'GLB' : 'GLTF'} successfully`);
|
|
1036
1100
|
}, (err) => {
|
|
1037
|
-
log.error(
|
|
1101
|
+
log.error('Export error:', err);
|
|
1038
1102
|
setStatusText(`Export error: ${String(err)}`);
|
|
1039
1103
|
}, options);
|
|
1040
1104
|
}, []);
|
|
1041
|
-
const exportOBJ =
|
|
1105
|
+
const exportOBJ = _e.useCallback(() => {
|
|
1042
1106
|
const root = rootObjectRef.current;
|
|
1043
1107
|
if (!root) {
|
|
1044
|
-
setStatusText(
|
|
1108
|
+
setStatusText('Nothing to export');
|
|
1045
1109
|
return;
|
|
1046
1110
|
}
|
|
1047
1111
|
const exporter = new OBJExporter_js.OBJExporter();
|
|
1048
1112
|
const result = exporter.parse(root);
|
|
1049
|
-
const blob = new Blob([result], { type:
|
|
1050
|
-
const a = document.createElement(
|
|
1113
|
+
const blob = new Blob([result], { type: 'text/plain' });
|
|
1114
|
+
const a = document.createElement('a');
|
|
1051
1115
|
a.href = URL.createObjectURL(blob);
|
|
1052
|
-
a.download =
|
|
1116
|
+
a.download = 'export.obj';
|
|
1053
1117
|
a.click();
|
|
1054
1118
|
URL.revokeObjectURL(a.href);
|
|
1055
|
-
setStatusText(
|
|
1119
|
+
setStatusText('Exported OBJ successfully');
|
|
1056
1120
|
}, []);
|
|
1057
|
-
const exportSTL =
|
|
1121
|
+
const exportSTL = _e.useCallback((binary) => {
|
|
1058
1122
|
const root = rootObjectRef.current;
|
|
1059
1123
|
if (!root) {
|
|
1060
|
-
setStatusText(
|
|
1124
|
+
setStatusText('Nothing to export');
|
|
1061
1125
|
return;
|
|
1062
1126
|
}
|
|
1063
1127
|
const exporter = new STLExporter_js.STLExporter();
|
|
1064
1128
|
const result = exporter.parse(root, { binary });
|
|
1065
1129
|
let blob;
|
|
1066
1130
|
if (binary) {
|
|
1067
|
-
blob = new Blob([result], { type:
|
|
1131
|
+
blob = new Blob([result], { type: 'application/octet-stream' });
|
|
1068
1132
|
}
|
|
1069
1133
|
else {
|
|
1070
|
-
blob = new Blob([result], { type:
|
|
1134
|
+
blob = new Blob([result], { type: 'text/plain' });
|
|
1071
1135
|
}
|
|
1072
|
-
const a = document.createElement(
|
|
1136
|
+
const a = document.createElement('a');
|
|
1073
1137
|
a.href = URL.createObjectURL(blob);
|
|
1074
|
-
a.download =
|
|
1138
|
+
a.download = 'export.stl';
|
|
1075
1139
|
a.click();
|
|
1076
1140
|
URL.revokeObjectURL(a.href);
|
|
1077
|
-
setStatusText(`Exported STL${binary ?
|
|
1141
|
+
setStatusText(`Exported STL${binary ? ' (binary)' : ''} successfully`);
|
|
1078
1142
|
}, []);
|
|
1079
|
-
const exportPLY =
|
|
1143
|
+
const exportPLY = _e.useCallback((binary) => {
|
|
1080
1144
|
const root = rootObjectRef.current;
|
|
1081
1145
|
if (!root) {
|
|
1082
|
-
setStatusText(
|
|
1146
|
+
setStatusText('Nothing to export');
|
|
1083
1147
|
return;
|
|
1084
1148
|
}
|
|
1085
1149
|
const exporter = new PLYExporter_js.PLYExporter();
|
|
@@ -1087,48 +1151,48 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1087
1151
|
exporter.parse(root, (result) => {
|
|
1088
1152
|
let blob;
|
|
1089
1153
|
if (binary) {
|
|
1090
|
-
blob = new Blob([result], { type:
|
|
1154
|
+
blob = new Blob([result], { type: 'application/octet-stream' });
|
|
1091
1155
|
}
|
|
1092
1156
|
else {
|
|
1093
|
-
blob = new Blob([result], { type:
|
|
1157
|
+
blob = new Blob([result], { type: 'text/plain' });
|
|
1094
1158
|
}
|
|
1095
|
-
const a = document.createElement(
|
|
1159
|
+
const a = document.createElement('a');
|
|
1096
1160
|
a.href = URL.createObjectURL(blob);
|
|
1097
|
-
a.download =
|
|
1161
|
+
a.download = 'export.ply';
|
|
1098
1162
|
a.click();
|
|
1099
1163
|
URL.revokeObjectURL(a.href);
|
|
1100
|
-
setStatusText(`Exported PLY${binary ?
|
|
1164
|
+
setStatusText(`Exported PLY${binary ? ' (binary)' : ''} successfully`);
|
|
1101
1165
|
}, { binary });
|
|
1102
1166
|
}, []);
|
|
1103
|
-
const exportUSDZ =
|
|
1167
|
+
const exportUSDZ = _e.useCallback(async () => {
|
|
1104
1168
|
const root = rootObjectRef.current;
|
|
1105
1169
|
if (!root) {
|
|
1106
|
-
setStatusText(
|
|
1170
|
+
setStatusText('Nothing to export');
|
|
1107
1171
|
return;
|
|
1108
1172
|
}
|
|
1109
1173
|
try {
|
|
1110
1174
|
const exporter = new USDZExporter_js.USDZExporter();
|
|
1111
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1112
1175
|
const result = await new Promise((resolve) => {
|
|
1176
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- USDZExporter.parse callback signature varies between three.js minor versions
|
|
1113
1177
|
exporter.parse(root, resolve);
|
|
1114
1178
|
});
|
|
1115
|
-
const blob = new Blob([result.buffer], { type:
|
|
1116
|
-
const a = document.createElement(
|
|
1179
|
+
const blob = new Blob([result.buffer], { type: 'application/octet-stream' });
|
|
1180
|
+
const a = document.createElement('a');
|
|
1117
1181
|
a.href = URL.createObjectURL(blob);
|
|
1118
|
-
a.download =
|
|
1182
|
+
a.download = 'export.usdz';
|
|
1119
1183
|
a.click();
|
|
1120
1184
|
URL.revokeObjectURL(a.href);
|
|
1121
|
-
setStatusText(
|
|
1185
|
+
setStatusText('Exported USDZ successfully');
|
|
1122
1186
|
}
|
|
1123
1187
|
catch (err) {
|
|
1124
|
-
log.error(
|
|
1188
|
+
log.error('USDZ export error:', err);
|
|
1125
1189
|
setStatusText(`USDZ export error: ${String(err)}`);
|
|
1126
1190
|
}
|
|
1127
1191
|
}, []);
|
|
1128
1192
|
/* ─────────────────────────────────────────
|
|
1129
1193
|
Dispose & clear
|
|
1130
1194
|
───────────────────────────────────────── */
|
|
1131
|
-
const disposeObject =
|
|
1195
|
+
const disposeObject = _e.useCallback((obj) => {
|
|
1132
1196
|
obj.traverse((child) => {
|
|
1133
1197
|
if (child.geometry) {
|
|
1134
1198
|
child.geometry.dispose();
|
|
@@ -1139,23 +1203,38 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1139
1203
|
: [child.material];
|
|
1140
1204
|
mats.forEach((mat) => {
|
|
1141
1205
|
const m = mat;
|
|
1142
|
-
[
|
|
1143
|
-
|
|
1206
|
+
[
|
|
1207
|
+
'map',
|
|
1208
|
+
'normalMap',
|
|
1209
|
+
'roughnessMap',
|
|
1210
|
+
'metalnessMap',
|
|
1211
|
+
'emissiveMap',
|
|
1212
|
+
'aoMap',
|
|
1213
|
+
'alphaMap',
|
|
1214
|
+
'envMap',
|
|
1215
|
+
'lightMap',
|
|
1216
|
+
'bumpMap',
|
|
1217
|
+
'displacementMap',
|
|
1218
|
+
'specularMap',
|
|
1219
|
+
].forEach((prop) => {
|
|
1220
|
+
if (m[prop] && typeof m[prop].dispose === 'function') {
|
|
1144
1221
|
m[prop].dispose();
|
|
1222
|
+
}
|
|
1145
1223
|
});
|
|
1146
1224
|
mat.dispose();
|
|
1147
1225
|
});
|
|
1148
1226
|
}
|
|
1149
1227
|
});
|
|
1150
1228
|
}, []);
|
|
1151
|
-
const clearScene =
|
|
1229
|
+
const clearScene = _e.useCallback(() => {
|
|
1152
1230
|
var _a;
|
|
1153
|
-
if (!window.confirm(
|
|
1231
|
+
if (!window.confirm('Clear the entire scene? This cannot be undone.')) {
|
|
1154
1232
|
return;
|
|
1233
|
+
}
|
|
1155
1234
|
const scene = sceneRef.current;
|
|
1156
1235
|
const toRemove = [];
|
|
1157
1236
|
scene.traverse((obj) => {
|
|
1158
|
-
if (!obj.name.startsWith(
|
|
1237
|
+
if (!obj.name.startsWith('__') && obj !== scene && obj.parent === scene) {
|
|
1159
1238
|
toRemove.push(obj);
|
|
1160
1239
|
}
|
|
1161
1240
|
});
|
|
@@ -1181,89 +1260,96 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1181
1260
|
setAnimDuration(0);
|
|
1182
1261
|
setSelectedId(null);
|
|
1183
1262
|
rebuildTree();
|
|
1184
|
-
setStatusText(
|
|
1263
|
+
setStatusText('Scene cleared');
|
|
1185
1264
|
}, [rebuildTree, disposeObject]);
|
|
1186
1265
|
/* ─────────────────────────────────────────
|
|
1187
1266
|
Save to Library
|
|
1188
1267
|
───────────────────────────────────────── */
|
|
1189
|
-
const saveToLibrary =
|
|
1190
|
-
if (!onSaveToLibrary)
|
|
1268
|
+
const saveToLibrary = _e.useCallback(() => {
|
|
1269
|
+
if (!onSaveToLibrary) {
|
|
1191
1270
|
return;
|
|
1271
|
+
}
|
|
1192
1272
|
const root = rootObjectRef.current;
|
|
1193
1273
|
if (!root) {
|
|
1194
|
-
setStatusText(
|
|
1274
|
+
setStatusText('Nothing to save');
|
|
1195
1275
|
return;
|
|
1196
1276
|
}
|
|
1197
1277
|
const exporter = new GLTFExporter_js.GLTFExporter();
|
|
1198
1278
|
exporter.parse(root, (result) => {
|
|
1199
|
-
const blob = new Blob([result], { type:
|
|
1279
|
+
const blob = new Blob([result], { type: 'model/gltf-binary' });
|
|
1200
1280
|
const reader = new FileReader();
|
|
1201
1281
|
reader.onload = () => {
|
|
1202
|
-
const defaultName =
|
|
1203
|
-
const name = window.prompt(
|
|
1204
|
-
if (!name || !name.trim())
|
|
1282
|
+
const defaultName = 'model.glb';
|
|
1283
|
+
const name = window.prompt('Save to library as:', defaultName);
|
|
1284
|
+
if (!name || !name.trim()) {
|
|
1205
1285
|
return;
|
|
1206
|
-
|
|
1286
|
+
}
|
|
1287
|
+
onSaveToLibrary(reader.result, name.trim(), 'model/gltf-binary');
|
|
1207
1288
|
};
|
|
1208
1289
|
reader.readAsDataURL(blob);
|
|
1209
1290
|
}, (err) => {
|
|
1210
|
-
log.error(
|
|
1291
|
+
log.error('Save to library error:', err);
|
|
1211
1292
|
setStatusText(`Save error: ${String(err)}`);
|
|
1212
1293
|
}, { binary: true, animations: allClipsRef.current.map((a) => a.clip) });
|
|
1213
1294
|
}, [onSaveToLibrary]);
|
|
1214
1295
|
/* ─────────────────────────────────────────
|
|
1215
1296
|
Add Mesh / Light / Camera / Empty
|
|
1216
1297
|
───────────────────────────────────────── */
|
|
1217
|
-
const addPrimitive =
|
|
1298
|
+
const addPrimitive = _e.useCallback((type) => {
|
|
1218
1299
|
let geo;
|
|
1219
1300
|
switch (type) {
|
|
1220
|
-
case
|
|
1301
|
+
case 'cube':
|
|
1221
1302
|
geo = new THREE__namespace.BoxGeometry(1, 1, 1);
|
|
1222
1303
|
break;
|
|
1223
|
-
case
|
|
1304
|
+
case 'sphere':
|
|
1224
1305
|
geo = new THREE__namespace.SphereGeometry(0.5, 32, 32);
|
|
1225
1306
|
break;
|
|
1226
|
-
case
|
|
1307
|
+
case 'cylinder':
|
|
1227
1308
|
geo = new THREE__namespace.CylinderGeometry(0.5, 0.5, 1, 32);
|
|
1228
1309
|
break;
|
|
1229
|
-
case
|
|
1310
|
+
case 'cone':
|
|
1230
1311
|
geo = new THREE__namespace.ConeGeometry(0.5, 1, 32);
|
|
1231
1312
|
break;
|
|
1232
|
-
case
|
|
1313
|
+
case 'torus':
|
|
1233
1314
|
geo = new THREE__namespace.TorusGeometry(0.5, 0.2, 16, 32);
|
|
1234
1315
|
break;
|
|
1235
|
-
case
|
|
1316
|
+
case 'plane':
|
|
1236
1317
|
geo = new THREE__namespace.PlaneGeometry(2, 2);
|
|
1237
1318
|
break;
|
|
1238
|
-
case
|
|
1319
|
+
case 'circle':
|
|
1239
1320
|
geo = new THREE__namespace.CircleGeometry(0.5, 32);
|
|
1240
1321
|
break;
|
|
1241
|
-
case
|
|
1322
|
+
case 'ring':
|
|
1242
1323
|
geo = new THREE__namespace.RingGeometry(0.3, 0.5, 32);
|
|
1243
1324
|
break;
|
|
1244
|
-
case
|
|
1325
|
+
case 'dodecahedron':
|
|
1245
1326
|
geo = new THREE__namespace.DodecahedronGeometry(0.5);
|
|
1246
1327
|
break;
|
|
1247
|
-
case
|
|
1328
|
+
case 'icosahedron':
|
|
1248
1329
|
geo = new THREE__namespace.IcosahedronGeometry(0.5);
|
|
1249
1330
|
break;
|
|
1250
|
-
case
|
|
1331
|
+
case 'octahedron':
|
|
1251
1332
|
geo = new THREE__namespace.OctahedronGeometry(0.5);
|
|
1252
1333
|
break;
|
|
1253
|
-
case
|
|
1334
|
+
case 'tetrahedron':
|
|
1254
1335
|
geo = new THREE__namespace.TetrahedronGeometry(0.5);
|
|
1255
1336
|
break;
|
|
1256
|
-
case
|
|
1337
|
+
case 'torusKnot':
|
|
1257
1338
|
geo = new THREE__namespace.TorusKnotGeometry(0.4, 0.15, 64, 16);
|
|
1258
1339
|
break;
|
|
1259
|
-
default:
|
|
1340
|
+
default:
|
|
1341
|
+
return;
|
|
1260
1342
|
}
|
|
1261
|
-
const mat = new THREE__namespace.MeshStandardMaterial({
|
|
1343
|
+
const mat = new THREE__namespace.MeshStandardMaterial({
|
|
1344
|
+
color: 0x888888,
|
|
1345
|
+
metalness: 0.1,
|
|
1346
|
+
roughness: 0.7,
|
|
1347
|
+
});
|
|
1262
1348
|
const mesh = new THREE__namespace.Mesh(geo, mat);
|
|
1263
1349
|
mesh.name = type.charAt(0).toUpperCase() + type.slice(1);
|
|
1264
1350
|
mesh.castShadow = true;
|
|
1265
1351
|
mesh.receiveShadow = true;
|
|
1266
|
-
if (type ===
|
|
1352
|
+
if (type === 'plane' || type === 'circle') {
|
|
1267
1353
|
mesh.rotation.x = -Math.PI / 2;
|
|
1268
1354
|
mesh.position.y = 0.001;
|
|
1269
1355
|
}
|
|
@@ -1275,37 +1361,38 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1275
1361
|
setStatusText(`Added ${mesh.name}`);
|
|
1276
1362
|
setAddMenuOpen(null);
|
|
1277
1363
|
}, [rebuildTree]);
|
|
1278
|
-
const addSceneLight =
|
|
1364
|
+
const addSceneLight = _e.useCallback((type) => {
|
|
1279
1365
|
let light;
|
|
1280
1366
|
switch (type) {
|
|
1281
|
-
case
|
|
1367
|
+
case 'point': {
|
|
1282
1368
|
const l = new THREE__namespace.PointLight(0xffffff, 1, 20);
|
|
1283
1369
|
l.position.set(0, 3, 0);
|
|
1284
1370
|
l.castShadow = true;
|
|
1285
1371
|
light = l;
|
|
1286
1372
|
break;
|
|
1287
1373
|
}
|
|
1288
|
-
case
|
|
1374
|
+
case 'spot': {
|
|
1289
1375
|
const l = new THREE__namespace.SpotLight(0xffffff, 1, 20, Math.PI / 6, 0.3);
|
|
1290
1376
|
l.position.set(0, 4, 2);
|
|
1291
1377
|
l.castShadow = true;
|
|
1292
1378
|
light = l;
|
|
1293
1379
|
break;
|
|
1294
1380
|
}
|
|
1295
|
-
case
|
|
1381
|
+
case 'directional': {
|
|
1296
1382
|
const l = new THREE__namespace.DirectionalLight(0xffffff, 1);
|
|
1297
1383
|
l.position.set(3, 5, 3);
|
|
1298
1384
|
l.castShadow = true;
|
|
1299
1385
|
light = l;
|
|
1300
1386
|
break;
|
|
1301
1387
|
}
|
|
1302
|
-
case
|
|
1388
|
+
case 'hemisphere': {
|
|
1303
1389
|
light = new THREE__namespace.HemisphereLight(0x8888ff, 0x442200, 0.5);
|
|
1304
1390
|
break;
|
|
1305
1391
|
}
|
|
1306
|
-
default:
|
|
1392
|
+
default:
|
|
1393
|
+
return;
|
|
1307
1394
|
}
|
|
1308
|
-
light.name = type.charAt(0).toUpperCase() + type.slice(1) +
|
|
1395
|
+
light.name = type.charAt(0).toUpperCase() + type.slice(1) + 'Light';
|
|
1309
1396
|
sceneRef.current.add(light);
|
|
1310
1397
|
let helper = null;
|
|
1311
1398
|
if (light instanceof THREE__namespace.PointLight) {
|
|
@@ -1328,26 +1415,27 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1328
1415
|
setStatusText(`Added ${light.name}`);
|
|
1329
1416
|
setAddMenuOpen(null);
|
|
1330
1417
|
}, [rebuildTree]);
|
|
1331
|
-
const addCameraObject =
|
|
1418
|
+
const addCameraObject = _e.useCallback(() => {
|
|
1332
1419
|
const cam = new THREE__namespace.PerspectiveCamera(50, 16 / 9, 0.1, 1000);
|
|
1333
|
-
cam.name =
|
|
1420
|
+
cam.name = 'Camera';
|
|
1334
1421
|
cam.position.set(5, 3, 5);
|
|
1335
1422
|
cam.lookAt(0, 0, 0);
|
|
1336
1423
|
sceneRef.current.add(cam);
|
|
1337
1424
|
const helper = new THREE__namespace.CameraHelper(cam);
|
|
1338
|
-
helper.name =
|
|
1425
|
+
helper.name = '__helper_Camera';
|
|
1339
1426
|
sceneRef.current.add(helper);
|
|
1340
1427
|
rebuildTree();
|
|
1341
|
-
setStatusText(
|
|
1428
|
+
setStatusText('Added Camera');
|
|
1342
1429
|
setAddMenuOpen(null);
|
|
1343
1430
|
}, [rebuildTree]);
|
|
1344
|
-
const addEmpty =
|
|
1431
|
+
const addEmpty = _e.useCallback((type) => {
|
|
1345
1432
|
const obj = new THREE__namespace.Object3D();
|
|
1346
|
-
obj.name =
|
|
1433
|
+
obj.name =
|
|
1434
|
+
type === 'arrows' ? 'Empty_Arrows' : type === 'cube_empty' ? 'Empty_Cube' : 'Empty';
|
|
1347
1435
|
sceneRef.current.add(obj);
|
|
1348
|
-
if (type ===
|
|
1436
|
+
if (type === 'arrows') {
|
|
1349
1437
|
const h = new THREE__namespace.AxesHelper(0.5);
|
|
1350
|
-
h.name =
|
|
1438
|
+
h.name = '__emptyAxes';
|
|
1351
1439
|
obj.add(h);
|
|
1352
1440
|
}
|
|
1353
1441
|
rebuildTree();
|
|
@@ -1357,21 +1445,25 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1357
1445
|
/* ─────────────────────────────────────────
|
|
1358
1446
|
Raycaster click-to-select
|
|
1359
1447
|
───────────────────────────────────────── */
|
|
1360
|
-
const handleViewportClick =
|
|
1361
|
-
if (editorMode !==
|
|
1448
|
+
const handleViewportClick = _e.useCallback((e) => {
|
|
1449
|
+
if (editorMode !== 'object') {
|
|
1362
1450
|
return;
|
|
1363
|
-
|
|
1451
|
+
}
|
|
1452
|
+
if (!mountRef.current || !rendererRef.current || !cameraRef.current) {
|
|
1364
1453
|
return;
|
|
1365
|
-
|
|
1454
|
+
}
|
|
1455
|
+
if (e.target !== rendererRef.current.domElement) {
|
|
1366
1456
|
return;
|
|
1457
|
+
}
|
|
1367
1458
|
const rect = mountRef.current.getBoundingClientRect();
|
|
1368
1459
|
const x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
|
|
1369
1460
|
const y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
|
|
1370
1461
|
raycasterRef.current.setFromCamera(new THREE__namespace.Vector2(x, y), cameraRef.current);
|
|
1371
1462
|
const meshes = [];
|
|
1372
1463
|
sceneRef.current.traverse((obj) => {
|
|
1373
|
-
if (obj instanceof THREE__namespace.Mesh && !obj.name.startsWith(
|
|
1464
|
+
if (obj instanceof THREE__namespace.Mesh && !obj.name.startsWith('__')) {
|
|
1374
1465
|
meshes.push(obj);
|
|
1466
|
+
}
|
|
1375
1467
|
});
|
|
1376
1468
|
const intersects = raycasterRef.current.intersectObjects(meshes, false);
|
|
1377
1469
|
if (intersects.length > 0) {
|
|
@@ -1393,33 +1485,35 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1393
1485
|
/* ─────────────────────────────────────────
|
|
1394
1486
|
Context menu
|
|
1395
1487
|
───────────────────────────────────────── */
|
|
1396
|
-
const handleContextMenu =
|
|
1488
|
+
const handleContextMenu = _e.useCallback((e) => {
|
|
1397
1489
|
e.preventDefault();
|
|
1398
1490
|
setContextMenu({ x: e.clientX, y: e.clientY });
|
|
1399
1491
|
}, []);
|
|
1400
|
-
const closeContextMenu =
|
|
1401
|
-
|
|
1402
|
-
if (!contextMenu)
|
|
1492
|
+
const closeContextMenu = _e.useCallback(() => setContextMenu(null), []);
|
|
1493
|
+
_e.useEffect(() => {
|
|
1494
|
+
if (!contextMenu) {
|
|
1403
1495
|
return;
|
|
1496
|
+
}
|
|
1404
1497
|
const close = () => setContextMenu(null);
|
|
1405
|
-
window.addEventListener(
|
|
1406
|
-
return () => window.removeEventListener(
|
|
1498
|
+
window.addEventListener('click', close);
|
|
1499
|
+
return () => window.removeEventListener('click', close);
|
|
1407
1500
|
}, [contextMenu]);
|
|
1408
1501
|
/* ─────────────────────────────────────────
|
|
1409
1502
|
AI animation from video
|
|
1410
1503
|
───────────────────────────────────────── */
|
|
1411
|
-
const handleAIVideo =
|
|
1504
|
+
const handleAIVideo = _e.useCallback(async (e) => {
|
|
1412
1505
|
var _a;
|
|
1413
1506
|
const file = (_a = e.target.files) === null || _a === void 0 ? void 0 : _a[0];
|
|
1414
|
-
if (!file)
|
|
1507
|
+
if (!file) {
|
|
1415
1508
|
return;
|
|
1416
|
-
|
|
1509
|
+
}
|
|
1510
|
+
e.target.value = '';
|
|
1417
1511
|
if (!onAIPoseVideo) {
|
|
1418
|
-
setAiStatus(
|
|
1512
|
+
setAiStatus('AI pose callback not configured.');
|
|
1419
1513
|
return;
|
|
1420
1514
|
}
|
|
1421
1515
|
setAiBusy(true);
|
|
1422
|
-
setAiStatus(
|
|
1516
|
+
setAiStatus('Uploading video for AI pose estimation...');
|
|
1423
1517
|
try {
|
|
1424
1518
|
const result = await onAIPoseVideo(file);
|
|
1425
1519
|
setAiStatus(`Got ${result.frame_count} frames at ${result.fps} fps. Creating animation clip...`);
|
|
@@ -1437,14 +1531,14 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1437
1531
|
allClipsRef.current = [...allClipsRef.current, newAnim];
|
|
1438
1532
|
setAnimations([...allClipsRef.current]);
|
|
1439
1533
|
playClip(allClipsRef.current.length - 1);
|
|
1440
|
-
setAiStatus(
|
|
1534
|
+
setAiStatus('Animation created successfully!');
|
|
1441
1535
|
}
|
|
1442
1536
|
else {
|
|
1443
|
-
setAiStatus(
|
|
1537
|
+
setAiStatus('Could not create animation — load a rigged model first.');
|
|
1444
1538
|
}
|
|
1445
1539
|
}
|
|
1446
1540
|
catch (err) {
|
|
1447
|
-
log.error(
|
|
1541
|
+
log.error('AI pose error:', err);
|
|
1448
1542
|
setAiStatus(`Error: ${String(err)}`);
|
|
1449
1543
|
}
|
|
1450
1544
|
finally {
|
|
@@ -1454,24 +1548,24 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1454
1548
|
/* ─────────────────────────────────────────
|
|
1455
1549
|
Node helpers
|
|
1456
1550
|
───────────────────────────────────────── */
|
|
1457
|
-
const toggleNodeVisibility =
|
|
1551
|
+
const toggleNodeVisibility = _e.useCallback((node) => {
|
|
1458
1552
|
node.object.visible = !node.object.visible;
|
|
1459
1553
|
rebuildTree();
|
|
1460
1554
|
}, [rebuildTree]);
|
|
1461
|
-
const toggleNodeExpanded =
|
|
1555
|
+
const toggleNodeExpanded = _e.useCallback((node) => {
|
|
1462
1556
|
node.expanded = !node.expanded;
|
|
1463
1557
|
setSceneTree((prev) => [...prev]);
|
|
1464
1558
|
}, []);
|
|
1465
1559
|
/* ─────────────────────────────────────────
|
|
1466
1560
|
Undo / Redo
|
|
1467
1561
|
───────────────────────────────────────── */
|
|
1468
|
-
const undoRedoRef =
|
|
1469
|
-
const [undoRedoVer, setUndoRedoVer] =
|
|
1470
|
-
const pushUndo =
|
|
1562
|
+
const undoRedoRef = _e.useRef(new modelEditorUtils.UndoRedoStack());
|
|
1563
|
+
const [undoRedoVer, setUndoRedoVer] = _e.useState(0);
|
|
1564
|
+
const pushUndo = _e.useCallback((action) => {
|
|
1471
1565
|
undoRedoRef.current.push(action);
|
|
1472
1566
|
setUndoRedoVer((v) => v + 1);
|
|
1473
1567
|
}, []);
|
|
1474
|
-
const undo =
|
|
1568
|
+
const undo = _e.useCallback(() => {
|
|
1475
1569
|
const label = undoRedoRef.current.undo();
|
|
1476
1570
|
if (label) {
|
|
1477
1571
|
setStatusText(`Undo: ${label}`);
|
|
@@ -1479,7 +1573,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1479
1573
|
}
|
|
1480
1574
|
setUndoRedoVer((v) => v + 1);
|
|
1481
1575
|
}, [rebuildTree]);
|
|
1482
|
-
const redo =
|
|
1576
|
+
const redo = _e.useCallback(() => {
|
|
1483
1577
|
const label = undoRedoRef.current.redo();
|
|
1484
1578
|
if (label) {
|
|
1485
1579
|
setStatusText(`Redo: ${label}`);
|
|
@@ -1492,30 +1586,30 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1492
1586
|
const undoLabel = undoRedoRef.current.undoLabel;
|
|
1493
1587
|
const redoLabel = undoRedoRef.current.redoLabel;
|
|
1494
1588
|
// Keyboard shortcuts for undo/redo
|
|
1495
|
-
|
|
1589
|
+
_e.useEffect(() => {
|
|
1496
1590
|
const handleKeyDown = (e) => {
|
|
1497
|
-
if ((e.ctrlKey || e.metaKey) && e.key ===
|
|
1591
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 'z' && !e.shiftKey) {
|
|
1498
1592
|
e.preventDefault();
|
|
1499
1593
|
undo();
|
|
1500
1594
|
}
|
|
1501
|
-
else if ((e.ctrlKey || e.metaKey) && (e.key ===
|
|
1595
|
+
else if ((e.ctrlKey || e.metaKey) && (e.key === 'y' || (e.key === 'z' && e.shiftKey))) {
|
|
1502
1596
|
e.preventDefault();
|
|
1503
1597
|
redo();
|
|
1504
1598
|
}
|
|
1505
1599
|
};
|
|
1506
|
-
window.addEventListener(
|
|
1507
|
-
return () => window.removeEventListener(
|
|
1600
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
1601
|
+
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
1508
1602
|
}, [undo, redo]);
|
|
1509
1603
|
/* ─────────────────────────────────────────
|
|
1510
1604
|
Batch Import
|
|
1511
1605
|
───────────────────────────────────────── */
|
|
1512
|
-
const loadFiles =
|
|
1606
|
+
const loadFiles = _e.useCallback(async (files) => {
|
|
1513
1607
|
const fileArray = Array.from(files).filter((f) => {
|
|
1514
|
-
const ext = f.name.substring(f.name.lastIndexOf(
|
|
1608
|
+
const ext = f.name.substring(f.name.lastIndexOf('.')).toLowerCase();
|
|
1515
1609
|
return modelEditorTypes.SUPPORTED_EXTENSIONS.includes(ext);
|
|
1516
1610
|
});
|
|
1517
1611
|
if (fileArray.length === 0) {
|
|
1518
|
-
setStatusText(
|
|
1612
|
+
setStatusText('No supported files found');
|
|
1519
1613
|
return;
|
|
1520
1614
|
}
|
|
1521
1615
|
setStatusText(`Importing ${fileArray.length} file(s)...`);
|
|
@@ -1535,28 +1629,34 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1535
1629
|
/* ─────────────────────────────────────────
|
|
1536
1630
|
PBR Presets
|
|
1537
1631
|
───────────────────────────────────────── */
|
|
1538
|
-
const applyPreset =
|
|
1632
|
+
const applyPreset = _e.useCallback((preset) => {
|
|
1539
1633
|
const node = selectedNode;
|
|
1540
1634
|
if (!node || !(node.object instanceof THREE__namespace.Mesh)) {
|
|
1541
|
-
setStatusText(
|
|
1635
|
+
setStatusText('Select a mesh to apply material preset');
|
|
1542
1636
|
return;
|
|
1543
1637
|
}
|
|
1544
1638
|
const mesh = node.object;
|
|
1545
|
-
const oldMaterial = Array.isArray(mesh.material)
|
|
1546
|
-
|
|
1639
|
+
const oldMaterial = Array.isArray(mesh.material)
|
|
1640
|
+
? mesh.material[selMaterialIdx]
|
|
1641
|
+
: mesh.material;
|
|
1642
|
+
if (!oldMaterial) {
|
|
1547
1643
|
return;
|
|
1644
|
+
}
|
|
1548
1645
|
// Save for undo
|
|
1549
1646
|
const oldClone = oldMaterial.clone();
|
|
1550
1647
|
const materialIdx = selMaterialIdx;
|
|
1551
1648
|
const newMaterial = modelEditorUtils.createMaterialFromPreset(preset);
|
|
1552
1649
|
// Copy existing textures if present
|
|
1553
1650
|
if (oldMaterial instanceof THREE__namespace.MeshStandardMaterial) {
|
|
1554
|
-
if (oldMaterial.map)
|
|
1651
|
+
if (oldMaterial.map) {
|
|
1555
1652
|
newMaterial.map = oldMaterial.map;
|
|
1556
|
-
|
|
1653
|
+
}
|
|
1654
|
+
if (oldMaterial.normalMap) {
|
|
1557
1655
|
newMaterial.normalMap = oldMaterial.normalMap;
|
|
1558
|
-
|
|
1656
|
+
}
|
|
1657
|
+
if (oldMaterial.aoMap) {
|
|
1559
1658
|
newMaterial.aoMap = oldMaterial.aoMap;
|
|
1659
|
+
}
|
|
1560
1660
|
}
|
|
1561
1661
|
if (Array.isArray(mesh.material)) {
|
|
1562
1662
|
mesh.material[materialIdx] = newMaterial;
|
|
@@ -1589,10 +1689,10 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1589
1689
|
/* ─────────────────────────────────────────
|
|
1590
1690
|
LOD Generation
|
|
1591
1691
|
───────────────────────────────────────── */
|
|
1592
|
-
const generateSelectedLOD =
|
|
1692
|
+
const generateSelectedLOD = _e.useCallback(() => {
|
|
1593
1693
|
const node = selectedNode;
|
|
1594
1694
|
if (!node || !(node.object instanceof THREE__namespace.Mesh)) {
|
|
1595
|
-
setStatusText(
|
|
1695
|
+
setStatusText('Select a mesh to generate LOD');
|
|
1596
1696
|
return;
|
|
1597
1697
|
}
|
|
1598
1698
|
const mesh = node.object;
|
|
@@ -1602,8 +1702,14 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1602
1702
|
parent.add(lod);
|
|
1603
1703
|
pushUndo({
|
|
1604
1704
|
label: `Generate LOD for ${mesh.name}`,
|
|
1605
|
-
undo: () => {
|
|
1606
|
-
|
|
1705
|
+
undo: () => {
|
|
1706
|
+
parent.remove(lod);
|
|
1707
|
+
parent.add(mesh);
|
|
1708
|
+
},
|
|
1709
|
+
redo: () => {
|
|
1710
|
+
parent.remove(mesh);
|
|
1711
|
+
parent.add(lod);
|
|
1712
|
+
},
|
|
1607
1713
|
});
|
|
1608
1714
|
rebuildTree();
|
|
1609
1715
|
setStatusText(`Generated LOD for ${mesh.name} (4 levels)`);
|
|
@@ -1611,21 +1717,22 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1611
1717
|
/* ─────────────────────────────────────────
|
|
1612
1718
|
Instancing
|
|
1613
1719
|
───────────────────────────────────────── */
|
|
1614
|
-
const convertSelectedToInstanced =
|
|
1720
|
+
const convertSelectedToInstanced = _e.useCallback(() => {
|
|
1615
1721
|
const root = rootObjectRef.current || sceneRef.current;
|
|
1616
1722
|
const result = modelEditorUtils.convertToInstanced(root);
|
|
1617
1723
|
if (result.created === 0) {
|
|
1618
|
-
setStatusText(
|
|
1724
|
+
setStatusText('No duplicate meshes found to instance');
|
|
1619
1725
|
return;
|
|
1620
1726
|
}
|
|
1621
1727
|
rebuildTree();
|
|
1622
1728
|
setStatusText(`Created ${result.created} instanced meshes, removed ${result.removed} duplicates`);
|
|
1623
1729
|
}, [rebuildTree]);
|
|
1624
1730
|
/* ── HDRI Environment ── */
|
|
1625
|
-
const applyHDRI =
|
|
1731
|
+
const applyHDRI = _e.useCallback((preset) => {
|
|
1626
1732
|
const renderer = rendererRef.current;
|
|
1627
|
-
if (!renderer)
|
|
1733
|
+
if (!renderer) {
|
|
1628
1734
|
return;
|
|
1735
|
+
}
|
|
1629
1736
|
modelEditorUtils.applyHDRIPreset(sceneRef.current, renderer, preset);
|
|
1630
1737
|
setBgColor(`#${new THREE__namespace.Color(preset.bgColor).getHexString()}`);
|
|
1631
1738
|
if (preset.fog) {
|
|
@@ -1640,9 +1747,9 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1640
1747
|
setStatusText(`Environment: ${preset.name}`);
|
|
1641
1748
|
}, []);
|
|
1642
1749
|
/* ── Snap ── */
|
|
1643
|
-
const snapPoint =
|
|
1750
|
+
const snapPoint = _e.useCallback((point) => {
|
|
1644
1751
|
const root = rootObjectRef.current || sceneRef.current;
|
|
1645
|
-
const modes = snapEnabled ? [
|
|
1752
|
+
const modes = snapEnabled ? ['vertex', 'edge', 'grid'] : ['grid'];
|
|
1646
1753
|
return modelEditorUtils.snapMulti(point, root, snapGrid, modes, 0.5);
|
|
1647
1754
|
}, [snapEnabled, snapGrid]);
|
|
1648
1755
|
/* ═══════════════════════════════════════════
|