@nice2dev/ui-3d 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/CHANGELOG.md +115 -1
  2. package/dist/cjs/collaborative/collaborativeScene.js +210 -0
  3. package/dist/cjs/collaborative/collaborativeScene.js.map +1 -0
  4. package/dist/cjs/core/i18n.js +3 -3
  5. package/dist/cjs/core/i18n.js.map +1 -1
  6. package/dist/cjs/dance/DanceBridge.js +162 -0
  7. package/dist/cjs/dance/DanceBridge.js.map +1 -0
  8. package/dist/cjs/dance/DanceScoreEngine.js +210 -0
  9. package/dist/cjs/dance/DanceScoreEngine.js.map +1 -0
  10. package/dist/cjs/dance/PoseDetector.js +199 -0
  11. package/dist/cjs/dance/PoseDetector.js.map +1 -0
  12. package/dist/cjs/index.js +254 -0
  13. package/dist/cjs/index.js.map +1 -1
  14. package/dist/cjs/material/MaterialEditor.module.css.js +6 -0
  15. package/dist/cjs/material/MaterialEditor.module.css.js.map +1 -0
  16. package/dist/cjs/material/NiceMaterialEditor.js +737 -0
  17. package/dist/cjs/material/NiceMaterialEditor.js.map +1 -0
  18. package/dist/cjs/material/materialEditorTypes.js +73 -0
  19. package/dist/cjs/material/materialEditorTypes.js.map +1 -0
  20. package/dist/cjs/material/materialEditorUtils.js +841 -0
  21. package/dist/cjs/material/materialEditorUtils.js.map +1 -0
  22. package/dist/cjs/material/materialNodeDefinitions.js +1285 -0
  23. package/dist/cjs/material/materialNodeDefinitions.js.map +1 -0
  24. package/dist/cjs/model/ModelEditor.js +4 -1
  25. package/dist/cjs/model/ModelEditor.js.map +1 -1
  26. package/dist/cjs/model/ModelEditor.module.css.js +1 -1
  27. package/dist/cjs/model/ModelEditorLeftPanel.js +5 -4
  28. package/dist/cjs/model/ModelEditorLeftPanel.js.map +1 -1
  29. package/dist/cjs/model/ModelEditorMenuBar.js +8 -3
  30. package/dist/cjs/model/ModelEditorMenuBar.js.map +1 -1
  31. package/dist/cjs/model/ModelEditorRightPanel.js +27 -26
  32. package/dist/cjs/model/ModelEditorRightPanel.js.map +1 -1
  33. package/dist/cjs/model/ModelEditorSubComponents.js +20 -16
  34. package/dist/cjs/model/ModelEditorSubComponents.js.map +1 -1
  35. package/dist/cjs/model/ModelEditorTimeline.js +5 -4
  36. package/dist/cjs/model/ModelEditorTimeline.js.map +1 -1
  37. package/dist/cjs/model/ModelEditorToolbar.js +4 -3
  38. package/dist/cjs/model/ModelEditorToolbar.js.map +1 -1
  39. package/dist/cjs/model/ModelEditorViewport.js +2 -2
  40. package/dist/cjs/model/ModelEditorViewport.js.map +1 -1
  41. package/dist/cjs/model/ModelViewer.js +68 -0
  42. package/dist/cjs/model/ModelViewer.js.map +1 -0
  43. package/dist/cjs/model/ModelViewer.module.css.js +6 -0
  44. package/dist/cjs/model/ModelViewer.module.css.js.map +1 -0
  45. package/dist/cjs/model/NiceArmatureEditor.js +255 -0
  46. package/dist/cjs/model/NiceArmatureEditor.js.map +1 -0
  47. package/dist/cjs/model/NiceMorphTargetEditor.js +206 -0
  48. package/dist/cjs/model/NiceMorphTargetEditor.js.map +1 -0
  49. package/dist/cjs/model/NiceOctree.js +339 -0
  50. package/dist/cjs/model/NiceOctree.js.map +1 -0
  51. package/dist/cjs/model/NicePhysicsSimulation.js +283 -0
  52. package/dist/cjs/model/NicePhysicsSimulation.js.map +1 -0
  53. package/dist/cjs/model/NiceProceduralGeometry.js +269 -0
  54. package/dist/cjs/model/NiceProceduralGeometry.js.map +1 -0
  55. package/dist/cjs/model/NiceTerrainEditor.js +343 -0
  56. package/dist/cjs/model/NiceTerrainEditor.js.map +1 -0
  57. package/dist/cjs/model/NiceWeightPainter.js +258 -0
  58. package/dist/cjs/model/NiceWeightPainter.js.map +1 -0
  59. package/dist/cjs/model/NiceXRPreview.js +269 -0
  60. package/dist/cjs/model/NiceXRPreview.js.map +1 -0
  61. package/dist/cjs/model/cadModeUtils.js +130 -0
  62. package/dist/cjs/model/cadModeUtils.js.map +1 -0
  63. package/dist/cjs/model/editorShortcuts.js +187 -0
  64. package/dist/cjs/model/editorShortcuts.js.map +1 -0
  65. package/dist/cjs/model/modelEditorTypes.js +11 -0
  66. package/dist/cjs/model/modelEditorTypes.js.map +1 -1
  67. package/dist/cjs/model/modelEditorUtils.js +1049 -0
  68. package/dist/cjs/model/modelEditorUtils.js.map +1 -0
  69. package/dist/cjs/model/simsModeUtils.js +358 -0
  70. package/dist/cjs/model/simsModeUtils.js.map +1 -0
  71. package/dist/cjs/model/useModelEditor.js +319 -115
  72. package/dist/cjs/model/useModelEditor.js.map +1 -1
  73. package/dist/cjs/model/useModelViewer.js +634 -0
  74. package/dist/cjs/model/useModelViewer.js.map +1 -0
  75. package/dist/cjs/nice2dev-ui-3d.css +1 -1
  76. package/dist/cjs/particle/NiceParticleEditor.js +526 -0
  77. package/dist/cjs/particle/NiceParticleEditor.js.map +1 -0
  78. package/dist/cjs/particle/ParticleEditor.module.css.js +6 -0
  79. package/dist/cjs/particle/ParticleEditor.module.css.js.map +1 -0
  80. package/dist/cjs/particle/particleEditorTypes.js +92 -0
  81. package/dist/cjs/particle/particleEditorTypes.js.map +1 -0
  82. package/dist/cjs/particle/particleEditorUtils.js +1084 -0
  83. package/dist/cjs/particle/particleEditorUtils.js.map +1 -0
  84. package/dist/cjs/rendering/NiceCascadedShadows.js +266 -0
  85. package/dist/cjs/rendering/NiceCascadedShadows.js.map +1 -0
  86. package/dist/cjs/rendering/NiceRenderExport.js +341 -0
  87. package/dist/cjs/rendering/NiceRenderExport.js.map +1 -0
  88. package/dist/cjs/rendering/NiceSSAO.js +359 -0
  89. package/dist/cjs/rendering/NiceSSAO.js.map +1 -0
  90. package/dist/cjs/rendering/NiceSSR.js +277 -0
  91. package/dist/cjs/rendering/NiceSSR.js.map +1 -0
  92. package/dist/cjs/rendering/NiceWebGPURenderer.js +215 -0
  93. package/dist/cjs/rendering/NiceWebGPURenderer.js.map +1 -0
  94. package/dist/cjs/ui/dist/index.js +50089 -0
  95. package/dist/cjs/ui/dist/index.js.map +1 -0
  96. package/dist/cjs/uv/NiceUVEditor.js +520 -0
  97. package/dist/cjs/uv/NiceUVEditor.js.map +1 -0
  98. package/dist/cjs/uv/UVEditor.module.css.js +6 -0
  99. package/dist/cjs/uv/UVEditor.module.css.js.map +1 -0
  100. package/dist/cjs/uv/uvEditorTypes.js +98 -0
  101. package/dist/cjs/uv/uvEditorTypes.js.map +1 -0
  102. package/dist/cjs/uv/uvEditorUtils.js +670 -0
  103. package/dist/cjs/uv/uvEditorUtils.js.map +1 -0
  104. package/dist/esm/collaborative/collaborativeScene.js +206 -0
  105. package/dist/esm/collaborative/collaborativeScene.js.map +1 -0
  106. package/dist/esm/dance/DanceBridge.js +158 -0
  107. package/dist/esm/dance/DanceBridge.js.map +1 -0
  108. package/dist/esm/dance/DanceScoreEngine.js +207 -0
  109. package/dist/esm/dance/DanceScoreEngine.js.map +1 -0
  110. package/dist/esm/dance/PoseDetector.js +195 -0
  111. package/dist/esm/dance/PoseDetector.js.map +1 -0
  112. package/dist/esm/index.js +35 -1
  113. package/dist/esm/index.js.map +1 -1
  114. package/dist/esm/material/MaterialEditor.module.css.js +4 -0
  115. package/dist/esm/material/MaterialEditor.module.css.js.map +1 -0
  116. package/dist/esm/material/NiceMaterialEditor.js +734 -0
  117. package/dist/esm/material/NiceMaterialEditor.js.map +1 -0
  118. package/dist/esm/material/materialEditorTypes.js +62 -0
  119. package/dist/esm/material/materialEditorTypes.js.map +1 -0
  120. package/dist/esm/material/materialEditorUtils.js +811 -0
  121. package/dist/esm/material/materialEditorUtils.js.map +1 -0
  122. package/dist/esm/material/materialNodeDefinitions.js +1280 -0
  123. package/dist/esm/material/materialNodeDefinitions.js.map +1 -0
  124. package/dist/esm/model/ModelEditor.js +4 -2
  125. package/dist/esm/model/ModelEditor.js.map +1 -1
  126. package/dist/esm/model/ModelEditor.module.css.js +1 -1
  127. package/dist/esm/model/ModelEditorLeftPanel.js +5 -4
  128. package/dist/esm/model/ModelEditorLeftPanel.js.map +1 -1
  129. package/dist/esm/model/ModelEditorMenuBar.js +8 -3
  130. package/dist/esm/model/ModelEditorMenuBar.js.map +1 -1
  131. package/dist/esm/model/ModelEditorRightPanel.js +27 -26
  132. package/dist/esm/model/ModelEditorRightPanel.js.map +1 -1
  133. package/dist/esm/model/ModelEditorSubComponents.js +17 -13
  134. package/dist/esm/model/ModelEditorSubComponents.js.map +1 -1
  135. package/dist/esm/model/ModelEditorTimeline.js +5 -4
  136. package/dist/esm/model/ModelEditorTimeline.js.map +1 -1
  137. package/dist/esm/model/ModelEditorToolbar.js +4 -3
  138. package/dist/esm/model/ModelEditorToolbar.js.map +1 -1
  139. package/dist/esm/model/ModelEditorViewport.js +2 -2
  140. package/dist/esm/model/ModelEditorViewport.js.map +1 -1
  141. package/dist/esm/model/ModelViewer.js +65 -0
  142. package/dist/esm/model/ModelViewer.js.map +1 -0
  143. package/dist/esm/model/ModelViewer.module.css.js +4 -0
  144. package/dist/esm/model/ModelViewer.module.css.js.map +1 -0
  145. package/dist/esm/model/NiceArmatureEditor.js +233 -0
  146. package/dist/esm/model/NiceArmatureEditor.js.map +1 -0
  147. package/dist/esm/model/NiceMorphTargetEditor.js +184 -0
  148. package/dist/esm/model/NiceMorphTargetEditor.js.map +1 -0
  149. package/dist/esm/model/NiceOctree.js +317 -0
  150. package/dist/esm/model/NiceOctree.js.map +1 -0
  151. package/dist/esm/model/NicePhysicsSimulation.js +261 -0
  152. package/dist/esm/model/NicePhysicsSimulation.js.map +1 -0
  153. package/dist/esm/model/NiceProceduralGeometry.js +242 -0
  154. package/dist/esm/model/NiceProceduralGeometry.js.map +1 -0
  155. package/dist/esm/model/NiceTerrainEditor.js +321 -0
  156. package/dist/esm/model/NiceTerrainEditor.js.map +1 -0
  157. package/dist/esm/model/NiceWeightPainter.js +236 -0
  158. package/dist/esm/model/NiceWeightPainter.js.map +1 -0
  159. package/dist/esm/model/NiceXRPreview.js +247 -0
  160. package/dist/esm/model/NiceXRPreview.js.map +1 -0
  161. package/dist/esm/model/cadModeUtils.js +103 -0
  162. package/dist/esm/model/cadModeUtils.js.map +1 -0
  163. package/dist/esm/model/editorShortcuts.js +185 -0
  164. package/dist/esm/model/editorShortcuts.js.map +1 -0
  165. package/dist/esm/model/modelEditorTypes.js +11 -0
  166. package/dist/esm/model/modelEditorTypes.js.map +1 -1
  167. package/dist/esm/model/modelEditorUtils.js +997 -0
  168. package/dist/esm/model/modelEditorUtils.js.map +1 -0
  169. package/dist/esm/model/simsModeUtils.js +325 -0
  170. package/dist/esm/model/simsModeUtils.js.map +1 -0
  171. package/dist/esm/model/useModelEditor.js +204 -0
  172. package/dist/esm/model/useModelEditor.js.map +1 -1
  173. package/dist/esm/model/useModelViewer.js +613 -0
  174. package/dist/esm/model/useModelViewer.js.map +1 -0
  175. package/dist/esm/nice2dev-ui-3d.css +1 -1
  176. package/dist/esm/particle/NiceParticleEditor.js +523 -0
  177. package/dist/esm/particle/NiceParticleEditor.js.map +1 -0
  178. package/dist/esm/particle/ParticleEditor.module.css.js +4 -0
  179. package/dist/esm/particle/ParticleEditor.module.css.js.map +1 -0
  180. package/dist/esm/particle/particleEditorTypes.js +84 -0
  181. package/dist/esm/particle/particleEditorTypes.js.map +1 -0
  182. package/dist/esm/particle/particleEditorUtils.js +1054 -0
  183. package/dist/esm/particle/particleEditorUtils.js.map +1 -0
  184. package/dist/esm/rendering/NiceCascadedShadows.js +244 -0
  185. package/dist/esm/rendering/NiceCascadedShadows.js.map +1 -0
  186. package/dist/esm/rendering/NiceRenderExport.js +319 -0
  187. package/dist/esm/rendering/NiceRenderExport.js.map +1 -0
  188. package/dist/esm/rendering/NiceSSAO.js +337 -0
  189. package/dist/esm/rendering/NiceSSAO.js.map +1 -0
  190. package/dist/esm/rendering/NiceSSR.js +255 -0
  191. package/dist/esm/rendering/NiceSSR.js.map +1 -0
  192. package/dist/esm/rendering/NiceWebGPURenderer.js +193 -0
  193. package/dist/esm/rendering/NiceWebGPURenderer.js.map +1 -0
  194. package/dist/esm/ui/dist/index.js +49686 -0
  195. package/dist/esm/ui/dist/index.js.map +1 -0
  196. package/dist/esm/uv/NiceUVEditor.js +518 -0
  197. package/dist/esm/uv/NiceUVEditor.js.map +1 -0
  198. package/dist/esm/uv/UVEditor.module.css.js +4 -0
  199. package/dist/esm/uv/UVEditor.module.css.js.map +1 -0
  200. package/dist/esm/uv/uvEditorTypes.js +88 -0
  201. package/dist/esm/uv/uvEditorTypes.js.map +1 -0
  202. package/dist/esm/uv/uvEditorUtils.js +621 -0
  203. package/dist/esm/uv/uvEditorUtils.js.map +1 -0
  204. package/package.json +3 -4
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var React = require('react');
3
+ var Ae = 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');
@@ -24,6 +24,7 @@ var VRMLLoader_js = require('three/examples/jsm/loaders/VRMLLoader.js');
24
24
  var GCodeLoader_js = require('three/examples/jsm/loaders/GCodeLoader.js');
