@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,263 @@
1
+ import {
2
+ type AnyNodeId,
3
+ type BuildingNode,
4
+ type ElevatorNode,
5
+ ElevatorNode as ElevatorNodeSchema,
6
+ emitter,
7
+ type GridEvent,
8
+ type LevelNode,
9
+ pauseSceneHistory,
10
+ resumeSceneHistory,
11
+ sceneRegistry,
12
+ useLiveTransforms,
13
+ useScene,
14
+ } from '@pascal-app/core'
15
+ import { useCallback, useEffect, useRef, useState } from 'react'
16
+ import { markToolCancelConsumed } from '../../../hooks/use-keyboard'
17
+ import { resolveElevatorSupportY } from '../../../lib/elevator-support'
18
+ import { consumePlacementDragRelease } from '../../../lib/placement-drag-release'
19
+ import { sfxEmitter } from '../../../lib/sfx-bus'
20
+ import useEditor from '../../../store/use-editor'
21
+ import { CursorSphere } from '../shared/cursor-sphere'
22
+
23
+ function stripMoveMetadata(metadata: ElevatorNode['metadata']) {
24
+ if (typeof metadata !== 'object' || metadata === null || Array.isArray(metadata)) {
25
+ return metadata
26
+ }
27
+
28
+ const nextMeta = { ...(metadata as Record<string, unknown>) }
29
+ delete nextMeta.isNew
30
+ delete nextMeta.isTransient
31
+ return nextMeta as ElevatorNode['metadata']
32
+ }
33
+
34
+ export function MoveElevatorTool({
35
+ node: movingNode,
36
+ onCommitted,
37
+ }: {
38
+ node: ElevatorNode
39
+ onCommitted?: (nodeId: AnyNodeId) => void
40
+ }) {
41
+ const onCommittedRef = useRef(onCommitted)
42
+ const historyPausedRef = useRef(false)
43
+ const previousGridPosRef = useRef<[number, number] | null>(null)
44
+ const dragAnchorRef = useRef<[number, number] | null>(null)
45
+ const previewPositionRef = useRef<ElevatorNode['position']>([
46
+ movingNode.position[0],
47
+ movingNode.position[1],
48
+ movingNode.position[2],
49
+ ])
50
+ const [cursorPosition, setCursorPosition] = useState<[number, number, number]>(() => [
51
+ movingNode.position[0],
52
+ movingNode.position[1],
53
+ movingNode.position[2],
54
+ ])
55
+
56
+ const exitMoveMode = useCallback(() => {
57
+ useEditor.getState().setMovingNode(null)
58
+ }, [])
59
+
60
+ useEffect(() => {
61
+ onCommittedRef.current = onCommitted
62
+ }, [onCommitted])
63
+
64
+ useEffect(() => {
65
+ const pauseHistory = () => {
66
+ const temporal = useScene.temporal.getState()
67
+ if (historyPausedRef.current || !temporal.isTracking) return
68
+ pauseSceneHistory(useScene)
69
+ historyPausedRef.current = true
70
+ }
71
+ const resumeHistory = () => {
72
+ if (!historyPausedRef.current) return
73
+ resumeSceneHistory(useScene)
74
+ historyPausedRef.current = false
75
+ }
76
+
77
+ pauseHistory()
78
+ dragAnchorRef.current = null
79
+ previousGridPosRef.current = null
80
+ const movingNodeId = (movingNode as { id?: ElevatorNode['id'] }).id
81
+
82
+ const meta =
83
+ typeof movingNode.metadata === 'object' && movingNode.metadata !== null
84
+ ? (movingNode.metadata as Record<string, unknown>)
85
+ : {}
86
+ const isNew = !!meta.isNew
87
+ const committedMeta = stripMoveMetadata(movingNode.metadata)
88
+ const original = {
89
+ position: [...movingNode.position] as ElevatorNode['position'],
90
+ rotation: movingNode.rotation,
91
+ metadata: movingNode.metadata,
92
+ }
93
+
94
+ let wasCommitted = false
95
+ let wasCancelled = false
96
+ let pendingRotation = movingNode.rotation
97
+ const supportBuildingId = movingNode.parentId as BuildingNode['id'] | null | undefined
98
+ const supportLevelId = (movingNode.fromLevelId ?? movingNode.defaultLevelId) as
99
+ | LevelNode['id']
100
+ | null
101
+
102
+ const applyPreview = (
103
+ position: ElevatorNode['position'],
104
+ rotation: ElevatorNode['rotation'],
105
+ ) => {
106
+ if (movingNodeId) {
107
+ useLiveTransforms.getState().set(movingNodeId, { position, rotation })
108
+ }
109
+
110
+ const object = movingNodeId ? sceneRegistry.nodes.get(movingNodeId) : null
111
+ if (object) {
112
+ object.position.set(position[0], position[1], position[2])
113
+ object.rotation.y = rotation
114
+ }
115
+ }
116
+
117
+ const resetObject = (
118
+ position: ElevatorNode['position'],
119
+ rotation: ElevatorNode['rotation'],
120
+ ) => {
121
+ const object = movingNodeId ? sceneRegistry.nodes.get(movingNodeId) : null
122
+ if (object) {
123
+ object.position.set(position[0], position[1], position[2])
124
+ object.rotation.y = rotation
125
+ }
126
+ }
127
+
128
+ const clearPreview = () => {
129
+ if (movingNodeId) {
130
+ useLiveTransforms.getState().clear(movingNodeId)
131
+ }
132
+ }
133
+
134
+ const onGridMove = (event: GridEvent) => {
135
+ const bypassSnap = event.nativeEvent?.shiftKey === true
136
+ const rawX = bypassSnap ? event.localPosition[0] : Math.round(event.localPosition[0] * 2) / 2
137
+ const rawZ = bypassSnap ? event.localPosition[2] : Math.round(event.localPosition[2] * 2) / 2
138
+ const anchor = dragAnchorRef.current ?? [rawX, rawZ]
139
+ dragAnchorRef.current = anchor
140
+ const gridX = movingNode.position[0] + (rawX - anchor[0])
141
+ const gridZ = movingNode.position[2] + (rawZ - anchor[1])
142
+ const supportY = resolveElevatorSupportY({
143
+ buildingId: supportBuildingId,
144
+ preferredLevelId: supportLevelId,
145
+ x: gridX,
146
+ z: gridZ,
147
+ })
148
+
149
+ if (
150
+ !bypassSnap &&
151
+ previousGridPosRef.current &&
152
+ (gridX !== previousGridPosRef.current[0] || gridZ !== previousGridPosRef.current[1])
153
+ ) {
154
+ sfxEmitter.emit('sfx:grid-snap')
155
+ }
156
+
157
+ previousGridPosRef.current = [gridX, gridZ]
158
+ setCursorPosition([gridX, supportY, gridZ])
159
+ previewPositionRef.current = [gridX, supportY, gridZ]
160
+ applyPreview(previewPositionRef.current, pendingRotation)
161
+ }
162
+
163
+ const onGridClick = (event: GridEvent) => {
164
+ if (wasCommitted) return
165
+ const nextPosition: ElevatorNode['position'] = [...previewPositionRef.current]
166
+
167
+ wasCommitted = true
168
+ clearPreview()
169
+ resumeHistory()
170
+ if (movingNodeId && useScene.getState().nodes[movingNodeId as AnyNodeId]) {
171
+ useScene.getState().updateNode(movingNodeId as AnyNodeId, {
172
+ position: nextPosition,
173
+ rotation: pendingRotation,
174
+ metadata: committedMeta,
175
+ })
176
+ onCommittedRef.current?.(movingNodeId as AnyNodeId)
177
+ } else if (movingNode.parentId) {
178
+ const elevator = ElevatorNodeSchema.parse({
179
+ ...movingNode,
180
+ id: undefined,
181
+ position: nextPosition,
182
+ rotation: pendingRotation,
183
+ metadata: committedMeta,
184
+ })
185
+ useScene.getState().createNode(elevator, movingNode.parentId as AnyNodeId)
186
+ onCommittedRef.current?.(elevator.id as AnyNodeId)
187
+ }
188
+
189
+ sfxEmitter.emit('sfx:item-place')
190
+ exitMoveMode()
191
+ event.nativeEvent?.stopPropagation?.()
192
+ }
193
+
194
+ const onPlacementDragPointerUp = (event: PointerEvent) => {
195
+ if (!consumePlacementDragRelease(event)) return
196
+ onGridClick({ nativeEvent: event } as unknown as GridEvent)
197
+ }
198
+
199
+ const onCancel = () => {
200
+ wasCancelled = true
201
+ clearPreview()
202
+ if (isNew && movingNodeId) {
203
+ useScene.getState().deleteNode(movingNodeId as AnyNodeId)
204
+ } else {
205
+ if (movingNodeId) {
206
+ useScene.getState().updateNode(movingNodeId as AnyNodeId, {
207
+ position: original.position,
208
+ rotation: original.rotation,
209
+ metadata: original.metadata,
210
+ })
211
+ }
212
+ }
213
+ resetObject(original.position, original.rotation)
214
+ resumeHistory()
215
+ markToolCancelConsumed()
216
+ exitMoveMode()
217
+ }
218
+
219
+ const onKeyDown = (event: KeyboardEvent) => {
220
+ if (event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement) {
221
+ return
222
+ }
223
+
224
+ const ROTATION_STEP = Math.PI / 4
225
+ let rotationDelta = 0
226
+ if (event.key === 'r' || event.key === 'R') rotationDelta = ROTATION_STEP
227
+ else if (event.key === 't' || event.key === 'T') rotationDelta = -ROTATION_STEP
228
+
229
+ if (rotationDelta !== 0) {
230
+ event.preventDefault()
231
+ sfxEmitter.emit('sfx:item-rotate')
232
+ pendingRotation += rotationDelta
233
+ applyPreview(previewPositionRef.current, pendingRotation)
234
+ }
235
+ }
236
+
237
+ emitter.on('grid:move', onGridMove)
238
+ emitter.on('grid:click', onGridClick)
239
+ emitter.on('tool:cancel', onCancel)
240
+ window.addEventListener('keydown', onKeyDown)
241
+ window.addEventListener('pointerup', onPlacementDragPointerUp)
242
+
243
+ return () => {
244
+ clearPreview()
245
+ if (!(wasCommitted || wasCancelled || isNew) && movingNodeId) {
246
+ useScene.getState().updateNode(movingNodeId as AnyNodeId, {
247
+ position: original.position,
248
+ rotation: original.rotation,
249
+ metadata: original.metadata,
250
+ })
251
+ resetObject(original.position, original.rotation)
252
+ }
253
+ resumeHistory()
254
+ emitter.off('grid:move', onGridMove)
255
+ emitter.off('grid:click', onGridClick)
256
+ emitter.off('tool:cancel', onCancel)
257
+ window.removeEventListener('keydown', onKeyDown)
258
+ window.removeEventListener('pointerup', onPlacementDragPointerUp)
259
+ }
260
+ }, [movingNode, exitMoveMode])
261
+
262
+ return <CursorSphere position={cursorPosition} showTooltip={false} />
263
+ }
@@ -1,25 +1,30 @@
1
1
  import {
2
+ DEFAULT_ANGLE_STEP,
2
3
  FenceNode,
4
+ getTwoPointFenceCurveTangents,
3
5
  getWallCurveFrameAt,
4
6
  getWallCurveLength,
5
7
  isCurvedWall,
8
+ snapPointAlongAngleRay,
6
9
  useScene,
7
10
  type WallNode,
8
11
  } from '@pascal-app/core'
