@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
@@ -2,20 +2,27 @@
2
2
 
3
3
  import { emitter, type FenceNode, isCurvedWall, type WallNode } from '@pascal-app/core'
4
4
  import { type MouseEvent as ReactMouseEvent, useCallback } from 'react'
5
- import { getPlanPointDistance } from '../../lib/floorplan'
5
+ import { resolveCeilingPlanPointSnap } from '../../lib/ceiling-plan-snap'
6
+ import { alignFloorplanDraftPoint, getPlanPointDistance } from '../../lib/floorplan'
7
+ import { resolveSlabPlanPointSnap } from '../../lib/slab-plan-snap'
8
+ import useAlignmentGuides from '../../store/use-alignment-guides'
9
+ import useEditor, { isAngleSnapActive, isMagneticSnapActive } from '../../store/use-editor'
10
+ import usePlacementPreview from '../../store/use-placement-preview'
11
+ import useSegmentDraftChain from '../../store/use-segment-draft-chain'
6
12
  import { snapFenceDraftPoint } from '../tools/fence/fence-drafting'
7
- import type { WallPlanPoint } from '../tools/wall/wall-drafting'
13
+ import { getSegmentGridStep, type WallPlanPoint } from '../tools/wall/wall-drafting'
8
14
 
9
15
  type UseFloorplanBackgroundPlacementArgs = {
10
16
  activePolygonDraftPoints: WallPlanPoint[]
11
17
  ceilingDraftPoints: WallPlanPoint[]
12
18
  clearFencePlacementDraft: () => void
13
19
  clearRoofPlacementDraft: () => void
20
+ clearWallPlacementDraft: () => void
14
21
  emitFloorplanGridEvent: (
15
22
  type: 'click' | 'double-click' | 'move',
16
23
  planPoint: WallPlanPoint,
17
24
  event: ReactMouseEvent<SVGSVGElement>,
18
- ) => WallPlanPoint
25
+ ) => void
19
26
  fenceDraftStart: WallPlanPoint | null
20
27
  fences: FenceNode[]
