@pascal-app/editor 0.8.0 → 0.9.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 (350) hide show
  1. package/package.json +15 -13
  2. package/src/components/editor/alignment-3d-guide-layer.tsx +168 -0
  3. package/src/components/editor/bake-exporter.tsx +47 -0
  4. package/src/components/editor/custom-camera-controls.tsx +795 -28
  5. package/src/components/editor/editor-layout-mobile.tsx +8 -3
  6. package/src/components/editor/editor-layout-v2.tsx +76 -51
  7. package/src/components/editor/export-manager.tsx +69 -32
  8. package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
  9. package/src/components/editor/first-person/build-collider-world.test.ts +182 -0
  10. package/src/components/editor/first-person/build-collider-world.ts +228 -83
  11. package/src/components/editor/first-person-controls.tsx +943 -27
  12. package/src/components/editor/floating-action-menu.tsx +676 -208
  13. package/src/components/editor/floating-building-action-menu.tsx +2 -2
  14. package/src/components/editor/floorplan-background-selection.test.ts +58 -0
  15. package/src/components/editor/floorplan-background-selection.ts +3 -2
  16. package/src/components/editor/floorplan-panel.tsx +6916 -12137
  17. package/src/components/editor/grid.tsx +197 -36
  18. package/src/components/editor/group-actions.ts +457 -0
  19. package/src/components/editor/group-floating-action-menu.tsx +130 -0
  20. package/src/components/editor/group-move-3d.ts +397 -0
  21. package/src/components/editor/group-rotate-handle.tsx +420 -0
  22. package/src/components/editor/group-selection-box-3d.tsx +173 -0
  23. package/src/components/editor/group-transform-shared.test.ts +396 -0
  24. package/src/components/editor/group-transform-shared.ts +489 -0
  25. package/src/components/editor/handles/handle-arrow.tsx +581 -0
  26. package/src/components/editor/handles/use-handle-drag.ts +246 -0
  27. package/src/components/editor/index.tsx +412 -162
  28. package/src/components/editor/measurement-pill.tsx +115 -0
  29. package/src/components/editor/node-action-menu.tsx +14 -1
  30. package/src/components/editor/node-arrow-handles.tsx +1646 -0
  31. package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
  32. package/src/components/editor/quick-measurement-card.tsx +70 -0
  33. package/src/components/editor/quick-measurement-hud.tsx +28 -0
  34. package/src/components/editor/riser-diagram-panel.tsx +137 -0
  35. package/src/components/editor/selection-manager.tsx +1106 -483
  36. package/src/components/editor/site-edge-labels.tsx +45 -21
  37. package/src/components/editor/slab-hole-highlights.tsx +474 -0
  38. package/src/components/editor/snapshot-capture-overlay.tsx +369 -155
  39. package/src/components/editor/three-context-bridge.ts +22 -0
  40. package/src/components/editor/thumbnail-generator.tsx +140 -41
  41. package/src/components/editor/use-floorplan-background-placement.ts +190 -35
  42. package/src/components/editor/use-floorplan-hit-testing.ts +24 -0
  43. package/src/components/editor/use-floorplan-scene-data.ts +19 -7
  44. package/src/components/editor/use-mesh-settle-epoch.ts +26 -0
  45. package/src/components/editor/wall-measurement-label.tsx +14 -19
  46. package/src/components/editor/wall-move-side-handles.tsx +870 -0
  47. package/src/components/editor/wall-opening-highlights.tsx +181 -0
  48. package/src/components/editor/wall-snap-beacon-layer.tsx +304 -0
  49. package/src/components/editor-2d/floorplan-action-menu-layer.tsx +15 -6
  50. package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +142 -0
  51. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +5 -3
  52. package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
  53. package/src/components/editor-2d/floorplan-group-move.tsx +701 -0
  54. package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
  55. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1656 -0
  56. package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
  57. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +392 -0
  58. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +857 -0
  59. package/src/components/editor-2d/floorplan-render-context.tsx +62 -0
  60. package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +77 -0
  61. package/src/components/editor-2d/floorplan-wall-move-ghost-layer.tsx +47 -0
  62. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +266 -0
  63. package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +15 -0
  64. package/src/components/editor-2d/renderers/floorplan-label-angle.ts +14 -0
  65. package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -66
  66. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +62 -0
  67. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +308 -0
  68. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +3325 -0
  69. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +24 -9
  70. package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
  71. package/src/components/editor-2d/svg-paths.ts +10 -2
  72. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +439 -57
  73. package/src/components/systems/ceiling/ceiling-system.tsx +109 -12
  74. package/src/components/systems/roof/roof-edit-system.tsx +1851 -22
  75. package/src/components/systems/selection-affordance-manager.tsx +38 -0
  76. package/src/components/systems/stair/stair-edit-system.tsx +1 -1
  77. package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
  78. package/src/components/systems/zone/zone-system.tsx +42 -13
  79. package/src/components/tools/elevator/elevator-defaults.ts +8 -0
  80. package/src/components/tools/elevator/elevator-tool.tsx +375 -0
  81. package/src/components/tools/elevator/move-elevator-tool.tsx +263 -0
  82. package/src/components/tools/fence/fence-drafting.ts +141 -27
  83. package/src/components/tools/item/move-tool.tsx +45 -105
  84. package/src/components/tools/item/placement-math.test.ts +22 -0
  85. package/src/components/tools/item/placement-math.ts +59 -7
  86. package/src/components/tools/item/placement-strategies.ts +432 -16
  87. package/src/components/tools/item/placement-types.ts +22 -1
  88. package/src/components/tools/item/use-draft-node.ts +51 -2
  89. package/src/components/tools/item/use-placement-coordinator.tsx +1399 -256
  90. package/src/components/tools/registry/move-registry-node-tool.tsx +1064 -0
  91. package/src/components/tools/roof/roof-tool.tsx +350 -28
  92. package/src/components/tools/select/box-select-state.ts +88 -0
  93. package/src/components/tools/select/box-select-tool.tsx +442 -520
  94. package/src/components/tools/select/marquee-geometry.test.ts +161 -0
  95. package/src/components/tools/select/marquee-geometry.ts +155 -0
  96. package/src/components/tools/select/plane-box-select-tool.tsx +570 -0
  97. package/src/components/tools/select/screen-rectangle-selection.ts +84 -0
  98. package/src/components/tools/select/select-candidates.test.ts +109 -0
  99. package/src/components/tools/select/select-candidates.ts +85 -0
  100. package/src/components/tools/shared/affordance-dispatch.ts +30 -0
  101. package/src/components/tools/shared/cursor-sphere.tsx +62 -26
  102. package/src/components/tools/shared/drag-bounding-box.tsx +195 -0
  103. package/src/components/tools/shared/facing-indicator.tsx +87 -0
  104. package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
  105. package/src/components/tools/shared/floor-stack-preview.ts +25 -0
  106. package/src/components/tools/shared/fresh-placement-visibility.ts +61 -0
  107. package/src/components/tools/shared/placement-box-geometry.ts +123 -0
  108. package/src/components/tools/shared/placement-box.tsx +305 -0
  109. package/src/components/tools/shared/polygon-editor.tsx +866 -175
  110. package/src/components/tools/shared/segment-angle.ts +56 -0
  111. package/src/components/tools/site/site-boundary-editor.tsx +520 -16
  112. package/src/components/tools/site/site-flag-model.ts +18 -0
  113. package/src/components/tools/stair/stair-defaults.ts +2 -1
  114. package/src/components/tools/stair/stair-tool.tsx +342 -37
  115. package/src/components/tools/tool-manager.tsx +239 -62
  116. package/src/components/tools/wall/wall-drafting.test.ts +330 -0
  117. package/src/components/tools/wall/wall-drafting.ts +248 -171
  118. package/src/components/tools/wall/wall-snap-geometry.test.ts +123 -0
  119. package/src/components/tools/wall/wall-snap-geometry.ts +331 -0
  120. package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
  121. package/src/components/tools/zone/zone-tool.tsx +86 -82
  122. package/src/components/ui/action-menu/action-button.tsx +21 -1
  123. package/src/components/ui/action-menu/camera-actions.tsx +24 -17
  124. package/src/components/ui/action-menu/control-modes.tsx +40 -170
  125. package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
  126. package/src/components/ui/action-menu/index.tsx +3 -117
  127. package/src/components/ui/action-menu/measurement-control.tsx +188 -0
  128. package/src/components/ui/action-menu/structure-tools.tsx +27 -93
  129. package/src/components/ui/action-menu/view-toggles.tsx +279 -90
  130. package/src/components/ui/command-palette/editor-commands.tsx +20 -15
  131. package/src/components/ui/command-palette/index.tsx +5 -3
  132. package/src/components/ui/controls/material-paint-panel.tsx +152 -0
  133. package/src/components/ui/controls/material-picker.tsx +87 -177
  134. package/src/components/ui/controls/material-properties-editor.tsx +108 -0
  135. package/src/components/ui/controls/metric-control.tsx +142 -46
  136. package/src/components/ui/controls/scene-material-list.tsx +247 -0
  137. package/src/components/ui/controls/slider-control.tsx +88 -38
  138. package/src/components/ui/floating-level-selector.tsx +47 -10
  139. package/src/components/ui/helpers/building-helper.tsx +10 -23
  140. package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
  141. package/src/components/ui/helpers/helper-manager.tsx +235 -23
  142. package/src/components/ui/helpers/item-helper.tsx +28 -32
  143. package/src/components/ui/helpers/registered-tool-helper.tsx +59 -0
  144. package/src/components/ui/helpers/roof-helper.tsx +10 -12
  145. package/src/components/ui/icon-ref.tsx +47 -0
  146. package/src/components/ui/item-catalog/catalog-items.tsx +40 -1
  147. package/src/components/ui/item-catalog/item-catalog.tsx +16 -36
  148. package/src/components/ui/level-duplicate-dialog.tsx +2 -1
  149. package/src/components/ui/panels/node-display.ts +17 -16
  150. package/src/components/ui/panels/panel-manager.tsx +55 -58
  151. package/src/components/ui/panels/panel-wrapper.tsx +238 -12
  152. package/src/components/ui/panels/parametric-inspector.tsx +482 -0
  153. package/src/components/ui/panels/reference-panel.tsx +54 -19
  154. package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
  155. package/src/components/ui/primitives/shortcut-token.tsx +39 -3
  156. package/src/components/ui/primitives/sidebar.tsx +1 -0
  157. package/src/components/ui/scene-loader.tsx +1 -3
  158. package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
  159. package/src/components/ui/sidebar/icon-rail.tsx +50 -31
  160. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +263 -0
  161. package/src/components/ui/sidebar/panels/items-panel/index.tsx +82 -8
  162. package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
  163. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +122 -41
  164. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +66 -7
  165. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +267 -0
  166. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -1
  167. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +9 -5
  168. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +86 -0
  169. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
  170. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
  171. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +92 -0
  172. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +75 -0
  173. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +3 -3
  174. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +86 -0
  175. package/src/components/ui/sidebar/panels/site-panel/index.tsx +42 -21
  176. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
  177. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -1
  178. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +140 -0
  179. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +48 -8
  180. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +132 -0
  181. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +9 -5
  182. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +86 -0
  183. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +7 -1
  184. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
  185. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +112 -39
  186. package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
  187. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
  188. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
  189. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
  190. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +51 -2
  191. package/src/components/ui/sidebar/tab-bar.tsx +75 -1
  192. package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
  193. package/src/components/ui/snap-target-badge.test.tsx +40 -0
  194. package/src/components/ui/snap-target-badge.tsx +88 -0
  195. package/src/components/viewer-overlay.tsx +253 -61
  196. package/src/components/viewer-zone-system.tsx +7 -2
  197. package/src/hooks/use-auto-save.test.ts +14 -0
  198. package/src/hooks/use-auto-save.ts +64 -10
  199. package/src/hooks/use-ceiling-events.ts +176 -0
  200. package/src/hooks/use-drag-action.ts +127 -0
  201. package/src/hooks/use-keyboard.ts +493 -95
  202. package/src/hooks/use-selection.ts +64 -0
  203. package/src/index.tsx +451 -6
  204. package/src/lib/active-placement-surface.ts +35 -0
  205. package/src/lib/ceiling-plan-snap.ts +24 -0
  206. package/src/lib/constants.ts +6 -2
  207. package/src/lib/contextual-help.test.ts +112 -0
  208. package/src/lib/contextual-help.ts +144 -0
  209. package/src/lib/continuation.ts +64 -0
  210. package/src/lib/direct-manipulation.test.ts +192 -0
  211. package/src/lib/direct-manipulation.ts +109 -0
  212. package/src/lib/editor-api.ts +50 -0
  213. package/src/lib/elevator-support.ts +96 -0
  214. package/src/lib/floorplan/apply-alignment.test.ts +25 -0
  215. package/src/lib/floorplan/apply-alignment.ts +117 -0
  216. package/src/lib/floorplan/floorplan-export.tsx +364 -0
  217. package/src/lib/floorplan/geometry.ts +53 -0
  218. package/src/lib/floorplan/index.ts +10 -0
  219. package/src/lib/floorplan/items.ts +5 -2
  220. package/src/lib/floorplan/plan-coords.ts +21 -0
  221. package/src/lib/floorplan/selection-tool.ts +40 -0
  222. package/src/lib/floorplan/stairs.ts +12 -1
  223. package/src/lib/fresh-planar-placement.test.ts +339 -0
  224. package/src/lib/fresh-planar-placement.ts +130 -0
  225. package/src/lib/glb-export.test.ts +441 -0
  226. package/src/lib/glb-export.ts +874 -0
  227. package/src/lib/history.ts +11 -1
  228. package/src/lib/interaction/hot-set.test.ts +133 -0
  229. package/src/lib/interaction/hot-set.ts +67 -0
  230. package/src/lib/interaction/overlay-policy.test.ts +51 -0
  231. package/src/lib/interaction/overlay-policy.ts +59 -0
  232. package/src/lib/interaction/scope.ts +211 -0
  233. package/src/lib/level-duplication.ts +54 -42
  234. package/src/lib/level-selection.ts +2 -2
  235. package/src/lib/material-paint.ts +227 -48
  236. package/src/lib/measurement-kind.test.ts +30 -0
  237. package/src/lib/measurement-kind.ts +19 -0
  238. package/src/lib/measurement-label.test.ts +47 -0
  239. package/src/lib/measurement-label.ts +68 -0
  240. package/src/lib/measurement-parser.ts +116 -0
  241. package/src/lib/measurements.test.ts +183 -0
  242. package/src/lib/measurements.ts +199 -0
  243. package/src/lib/paint-scope.test.ts +454 -0
  244. package/src/lib/paint-scope.ts +461 -0
  245. package/src/lib/placement-drag-release.ts +23 -0
  246. package/src/lib/placement-metadata.ts +29 -0
  247. package/src/lib/planar-cursor-placement.test.ts +100 -0
  248. package/src/lib/planar-cursor-placement.ts +42 -0
  249. package/src/lib/plugin-panels.test.ts +19 -0
  250. package/src/lib/plugin-panels.ts +91 -0
  251. package/src/lib/quick-measurement.test.ts +77 -0
  252. package/src/lib/quick-measurement.ts +109 -0
  253. package/src/lib/roof-duplication.ts +7 -7
  254. package/src/lib/roof-hover-outline-proxy.ts +22 -0
  255. package/src/lib/roof-wall-hit.ts +164 -0
  256. package/src/lib/scene-clipboard.test.ts +196 -0
  257. package/src/lib/scene-clipboard.ts +332 -0
  258. package/src/lib/scene.ts +40 -8
  259. package/src/lib/selection-routing.test.ts +298 -0
  260. package/src/lib/selection-routing.ts +175 -0
  261. package/src/lib/sfx/index.ts +1 -0
  262. package/src/lib/sfx/movement-tick.test.ts +65 -0
  263. package/src/lib/sfx/movement-tick.ts +18 -0
  264. package/src/lib/sfx-bus.ts +69 -10
  265. package/src/lib/sfx-player.test.ts +124 -0
  266. package/src/lib/sfx-player.ts +148 -55
  267. package/src/lib/site-boundary.ts +1 -0
  268. package/src/lib/slab-plan-snap.test.ts +93 -0
  269. package/src/lib/slab-plan-snap.ts +108 -0
  270. package/src/lib/snapping-mode.test.ts +138 -0
  271. package/src/lib/snapping-mode.ts +179 -0
  272. package/src/lib/stair-duplication.ts +4 -4
  273. package/src/lib/stair-levels.test.ts +146 -0
  274. package/src/lib/stair-levels.ts +177 -0
  275. package/src/lib/surface-plan-snap.test.ts +71 -0
  276. package/src/lib/surface-plan-snap.ts +256 -0
  277. package/src/lib/use-linear-display.ts +40 -0
  278. package/src/lib/world-grid-snap.ts +231 -0
  279. package/src/lib/zone-content.ts +130 -0
  280. package/src/store/use-alignment-guides.ts +21 -0
  281. package/src/store/use-direct-manipulation-feedback.ts +20 -0
  282. package/src/store/use-editor.tsx +767 -114
  283. package/src/store/use-facing-pose.ts +44 -0
  284. package/src/store/use-fence-curve-draft.ts +21 -0
  285. package/src/store/use-floorplan-draft-preview.ts +101 -0
  286. package/src/store/use-floorplan-marquee.ts +50 -0
  287. package/src/store/use-interaction-scope.test.ts +186 -0
  288. package/src/store/use-interaction-scope.ts +132 -0
  289. package/src/store/use-measurement-draft.test.ts +530 -0
  290. package/src/store/use-measurement-draft.ts +543 -0
  291. package/src/store/use-opening-guides.ts +41 -0
  292. package/src/store/use-placement-preview.ts +39 -0
  293. package/src/store/use-quick-measurement-hud.test.ts +53 -0
  294. package/src/store/use-quick-measurement-hud.ts +77 -0
  295. package/src/store/use-segment-draft-chain.ts +28 -0
  296. package/src/store/use-stair-build-preview.ts +43 -0
  297. package/src/store/use-wall-move-ghosts.ts +36 -0
  298. package/src/store/use-wall-snap-indicator.ts +34 -0
  299. package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
  300. package/src/components/editor/preset-thumbnail-generator.tsx +0 -125
  301. package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +0 -113
  302. package/src/components/tools/building/move-building-tool.tsx +0 -157
  303. package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +0 -43
  304. package/src/components/tools/ceiling/ceiling-hole-editor.tsx +0 -49
  305. package/src/components/tools/ceiling/ceiling-tool.tsx +0 -465
  306. package/src/components/tools/ceiling/move-ceiling-tool.tsx +0 -264
  307. package/src/components/tools/column/column-tool.tsx +0 -97
  308. package/src/components/tools/column/move-column-tool.tsx +0 -105
  309. package/src/components/tools/door/door-math.ts +0 -110
  310. package/src/components/tools/door/door-tool.tsx +0 -324
  311. package/src/components/tools/door/move-door-tool.tsx +0 -412
  312. package/src/components/tools/fence/curve-fence-tool.tsx +0 -178
  313. package/src/components/tools/fence/fence-tool.tsx +0 -346
  314. package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -441
  315. package/src/components/tools/fence/move-fence-tool.tsx +0 -302
  316. package/src/components/tools/item/item-tool.tsx +0 -26
  317. package/src/components/tools/roof/move-roof-tool.tsx +0 -364
  318. package/src/components/tools/slab/move-slab-tool.tsx +0 -182
  319. package/src/components/tools/slab/slab-boundary-editor.tsx +0 -43
  320. package/src/components/tools/slab/slab-hole-editor.tsx +0 -49
  321. package/src/components/tools/slab/slab-tool.tsx +0 -322
  322. package/src/components/tools/spawn/move-spawn-tool.tsx +0 -101
  323. package/src/components/tools/spawn/spawn-tool.tsx +0 -130
  324. package/src/components/tools/wall/curve-wall-tool.tsx +0 -178
  325. package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -426
  326. package/src/components/tools/wall/move-wall-tool.tsx +0 -358
  327. package/src/components/tools/wall/wall-tool.tsx +0 -332
  328. package/src/components/tools/window/move-window-tool.tsx +0 -447
  329. package/src/components/tools/window/window-math.ts +0 -117
  330. package/src/components/tools/window/window-tool.tsx +0 -332
  331. package/src/components/ui/helpers/ceiling-helper.tsx +0 -20
  332. package/src/components/ui/helpers/slab-helper.tsx +0 -20
  333. package/src/components/ui/helpers/wall-helper.tsx +0 -20
  334. package/src/components/ui/panels/ceiling-panel.tsx +0 -249
  335. package/src/components/ui/panels/column-panel.tsx +0 -759
  336. package/src/components/ui/panels/door-panel.tsx +0 -1299
  337. package/src/components/ui/panels/fence-panel.tsx +0 -222
  338. package/src/components/ui/panels/item-panel.tsx +0 -306
  339. package/src/components/ui/panels/paint-panel.tsx +0 -163
  340. package/src/components/ui/panels/presets/presets-popover.tsx +0 -511
  341. package/src/components/ui/panels/roof-panel.tsx +0 -280
  342. package/src/components/ui/panels/roof-segment-panel.tsx +0 -311
  343. package/src/components/ui/panels/slab-panel.tsx +0 -250
  344. package/src/components/ui/panels/spawn-panel.tsx +0 -161
  345. package/src/components/ui/panels/stair-panel.tsx +0 -577
  346. package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
  347. package/src/components/ui/panels/wall-panel.tsx +0 -273
  348. package/src/components/ui/panels/window-panel.tsx +0 -968
  349. package/src/contexts/presets-context.tsx +0 -121
  350. package/src/hooks/use-contextual-tools.ts +0 -61