9
12
  import { useViewer } from '@pascal-app/viewer'
10
13
  import { sfxEmitter } from '../../../lib/sfx-bus'
14
+ import useEditor from '../../../store/use-editor'
11
15
  import {
12
16
  findWallSnapTarget,
13
- getWallAngleSnapStep,
14
- getWallGridStep,
15
- isWallLongEnough,
16
- snapPointTo45Degrees,
17
+ getSegmentGridStep,
18
+ isSegmentLongEnough,
17
19
  snapPointToGrid,
18
20
  type WallPlanPoint,
19
21
  } from '../wall/wall-drafting'
20
22
 
21
23
  export type FencePlanPoint = WallPlanPoint
22
24
 
25
+ const FENCE_CORNER_SNAP_RADIUS = 0.28
26
+ const FENCE_SPAN_SNAP_RADIUS = 0.16
27
+
23
28
  type SegmentNode = {
24
29
  start: FencePlanPoint
25
30
  end: FencePlanPoint
@@ -57,46 +62,68 @@ function findFenceSnapTarget(
57
62
  fences: FenceNode[],
58
63
  ignoreFenceIds: string[] = [],
59
64
  ): FencePlanPoint | null {
60
- const radiusSquared = 0.35 ** 2
65
+ const cornerRadiusSquared = FENCE_CORNER_SNAP_RADIUS ** 2
66
+ const spanRadiusSquared = FENCE_SPAN_SNAP_RADIUS ** 2
61
67
  const ignoredFenceIds = new Set(ignoreFenceIds)
62
- let bestTarget: FencePlanPoint | null = null
63
- let bestDistanceSquared = Number.POSITIVE_INFINITY
68
+ let bestCornerTarget: FencePlanPoint | null = null
69
+ let bestCornerDistanceSquared = Number.POSITIVE_INFINITY
70
+ let bestSpanTarget: FencePlanPoint | null = null
71
+ let bestSpanDistanceSquared = Number.POSITIVE_INFINITY
64
72
 
65
73
  for (const fence of fences) {
66
74
  if (ignoredFenceIds.has(fence.id)) {
67
75
  continue
68
76
  }
69
77
 
70
- const candidates: Array<FencePlanPoint | null> = [fence.start, fence.end]
78
+ for (const candidate of [fence.start, fence.end]) {
79
+ const candidateDistanceSquared = distanceSquared(point, candidate)
80
+ if (
81
+ candidateDistanceSquared > cornerRadiusSquared ||
82
+ candidateDistanceSquared >= bestCornerDistanceSquared
83
+ ) {
84
+ continue
85
+ }
86
+
87
+ bestCornerTarget = candidate
88
+ bestCornerDistanceSquared = candidateDistanceSquared
89
+ }
90
+
71
91
  if (isCurvedWall(fence)) {
72
92
  const sampleCount = Math.max(8, Math.ceil(getWallCurveLength(fence) / 0.3))
73
- for (let index = 0; index <= sampleCount; index += 1) {
93
+ for (let index = 1; index < sampleCount; index += 1) {
74
94
  const frame = getWallCurveFrameAt(fence, index / sampleCount)
75
- candidates.push([frame.point.x, frame.point.y])
95
+ const candidate: FencePlanPoint = [frame.point.x, frame.point.y]
96
+ const candidateDistanceSquared = distanceSquared(point, candidate)
97
+ if (
98
+ candidateDistanceSquared > spanRadiusSquared ||
99
+ candidateDistanceSquared >= bestSpanDistanceSquared
100
+ ) {
101
+ continue
102
+ }
103
+
104
+ bestSpanTarget = candidate
105
+ bestSpanDistanceSquared = candidateDistanceSquared
76
106
  }
77
107
  } else {
78
- candidates.push(projectPointOntoSegment(point, fence))
79
- }
80
-
81
- for (const candidate of candidates) {
108
+ const candidate = projectPointOntoSegment(point, fence)
82
109
  if (!candidate) {
83
110
  continue
84
111
  }
85
112
 
86
113
  const candidateDistanceSquared = distanceSquared(point, candidate)
87
114
  if (
88
- candidateDistanceSquared > radiusSquared ||
89
- candidateDistanceSquared >= bestDistanceSquared
115
+ candidateDistanceSquared > spanRadiusSquared ||
116
+ candidateDistanceSquared >= bestSpanDistanceSquared
90
117
  ) {
91
118
  continue
92
119
  }
93
120
 
94
- bestTarget = candidate
95
- bestDistanceSquared = candidateDistanceSquared
121
+ bestSpanTarget = candidate
122
+ bestSpanDistanceSquared = candidateDistanceSquared
96
123
  }
97
124
  }
98
125
 
99
- return bestTarget
126
+ return bestCornerTarget ?? bestSpanTarget
100
127
  }
101
128
 
102
129
  export function snapFenceDraftPoint(args: {
@@ -106,16 +133,57 @@ export function snapFenceDraftPoint(args: {
106
133
  start?: FencePlanPoint
107
134
  angleSnap?: boolean
108
135
  ignoreFenceIds?: string[]
136
+ bypassSnap?: boolean
137
+ magnetic?: boolean
138
+ /** Override the grid step. */
139
+ step?: number
140
+ /**
141
+ * Optional grid-snap function. When provided, replaces the default
142
+ * local-axis snap — lets the 2D floor-plan keep snapping to the
143
+ * world XZ grid even when the building is rotated. Wall / fence
144
+ * endpoint snap precedence is preserved.
145
+ */
146
+ gridSnap?: (point: FencePlanPoint) => FencePlanPoint
109
147
  }): FencePlanPoint {
110
- const { point, walls, fences, start, angleSnap = false, ignoreFenceIds } = args
111
- const gridStep = getWallGridStep()
112
- const angleStep = getWallAngleSnapStep(gridStep)
113
- const basePoint =
148
+ const {
149
+ point,
150
+ walls,
151
+ fences,
152
+ start,
153
+ angleSnap = false,
154
+ ignoreFenceIds,
155
+ bypassSnap = false,
156
+ magnetic = true,
157
+ step,
158
+ gridSnap,
159
+ } = args
160
+ if (bypassSnap) return point
161
+
162
+ const gridStep = step ?? getSegmentGridStep()
163
+
164
+ // Magnetic endpoint snap must beat the angle lock, and the lock can pull
165
+ // the cursor far enough off an endpoint that probing the locked point
166
+ // would never engage — so under the lock, probe from the RAW cursor
167
+ // first (mirrors `snapWallDraftPointDetailed`'s special-point pre-pass).
168
+ if (start && angleSnap) {
169
+ const rawTarget =
170
+ magnetic &&
171
+ (findFenceSnapTarget(point, fences, ignoreFenceIds) ?? findWallSnapTarget(point, walls))
172
+ if (rawTarget) return rawTarget
173
+ }
174
+
175
+ // The angle path snaps the distance ALONG the 15° ray — a scalar, the
176
+ // same in world and local frames — so the `gridSnap` world-grid override
177
+ // only applies when the angle lock is off.
178
+ const basePoint: FencePlanPoint =
114
179
  start && angleSnap
115
- ? snapPointTo45Degrees(start, point, gridStep, angleStep)
116
- : snapPointToGrid(point, gridStep)
117
- const fenceSnapTarget = findFenceSnapTarget(basePoint, fences, ignoreFenceIds)
180
+ ? [...snapPointAlongAngleRay(start, point, DEFAULT_ANGLE_STEP, gridStep)]
181
+ : gridSnap
182
+ ? gridSnap(point)
183
+ : snapPointToGrid(point, gridStep)
184
+ if (!magnetic) return basePoint
118
185
 
186
+ const fenceSnapTarget = findFenceSnapTarget(basePoint, fences, ignoreFenceIds)
119
187
  return fenceSnapTarget ?? findWallSnapTarget(basePoint, walls) ?? basePoint
120
188
  }
121
189
 
@@ -126,15 +194,61 @@ export function createFenceOnCurrentLevel(
126
194
  const currentLevelId = useViewer.getState().selection.levelId
127
195
  const { createNode, nodes } = useScene.getState()
128
196
 
129
- if (!(currentLevelId && isWallLongEnough(start, end))) {
197
+ if (!(currentLevelId && isSegmentLongEnough(start, end))) {
198
+ return null
199
+ }
200
+
201
+ const fenceCount = Object.values(nodes).filter((node) => node.type === 'fence').length
202
+ // Build parameters seeded by a placed preset (height, style, post
203
+ // spacing, …) merge in first; `name`/`start`/`end` always win. The
204
+ // schema parse validates and drops anything unexpected.
205
+ const defaults = useEditor.getState().toolDefaults.fence ?? {}
206
+ const fence = FenceNode.parse({
207
+ ...defaults,
208
+ name: `Fence ${fenceCount + 1}`,
209
+ start,
210
+ end,
211
+ })
212
+
213
+ createNode(fence, currentLevelId)
214
+ sfxEmitter.emit('sfx:structure-build')
215
+
216
+ return fence
217
+ }
218
+
219
+ /**
220
+ * Commit a smooth spline fence from a list of drawn control points. The
221
+ * centerline becomes a Catmull-Rom curve through `path`; `start`/`end` are
222
+ * pinned to the first/last point so endpoint handles, bbox, and miter
223
+ * references stay valid. Requires >= 2 points spanning a usable distance.
224
+ */
225
+ export function createSplineFenceOnCurrentLevel(
226
+ path: FencePlanPoint[],
227
+ tangents = getTwoPointFenceCurveTangents(path),
228
+ ): FenceNode | null {
229
+ const currentLevelId = useViewer.getState().selection.levelId
230
+ const { createNode, nodes } = useScene.getState()
231
+
232
+ if (!currentLevelId || path.length < 2) {
233
+ return null
234
+ }
235
+ const start = path[0]!
236
+ const end = path[path.length - 1]!
237
+ // A degenerate single-point-ish path (all clicks on one spot) is rejected
238
+ // the same way a too-short straight segment is.
239
+ if (!isSegmentLongEnough(start, end) && path.length < 3) {
130
240
  return null
131
241
  }
132
242
 
133
243
  const fenceCount = Object.values(nodes).filter((node) => node.type === 'fence').length
244
+ const defaults = useEditor.getState().toolDefaults.fence ?? {}
134
245
  const fence = FenceNode.parse({
246
+ ...defaults,
135
247
  name: `Fence ${fenceCount + 1}`,
136
248
  start,
137
249
  end,
250
+ path,
251
+ tangents,
138
252
  })
139
253
 
140
254
  createNode(fence, currentLevelId)
@@ -1,112 +1,52 @@
1
- import type {
2
- BuildingNode,
3
- CeilingNode,
4
- ColumnNode,
5
- DoorNode,
6
- FenceNode,
7
- ItemNode,
8
- RoofNode,
9
- RoofSegmentNode,
10
- SlabNode,
11
- SpawnNode,
12
- StairNode,
13
- StairSegmentNode,
14
- WallNode,
15
- WindowNode,
16
- } from '@pascal-app/core'
17
- import { Vector3 } from 'three'
18
- import { sfxEmitter } from '../../../lib/sfx-bus'
19
- import useEditor from '../../../store/use-editor'
20
- import { MoveBuildingContent } from '../building/move-building-tool'
21
- import { MoveCeilingTool } from '../ceiling/move-ceiling-tool'
22
- import { MoveColumnTool } from '../column/move-column-tool'
23
- import { MoveDoorTool } from '../door/move-door-tool'
24
- import { MoveFenceTool } from '../fence/move-fence-tool'
25
- import { MoveRoofTool } from '../roof/move-roof-tool'
26
- import { MoveSlabTool } from '../slab/move-slab-tool'
27
- import { MoveSpawnTool } from '../spawn/move-spawn-tool'
28
- import { MoveWallTool } from '../wall/move-wall-tool'
29
- import { MoveWindowTool } from '../window/move-window-tool'
30
- import type { PlacementState } from './placement-types'
31
- import { useDraftNode } from './use-draft-node'
32
- import { usePlacementCoordinator } from './use-placement-coordinator'
1
+ import type { AnyNodeId, ElevatorNode, SpawnNode } from '@pascal-app/core'
2
+ import { nodeRegistry } from '@pascal-app/core'
3
+ import { Suspense } from 'react'
4
+ import { useMovingNode } from '../../../store/use-interaction-scope'
5
+ import { MoveElevatorTool } from '../elevator/move-elevator-tool'
6
+ import { MoveRegistryNodeTool } from '../registry/move-registry-node-tool'
7
+ import { getRegistryAffordanceTool } from '../shared/affordance-dispatch'
33
8
 
34
- function getInitialState(node: {
35
- asset: { attachTo?: string }
36
- parentId: string | null
37
- }): PlacementState {
38
- const attachTo = node.asset.attachTo
39
- if (attachTo === 'wall' || attachTo === 'wall-side') {
40
- return { surface: 'wall', wallId: node.parentId, ceilingId: null, surfaceItemId: null }
41
- }
42
- if (attachTo === 'ceiling') {
43
- return { surface: 'ceiling', wallId: null, ceilingId: node.parentId, surfaceItemId: null }
44
- }
45
- return { surface: 'floor', wallId: null, ceilingId: null, surfaceItemId: null }
46
- }
9
+ /**
10
+ * MoveTool dispatcher. Routes to (in order):
11
+ *
12
+ * 1. `def.affordanceTools.move` — kind-owned move component, lazy-loaded
13
+ * via `getRegistryAffordanceTool`. Covers generic movers
14
+ * (slab / ceiling / wall / fence / column / item / door / window), the
15
+ * bespoke roof / roof-segment / stair / stair-segment / building
16
+ * movers, and the polyline / fitting ghost-placement movers
17
+ * (duct-segment / duct-fitting). A kind that ships its own mover wins
18
+ * even if it also declares `capabilities.movable` (duct-fitting keeps
19
+ * `movable` for the inspector / hint readers but places via its ghost).
20
+ * 2. `MoveRegistryNodeTool` generic translate-on-XZ for kinds that only
21
+ * declare `capabilities.movable` (shelf, spawn, duct-terminal,
22
+ * hvac-equipment, …).
23
+ * 3. `elevator` is the lone remaining legacy arm — its bespoke cab/shaft
24
+ * mover hasn't been ported to a kind-owned affordance yet.
25
+ */
26
+ export const MoveTool: React.FC<{
27
+ onNodeMoved?: (nodeId: AnyNodeId) => void
28
+ onSpawnMoved?: (nodeId: SpawnNode['id']) => void
29
+ }> = ({ onNodeMoved }) => {
30
+ const movingNode = useMovingNode()
47
31
 
48
- function MoveItemContent({ movingNode }: { movingNode: ItemNode }) {
49
- const draftNode = useDraftNode()
50
-
51
- const meta =
52
- typeof movingNode.metadata === 'object' && movingNode.metadata !== null
53
- ? (movingNode.metadata as Record<string, unknown>)
54
- : {}
55
- const isNew = !!meta.isNew
32
+ if (!movingNode) return null
56
33
 
57
- const cursor = usePlacementCoordinator({
58
- asset: movingNode.asset,
59
- draftNode,
60
- // Duplicates start fresh in floor mode; wall/ceiling draft is created lazily by ensureDraft
61
- initialState: isNew
62
- ? { surface: 'floor', wallId: null, ceilingId: null, surfaceItemId: null }
63
- : getInitialState(movingNode),
64
- // Preserve the original item's scale so Y-position calculations use the correct height
65
- defaultScale: isNew ? movingNode.scale : undefined,
66
- initDraft: (gridPosition) => {
67
- if (isNew) {
68
- // Duplicate: use the same create() path as ItemTool so ghost rendering works correctly.
69
- // Floor items get a draft immediately; wall/ceiling items are created lazily on surface entry.
70
- gridPosition.copy(new Vector3(...movingNode.position))
71
- if (!movingNode.asset.attachTo) {
72
- draftNode.create(gridPosition, movingNode.asset, movingNode.rotation, movingNode.scale)
73
- }
74
- } else {
75
- draftNode.adopt(movingNode)
76
- gridPosition.copy(new Vector3(...movingNode.position))
77
- }
78
- },
79
- onCommitted: () => {
80
- sfxEmitter.emit('sfx:item-place')
81
- useEditor.getState().setMovingNode(null)
82
- return false
83
- },
84
- onCancel: () => {
85
- draftNode.destroy()
86
- useEditor.getState().setMovingNode(null)
87
- },
88
- })
34
+ const def = nodeRegistry.get(movingNode.type)
89
35
 
90
- return <>{cursor}</>
91
- }
36
+ const RegistryMove = getRegistryAffordanceTool(movingNode.type, 'move')
37
+ if (RegistryMove) {
38
+ return (
39
+ <Suspense fallback={null}>
40
+ <RegistryMove node={movingNode} />
41
+ </Suspense>
42
+ )
43
+ }
92
44
 
93
- export const MoveTool: React.FC = () => {
94
- const movingNode = useEditor((state) => state.movingNode)
45
+ if (def?.capabilities?.movable) {
46
+ return <MoveRegistryNodeTool node={movingNode} />
47
+ }
95
48
 
96
- if (!movingNode) return null
97
- if (movingNode.type === 'building')
98
- return <MoveBuildingContent node={movingNode as BuildingNode} />
99
- if (movingNode.type === 'door') return <MoveDoorTool node={movingNode as DoorNode} />
100
- if (movingNode.type === 'window') return <MoveWindowTool node={movingNode as WindowNode} />
101
- if (movingNode.type === 'ceiling') return <MoveCeilingTool node={movingNode as CeilingNode} />
102
- if (movingNode.type === 'column') return <MoveColumnTool node={movingNode as ColumnNode} />
103
- if (movingNode.type === 'slab') return <MoveSlabTool node={movingNode as SlabNode} />
104
- if (movingNode.type === 'wall') return <MoveWallTool node={movingNode as WallNode} />
105
- if (movingNode.type === 'fence') return <MoveFenceTool node={movingNode as FenceNode} />
106
- if (movingNode.type === 'roof' || movingNode.type === 'roof-segment')
107
- return <MoveRoofTool node={movingNode as RoofNode | RoofSegmentNode} />
108
- if (movingNode.type === 'spawn') return <MoveSpawnTool node={movingNode as SpawnNode} />
109
- if (movingNode.type === 'stair' || movingNode.type === 'stair-segment')
110
- return <MoveRoofTool node={movingNode as StairNode | StairSegmentNode} />
111
- return <MoveItemContent movingNode={movingNode as ItemNode} />
49
+ if (movingNode.type === 'elevator')
50
+ return <MoveElevatorTool node={movingNode as ElevatorNode} onCommitted={onNodeMoved} />
51
+ return null
112
52
  }
@@ -0,0 +1,22 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { getDetachedAttachmentPreviewLift, stripTransient } from './placement-math'
3
+
4
+ describe('stripTransient', () => {
5
+ test('removes placement-only metadata flags before commit', () => {
6
+ expect(stripTransient({ isNew: true, isTransient: true, label: 'copy' })).toEqual({
7
+ label: 'copy',
8
+ })
9
+ })
10
+ })
11
+
12
+ describe('getDetachedAttachmentPreviewLift', () => {
13
+ test('raises attach-only item previews while they are detached from their host', () => {
14
+ expect(getDetachedAttachmentPreviewLift('wall')).toBeGreaterThan(0)
15
+ expect(getDetachedAttachmentPreviewLift('wall-side')).toBeGreaterThan(0)
16
+ expect(getDetachedAttachmentPreviewLift('ceiling')).toBeGreaterThan(0)
17
+ })
18
+
19
+ test('keeps floor item previews on the floor', () => {
20
+ expect(getDetachedAttachmentPreviewLift(undefined)).toBe(0)
21
+ })
22
+ })