25
25
  var SVGLoader_js = require('three/examples/jsm/loaders/SVGLoader.js');
26
26
  var modelEditorTypes = require('./modelEditorTypes.js');
27
+ var modelEditorUtils = require('./modelEditorUtils.js');
27
28
 
28
29
  function _interopNamespaceDefault(e) {
29
30
  var n = Object.create(null);
@@ -71,82 +72,82 @@ function findNodeById(nodes, id) {
71
72
  ═══════════════════════════════════════════ */
72
73
  function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
73
74
  /* ── refs ── */
74
- const mountRef = React.useRef(null);
75
- const rendererRef = React.useRef(null);
76
- const sceneRef = React.useRef(new THREE__namespace.Scene());
77
- const cameraRef = React.useRef(new THREE__namespace.PerspectiveCamera(50, 1, 0.01, 10000));
78
- const orbitRef = React.useRef(null);
79
- const transformRef = React.useRef(null);
80
- const clockRef = React.useRef(new THREE__namespace.Clock());
81
- const mixerRef = React.useRef(null);
82
- const rafRef = React.useRef(0);
83
- const fileInputRef = React.useRef(null);
84
- const mergeInputRef = React.useRef(null);
85
- const videoInputRef = React.useRef(null);
86
- const raycasterRef = React.useRef(new THREE__namespace.Raycaster());
87
- const skeletonHelperRef = React.useRef(null);
75
+ const mountRef = Ae.useRef(null);
76
+ const rendererRef = Ae.useRef(null);
77
+ const sceneRef = Ae.useRef(new THREE__namespace.Scene());
78
+ const cameraRef = Ae.useRef(new THREE__namespace.PerspectiveCamera(50, 1, 0.01, 10000));
79
+ const orbitRef = Ae.useRef(null);
80
+ const transformRef = Ae.useRef(null);
81
+ const clockRef = Ae.useRef(new THREE__namespace.Clock());
82
+ const mixerRef = Ae.useRef(null);
83
+ const rafRef = Ae.useRef(0);
84
+ const fileInputRef = Ae.useRef(null);
85
+ const mergeInputRef = Ae.useRef(null);
86
+ const videoInputRef = Ae.useRef(null);
87
+ const raycasterRef = Ae.useRef(new THREE__namespace.Raycaster());
88
+ const skeletonHelperRef = Ae.useRef(null);
88
89
  /* ── state ── */
89
- const [sceneTree, setSceneTree] = React.useState([]);
90
- const [selectedId, setSelectedId] = React.useState(null);
91
- const [transformMode, setTransformMode] = React.useState("translate");
92
- const [animations, setAnimations] = React.useState([]);
93
- const [activeAnimIdx, setActiveAnimIdx] = React.useState(-1);
94
- const [isPlaying, setIsPlaying] = React.useState(false);
95
- const [animTime, setAnimTime] = React.useState(0);
96
- const [animDuration, setAnimDuration] = React.useState(0);
97
- const [animSpeed, setAnimSpeed] = React.useState(1);
98
- const [loopAnim, setLoopAnim] = React.useState(true);
99
- const [bottomCollapsed, setBottomCollapsed] = React.useState(false);
100
- const [showGrid, setShowGrid] = React.useState(true);
101
- const [showAxes, setShowAxes] = React.useState(true);
102
- const [wireframe, setWireframe] = React.useState(false);
103
- const [bgColor, setBgColor] = React.useState("#111122");
104
- const [statusText, setStatusText] = React.useState("Ready");
105
- const [dragOver, setDragOver] = React.useState(false);
106
- const [polyCount, setPolyCount] = React.useState(0);
107
- const [meshCount, setMeshCount] = React.useState(0);
108
- const [boneCount, setBoneCount] = React.useState(0);
90
+ const [sceneTree, setSceneTree] = Ae.useState([]);
91
+ const [selectedId, setSelectedId] = Ae.useState(null);
92
+ const [transformMode, setTransformMode] = Ae.useState("translate");
93
+ const [animations, setAnimations] = Ae.useState([]);
94
+ const [activeAnimIdx, setActiveAnimIdx] = Ae.useState(-1);
95
+ const [isPlaying, setIsPlaying] = Ae.useState(false);
96
+ const [animTime, setAnimTime] = Ae.useState(0);
97
+ const [animDuration, setAnimDuration] = Ae.useState(0);
98
+ const [animSpeed, setAnimSpeed] = Ae.useState(1);
99
+ const [loopAnim, setLoopAnim] = Ae.useState(true);
100
+ const [bottomCollapsed, setBottomCollapsed] = Ae.useState(false);
101
+ const [showGrid, setShowGrid] = Ae.useState(true);
102
+ const [showAxes, setShowAxes] = Ae.useState(true);
103
+ const [wireframe, setWireframe] = Ae.useState(false);
104
+ const [bgColor, setBgColor] = Ae.useState("#111122");
105
+ const [statusText, setStatusText] = Ae.useState("Ready");
106
+ const [dragOver, setDragOver] = Ae.useState(false);
107
+ const [polyCount, setPolyCount] = Ae.useState(0);
108
+ const [meshCount, setMeshCount] = Ae.useState(0);
109
+ const [boneCount, setBoneCount] = Ae.useState(0);
109
110
  // Material editor
110
- const [selMaterialIdx, setSelMaterialIdx] = React.useState(0);
111
- const [matRefresh, setMatRefresh] = React.useState(0);
111
+ const [selMaterialIdx, setSelMaterialIdx] = Ae.useState(0);
112
+ const [matRefresh, setMatRefresh] = Ae.useState(0);
112
113
  // AI panel
113
- const [aiStatus, setAiStatus] = React.useState("");
114
- const [aiBusy, setAiBusy] = React.useState(false);
114
+ const [aiStatus, setAiStatus] = Ae.useState("");
115
+ const [aiBusy, setAiBusy] = Ae.useState(false);
115
116
  // Blender 2.0 — viewport & editor
116
- const [shadingMode, setShadingMode] = React.useState("solid");
117
- const [editorMode, setEditorMode] = React.useState("object");
118
- const [contextMenu, setContextMenu] = React.useState(null);
119
- const [outlinerSearch, setOutlinerSearch] = React.useState("");
120
- const [snapEnabled, setSnapEnabled] = React.useState(false);
121
- const [snapGrid, setSnapGrid] = React.useState(1);
122
- const [showSkeleton, setShowSkeleton] = React.useState(true);
123
- const [addMenuOpen, setAddMenuOpen] = React.useState(null);
124
- const [gizmoSpace, setGizmoSpace] = React.useState("world");
125
- const [propTab, setPropTab] = React.useState("object");
126
- const [fogEnabled, setFogEnabled] = React.useState(false);
127
- const [fogColor, setFogColor] = React.useState("#111122");
128
- const [fogNear, setFogNear] = React.useState(10);
129
- const [fogFar, setFogFar] = React.useState(100);
130
- const [showLightHelpers, setShowLightHelpers] = React.useState(true);
117
+ const [shadingMode, setShadingMode] = Ae.useState("solid");
118
+ const [editorMode, setEditorMode] = Ae.useState("object");
119
+ const [contextMenu, setContextMenu] = Ae.useState(null);
120
+ const [outlinerSearch, setOutlinerSearch] = Ae.useState("");
121
+ const [snapEnabled, setSnapEnabled] = Ae.useState(false);
122
+ const [snapGrid, setSnapGrid] = Ae.useState(1);
123
+ const [showSkeleton, setShowSkeleton] = Ae.useState(true);
124
+ const [addMenuOpen, setAddMenuOpen] = Ae.useState(null);
125
+ const [gizmoSpace, setGizmoSpace] = Ae.useState("world");
126
+ const [propTab, setPropTab] = Ae.useState("object");
127
+ const [fogEnabled, setFogEnabled] = Ae.useState(false);
128
+ const [fogColor, setFogColor] = Ae.useState("#111122");
129
+ const [fogNear, setFogNear] = Ae.useState(10);
130
+ const [fogFar, setFogFar] = Ae.useState(100);
131
+ const [showLightHelpers, setShowLightHelpers] = Ae.useState(true);
131
132
  // The root object that was loaded (mesh/character)
132
- const rootObjectRef = React.useRef(null);
133
+ const rootObjectRef = Ae.useRef(null);
133
134
  // Active animation action
134
- const activeActionRef = React.useRef(null);
135
+ const activeActionRef = Ae.useRef(null);
135
136
  // All loaded animation clips (for the current model)
136
- const allClipsRef = React.useRef([]);
137
+ const allClipsRef = Ae.useRef([]);
137
138
  /* keep refs current */
138
- const animTimeRef = React.useRef(animTime);
139
+ const animTimeRef = Ae.useRef(animTime);
139
140
  animTimeRef.current = animTime;
140
- const isPlayingRef = React.useRef(isPlaying);
141
+ const isPlayingRef = Ae.useRef(isPlaying);
141
142
  isPlayingRef.current = isPlaying;
142
- const animSpeedRef = React.useRef(animSpeed);
143
+ const animSpeedRef = Ae.useRef(animSpeed);
143
144
  animSpeedRef.current = animSpeed;
144
- const loopAnimRef = React.useRef(loopAnim);
145
+ const loopAnimRef = Ae.useRef(loopAnim);
145
146
  loopAnimRef.current = loopAnim;
146
147
  /* ─────────────────────────────────────────
147
148
  Scene setup
148
149
  ───────────────────────────────────────── */
149
- React.useEffect(() => {
150
+ Ae.useEffect(() => {
150
151
  const container = mountRef.current;
151
152
  if (!container)
152
153
  return;
@@ -279,7 +280,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
279
280
  /* ─────────────────────────────────────────
280
281
  Rebuild scene tree from Three.js scene
281
282
  ───────────────────────────────────────── */
282
- const rebuildTree = React.useCallback(() => {
283
+ const rebuildTree = Ae.useCallback(() => {
283
284
  const scene = sceneRef.current;
284
285
  let polys = 0;
285
286
  let meshes = 0;
@@ -346,9 +347,9 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
346
347
  /* ─────────────────────────────────────────
347
348
  File loading
348
349
  ───────────────────────────────────────── */
349
- const playClipRef = React.useRef(() => { });
350
- const focusOnObjectRef = React.useRef(() => { });
351
- const addObjectToScene = React.useCallback((obj, clips, filename) => {
350
+ const playClipRef = Ae.useRef(() => { });
351
+ const focusOnObjectRef = Ae.useRef(() => { });
352
+ const addObjectToScene = Ae.useCallback((obj, clips, filename) => {
352
353
  const scene = sceneRef.current;
353
354
  obj.traverse((child) => {
354
355
  if (child instanceof THREE__namespace.Mesh) {
@@ -395,7 +396,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
395
396
  rebuildTree();
396
397
  setStatusText(`Loaded: ${filename} (${clips.length} animation(s))`);
397
398
  }, [rebuildTree]);
398
- const loadFile = React.useCallback(async (file) => {
399
+ const loadFile = Ae.useCallback(async (file) => {
399
400
  var _a, _b, _c, _d, _e, _f;
400
401
  const name = file.name.toLowerCase();
401
402
  const ext = "." + name.split(".").pop();
@@ -514,7 +515,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
514
515
  URL.revokeObjectURL(url);
515
516
  }
516
517
  }, [addObjectToScene]);
517
- const mergeAnimationFBX = React.useCallback(async (file) => {
518
+ const mergeAnimationFBX = Ae.useCallback(async (file) => {
518
519
  var _a;
519
520
  const url = URL.createObjectURL(file);
520
521
  setStatusText(`Merging animations from ${file.name}...`);
@@ -555,7 +556,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
555
556
  /* ─────────────────────────────────────────
556
557
  Animation controls
557
558
  ───────────────────────────────────────── */
558
- const playClip = React.useCallback((index) => {
559
+ const playClip = Ae.useCallback((index) => {
559
560
  const mixer = mixerRef.current;
560
561
  if (!mixer || index < 0 || index >= allClipsRef.current.length)
561
562
  return;
@@ -575,7 +576,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
575
576
  setIsPlaying(true);
576
577
  }, []);
577
578
  playClipRef.current = playClip;
578
- const togglePlay = React.useCallback(() => {
579
+ const togglePlay = Ae.useCallback(() => {
579
580
  if (activeAnimIdx < 0 && allClipsRef.current.length > 0) {
580
581
  playClip(0);
581
582
  return;
@@ -592,7 +593,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
592
593
  setIsPlaying(true);
593
594
  }
594
595
  }, [activeAnimIdx, playClip]);
595
- const stopAnim = React.useCallback(() => {
596
+ const stopAnim = Ae.useCallback(() => {
596
597
  const action = activeActionRef.current;
597
598
  if (action) {
598
599
  action.stop();
@@ -601,7 +602,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
601
602
  setIsPlaying(false);
602
603
  setAnimTime(0);
603
604
  }, []);
604
- const seekAnim = React.useCallback((t) => {
605
+ const seekAnim = Ae.useCallback((t) => {
605
606
  var _a;
606
607
  const action = activeActionRef.current;
607
608
  if (!action)
@@ -615,7 +616,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
615
616
  /* ─────────────────────────────────────────
616
617
  Camera helpers
617
618
  ───────────────────────────────────────── */
618
- const focusOnObject = React.useCallback((obj) => {
619
+ const focusOnObject = Ae.useCallback((obj) => {
619
620
  const box = new THREE__namespace.Box3().setFromObject(obj);
620
621
  const center = box.getCenter(new THREE__namespace.Vector3());
621
622
  const size = box.getSize(new THREE__namespace.Vector3());
@@ -629,7 +630,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
629
630
  }
630
631
  }, []);
631
632
  focusOnObjectRef.current = focusOnObject;
632
- const setCameraPreset = React.useCallback((preset) => {
633
+ const setCameraPreset = Ae.useCallback((preset) => {
633
634
  const orbit = orbitRef.current;
634
635
  if (!orbit)
635
636
  return;
@@ -663,7 +664,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
663
664
  /* ─────────────────────────────────────────
664
665
  Selection & transform
665
666
  ───────────────────────────────────────── */
666
- const selectObject = React.useCallback((node) => {
667
+ const selectObject = Ae.useCallback((node) => {
667
668
  var _a;
668
669
  setSelectedId((_a = node === null || node === void 0 ? void 0 : node.id) !== null && _a !== void 0 ? _a : null);
669
670
  const tc = transformRef.current;
@@ -677,29 +678,29 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
677
678
  tc.detach();
678
679
  }
679
680
  }, [transformMode]);
680
- React.useEffect(() => {
681
+ Ae.useEffect(() => {
681
682
  var _a;
682
683
  (_a = transformRef.current) === null || _a === void 0 ? void 0 : _a.setMode(transformMode);
683
684
  }, [transformMode]);
684
685
  /* ─────────────────────────────────────────
685
686
  Toggle helpers
686
687
  ───────────────────────────────────────── */
687
- React.useEffect(() => {
688
+ Ae.useEffect(() => {
688
689
  const scene = sceneRef.current;
689
690
  const grid = scene.getObjectByName("__grid");
690
691
  if (grid)
691
692
  grid.visible = showGrid;
692
693
  }, [showGrid]);
693
- React.useEffect(() => {
694
+ Ae.useEffect(() => {
694
695
  const scene = sceneRef.current;
695
696
  const axes = scene.getObjectByName("__axes");
696
697
  if (axes)
697
698
  axes.visible = showAxes;
698
699
  }, [showAxes]);
699
- React.useEffect(() => {
700
+ Ae.useEffect(() => {
700
701
  sceneRef.current.background = new THREE__namespace.Color(bgColor);
701
702
  }, [bgColor]);
702
- React.useEffect(() => {
703
+ Ae.useEffect(() => {
703
704
  const root = rootObjectRef.current;
704
705
  if (!root)
705
706
  return;
@@ -718,7 +719,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
718
719
  /* ─────────────────────────────────────────
719
720
  Viewport shading mode
720
721
  ───────────────────────────────────────── */
721
- React.useEffect(() => {
722
+ Ae.useEffect(() => {
722
723
  const renderer = rendererRef.current;
723
724
  if (!renderer)
724
725
  return;
@@ -771,12 +772,12 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
771
772
  }
772
773
  }, [shadingMode]);
773
774
  /* Gizmo space */
774
- React.useEffect(() => {
775
+ Ae.useEffect(() => {
775
776
  var _a;
776
777
  (_a = transformRef.current) === null || _a === void 0 ? void 0 : _a.setSpace(gizmoSpace);
777
778
  }, [gizmoSpace]);
778
779
  /* Snap settings */
779
- React.useEffect(() => {
780
+ Ae.useEffect(() => {
780
781
  const tc = transformRef.current;
781
782
  if (!tc)
782
783
  return;
@@ -792,7 +793,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
792
793
  }
793
794
  }, [snapEnabled, snapGrid]);
794
795
  /* Skeleton helper */
795
- React.useEffect(() => {
796
+ Ae.useEffect(() => {
796
797
  const scene = sceneRef.current;
797
798
  if (skeletonHelperRef.current) {
798
799
  scene.remove(skeletonHelperRef.current);
@@ -813,7 +814,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
813
814
  }
814
815
  }, [showSkeleton, sceneTree]);
815
816
  /* Fog */
816
- React.useEffect(() => {
817
+ Ae.useEffect(() => {
817
818
  if (fogEnabled) {
818
819
  sceneRef.current.fog = new THREE__namespace.Fog(fogColor, fogNear, fogFar);
819
820
  }
@@ -822,7 +823,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
822
823
  }
823
824
  }, [fogEnabled, fogColor, fogNear, fogFar]);
824
825
  /* Light helpers visibility */
825
- React.useEffect(() => {
826
+ Ae.useEffect(() => {
826
827
  sceneRef.current.traverse((obj) => {
827
828
  if (obj.name.startsWith("__helper_")) {
828
829
  obj.visible = showLightHelpers;
@@ -832,16 +833,16 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
832
833
  /* ─────────────────────────────────────────
833
834
  Drag-and-drop
834
835
  ───────────────────────────────────────── */
835
- const handleDragOver = React.useCallback((e) => {
836
+ const handleDragOver = Ae.useCallback((e) => {
836
837
  e.preventDefault();
837
838
  e.stopPropagation();
838
839
  setDragOver(true);
839
840
  }, []);
840
- const handleDragLeave = React.useCallback((e) => {
841
+ const handleDragLeave = Ae.useCallback((e) => {
841
842
  e.preventDefault();
842
843
  setDragOver(false);
843
844
  }, []);
844
- const handleDrop = React.useCallback((e) => {
845
+ const handleDrop = Ae.useCallback((e) => {
845
846
  e.preventDefault();
846
847
  e.stopPropagation();
847
848
  setDragOver(false);
@@ -856,14 +857,14 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
856
857
  /* ─────────────────────────────────────────
857
858
  Helpers
858
859
  ───────────────────────────────────────── */
859
- const selectedNode = React.useMemo(() => (selectedId ? findNodeById(sceneTree, selectedId) : null), [selectedId, sceneTree]);
860
- const selectedMaterials = React.useMemo(() => {
860
+ const selectedNode = Ae.useMemo(() => (selectedId ? findNodeById(sceneTree, selectedId) : null), [selectedId, sceneTree]);
861
+ const selectedMaterials = Ae.useMemo(() => {
861
862
  if (!selectedNode || !(selectedNode.object instanceof THREE__namespace.Mesh))
862
863
  return [];
863
864
  const m = selectedNode.object.material;
864
865
  return Array.isArray(m) ? m : [m];
865
866
  }, [selectedNode, matRefresh]);
866
- const deleteSelected = React.useCallback(() => {
867
+ const deleteSelected = Ae.useCallback(() => {
867
868
  var _a;
868
869
  if (!selectedNode)
869
870
  return;
@@ -872,7 +873,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
872
873
  setSelectedId(null);
873
874
  rebuildTree();
874
875
  }, [selectedNode, rebuildTree]);
875
- const duplicateSelected = React.useCallback(() => {
876
+ const duplicateSelected = Ae.useCallback(() => {
876
877
  if (!selectedNode)
877
878
  return;
878
879
  const clone = selectedNode.object.clone(true);
@@ -885,7 +886,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
885
886
  /* ─────────────────────────────────────────
886
887
  Keyboard shortcuts
887
888
  ───────────────────────────────────────── */
888
- React.useEffect(() => {
889
+ Ae.useEffect(() => {
889
890
  const handleKey = (e) => {
890
891
  var _a, _b, _c;
891
892
  const tag = (_a = e.target) === null || _a === void 0 ? void 0 : _a.tagName;
@@ -993,14 +994,14 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
993
994
  /* ─────────────────────────────────────────
994
995
  File input handlers
995
996
  ───────────────────────────────────────── */
996
- const handleFileInput = React.useCallback((e) => {
997
+ const handleFileInput = Ae.useCallback((e) => {
997
998
  const files = e.target.files;
998
999
  if (!files)
999
1000
  return;
1000
1001
  Array.from(files).forEach(loadFile);
1001
1002
  e.target.value = "";
1002
1003
  }, [loadFile]);
1003
- const handleMergeInput = React.useCallback((e) => {
1004
+ const handleMergeInput = Ae.useCallback((e) => {
1004
1005
  const files = e.target.files;
1005
1006
  if (!files)
1006
1007
  return;
@@ -1010,7 +1011,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1010
1011
  /* ─────────────────────────────────────────
1011
1012
  Export functions
1012
1013
  ───────────────────────────────────────── */
1013
- const exportGLTF = React.useCallback((binary) => {
1014
+ const exportGLTF = Ae.useCallback((binary) => {
1014
1015
  const root = rootObjectRef.current;
1015
1016
  if (!root) {
1016
1017
  setStatusText("Nothing to export");
@@ -1037,7 +1038,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1037
1038
  setStatusText(`Export error: ${String(err)}`);
1038
1039
  }, options);
1039
1040
  }, []);
1040
- const exportOBJ = React.useCallback(() => {
1041
+ const exportOBJ = Ae.useCallback(() => {
1041
1042
  const root = rootObjectRef.current;
1042
1043
  if (!root) {
1043
1044
  setStatusText("Nothing to export");
@@ -1053,7 +1054,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1053
1054
  URL.revokeObjectURL(a.href);
1054
1055
  setStatusText("Exported OBJ successfully");
1055
1056
  }, []);
1056
- const exportSTL = React.useCallback((binary) => {
1057
+ const exportSTL = Ae.useCallback((binary) => {
1057
1058
  const root = rootObjectRef.current;
1058
1059
  if (!root) {
1059
1060
  setStatusText("Nothing to export");
@@ -1075,7 +1076,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1075
1076
  URL.revokeObjectURL(a.href);
1076
1077
  setStatusText(`Exported STL${binary ? " (binary)" : ""} successfully`);
1077
1078
  }, []);
1078
- const exportPLY = React.useCallback((binary) => {
1079
+ const exportPLY = Ae.useCallback((binary) => {
1079
1080
  const root = rootObjectRef.current;
1080
1081
  if (!root) {
1081
1082
  setStatusText("Nothing to export");
@@ -1099,7 +1100,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1099
1100
  setStatusText(`Exported PLY${binary ? " (binary)" : ""} successfully`);
1100
1101
  }, { binary });
1101
1102
  }, []);
1102
- const exportUSDZ = React.useCallback(async () => {
1103
+ const exportUSDZ = Ae.useCallback(async () => {
1103
1104
  const root = rootObjectRef.current;
1104
1105
  if (!root) {
1105
1106
  setStatusText("Nothing to export");
@@ -1127,7 +1128,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1127
1128
  /* ─────────────────────────────────────────
1128
1129
  Dispose & clear
1129
1130
  ───────────────────────────────────────── */
1130
- const disposeObject = React.useCallback((obj) => {
1131
+ const disposeObject = Ae.useCallback((obj) => {
1131
1132
  obj.traverse((child) => {
1132
1133
  if (child.geometry) {
1133
1134
  child.geometry.dispose();
@@ -1147,7 +1148,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1147
1148
  }
1148
1149
  });
1149
1150
  }, []);
1150
- const clearScene = React.useCallback(() => {
1151
+ const clearScene = Ae.useCallback(() => {
1151
1152
  var _a;
1152
1153
  if (!window.confirm("Clear the entire scene? This cannot be undone."))
1153
1154
  return;
@@ -1185,7 +1186,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1185
1186
  /* ─────────────────────────────────────────
1186
1187
  Save to Library
1187
1188
  ───────────────────────────────────────── */
1188
- const saveToLibrary = React.useCallback(() => {
1189
+ const saveToLibrary = Ae.useCallback(() => {
1189
1190
  if (!onSaveToLibrary)
1190
1191
  return;
1191
1192
  const root = rootObjectRef.current;
@@ -1213,7 +1214,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1213
1214
  /* ─────────────────────────────────────────
1214
1215
  Add Mesh / Light / Camera / Empty
1215
1216
  ───────────────────────────────────────── */
1216
- const addPrimitive = React.useCallback((type) => {
1217
+ const addPrimitive = Ae.useCallback((type) => {
1217
1218
  let geo;
1218
1219
  switch (type) {
1219
1220
  case "cube":
@@ -1274,7 +1275,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1274
1275
  setStatusText(`Added ${mesh.name}`);
1275
1276
  setAddMenuOpen(null);
1276
1277
  }, [rebuildTree]);
1277
- const addSceneLight = React.useCallback((type) => {
1278
+ const addSceneLight = Ae.useCallback((type) => {
1278
1279
  let light;
1279
1280
  switch (type) {
1280
1281
  case "point": {
@@ -1327,7 +1328,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1327
1328
  setStatusText(`Added ${light.name}`);
1328
1329
  setAddMenuOpen(null);
1329
1330
  }, [rebuildTree]);
1330
- const addCameraObject = React.useCallback(() => {
1331
+ const addCameraObject = Ae.useCallback(() => {
1331
1332
  const cam = new THREE__namespace.PerspectiveCamera(50, 16 / 9, 0.1, 1000);
1332
1333
  cam.name = "Camera";
1333
1334
  cam.position.set(5, 3, 5);
@@ -1340,7 +1341,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1340
1341
  setStatusText("Added Camera");
1341
1342
  setAddMenuOpen(null);
1342
1343
  }, [rebuildTree]);
1343
- const addEmpty = React.useCallback((type) => {
1344
+ const addEmpty = Ae.useCallback((type) => {
1344
1345
  const obj = new THREE__namespace.Object3D();
1345
1346
  obj.name = type === "arrows" ? "Empty_Arrows" : type === "cube_empty" ? "Empty_Cube" : "Empty";
1346
1347
  sceneRef.current.add(obj);
@@ -1356,7 +1357,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1356
1357
  /* ─────────────────────────────────────────
1357
1358
  Raycaster click-to-select
1358
1359
  ───────────────────────────────────────── */
1359
- const handleViewportClick = React.useCallback((e) => {
1360
+ const handleViewportClick = Ae.useCallback((e) => {
1360
1361
  if (editorMode !== "object")
1361
1362
  return;
1362
1363
  if (!mountRef.current || !rendererRef.current || !cameraRef.current)
@@ -1392,12 +1393,12 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1392
1393
  /* ─────────────────────────────────────────
1393
1394
  Context menu
1394
1395
  ───────────────────────────────────────── */
1395
- const handleContextMenu = React.useCallback((e) => {
1396
+ const handleContextMenu = Ae.useCallback((e) => {
1396
1397
  e.preventDefault();
1397
1398
  setContextMenu({ x: e.clientX, y: e.clientY });
1398
1399
  }, []);
1399
- const closeContextMenu = React.useCallback(() => setContextMenu(null), []);
1400
- React.useEffect(() => {
1400
+ const closeContextMenu = Ae.useCallback(() => setContextMenu(null), []);
1401
+ Ae.useEffect(() => {
1401
1402
  if (!contextMenu)
1402
1403
  return;
1403
1404
  const close = () => setContextMenu(null);
@@ -1407,7 +1408,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1407
1408
  /* ─────────────────────────────────────────
1408
1409
  AI animation from video
1409
1410
  ───────────────────────────────────────── */
1410
- const handleAIVideo = React.useCallback(async (e) => {
1411
+ const handleAIVideo = Ae.useCallback(async (e) => {
1411
1412
  var _a;
1412
1413
  const file = (_a = e.target.files) === null || _a === void 0 ? void 0 : _a[0];
1413
1414
  if (!file)
@@ -1453,14 +1454,197 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1453
1454
  /* ─────────────────────────────────────────
1454
1455
  Node helpers
1455
1456
  ───────────────────────────────────────── */
1456
- const toggleNodeVisibility = React.useCallback((node) => {
1457
+ const toggleNodeVisibility = Ae.useCallback((node) => {
1457
1458
  node.object.visible = !node.object.visible;
1458
1459
  rebuildTree();
1459
1460
  }, [rebuildTree]);
1460
- const toggleNodeExpanded = React.useCallback((node) => {
1461
+ const toggleNodeExpanded = Ae.useCallback((node) => {
1461
1462
  node.expanded = !node.expanded;
1462
1463
  setSceneTree((prev) => [...prev]);
1463
1464
  }, []);
1465
+ /* ─────────────────────────────────────────
1466
+ Undo / Redo
1467
+ ───────────────────────────────────────── */
1468
+ const undoRedoRef = Ae.useRef(new modelEditorUtils.UndoRedoStack());
1469
+ const [undoRedoVer, setUndoRedoVer] = Ae.useState(0);
1470
+ const pushUndo = Ae.useCallback((action) => {
1471
+ undoRedoRef.current.push(action);
1472
+ setUndoRedoVer((v) => v + 1);
1473
+ }, []);
1474
+ const undo = Ae.useCallback(() => {
1475
+ const label = undoRedoRef.current.undo();
1476
+ if (label) {
1477
+ setStatusText(`Undo: ${label}`);
1478
+ rebuildTree();
1479
+ }
1480
+ setUndoRedoVer((v) => v + 1);
1481
+ }, [rebuildTree]);
1482
+ const redo = Ae.useCallback(() => {
1483
+ const label = undoRedoRef.current.redo();
1484
+ if (label) {
1485
+ setStatusText(`Redo: ${label}`);
1486
+ rebuildTree();
1487
+ }
1488
+ setUndoRedoVer((v) => v + 1);
1489
+ }, [rebuildTree]);
1490
+ const canUndo = undoRedoRef.current.canUndo();
1491
+ const canRedo = undoRedoRef.current.canRedo();
1492
+ const undoLabel = undoRedoRef.current.undoLabel;
1493
+ const redoLabel = undoRedoRef.current.redoLabel;
1494
+ // Keyboard shortcuts for undo/redo
1495
+ Ae.useEffect(() => {
1496
+ const handleKeyDown = (e) => {
1497
+ if ((e.ctrlKey || e.metaKey) && e.key === "z" && !e.shiftKey) {
1498
+ e.preventDefault();
1499
+ undo();
1500
+ }
1501
+ else if ((e.ctrlKey || e.metaKey) && (e.key === "y" || (e.key === "z" && e.shiftKey))) {
1502
+ e.preventDefault();
1503
+ redo();
1504
+ }
1505
+ };
1506
+ window.addEventListener("keydown", handleKeyDown);
1507
+ return () => window.removeEventListener("keydown", handleKeyDown);
1508
+ }, [undo, redo]);
1509
+ /* ─────────────────────────────────────────
1510
+ Batch Import
1511
+ ───────────────────────────────────────── */
1512
+ const loadFiles = Ae.useCallback(async (files) => {
1513
+ const fileArray = Array.from(files).filter((f) => {
1514
+ const ext = f.name.substring(f.name.lastIndexOf(".")).toLowerCase();
1515
+ return modelEditorTypes.SUPPORTED_EXTENSIONS.includes(ext);
1516
+ });
1517
+ if (fileArray.length === 0) {
1518
+ setStatusText("No supported files found");
1519
+ return;
1520
+ }
1521
+ setStatusText(`Importing ${fileArray.length} file(s)...`);
1522
+ let loaded = 0;
1523
+ for (const file of fileArray) {
1524
+ try {
1525
+ await loadFile(file);
1526
+ loaded++;
1527
+ setStatusText(`Imported ${loaded}/${fileArray.length}: ${file.name}`);
1528
+ }
1529
+ catch (err) {
1530
+ log.error(`Failed to import ${file.name}:`, err);
1531
+ }
1532
+ }
1533
+ setStatusText(`Batch import complete: ${loaded}/${fileArray.length} files loaded`);
1534
+ }, [loadFile]);
1535
+ /* ─────────────────────────────────────────
1536
+ PBR Presets
1537
+ ───────────────────────────────────────── */
1538
+ const applyPreset = Ae.useCallback((preset) => {
1539
+ const node = selectedNode;
1540
+ if (!node || !(node.object instanceof THREE__namespace.Mesh)) {
1541
+ setStatusText("Select a mesh to apply material preset");
1542
+ return;
1543
+ }
1544
+ const mesh = node.object;
1545
+ const oldMaterial = Array.isArray(mesh.material) ? mesh.material[selMaterialIdx] : mesh.material;
1546
+ if (!oldMaterial)
1547
+ return;
1548
+ // Save for undo
1549
+ const oldClone = oldMaterial.clone();
1550
+ const materialIdx = selMaterialIdx;
1551
+ const newMaterial = modelEditorUtils.createMaterialFromPreset(preset);
1552
+ // Copy existing textures if present
1553
+ if (oldMaterial instanceof THREE__namespace.MeshStandardMaterial) {
1554
+ if (oldMaterial.map)
1555
+ newMaterial.map = oldMaterial.map;
1556
+ if (oldMaterial.normalMap)
1557
+ newMaterial.normalMap = oldMaterial.normalMap;
1558
+ if (oldMaterial.aoMap)
1559
+ newMaterial.aoMap = oldMaterial.aoMap;
1560
+ }
1561
+ if (Array.isArray(mesh.material)) {
1562
+ mesh.material[materialIdx] = newMaterial;
1563
+ }
1564
+ else {
1565
+ mesh.material = newMaterial;
1566
+ }
1567
+ pushUndo({
1568
+ label: `Apply ${preset.name} preset`,
1569
+ undo: () => {
1570
+ if (Array.isArray(mesh.material)) {
1571
+ mesh.material[materialIdx] = oldClone;
1572
+ }
1573
+ else {
1574
+ mesh.material = oldClone;
1575
+ }
1576
+ },
1577
+ redo: () => {
1578
+ if (Array.isArray(mesh.material)) {
1579
+ mesh.material[materialIdx] = newMaterial;
1580
+ }
1581
+ else {
1582
+ mesh.material = newMaterial;
1583
+ }
1584
+ },
1585
+ });
1586
+ setMatRefresh((v) => v + 1);
1587
+ setStatusText(`Applied ${preset.name} preset`);
1588
+ }, [selectedNode, selMaterialIdx, pushUndo]);
1589
+ /* ─────────────────────────────────────────
1590
+ LOD Generation
1591
+ ───────────────────────────────────────── */
1592
+ const generateSelectedLOD = Ae.useCallback(() => {
1593
+ const node = selectedNode;
1594
+ if (!node || !(node.object instanceof THREE__namespace.Mesh)) {
1595
+ setStatusText("Select a mesh to generate LOD");
1596
+ return;
1597
+ }
1598
+ const mesh = node.object;
1599
+ const parent = mesh.parent || sceneRef.current;
1600
+ const lod = modelEditorUtils.generateLOD(mesh);
1601
+ parent.remove(mesh);
1602
+ parent.add(lod);
1603
+ pushUndo({
1604
+ label: `Generate LOD for ${mesh.name}`,
1605
+ undo: () => { parent.remove(lod); parent.add(mesh); },
1606
+ redo: () => { parent.remove(mesh); parent.add(lod); },
1607
+ });
1608
+ rebuildTree();
1609
+ setStatusText(`Generated LOD for ${mesh.name} (4 levels)`);
1610
+ }, [selectedNode, pushUndo, rebuildTree]);
1611
+ /* ─────────────────────────────────────────
1612
+ Instancing
1613
+ ───────────────────────────────────────── */
1614
+ const convertSelectedToInstanced = Ae.useCallback(() => {
1615
+ const root = rootObjectRef.current || sceneRef.current;
1616
+ const result = modelEditorUtils.convertToInstanced(root);
1617
+ if (result.created === 0) {
1618
+ setStatusText("No duplicate meshes found to instance");
1619
+ return;
1620
+ }
1621
+ rebuildTree();
1622
+ setStatusText(`Created ${result.created} instanced meshes, removed ${result.removed} duplicates`);
1623
+ }, [rebuildTree]);
1624
+ /* ── HDRI Environment ── */
1625
+ const applyHDRI = Ae.useCallback((preset) => {
1626
+ const renderer = rendererRef.current;
1627
+ if (!renderer)
1628
+ return;
1629
+ modelEditorUtils.applyHDRIPreset(sceneRef.current, renderer, preset);
1630
+ setBgColor(`#${new THREE__namespace.Color(preset.bgColor).getHexString()}`);
1631
+ if (preset.fog) {
1632
+ setFogEnabled(true);
1633
+ setFogColor(`#${new THREE__namespace.Color(preset.fog.color).getHexString()}`);
1634
+ setFogNear(preset.fog.near);
1635
+ setFogFar(preset.fog.far);
1636
+ }
1637
+ else {
1638
+ setFogEnabled(false);
1639
+ }
1640
+ setStatusText(`Environment: ${preset.name}`);
1641
+ }, []);
1642
+ /* ── Snap ── */
1643
+ const snapPoint = Ae.useCallback((point) => {
1644
+ const root = rootObjectRef.current || sceneRef.current;
1645
+ const modes = snapEnabled ? ["vertex", "edge", "grid"] : ["grid"];
1646
+ return modelEditorUtils.snapMulti(point, root, snapGrid, modes, 0.5);
1647
+ }, [snapEnabled, snapGrid]);
1464
1648
  /* ═══════════════════════════════════════════
1465
1649
  Return API
1466
1650
  ═══════════════════════════════════════════ */
@@ -1574,6 +1758,26 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1574
1758
  toggleNodeVisibility,
1575
1759
  toggleNodeExpanded,
1576
1760
  setStatusText,
1761
+ // undo/redo
1762
+ undo,
1763
+ redo,
1764
+ canUndo,
1765
+ canRedo,
1766
+ undoLabel,
1767
+ redoLabel,
1768
+ // batch import
1769
+ loadFiles,
1770
+ // PBR presets
1771
+ pbrPresets: modelEditorUtils.PBR_PRESETS,
1772
+ applyPreset,
1773
+ // LOD & instancing
1774
+ generateSelectedLOD,
1775
+ convertSelectedToInstanced,
1776
+ // HDRI environment
1777
+ hdriPresets: modelEditorUtils.HDRI_PRESETS,
1778
+ applyHDRIPreset: applyHDRI,
1779
+ // Snap
1780
+ snapPoint,
1577
1781
  };
1578
1782
  }
1579
1783