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