@@ -0,0 +1,489 @@
1
+ import {
2
+ type AnyNode,
3
+ type AnyNodeId,
4
+ nodeRegistry,
5
+ resolveBuildingForLevel,
6
+ sceneRegistry,
7
+ } from '@pascal-app/core'
8
+ import { Box3, Matrix4 } from 'three'
9
+
10
+ // Shared plumbing for the group transform gizmos (rotate + move). Both operate
11
+ // on the same multi-selection: classify each participant by how its placement
12
+ // transforms, snapshot pre-drag state, and pull connected wall/fence neighbours
13
+ // along so junctions stay welded.
14
+
15
+ // Outward clearance from a bbox corner so a gizmo doesn't sit on the geometry.
16
+ export const CORNER_OFFSET = 0.3
17
+ // Two endpoints within this distance count as the same junction — a hair looser
18
+ // than the store's 1e-6 so near-but-not-exact corners still hold together.
19
+ const JUNCTION_EPS = 1e-4
20
+
21
+ export type Vec2 = [number, number]
22
+ export type Vec3 = [number, number, number]
23
+
24
+ const isVec3 = (v: unknown): v is Vec3 =>
25
+ Array.isArray(v) && v.length === 3 && v.every((n) => typeof n === 'number')
26
+ const isVec2 = (v: unknown): v is Vec2 =>
27
+ Array.isArray(v) && v.length === 2 && v.every((n) => typeof n === 'number')
28
+ const isVec2Array = (v: unknown): v is Vec2[] => Array.isArray(v) && v.length > 0 && v.every(isVec2)
29
+
30
+ // How a participant's placement transforms rigidly around / with the group:
31
+ // - 'vec3' position + [x,y,z] rotation (items, …)
32
+ // - 'scalar' position + numeric rotation (columns)
33
+ // - 'endpoint' start/end tuples (walls, fences)
34
+ // - 'polygon' [x,z] vertex arrays (slabs, ceilings, zones) — the placement
35
+ // lives in the vertices themselves (plus optional hole rings)
36
+ export type ParticipantKind = 'vec3' | 'scalar' | 'endpoint' | 'polygon'
37
+
38
+ // A selected node qualifies when it belongs to the active level's horizontal
39
+ // frame: either parented to that level, or declared building-scoped and parented
40
+ // to the active level's building. Doors/windows parent to their wall, so they're
41
+ // excluded here and ride their wall.
42
+ function isInGroupTransformScope(
43
+ node: AnyNode | undefined,
44
+ levelId: string | null,
45
+ sceneNodes: Record<string, AnyNode | undefined>,
46
+ ): boolean {
47
+ if (!node || !levelId) return false
48
+ if (node.parentId === levelId) return true
49
+
50
+ if (nodeRegistry.get(node.type)?.floorplanScope !== 'building') {
51
+ return false
52
+ }
53
+
54
+ const buildingId = resolveBuildingForLevel(
55
+ levelId as AnyNodeId,
56
+ sceneNodes as Record<AnyNodeId, AnyNode>,
57
+ )
58
+ return Boolean(buildingId && node.parentId === buildingId)
59
+ }
60
+
61
+ function getLegacyScenePosition(node: AnyNode): Vec3 | null {
62
+ if (node.type !== 'elevator') return null
63
+ const object = sceneRegistry.nodes.get(node.id)
64
+ if (!object) return [0, 0, 0]
65
+ return [object.position.x, object.position.y, object.position.z]
66
+ }
67
+
68
+ function getParticipantPosition(node: AnyNode): Vec3 | null {
69
+ const p = (node as { position?: unknown }).position
70
+ if (isVec3(p)) return p
71
+ return getLegacyScenePosition(node)
72
+ }
73
+
74
+ function getParticipantScalarRotation(node: AnyNode): number | null {
75
+ const r = (node as { rotation?: unknown }).rotation
76
+ if (typeof r === 'number' && Number.isFinite(r)) return r
77
+ if (node.type !== 'elevator') return null
78
+ return sceneRegistry.nodes.get(node.id)?.rotation.y ?? 0
79
+ }
80
+
81
+ // Shape-only classification of a positioned placement (no level-scope check).
82
+ // Used for polygon hosts' attached children, whose parent is the host rather
83
+ // than the level.
84
+ function classifyPlacementShape(node: AnyNode): 'vec3' | 'scalar' | null {
85
+ const p = getParticipantPosition(node)
86
+ const r = (node as { rotation?: unknown }).rotation
87
+ if (isVec3(p) && isVec3(r)) return 'vec3'
88
+ if (isVec3(p) && getParticipantScalarRotation(node) !== null) return 'scalar'
89
+ return null
90
+ }
91
+
92
+ export function classifyParticipant(
93
+ node: AnyNode | undefined,
94
+ levelId: string | null,
95
+ sceneNodes: Record<string, AnyNode | undefined>,
96
+ ): ParticipantKind | null {
97
+ if (!node || !isInGroupTransformScope(node, levelId, sceneNodes)) return null
98
+ const shape = classifyPlacementShape(node)
99
+ if (shape) return shape
100
+ const start = (node as { start?: unknown }).start
101
+ const end = (node as { end?: unknown }).end
102
+ if (isVec2(start) && isVec2(end)) return 'endpoint'
103
+ if (isVec2Array((node as { polygon?: unknown }).polygon)) return 'polygon'
104
+ return null
105
+ }
106
+
107
+ // Pre-drag placement snapshot + how to transform it. `holes` is null when the
108
+ // kind carries no holes field (zone), so patches never write one onto it.
109
+ export type ParticipantStart =
110
+ | { id: AnyNodeId; kind: 'vec3'; position: Vec3; rotation: Vec3 }
111
+ | { id: AnyNodeId; kind: 'scalar'; position: Vec3; rotation: number }
112
+ | { id: AnyNodeId; kind: 'endpoint'; start: Vec2; end: Vec2 }
113
+ | { id: AnyNodeId; kind: 'polygon'; polygon: Vec2[]; holes: Vec2[][] | null }
114
+
115
+ // An unselected wall/fence sharing a junction with a transforming endpoint. Only
116
+ // the touching endpoint(s) follow, so the neighbour stays attached while its far
117
+ // end stays put (it stretches, mirroring single-wall move).
118
+ export type LinkedNeighbor = {
119
+ id: AnyNodeId
120
+ start: Vec2
121
+ end: Vec2
122
+ startLinked: boolean
123
+ endLinked: boolean
124
+ }
125
+
126
+ const nearPoint = (a: Vec2, b: Vec2) =>
127
+ Math.abs(a[0] - b[0]) <= JUNCTION_EPS && Math.abs(a[1] - b[1]) <= JUNCTION_EPS
128
+
129
+ // Snapshot the selected participants and the connected (unselected) wall/fence
130
+ // neighbours whose shared endpoints should follow the transform.
131
+ export function collectParticipants(
132
+ ids: string[],
133
+ sceneNodes: Record<string, AnyNode | undefined>,
134
+ levelId: string | null,
135
+ ): { starts: ParticipantStart[]; links: LinkedNeighbor[] } {
136
+ const starts: ParticipantStart[] = []
137
+ for (const id of ids) {
138
+ const node = sceneNodes[id]
139
+ const kind = classifyParticipant(node, levelId, sceneNodes)
140
+ if (!node || !kind) continue
141
+ if (kind === 'vec3') {
142
+ const n = node as AnyNode & { position: Vec3; rotation: Vec3 }
143
+ const position = getParticipantPosition(node)
144
+ if (!position) continue
145
+ starts.push({
146
+ id: id as AnyNodeId,
147
+ kind,
148
+ position: [position[0], position[1], position[2]],
149
+ rotation: [n.rotation[0], n.rotation[1], n.rotation[2]],
150
+ })
151
+ } else if (kind === 'scalar') {
152
+ const position = getParticipantPosition(node)
153
+ const rotation = getParticipantScalarRotation(node)
154
+ if (!(position && rotation !== null)) continue
155
+ starts.push({
156
+ id: id as AnyNodeId,
157
+ kind,
158
+ position: [position[0], position[1], position[2]],
159
+ rotation,
160
+ })
161
+ } else if (kind === 'endpoint') {
162
+ const n = node as AnyNode & { start: Vec2; end: Vec2 }
163
+ starts.push({
164
+ id: id as AnyNodeId,
165
+ kind,
166
+ start: [n.start[0], n.start[1]],
167
+ end: [n.end[0], n.end[1]],
168
+ })
169
+ } else {
170
+ const n = node as AnyNode & { polygon: Vec2[]; holes?: Vec2[][] }
171
+ starts.push({
172
+ id: id as AnyNodeId,
173
+ kind,
174
+ polygon: n.polygon.map(([x, z]) => [x, z] as Vec2),
175
+ holes: Array.isArray(n.holes)
176
+ ? n.holes.map((hole) => hole.map(([x, z]) => [x, z] as Vec2))
177
+ : null,
178
+ })
179
+ }
180
+ }
181
+
182
+ // Polygon hosts (slab/ceiling) rebuild their geometry from vertices rather
183
+ // than transforming a group, so — unlike wall children, which ride the wall
184
+ // mesh — their attached children (ceiling-mounted items) must transform
185
+ // explicitly. Their positions are stored in the level frame (the host group
186
+ // sits at the origin), so the same rigid patches apply.
187
+ const includedIds = new Set<string>(starts.map((s) => s.id))
188
+ for (const s of [...starts]) {
189
+ if (s.kind !== 'polygon') continue
190
+ const host = sceneNodes[s.id]
191
+ const childIds = (host as { children?: string[] } | undefined)?.children
192
+ if (!Array.isArray(childIds)) continue
193
+ for (const childId of childIds) {
194
+ if (includedIds.has(childId)) continue
195
+ const child = sceneNodes[childId]
196
+ if (!child) continue
197
+ const shape = classifyPlacementShape(child)
198
+ const position = child ? getParticipantPosition(child) : null
199
+ if (!(shape && position)) continue
200
+ if (shape === 'vec3') {
201
+ const c = child as AnyNode & { rotation: Vec3 }
202
+ starts.push({
203
+ id: childId as AnyNodeId,
204
+ kind: 'vec3',
205
+ position: [position[0], position[1], position[2]],
206
+ rotation: [c.rotation[0], c.rotation[1], c.rotation[2]],
207
+ })
208
+ } else {
209
+ const rotation = getParticipantScalarRotation(child)
210
+ if (rotation === null) continue
211
+ starts.push({
212
+ id: childId as AnyNodeId,
213
+ kind: 'scalar',
214
+ position: [position[0], position[1], position[2]],
215
+ rotation,
216
+ })
217
+ }
218
+ includedIds.add(childId)
219
+ }
220
+ }
221
+
222
+ const endpoints: Vec2[] = []
223
+ for (const s of starts) {
224
+ if (s.kind === 'endpoint') endpoints.push(s.start, s.end)
225
+ }
226
+ const links: LinkedNeighbor[] = []
227
+ if (endpoints.length > 0) {
228
+ const selected = new Set(starts.map((s) => s.id))
229
+ for (const [nid, node] of Object.entries(sceneNodes)) {
230
+ if (selected.has(nid as AnyNodeId)) continue
231
+ if (classifyParticipant(node, levelId, sceneNodes) !== 'endpoint') continue
232
+ const n = node as AnyNode & { start: Vec2; end: Vec2 }
233
+ const start: Vec2 = [n.start[0], n.start[1]]
234
+ const end: Vec2 = [n.end[0], n.end[1]]
235
+ const startLinked = endpoints.some((p) => nearPoint(start, p))
236
+ const endLinked = endpoints.some((p) => nearPoint(end, p))
237
+ if (startLinked || endLinked) {
238
+ links.push({ id: nid as AnyNodeId, start, end, startLinked, endLinked })
239
+ }
240
+ }
241
+ }
242
+ return { starts, links }
243
+ }
244
+
245
+ // Grow a selection to the full connected component of walls/fences: any
246
+ // endpoint node transitively reachable through shared junctions from a selected
247
+ // endpoint node joins in, so the whole rigid structure transforms as one piece
248
+ // (rather than tearing/stretching at the boundary). Non-endpoint selections
249
+ // (items, columns) pass through unchanged.
250
+ export function expandToComponent(
251
+ selectedIds: string[],
252
+ sceneNodes: Record<string, AnyNode | undefined>,
253
+ levelId: string | null,
254
+ ): string[] {
255
+ const endpoints: { id: string; start: Vec2; end: Vec2 }[] = []
256
+ for (const [id, node] of Object.entries(sceneNodes)) {
257
+ if (classifyParticipant(node, levelId, sceneNodes) === 'endpoint') {
258
+ const n = node as AnyNode & { start: Vec2; end: Vec2 }
259
+ endpoints.push({ id, start: [n.start[0], n.start[1]], end: [n.end[0], n.end[1]] })
260
+ }
261
+ }
262
+ const included = new Set(selectedIds)
263
+ if (!endpoints.some((e) => included.has(e.id))) return selectedIds
264
+
265
+ let changed = true
266
+ while (changed) {
267
+ changed = false
268
+ for (const e of endpoints) {
269
+ if (included.has(e.id)) continue
270
+ const touches = endpoints.some(
271
+ (o) =>
272
+ included.has(o.id) &&
273
+ (nearPoint(e.start, o.start) ||
274
+ nearPoint(e.start, o.end) ||
275
+ nearPoint(e.end, o.start) ||
276
+ nearPoint(e.end, o.end)),
277
+ )
278
+ if (touches) {
279
+ included.add(e.id)
280
+ changed = true
281
+ }
282
+ }
283
+ }
284
+ return Array.from(included)
285
+ }
286
+
287
+ // Per-node field patch, keyed for `useLiveNodeOverrides.setMany` during a live
288
+ // preview and for the single batched `updateNodes` on commit.
289
+ export type GroupPatch = readonly [AnyNodeId, Record<string, unknown>]
290
+
291
+ // Rigid group rotation: orbit each participant's anchor point(s) CCW by
292
+ // `delta` (atan2 x→z sense) around `center` (level-frame XZ) and turn yaws by
293
+ // `-delta` to match three.js Y-rotation handedness (same convention as the
294
+ // single-item rotate handle in item/definition.ts). Endpoint nodes
295
+ // (walls/fences) have no yaw — swinging both endpoints around the pivot
296
+ // rotates them rigidly; their curveOffset sagitta is rotation-invariant, so
297
+ // arcs are preserved. Linked neighbours' shared endpoints land exactly on the
298
+ // selected wall's rotated endpoint (rot is deterministic), keeping junctions
299
+ // welded while the far end stays put.
300
+ export function rotateGroupPatches(
301
+ starts: ParticipantStart[],
302
+ links: LinkedNeighbor[],
303
+ center: { x: number; z: number },
304
+ delta: number,
305
+ ): GroupPatch[] {
306
+ const cos = Math.cos(delta)
307
+ const sin = Math.sin(delta)
308
+ const rot = (x: number, z: number): Vec2 => {
309
+ const dx = x - center.x
310
+ const dz = z - center.z
311
+ return [center.x + dx * cos - dz * sin, center.z + dx * sin + dz * cos]
312
+ }
313
+ const patches: GroupPatch[] = []
314
+ for (const s of starts) {
315
+ if (s.kind === 'endpoint') {
316
+ patches.push([s.id, { start: rot(s.start[0], s.start[1]), end: rot(s.end[0], s.end[1]) }])
317
+ } else if (s.kind === 'polygon') {
318
+ const patch: Record<string, unknown> = { polygon: s.polygon.map(([x, z]) => rot(x, z)) }
319
+ if (s.holes) patch.holes = s.holes.map((hole) => hole.map(([x, z]) => rot(x, z)))
320
+ patches.push([s.id, patch])
321
+ } else {
322
+ const [px, pz] = rot(s.position[0], s.position[2])
323
+ const position: Vec3 = [px, s.position[1], pz]
324
+ const rotation =
325
+ s.kind === 'vec3'
326
+ ? ([s.rotation[0], s.rotation[1] - delta, s.rotation[2]] as Vec3)
327
+ : s.rotation - delta
328
+ patches.push([s.id, { position, rotation }])
329
+ }
330
+ }
331
+ for (const l of links) {
332
+ patches.push([
333
+ l.id,
334
+ {
335
+ start: l.startLinked ? rot(l.start[0], l.start[1]) : l.start,
336
+ end: l.endLinked ? rot(l.end[0], l.end[1]) : l.end,
337
+ },
338
+ ])
339
+ }
340
+ return patches
341
+ }
342
+
343
+ // Rotate the SNAPSHOTS themselves (same math as `rotateGroupPatches`, but
344
+ // producing new snapshot shapes instead of node patches). Lets an in-flight
345
+ // group move re-seed its rest state after a mid-drag R/T: subsequent
346
+ // translate patches then place the rotated layout at the live delta.
347
+ export function rotateGroupSnapshots(
348
+ starts: ParticipantStart[],
349
+ links: LinkedNeighbor[],
350
+ center: { x: number; z: number },
351
+ delta: number,
352
+ ): { starts: ParticipantStart[]; links: LinkedNeighbor[] } {
353
+ const cos = Math.cos(delta)
354
+ const sin = Math.sin(delta)
355
+ const rot = (x: number, z: number): Vec2 => {
356
+ const dx = x - center.x
357
+ const dz = z - center.z
358
+ return [center.x + dx * cos - dz * sin, center.z + dx * sin + dz * cos]
359
+ }
360
+ const rotatedStarts = starts.map((s): ParticipantStart => {
361
+ if (s.kind === 'endpoint') {
362
+ return { ...s, start: rot(s.start[0], s.start[1]), end: rot(s.end[0], s.end[1]) }
363
+ }
364
+ if (s.kind === 'polygon') {
365
+ return {
366
+ ...s,
367
+ polygon: s.polygon.map(([x, z]) => rot(x, z)),
368
+ holes: s.holes ? s.holes.map((hole) => hole.map(([x, z]) => rot(x, z))) : null,
369
+ }
370
+ }
371
+ const [px, pz] = rot(s.position[0], s.position[2])
372
+ const position: Vec3 = [px, s.position[1], pz]
373
+ if (s.kind === 'vec3') {
374
+ return { ...s, position, rotation: [s.rotation[0], s.rotation[1] - delta, s.rotation[2]] }
375
+ }
376
+ return { ...s, position, rotation: s.rotation - delta }
377
+ })
378
+ // Only the welded (linked) endpoints follow the rotation; the far ends
379
+ // stay put, exactly like the per-tick patch path.
380
+ const rotatedLinks = links.map(
381
+ (l): LinkedNeighbor => ({
382
+ ...l,
383
+ start: l.startLinked ? rot(l.start[0], l.start[1]) : l.start,
384
+ end: l.endLinked ? rot(l.end[0], l.end[1]) : l.end,
385
+ }),
386
+ )
387
+ return { starts: rotatedStarts, links: rotatedLinks }
388
+ }
389
+
390
+ // Level-frame XZ extents of the participant DATA — the mesh-free sibling of
391
+ // `computeGroupBox`, used when meshes aren't mounted yet and to re-seed
392
+ // alignment anchors after a mid-drag rotation.
393
+ export function participantExtents(
394
+ starts: ParticipantStart[],
395
+ ): { minX: number; minZ: number; maxX: number; maxZ: number } | null {
396
+ let minX = Number.POSITIVE_INFINITY
397
+ let minZ = Number.POSITIVE_INFINITY
398
+ let maxX = Number.NEGATIVE_INFINITY
399
+ let maxZ = Number.NEGATIVE_INFINITY
400
+ const reach = (x: number, z: number) => {
401
+ minX = Math.min(minX, x)
402
+ minZ = Math.min(minZ, z)
403
+ maxX = Math.max(maxX, x)
404
+ maxZ = Math.max(maxZ, z)
405
+ }
406
+ for (const s of starts) {
407
+ if (s.kind === 'endpoint') {
408
+ reach(s.start[0], s.start[1])
409
+ reach(s.end[0], s.end[1])
410
+ } else if (s.kind === 'polygon') {
411
+ for (const [x, z] of s.polygon) {
412
+ reach(x, z)
413
+ }
414
+ } else {
415
+ reach(s.position[0], s.position[2])
416
+ }
417
+ }
418
+ if (!Number.isFinite(minX)) return null
419
+ return { minX, minZ, maxX, maxZ }
420
+ }
421
+
422
+ // Rigid group slide: shift every participant (and each linked neighbour's
423
+ // shared endpoint) by the same level-frame XZ delta. Y and rotations untouched.
424
+ export function translateGroupPatches(
425
+ starts: ParticipantStart[],
426
+ links: LinkedNeighbor[],
427
+ dx: number,
428
+ dz: number,
429
+ ): GroupPatch[] {
430
+ const patches: GroupPatch[] = []
431
+ const shift = ([x, z]: Vec2): Vec2 => [x + dx, z + dz]
432
+ for (const s of starts) {
433
+ if (s.kind === 'endpoint') {
434
+ patches.push([s.id, { start: shift(s.start), end: shift(s.end) }])
435
+ } else if (s.kind === 'polygon') {
436
+ const patch: Record<string, unknown> = { polygon: s.polygon.map(shift) }
437
+ if (s.holes) patch.holes = s.holes.map((hole) => hole.map(shift))
438
+ patches.push([s.id, patch])
439
+ } else {
440
+ patches.push([s.id, { position: [s.position[0] + dx, s.position[1], s.position[2] + dz] }])
441
+ }
442
+ }
443
+ for (const l of links) {
444
+ patches.push([
445
+ l.id,
446
+ {
447
+ start: l.startLinked ? [l.start[0] + dx, l.start[1] + dz] : l.start,
448
+ end: l.endLinked ? [l.end[0] + dx, l.end[1] + dz] : l.end,
449
+ },
450
+ ])
451
+ }
452
+ return patches
453
+ }
454
+
455
+ // Frozen world matrix of the level group + its inverse. A node's placement
456
+ // (`position` / `start` / `end`) is stored in its parent level's frame, but the
457
+ // gizmos raycast the ground plane in WORLD space. When the building is rotated
458
+ // those frames diverge, so a world-space drag delta / rotation pivot must be
459
+ // converted into the level frame before it's written back to placements —
460
+ // otherwise the move drifts off-axis from the cursor and the rotation orbits a
461
+ // displaced centre. Returns identity matrices when the level isn't mounted, which
462
+ // collapses to the old behaviour (world == local) for an unrotated building.
463
+ export function levelFrame(levelId: string | null): { matrix: Matrix4; inverse: Matrix4 } {
464
+ const obj = levelId ? sceneRegistry.nodes.get(levelId as AnyNodeId) : null
465
+ if (!obj) return { matrix: new Matrix4(), inverse: new Matrix4() }
466
+ obj.updateWorldMatrix(true, false)
467
+ const matrix = obj.matrixWorld.clone()
468
+ return { matrix, inverse: matrix.clone().invert() }
469
+ }
470
+
471
+ // World-space union bounding box of the selected meshes, or null if none are
472
+ // mounted yet. Used to place the gizmos (which are portalled to the scene root,
473
+ // so they live in world space); placement writes convert back to the level frame
474
+ // via `levelFrame`.
475
+ export function computeGroupBox(ids: string[]): Box3 | null {
476
+ const box = new Box3()
477
+ const tmp = new Box3()
478
+ let found = false
479
+ for (const id of ids) {
480
+ const obj = sceneRegistry.nodes.get(id)
481
+ if (!obj) continue
482
+ obj.updateWorldMatrix(true, true)
483
+ tmp.setFromObject(obj)
484
+ if (tmp.isEmpty()) continue
485
+ box.union(tmp)
486
+ found = true
487
+ }
488
+ return found ? box : null
489
+ }