21
28
  findClosestWallPoint: (
@@ -30,11 +37,16 @@ type UseFloorplanBackgroundPlacementArgs = {
30
37
  } | null
31
38
  floorplanOpeningLocalY: number
32
39
  getSnappedFloorplanPoint: (point: WallPlanPoint) => WallPlanPoint
40
+ handleCeilingItemPlacementClick: (
41
+ planPoint: WallPlanPoint,
42
+ nativeEvent: ReactMouseEvent<SVGSVGElement>,
43
+ ) => boolean
33
44
  handleCeilingPlacementPoint: (point: WallPlanPoint) => void
34
45
  handleSlabPlacementPoint: (point: WallPlanPoint) => void
35
46
  handleWallPlacementPoint: (point: WallPlanPoint) => void
36
47
  handleZonePlacementPoint: (point: WallPlanPoint) => void
37
48
  isCeilingBuildActive: boolean
49
+ isCeilingItemPlacementActive: boolean
38
50
  isFenceBuildActive: boolean
39
51
  isFloorplanGridInteractionActive: boolean
40
52
  isOpeningPlacementActive: boolean
@@ -42,18 +54,21 @@ type UseFloorplanBackgroundPlacementArgs = {
42
54
  isRoofBuildActive: boolean
43
55
  isWallBuildActive: boolean
44
56
  isZoneBuildActive: boolean
57
+ levelId: string | null
45
58
  roofDraftStart: WallPlanPoint | null
46
59
  setCursorPoint: React.Dispatch<React.SetStateAction<WallPlanPoint | null>>
47
60
  setFenceDraftEnd: React.Dispatch<React.SetStateAction<WallPlanPoint | null>>
48
61
  setFenceDraftStart: React.Dispatch<React.SetStateAction<WallPlanPoint | null>>
49
62
  setRoofDraftEnd: React.Dispatch<React.SetStateAction<WallPlanPoint | null>>
50
63
  setRoofDraftStart: React.Dispatch<React.SetStateAction<WallPlanPoint | null>>
51
- shiftPressed: boolean
52
64
  snapWallDraftPoint: (args: {
53
65
  point: WallPlanPoint
54
66
  walls: WallNode[]
55
67
  start?: WallPlanPoint
56
- angleSnap: boolean
68
+ angleSnap?: boolean
69
+ bypassSnap?: boolean
70
+ step?: number
71
+ gridSnap?: (point: WallPlanPoint) => WallPlanPoint
57
72
  }) => WallPlanPoint
58
73
  snapPolygonDraftPoint: (args: {
59
74
  point: WallPlanPoint
@@ -62,6 +77,13 @@ type UseFloorplanBackgroundPlacementArgs = {
62
77
  }) => WallPlanPoint
63
78
  toPoint2D: (point: WallPlanPoint) => { x: number; y: number }
64
79
  walls: WallNode[]
80
+ /**
81
+ * Snap a building-local plan point to the world XZ grid at `step`.
82
+ * Injected so the hook doesn't have to know the building's rotation
83
+ * or position — used by wall / fence branches that snap at variable
84
+ * step.
85
+ */
86
+ worldGridSnap: (point: WallPlanPoint, step: number) => WallPlanPoint
65
87
  }
66
88
 
67
89
  export function useFloorplanBackgroundPlacement({
@@ -69,17 +91,20 @@ export function useFloorplanBackgroundPlacement({
69
91
  ceilingDraftPoints,
70
92
  clearFencePlacementDraft,
71
93
  clearRoofPlacementDraft,
94
+ clearWallPlacementDraft,
72
95
  emitFloorplanGridEvent,
73
96
  fenceDraftStart,
74
97
  fences,
75
98
  findClosestWallPoint,
76
99
  floorplanOpeningLocalY,
77
100
  getSnappedFloorplanPoint,
101
+ handleCeilingItemPlacementClick,
78
102
  handleCeilingPlacementPoint,
79
103
  handleSlabPlacementPoint,
80
104
  handleWallPlacementPoint,
81
105
  handleZonePlacementPoint,
82
106
  isCeilingBuildActive,
107
+ isCeilingItemPlacementActive,
83
108
  isFenceBuildActive,
84
109
  isFloorplanGridInteractionActive,
85
110
  isOpeningPlacementActive,
@@ -87,17 +112,18 @@ export function useFloorplanBackgroundPlacement({
87
112
  isRoofBuildActive,
88
113
  isWallBuildActive,
89
114
  isZoneBuildActive,
115
+ levelId,
90
116
  roofDraftStart,
91
117
  setCursorPoint,
92
118
  setFenceDraftEnd,
93
119
  setFenceDraftStart,
94
120
  setRoofDraftEnd,
95
121
  setRoofDraftStart,
96
- shiftPressed,
97
122
  snapWallDraftPoint,
98
123
  snapPolygonDraftPoint,
99
124
  toPoint2D,
100
125
  walls,
126
+ worldGridSnap,
101
127
  }: UseFloorplanBackgroundPlacementArgs) {
102
128
  const handleBackgroundPlacementClick = useCallback(
103
129
  (
@@ -123,24 +149,42 @@ export function useFloorplanBackgroundPlacement({
123
149
  stopPropagation: () => {},
124
150
  } as any)
125
151
  }
152
+ // Drop the off-wall ghost on commit so it doesn't linger at the
153
+ // just-placed spot before the next pointer move re-evaluates.
154
+ usePlacementPreview.getState().clear()
126
155
  return true
127
156
  }
128
157
 
129
158
  if (isCeilingBuildActive) {
130
- emitFloorplanGridEvent('click', planPoint, event)
131
-
132
- const snappedPoint = snapPolygonDraftPoint({
159
+ // Align the committed vertex the same way the move-preview did, so the
160
+ // placed point matches what the user saw — mode-driven (the chip):
161
+ // `grid` quantizes, `angles` locks 15° rays, `lines` snaps onto walls /
162
+ // alignment, `off` is free. Alt remains force/free at commit time.
163
+ const angleSnap = ceilingDraftPoints.length > 0 && isAngleSnapActive()
164
+ const fallbackPoint = snapPolygonDraftPoint({
133
165
  point: planPoint,
134
166
  start: ceilingDraftPoints[ceilingDraftPoints.length - 1],
135
- angleSnap: ceilingDraftPoints.length > 0 && !shiftPressed,
167
+ angleSnap,
136
168
  })
169
+ const snappedPoint = resolveCeilingPlanPointSnap({
170
+ rawPoint: planPoint,
171
+ fallbackPoint,
172
+ levelId,
173
+ align: !angleSnap,
174
+ }).point
137
175
 
176
+ emitFloorplanGridEvent('click', snappedPoint, event)
138
177
  handleCeilingPlacementPoint(snappedPoint)
139
178
  return true
140
179
  }
141
180
 
142
181
  if (isRoofBuildActive) {
143
- const snappedPoint = getSnappedFloorplanPoint(planPoint)
182
+ // Footprint placement (polygon context: grid / lines / off, no angle),
183
+ // mode-driven to match the chip. Alt is force/free at commit time;
184
+ // alignment display/pull follows the active magnetic mode.
185
+ const snappedPoint = alignFloorplanDraftPoint(getSnappedFloorplanPoint(planPoint), {
186
+ applySnap: isMagneticSnapActive(),
187
+ })
144
188
  emitFloorplanGridEvent('click', snappedPoint, event)
145
189
  setCursorPoint(snappedPoint)
146
190
 
@@ -154,43 +198,95 @@ export function useFloorplanBackgroundPlacement({
154
198
  }
155
199
 
156
200
  if (isFenceBuildActive) {
157
- emitFloorplanGridEvent('click', planPoint, event)
158
-
159
- const snappedPoint = snapFenceDraftPoint({
201
+ // Fence draft: mode-driven (matches the chip), same as the move
202
+ // preview. `grid` snaps to the world XZ grid (rotation-safe via the
203
+ // `gridSnap` callback), `angles` locks 15° rays from the start, `lines`
204
+ // pulls onto walls / fences / alignment, `off` is free.
205
+ const fenceStep = getSegmentGridStep()
206
+ const fenceAngleSnap = fenceDraftStart !== null && isAngleSnapActive()
207
+ const fenceSnapped = snapFenceDraftPoint({
160
208
  point: planPoint,
161
209
  walls,
162
210
  fences,
163
211
  start: fenceDraftStart ?? undefined,
164
- angleSnap: Boolean(fenceDraftStart) && !shiftPressed,
212
+ angleSnap: fenceAngleSnap,
213
+ magnetic: isMagneticSnapActive(),
214
+ gridSnap: (p) => worldGridSnap(p, fenceStep),
165
215
  })
216
+ const fenceGridBase = worldGridSnap(planPoint, fenceStep)
217
+ const fenceLocked =
218
+ fenceSnapped[0] !== fenceGridBase[0] || fenceSnapped[1] !== fenceGridBase[1]
219
+ const snappedPoint = fenceLocked
220
+ ? fenceSnapped
221
+ : alignFloorplanDraftPoint(fenceSnapped, {
222
+ applySnap: isMagneticSnapActive() && !fenceAngleSnap,
223
+ })
166
224
 
225
+ emitFloorplanGridEvent('click', snappedPoint, event)
167
226
  setCursorPoint(snappedPoint)
168
227
 
228
+ // Double-click finishes the chain. The emit above already made the
229
+ // 3D fence tool stopDrafting (its detail >= 2 guard), so close the
230
+ // 2D draft too — leaving it open desyncs the two views.
231
+ if (fenceDraftStart && event.detail >= 2) {
232
+ clearFencePlacementDraft()
233
+ return true
234
+ }
235
+
169
236
  if (!fenceDraftStart) {
170
237
  setFenceDraftStart(snappedPoint)
171
238
  setFenceDraftEnd(snappedPoint)
172
239
  } else if (
173
240
  getPlanPointDistance(toPoint2D(fenceDraftStart), toPoint2D(snappedPoint)) >= 0.01
174
241
  ) {
175
- clearFencePlacementDraft()
242
+ // Single mode commits one segment per click: the same emit above
243
+ // already made the 3D fence tool stopDrafting, so close the 2D
244
+ // draft too instead of chaining.
245
+ if (useEditor.getState().getContinuation('fence') === 'single') {
246
+ clearFencePlacementDraft()
247
+ setCursorPoint(snappedPoint)
248
+ return true
249
+ }
250
+ // The 3D fence tool owns creation and keeps chaining from the
251
+ // committed fence's resolved end — chain the 2D draft from the
252
+ // same published point so both views draft the next segment
253
+ // from the same start.
254
+ const nextStart = useSegmentDraftChain.getState().fence ?? snappedPoint
255
+ setFenceDraftStart(nextStart)
256
+ setFenceDraftEnd(nextStart)
176
257
  } else {
177
258
  setFenceDraftEnd(snappedPoint)
178
259
  }
179
260
  return true
180
261
  }
181
262
 
182
- if (isFloorplanGridInteractionActive) {
183
- const snappedPoint = emitFloorplanGridEvent('click', planPoint, event)
184
- setCursorPoint(snappedPoint)
185
- return true
186
- }
187
-
263
+ // Slab / zone polygon build — local draft state + grid emit.
264
+ // Must run BEFORE the `isFloorplanGridInteractionActive` catch-all
265
+ // (since slab is registry-driven, the catch-all would otherwise
266
+ // swallow the click and skip local draft state updates — leaving
267
+ // the 2D draft polygon invisible while the 3D tool builds fine).
188
268
  if (isPolygonBuildActive) {
189
- const snappedPoint = snapPolygonDraftPoint({
269
+ const angleSnap = activePolygonDraftPoints.length > 0 && isAngleSnapActive()
270
+ const fallbackPoint = snapPolygonDraftPoint({
190
271
  point: planPoint,
191
272
  start: activePolygonDraftPoints[activePolygonDraftPoints.length - 1],
192
- angleSnap: activePolygonDraftPoints.length > 0 && !shiftPressed,
273
+ angleSnap,
193
274
  })
275
+ // Zone shares the slab surface snap (wall corners / midpoints /
276
+ // crossings + alignment) — it's the same polygon-on-a-level draw.
277
+ const snappedPoint = resolveSlabPlanPointSnap({
278
+ rawPoint: planPoint,
279
+ fallbackPoint,
280
+ levelId,
281
+ align: !angleSnap,
282
+ }).point
283
+
284
+ // Emit the grid event so the registry-driven slab tool also
285
+ // sees the click (parity with ceiling / fence / roof branches
286
+ // above). Zone has no registry tool — emit-or-not is irrelevant.
287
+ if (!isZoneBuildActive) {
288
+ emitFloorplanGridEvent('click', snappedPoint, event)
289
+ }
194
290
 
195
291
  if (isZoneBuildActive) {
196
292
  handleZonePlacementPoint(snappedPoint)
@@ -200,35 +296,93 @@ export function useFloorplanBackgroundPlacement({
200
296
  return true
201
297
  }
202
298
 
203
- if (!isWallBuildActive) {
204
- return false
299
+ // Wall placement — local draft state + grid emit. Same reasoning
300
+ // as slab above: wall is registry-driven, so without this branch
301
+ // the catch-all would swallow the click and the local draftStart
302
+ // / draftEnd state in the floor plan would never update, leaving
303
+ // the dashed-line draft preview invisible.
304
+ if (isWallBuildActive) {
305
+ // Wall draft: mode-driven (matches the chip + the move-preview branch).
306
+ // `grid` snaps to the world XZ grid (rotation-safe via `gridSnap`),
307
+ // `angles` locks 15° rays from the start, `lines` pulls the endpoint
308
+ // onto existing wall corners / edges + alignment, `off` is free.
309
+ const wallStep = getSegmentGridStep()
310
+ const wallAngleSnap = draftStart !== null && isAngleSnapActive()
311
+ const wallSnapped = snapWallDraftPoint({
312
+ point: planPoint,
313
+ walls,
314
+ start: draftStart ?? undefined,
315
+ angleSnap: wallAngleSnap,
316
+ gridSnap: (p) => worldGridSnap(p, wallStep),
317
+ })
318
+ const wallGridBase = worldGridSnap(planPoint, wallStep)
319
+ const wallLocked = wallSnapped[0] !== wallGridBase[0] || wallSnapped[1] !== wallGridBase[1]
320
+ let snappedPoint = wallSnapped
321
+ if (wallLocked) {
322
+ useAlignmentGuides.getState().clear()
323
+ } else {
324
+ // Alignment lines are shown in every mode; the pull applies only when
325
+ // magnetic ('lines') and the segment isn't angle-locked.
326
+ snappedPoint = alignFloorplanDraftPoint(wallSnapped, {
327
+ applySnap: isMagneticSnapActive() && !wallAngleSnap,
328
+ })
329
+ }
330
+
331
+ emitFloorplanGridEvent('click', snappedPoint, event)
332
+
333
+ // Double-click finishes the chain. The emit above already made the
334
+ // 3D wall tool stopDrafting (its detail >= 2 guard), so close the
335
+ // 2D draft too — otherwise it stays open against a closed 3D tool
336
+ // and the next previewed segment is silently never created.
337
+ if (draftStart && event.detail >= 2) {
338
+ clearWallPlacementDraft()
339
+ setCursorPoint(snappedPoint)
340
+ return true
341
+ }
342
+
343
+ handleWallPlacementPoint(snappedPoint)
344
+ return true
205
345
  }
206
346
 
207
- const snappedPoint = snapWallDraftPoint({
208
- point: planPoint,
209
- walls,
210
- start: draftStart ?? undefined,
211
- angleSnap: Boolean(draftStart) && !shiftPressed,
212
- })
347
+ // Ceiling-attached item placement (lights, fans). Routes the click
348
+ // through `ceiling:click` instead of `grid:click` so the placement
349
+ // strategy parents the new item to the ceiling at the correct
350
+ // height mirrors the pointer-move handler in `floorplan-panel`.
351
+ if (isCeilingItemPlacementActive) {
352
+ handleCeilingItemPlacementClick(planPoint, event)
353
+ return true
354
+ }
355
+
356
+ // Generic catch-all — registry-driven tool whose kind has no
357
+ // local floor-plan draft handler (column / spawn / shelf / etc.).
358
+ // The tool's `grid:click` subscriber owns the placement.
359
+ if (isFloorplanGridInteractionActive) {
360
+ const snappedPoint = getSnappedFloorplanPoint(planPoint)
361
+ emitFloorplanGridEvent('click', snappedPoint, event)
362
+ setCursorPoint(snappedPoint)
363
+ return true
364
+ }
213
365
 
214
- handleWallPlacementPoint(snappedPoint)
215
- return true
366
+ return false
216
367
  },
217
368
  [
218
369
  activePolygonDraftPoints,
219
370
  ceilingDraftPoints,
220
371
  clearFencePlacementDraft,
221
372
  clearRoofPlacementDraft,
373
+ clearWallPlacementDraft,
222
374
  emitFloorplanGridEvent,
223
375
  fenceDraftStart,
224
376
  fences,
225
377
  findClosestWallPoint,
226
378
  floorplanOpeningLocalY,
227
379
  getSnappedFloorplanPoint,
380
+ handleCeilingItemPlacementClick,
228
381
  handleCeilingPlacementPoint,
229
382
  handleSlabPlacementPoint,
230
383
  handleZonePlacementPoint,
231
384
  isCeilingBuildActive,
385
+ isCeilingItemPlacementActive,
232
386
  isFenceBuildActive,
233
387
  isFloorplanGridInteractionActive,
234
388
  isOpeningPlacementActive,
@@ -236,18 +390,19 @@ export function useFloorplanBackgroundPlacement({
236
390
  isRoofBuildActive,
237
391
  isWallBuildActive,
238
392
  isZoneBuildActive,
393
+ levelId,
239
394
  roofDraftStart,
240
395
  setCursorPoint,
241
396
  setFenceDraftEnd,
242
397
  setFenceDraftStart,
243
398
  setRoofDraftEnd,
244
399
  setRoofDraftStart,
245
- shiftPressed,
246
400
  snapWallDraftPoint,
247
401
  snapPolygonDraftPoint,
248
402
  toPoint2D,
249
403
  walls,
250
404
  handleWallPlacementPoint,
405
+ worldGridSnap,
251
406
  ],
252
407
  )
253
408
 
@@ -3,7 +3,9 @@
3
3
  import type {
4
4
  AnyNode,
5
5
  CeilingNode,
6
+ ColumnNode,
6
7
  DoorNode,
8
+ ElevatorNode,
7
9
  ItemNode,
8
10
  Point2D,
9
11
  RoofNode,
@@ -46,6 +48,16 @@ type CeilingPolygonEntry = {
46
48
  holes: Point2D[][]
47
49
  }
48
50
 
51
+ type ColumnPolygonEntry = {
52
+ column: ColumnNode
53
+ polygon: Point2D[]
54
+ }
55
+
56
+ type ElevatorPolygonEntry = {
57
+ elevator: ElevatorNode
58
+ polygon: Point2D[]
59
+ }
60
+
49
61
  type FloorplanRoofEntry = {
50
62
  roof: RoofNode
51
63
  segments: Array<{
@@ -72,8 +84,10 @@ type FloorplanStairEntry = {
72
84
 
73
85
  type UseFloorplanHitTestingArgs = {
74
86
  ceilingPolygons: CeilingPolygonEntry[]
87
+ columnPolygons: ColumnPolygonEntry[]
75
88
  displaySlabPolygons: SlabPolygonEntry[]
76
89
  displayWallPolygons: WallPolygonEntry[]
90
+ floorplanElevatorEntries: ElevatorPolygonEntry[]
77
91
  floorplanItemEntries: FloorplanItemEntry[]
78
92
  floorplanOpeningHitTolerance: number
79
93
  floorplanRoofEntries: FloorplanRoofEntry[]
@@ -88,8 +102,10 @@ type UseFloorplanHitTestingArgs = {
88
102
 
89
103
  export function useFloorplanHitTesting({
90
104
  ceilingPolygons,
105
+ columnPolygons,
91
106
  displaySlabPolygons,
92
107
  displayWallPolygons,
108
+ floorplanElevatorEntries,
93
109
  floorplanItemEntries,
94
110
  floorplanOpeningHitTolerance,
95
111
  floorplanRoofEntries,
@@ -113,18 +129,22 @@ export function useFloorplanHitTesting({
113
129
  openings: openingsPolygons,
114
130
  roofs: floorplanRoofEntries,
115
131
  stairs: floorplanStairEntries,
132
+ elevators: floorplanElevatorEntries,
116
133
  walls: displayWallPolygons,
117
134
  slabs: displaySlabPolygons,
118
135
  openingHitTolerance: floorplanOpeningHitTolerance,
119
136
  wallHitTolerance: floorplanWallHitTolerance,
137
+ columns: columnPolygons,
120
138
  getOpeningCenterLine,
121
139
  })
122
140
  },
123
141
  [
124
142
  ceilingPolygons,
143
+ columnPolygons,
125
144
  displaySlabPolygons,
126
145
  displayWallPolygons,
127
146
  floorplanItemEntries,
147
+ floorplanElevatorEntries,
128
148
  floorplanOpeningHitTolerance,
129
149
  floorplanRoofEntries,
130
150
  floorplanStairEntries,
@@ -149,13 +169,17 @@ export function useFloorplanHitTesting({
149
169
  openings: openingsPolygons,
150
170
  roofs: floorplanRoofEntries,
151
171
  slabs: displaySlabPolygons,
172
+ columns: columnPolygons,
173
+ elevators: floorplanElevatorEntries,
152
174
  stairs: floorplanStairEntries,
153
175
  }),
154
176
  [
155
177
  ceilingPolygons,
178
+ columnPolygons,
156
179
  displaySlabPolygons,
157
180
  displayWallPolygons,
158
181
  floorplanItemEntries,
182
+ floorplanElevatorEntries,
159
183
  floorplanRoofEntries,
160
184
  floorplanStairEntries,
161
185
  isFloorplanItemContextActive,
@@ -12,6 +12,7 @@ import {
12
12
  type SiteNode,
13
13
  type SlabNode,
14
14
  type SpawnNode,
15
+ useLiveTransforms,
15
16
  useScene,
16
17
  type WallNode,
17
18
  type WindowNode,
@@ -35,7 +36,7 @@ function useLevelChildren<TNode extends AnyNode>(
35
36
  }
36
37
 
37
38
  const levelNode = state.nodes[levelId]
38
- if (!levelNode || levelNode.type !== 'level') {
39
+ if (levelNode?.type !== 'level') {
39
40
  return [] as TNode[]
40
41
  }
41
42
 
@@ -59,13 +60,22 @@ export function useFloorplanSceneData({
59
60
  ? (levelNode.parentId as BuildingNode['id'])
60
61
  : buildingId
61
62
 
62
- const buildingRotationY = useScene((state) => {
63
+ // Live transform override — when the building is mid-drag (the move
64
+ // tool publishes per-frame pose to useLiveTransforms), the floor-plan
65
+ // follows that pose so the dimmed reference floor tracks the cursor
66
+ // instead of snapping only on commit.
67
+ const buildingLiveTransform = useLiveTransforms((state) =>
68
+ currentBuildingId ? state.transforms.get(currentBuildingId) : undefined,
69
+ )
70
+
71
+ const committedBuildingRotationY = useScene((state) => {
63
72
  if (!currentBuildingId) return 0
64
73
  const node = state.nodes[currentBuildingId]
65
74
  return node?.type === 'building' ? (node.rotation[1] ?? 0) : 0
66
75
  })
76
+ const buildingRotationY = buildingLiveTransform?.rotation ?? committedBuildingRotationY
67
77
 
68
- const buildingPosition = useScene((state) => {
78
+ const committedBuildingPosition = useScene((state) => {
69
79
  if (!currentBuildingId) {
70
80
  return DEFAULT_BUILDING_POSITION
71
81
  }
@@ -75,6 +85,7 @@ export function useFloorplanSceneData({
75
85
  ? (node.position as [number, number, number])
76
86
  : DEFAULT_BUILDING_POSITION
77
87
  })
88
+ const buildingPosition = buildingLiveTransform?.position ?? committedBuildingPosition
78
89
 
79
90
  const site = useScene((state) => {
80
91
  for (const rootNodeId of state.rootNodeIds) {
@@ -94,7 +105,7 @@ export function useFloorplanSceneData({
94
105
  }
95
106
 
96
107
  const buildingNode = state.nodes[currentBuildingId]
97
- if (!buildingNode || buildingNode.type !== 'building') {
108
+ if (buildingNode?.type !== 'building') {
98
109
  return [] as LevelNode[]
99
110
  }
100
111
 
@@ -122,7 +133,7 @@ export function useFloorplanSceneData({
122
133
  }
123
134
 
124
135
  const nextLevelNode = state.nodes[levelId]
125
- if (!nextLevelNode || nextLevelNode.type !== 'level') {
136
+ if (nextLevelNode?.type !== 'level') {
126
137
  return [] as RoofNode[]
127
138
  }
128
139
 
@@ -138,7 +149,7 @@ export function useFloorplanSceneData({
138
149
  }
139
150
 
140
151
  const nextLevelNode = state.nodes[levelId]
141
- if (!nextLevelNode || nextLevelNode.type !== 'level') {
152
+ if (nextLevelNode?.type !== 'level') {
142
153
  return [] as OpeningNode[]
143
154
  }
144
155
 
@@ -160,7 +171,7 @@ export function useFloorplanSceneData({
160
171
  }
161
172
 
162
173
  const nextLevelNode = state.nodes[levelId]
163
- if (!nextLevelNode || nextLevelNode.type !== 'level') {
174
+ if (nextLevelNode?.type !== 'level') {
164
175
  return [] as AnyNode[]
165
176
  }
166
177
 
@@ -170,6 +181,7 @@ export function useFloorplanSceneData({
170
181
 
171
182
  return {
172
183
  buildingPosition,
184
+ committedBuildingPosition,
173
185
  buildingRotationY,
174
186
  currentBuildingId,
175
187
  ceilings,
@@ -0,0 +1,26 @@
1
+ import { useEffect, useState } from 'react'
2
+
3
+ /**
4
+ * Bumps after the 3D meshes have had a chance to settle following a scene
5
+ * change. `computeGroupBox` reads mesh WORLD bounds, but a scene commit
6
+ * (undo/redo included) reaches the meshes asynchronously — renderers
7
+ * reconcile on the next React commit and the geometry systems rebuild in the
8
+ * next frame — so anything that derives geometry from the meshes in the same
9
+ * render as the `nodes` change reads STALE positions. Depend on this epoch
10
+ * (alongside `nodes`) to recompute once more after two animation frames,
11
+ * when transforms and rebuilt geometry are in place.
12
+ */
13
+ export function useMeshSettleEpoch(nodes: unknown): number {
14
+ const [epoch, setEpoch] = useState(0)
15
+ useEffect(() => {
16
+ let raf2 = 0
17
+ const raf1 = requestAnimationFrame(() => {
18
+ raf2 = requestAnimationFrame(() => setEpoch((e) => e + 1))
19
+ })
20
+ return () => {
21
+ cancelAnimationFrame(raf1)
22
+ cancelAnimationFrame(raf2)
23
+ }
24
+ }, [nodes])
25
+ return epoch
26
+ }