@pascal-app/editor 1.0.3 → 1.0.4

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 (440) hide show
  1. package/package.json +11 -9
  2. package/src/components/editor/action-menu-button.tsx +83 -0
  3. package/src/components/editor/bake-thumbnail.tsx +315 -4
  4. package/src/components/editor/capture-level-picker.tsx +72 -0
  5. package/src/components/editor/custom-camera-controls.tsx +202 -57
  6. package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
  7. package/src/components/editor/duplicate-node.ts +244 -0
  8. package/src/components/editor/editor-layout-mobile.tsx +36 -25
  9. package/src/components/editor/editor-layout-v2.tsx +131 -58
  10. package/src/components/editor/export-manager.tsx +34 -8
  11. package/src/components/editor/first-person/build-collider-world.test.ts +51 -0
  12. package/src/components/editor/first-person/build-collider-world.ts +6 -2
  13. package/src/components/editor/first-person/mechanism-targets.test.ts +75 -0
  14. package/src/components/editor/first-person/mechanism-targets.ts +36 -0
  15. package/src/components/editor/first-person-controls.tsx +205 -2
  16. package/src/components/editor/floating-action-menu.tsx +63 -207
  17. package/src/components/editor/floor-aware-camera-controls.test.ts +442 -0
  18. package/src/components/editor/floor-aware-camera-controls.ts +324 -0
  19. package/src/components/editor/floor-openings-overlay.tsx +243 -0
  20. package/src/components/editor/floor-region-controls.tsx +846 -0
  21. package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
  22. package/src/components/editor/floor-surface.test.ts +165 -0
  23. package/src/components/editor/floor-surface.ts +182 -0
  24. package/src/components/editor/floorplan-background-selection.test.ts +11 -2
  25. package/src/components/editor/floorplan-background-selection.ts +1 -3
  26. package/src/components/editor/floorplan-panel.tsx +344 -342
  27. package/src/components/editor/footprint-height-handle.tsx +204 -0
  28. package/src/components/editor/group-actions.ts +54 -2
  29. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  30. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  31. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  32. package/src/components/editor/handles/use-handle-drag.ts +269 -197
  33. package/src/components/editor/index.tsx +294 -169
  34. package/src/components/editor/join-walls-pill.tsx +44 -0
  35. package/src/components/editor/node-action-menu.tsx +68 -70
  36. package/src/components/editor/node-arrow-handles.tsx +25 -6
  37. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  38. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  39. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  40. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  41. package/src/components/editor/registry-action-contributions.tsx +80 -1
  42. package/src/components/editor/room-controls.tsx +847 -0
  43. package/src/components/editor/room-floor-highlight.tsx +128 -0
  44. package/src/components/editor/room-handles-2d.tsx +354 -0
  45. package/src/components/editor/room-handles.tsx +675 -0
  46. package/src/components/editor/room-highlight.tsx +225 -0
  47. package/src/components/editor/room-labels.test.ts +357 -0
  48. package/src/components/editor/room-labels.tsx +990 -0
  49. package/src/components/editor/room-transform-ghost.tsx +256 -0
  50. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  51. package/src/components/editor/selection-manager.tsx +451 -317
  52. package/src/components/editor/slab-hole-highlights.tsx +3 -0
  53. package/src/components/editor/snapshot-capture-overlay.tsx +4 -0
  54. package/src/components/editor/snapshot-capture.test.ts +52 -4
  55. package/src/components/editor/snapshot-capture.ts +30 -5
  56. package/src/components/editor/thumbnail-generator.test.ts +308 -0
  57. package/src/components/editor/thumbnail-generator.tsx +1035 -229
  58. package/src/components/editor/use-floorplan-background-placement.ts +31 -11
  59. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  60. package/src/components/editor/views/host-view.tsx +61 -0
  61. package/src/components/editor/views/plugins-view.tsx +670 -0
  62. package/src/components/editor/views/scene-region.tsx +59 -0
  63. package/src/components/editor/views/use-editor-views.ts +57 -0
  64. package/src/components/editor/views/view-bar.tsx +220 -0
  65. package/src/components/editor/wall-justification.test.ts +140 -0
  66. package/src/components/editor/wall-move-side-handles.tsx +43 -142
  67. package/src/components/editor/wall-push-arrow.tsx +92 -0
  68. package/src/components/editor/wall-region-handles.tsx +509 -0
  69. package/src/components/editor/wall-snap-beacon-layer.tsx +3 -0
  70. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  71. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  72. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  73. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  74. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +113 -102
  75. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  76. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  77. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  78. package/src/components/editor-2d/renderers/{floorplan-draft-wall-measurement.tsx → floorplan-draft-measurement.tsx} +8 -8
  79. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  80. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +38 -8
  81. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +291 -227
  82. package/src/components/editor-2d/svg-paths.ts +12 -15
  83. package/src/components/procedural-items/inspector.tsx +40 -15
  84. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  85. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  86. package/src/components/systems/zone/zone-system.test.ts +47 -0
  87. package/src/components/systems/zone/zone-system.tsx +29 -68
  88. package/src/components/tools/item/placement-history.test.tsx +474 -0
  89. package/src/components/tools/item/placement-strategies.ts +8 -4
  90. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  91. package/src/components/tools/item/use-draft-node.ts +166 -95
  92. package/src/components/tools/item/use-placement-coordinator.tsx +93 -46
  93. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  94. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  95. package/src/components/tools/registry/move-registry-node-tool.tsx +3 -1
  96. package/src/components/tools/select/box-select-tool.tsx +2 -12
  97. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  98. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  99. package/src/components/tools/select/select-candidates.test.ts +37 -4
  100. package/src/components/tools/select/select-candidates.ts +15 -11
  101. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  102. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  103. package/src/components/tools/stair/stair-defaults.ts +0 -3
  104. package/src/components/tools/stair/stair-tool.tsx +90 -42
  105. package/src/components/tools/tool-manager.tsx +46 -20
  106. package/src/components/tools/wall/wall-drafting.test.ts +19 -4
  107. package/src/components/tools/zone/zone-tool.tsx +4 -12
  108. package/src/components/ui/action-menu/action-button.tsx +3 -0
  109. package/src/components/ui/action-menu/camera-actions.tsx +2 -1
  110. package/src/components/ui/action-menu/control-modes.tsx +2 -35
  111. package/src/components/ui/action-menu/measurement-control.tsx +3 -10
  112. package/src/components/ui/build-panel/build-panel.tsx +235 -0
  113. package/src/components/ui/command-palette/editor-commands.tsx +4 -30
  114. package/src/components/ui/command-palette/index.tsx +5 -24
  115. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  116. package/src/components/ui/controls/material-paint-panel.tsx +97 -26
  117. package/src/components/ui/controls/material-picker.tsx +4 -1
  118. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  119. package/src/components/ui/controls/select-control.tsx +65 -0
  120. package/src/components/ui/floating-level-selector.tsx +9 -14
  121. package/src/components/ui/helpers/building-helper.tsx +4 -1
  122. package/src/components/ui/helpers/contextual-helper-panel.tsx +154 -70
  123. package/src/components/ui/helpers/helper-manager.test.tsx +69 -0
  124. package/src/components/ui/helpers/helper-manager.tsx +325 -14
  125. package/src/components/ui/helpers/item-helper.tsx +8 -0
  126. package/src/components/ui/helpers/registered-tool-helper.tsx +8 -1
  127. package/src/components/ui/icon-ref.tsx +45 -1
  128. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  129. package/src/components/ui/node-icon-image.tsx +16 -0
  130. package/src/components/ui/panels/collections/collections-popover.tsx +20 -14
  131. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  132. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  133. package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
  134. package/src/components/ui/panels/mobile-selection-bar.tsx +2 -2
  135. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  136. package/src/components/ui/panels/multi-field-value.ts +6 -3
  137. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  138. package/src/components/ui/panels/multi-selection-panel.tsx +43 -4
  139. package/src/components/ui/panels/node-display.ts +8 -3
  140. package/src/components/ui/panels/panel-manager.tsx +103 -68
  141. package/src/components/ui/panels/panel-wrapper.tsx +78 -36
  142. package/src/components/ui/panels/parametric-field-control.tsx +1 -1
  143. package/src/components/ui/panels/parametric-inspector.tsx +4 -4
  144. package/src/components/ui/panels/reference-panel.tsx +2 -3
  145. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  146. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  147. package/src/components/ui/panels/room-name-options.ts +47 -0
  148. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  149. package/src/components/ui/panels/room-panel.tsx +725 -0
  150. package/src/components/ui/panels/shape-choice.tsx +58 -0
  151. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  152. package/src/components/ui/primitives/color-dot.tsx +36 -24
  153. package/src/components/ui/primitives/sidebar.tsx +2 -0
  154. package/src/components/ui/right-stack.test.ts +29 -0
  155. package/src/components/ui/right-stack.tsx +145 -0
  156. package/src/components/ui/sidebar/icon-rail.tsx +5 -0
  157. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +31 -4
  158. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +5 -5
  159. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  160. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  161. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  162. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  163. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  164. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  165. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  166. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  167. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  168. package/src/components/ui/sidebar/panels/site-panel/index.tsx +176 -211
  169. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  170. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  171. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  172. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  173. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  174. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  175. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  176. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  177. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  178. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  179. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +0 -27
  180. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  181. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  182. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  183. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  184. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +0 -2
  185. package/src/components/ui/sidebar/tab-bar.tsx +10 -0
  186. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  187. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  188. package/src/components/viewer/floorplan-preview.tsx +5 -6
  189. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  190. package/src/components/viewer/viewer-rooms.tsx +175 -0
  191. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  192. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  193. package/src/components/viewer/viewer-stage.tsx +2 -2
  194. package/src/components/viewer-zone-system.tsx +3 -8
  195. package/src/hooks/use-auto-save.integration.test.tsx +123 -9
  196. package/src/hooks/use-auto-save.ts +34 -8
  197. package/src/hooks/use-keyboard.test.ts +178 -3
  198. package/src/hooks/use-keyboard.ts +89 -71
  199. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  200. package/src/hooks/use-open-wall-ends.ts +238 -0
  201. package/src/hooks/use-placement-notice.test.ts +48 -0
  202. package/src/hooks/use-placement-notice.ts +42 -0
  203. package/src/hooks/use-selected-room.ts +143 -0
  204. package/src/index.tsx +246 -39
  205. package/src/lib/ceiling-edit-session.test.ts +321 -0
  206. package/src/lib/ceiling-edit-session.ts +206 -0
  207. package/src/lib/ceiling-region-session.test.ts +179 -0
  208. package/src/lib/continuation.test.ts +19 -1
  209. package/src/lib/continuation.ts +25 -4
  210. package/src/lib/door-interaction.ts +14 -0
  211. package/src/lib/editor-level-display.test.ts +96 -0
  212. package/src/lib/editor-level-display.ts +48 -0
  213. package/src/lib/editor-views.ts +144 -0
  214. package/src/lib/export-geometry-wait.test.ts +90 -0
  215. package/src/lib/floor-edit-session.ts +153 -0
  216. package/src/lib/floor-footprints.test.ts +563 -0
  217. package/src/lib/floor-footprints.ts +395 -0
  218. package/src/lib/floor-foundation-support.test.ts +283 -0
  219. package/src/lib/floor-opening-draft.ts +218 -0
  220. package/src/lib/floor-openings.test.ts +216 -0
  221. package/src/lib/floor-region-geometry.ts +191 -0
  222. package/src/lib/floor-region-session.test.ts +485 -0
  223. package/src/lib/floor-region-session.ts +589 -0
  224. package/src/lib/floor-region-snap.ts +101 -0
  225. package/src/lib/floor-step-hover.test.ts +32 -0
  226. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  227. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  228. package/src/lib/floorplan/dimension-drive.ts +479 -0
  229. package/src/lib/floorplan/floorplan-export.test.ts +12 -8
  230. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  231. package/src/lib/floorplan/floorplan-extension.ts +32 -0
  232. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  233. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  234. package/src/lib/floorplan/floorplan-pdfkit-document.ts +339 -18
  235. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +138 -8
  236. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +56 -25
  237. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  238. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  239. package/src/lib/floorplan/geometry.ts +13 -6
  240. package/src/lib/floorplan/index.ts +0 -9
  241. package/src/lib/floorplan/items.ts +5 -2
  242. package/src/lib/floorplan/marks.test.ts +213 -0
  243. package/src/lib/floorplan/marks.ts +238 -0
  244. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  245. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  246. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  247. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  248. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  249. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  250. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  251. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  252. package/src/lib/floorplan/schedules.test.ts +189 -0
  253. package/src/lib/floorplan/schedules.ts +339 -0
  254. package/src/lib/floorplan/sheet-export.test.ts +100 -0
  255. package/src/lib/floorplan/sheet-export.ts +318 -0
  256. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +779 -0
  257. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  258. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  259. package/src/lib/floorplan/site-plan/coverage.ts +232 -0
  260. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  261. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  262. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  263. package/src/lib/floorplan/site-plan/front-edge.ts +156 -0
  264. package/src/lib/floorplan/site-plan/geometry.test.ts +275 -0
  265. package/src/lib/floorplan/site-plan/geometry.ts +557 -0
  266. package/src/lib/floorplan/site-plan/index.ts +71 -0
  267. package/src/lib/floorplan/site-plan/site-annotations.test.ts +366 -0
  268. package/src/lib/floorplan/site-plan/site-annotations.ts +1083 -0
  269. package/src/lib/floorplan/site-plan/site-parts.ts +379 -0
  270. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  271. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  272. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  273. package/src/lib/floorplan/site-plan/yards.test.ts +69 -0
  274. package/src/lib/floorplan/types.ts +1 -27
  275. package/src/lib/fresh-planar-placement.test.ts +36 -0
  276. package/src/lib/fresh-planar-placement.ts +59 -36
  277. package/src/lib/geometry-script/author.ts +152 -0
  278. package/src/lib/geometry-script/client.ts +152 -0
  279. package/src/lib/geometry-script/protocol.ts +12 -0
  280. package/src/lib/gesture-lifecycle.test.ts +595 -0
  281. package/src/lib/gesture-lifecycle.ts +290 -0
  282. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  283. package/src/lib/glb-export.test.ts +485 -0
  284. package/src/lib/glb-export.ts +237 -119
  285. package/src/lib/history.test.ts +6 -0
  286. package/src/lib/history.ts +7 -0
  287. package/src/lib/hud-title.ts +109 -0
  288. package/src/lib/hydration-gated-storage.ts +49 -0
  289. package/src/lib/ifc-export.test.ts +174 -0
  290. package/src/lib/ifc-export.ts +201 -0
  291. package/src/lib/ifc-parts.ts +15 -0
  292. package/src/lib/inspector-expanded.test.ts +44 -0
  293. package/src/lib/inspector-expanded.ts +36 -0
  294. package/src/lib/interaction/scope.ts +18 -0
  295. package/src/lib/keyboard-pan.test.ts +129 -7
  296. package/src/lib/keyboard-pan.ts +161 -12
  297. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  298. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  299. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  300. package/src/lib/lot/clean-ring.test.ts +186 -0
  301. package/src/lib/lot/clean-ring.ts +217 -0
  302. package/src/lib/lot/dossier.test.ts +202 -0
  303. package/src/lib/lot/dossier.ts +498 -0
  304. package/src/lib/lot/drop-in.test.ts +326 -0
  305. package/src/lib/lot/drop-in.ts +407 -0
  306. package/src/lib/lot/index.ts +43 -0
  307. package/src/lib/lot/lot-patch.test.ts +340 -0
  308. package/src/lib/lot/lot-patch.ts +394 -0
  309. package/src/lib/lot/parcel-provider.ts +43 -0
  310. package/src/lib/lot/terrain.test.ts +161 -0
  311. package/src/lib/lot/terrain.ts +266 -0
  312. package/src/lib/material-catalog-model.ts +3 -1
  313. package/src/lib/material-paint-panel-model.ts +9 -5
  314. package/src/lib/material-paint.ts +4 -0
  315. package/src/lib/mezzanine-actions.test.ts +342 -0
  316. package/src/lib/mezzanine-draft.test.ts +501 -0
  317. package/src/lib/mezzanine-draft.ts +278 -0
  318. package/src/lib/mezzanine-messages.ts +26 -0
  319. package/src/lib/mezzanine-selection.test.ts +207 -0
  320. package/src/lib/mezzanine-stairs.ts +46 -0
  321. package/src/lib/model-export.ts +3 -1
  322. package/src/lib/node-action-movement.ts +17 -6
  323. package/src/lib/node-display-label.ts +37 -0
  324. package/src/lib/orbit-floor-clearance.test.ts +165 -0
  325. package/src/lib/orbit-floor-clearance.ts +106 -0
  326. package/src/lib/paint-commit-route.test.ts +37 -0
  327. package/src/lib/paint-commit-route.ts +17 -0
  328. package/src/lib/paint-eyedropper.test.tsx +165 -0
  329. package/src/lib/paint-eyedropper.ts +99 -0
  330. package/src/lib/paint-pass-through.test.ts +111 -0
  331. package/src/lib/paint-pass-through.ts +76 -0
  332. package/src/lib/paint-region-hover.ts +25 -0
  333. package/src/lib/paint-region-mode.ts +154 -0
  334. package/src/lib/paint-regions.test.ts +166 -0
  335. package/src/lib/paint-regions.ts +192 -0
  336. package/src/lib/paint-scope.test.ts +317 -35
  337. package/src/lib/paint-scope.ts +266 -25
  338. package/src/lib/panel-rows.ts +21 -0
  339. package/src/lib/parametric-node-update.ts +1 -1
  340. package/src/lib/plate-paint-affected.test.ts +300 -0
  341. package/src/lib/plate-paint-affected.ts +177 -0
  342. package/src/lib/plugin-chat.ts +50 -0
  343. package/src/lib/plugin-panels.test.ts +25 -0
  344. package/src/lib/plugin-panels.ts +94 -0
  345. package/src/lib/print-shell-compiler.ts +1 -0
  346. package/src/lib/registered-draft-snap.ts +12 -0
  347. package/src/lib/roof-duplication.ts +10 -2
  348. package/src/lib/room-assembly-overlay.test.ts +275 -0
  349. package/src/lib/room-assembly-overlay.ts +266 -0
  350. package/src/lib/room-built-on.test.ts +379 -0
  351. package/src/lib/room-built-on.ts +138 -0
  352. package/src/lib/room-construction-commands.ts +712 -0
  353. package/src/lib/room-construction.test.ts +746 -0
  354. package/src/lib/room-construction.ts +385 -0
  355. package/src/lib/room-controls-phase6.test.ts +630 -0
  356. package/src/lib/room-divide-session.ts +523 -0
  357. package/src/lib/room-handle-drag.ts +1068 -0
  358. package/src/lib/room-name-catalog.test.ts +90 -0
  359. package/src/lib/room-name-catalog.ts +168 -0
  360. package/src/lib/room-push-dimensions.test.ts +311 -0
  361. package/src/lib/room-push-dimensions.ts +251 -0
  362. package/src/lib/room-selection-commands.ts +62 -0
  363. package/src/lib/room-selection.test.ts +973 -0
  364. package/src/lib/room-selection.ts +417 -0
  365. package/src/lib/room-structure-commands.ts +94 -0
  366. package/src/lib/room-structure.test.ts +921 -0
  367. package/src/lib/room-transform-session.ts +570 -0
  368. package/src/lib/room-zone-routing.test.ts +294 -0
  369. package/src/lib/room-zone-routing.ts +197 -0
  370. package/src/lib/scene-clipboard.test.ts +364 -0
  371. package/src/lib/scene-clipboard.ts +229 -48
  372. package/src/lib/scene-material-refs.test.ts +79 -0
  373. package/src/lib/scene-material-refs.ts +79 -0
  374. package/src/lib/scene.test.ts +24 -36
  375. package/src/lib/scene.ts +33 -57
  376. package/src/lib/selection-routing.test.ts +129 -45
  377. package/src/lib/selection-routing.ts +53 -74
  378. package/src/lib/session-writes.test.ts +120 -0
  379. package/src/lib/session-writes.ts +205 -0
  380. package/src/lib/sidebar-panel.ts +62 -0
  381. package/src/lib/site-boundary.test.ts +4 -6
  382. package/src/lib/site-boundary.ts +1 -1
  383. package/src/lib/snapping-mode.test.ts +4 -0
  384. package/src/lib/snapping-mode.ts +8 -1
  385. package/src/lib/terrace-draft.test.ts +145 -0
  386. package/src/lib/terrace-draft.ts +213 -0
  387. package/src/lib/tool-cursor-icon.test.ts +16 -0
  388. package/src/lib/tool-cursor-icon.ts +11 -0
  389. package/src/lib/typing-target.ts +9 -0
  390. package/src/lib/units.ts +77 -39
  391. package/src/lib/vector-edges.test.ts +58 -0
  392. package/src/lib/vector-edges.ts +400 -0
  393. package/src/lib/viewer-selection.test.ts +577 -0
  394. package/src/lib/viewer-selection.ts +569 -0
  395. package/src/lib/wall-draw-variant.test.ts +71 -0
  396. package/src/lib/wall-draw-variant.ts +91 -0
  397. package/src/lib/wall-paint-region-session.ts +246 -0
  398. package/src/lib/wall-paint-region.test.ts +373 -0
  399. package/src/lib/wall-polygon-draft.test.ts +262 -0
  400. package/src/lib/wall-polygon-draft.ts +286 -0
  401. package/src/lib/wall-push.test.ts +453 -0
  402. package/src/lib/wall-region-face.ts +352 -0
  403. package/src/lib/wall-region-handles.test.ts +304 -0
  404. package/src/lib/wall-region-handles.ts +368 -0
  405. package/src/lib/wall-region-list.test.ts +119 -0
  406. package/src/lib/wall-region-list.ts +136 -0
  407. package/src/lib/wall-region-snap.ts +110 -0
  408. package/src/lib/window-interaction.ts +14 -0
  409. package/src/lib/zone-content.ts +4 -1
  410. package/src/lib/zone-name.test.ts +17 -0
  411. package/src/lib/zone-name.ts +17 -0
  412. package/src/store/opens-in-select-mode.test.ts +223 -0
  413. package/src/store/paint-mode.test.ts +188 -0
  414. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  415. package/src/store/tool-mode.test.ts +137 -6
  416. package/src/store/use-delete-confirmation.ts +28 -1
  417. package/src/store/use-drawing-view.test.ts +24 -3
  418. package/src/store/use-drawing-view.ts +29 -1
  419. package/src/store/use-editor.tsx +432 -321
  420. package/src/store/use-floorplan-draft-preview.ts +13 -0
  421. package/src/store/use-floorplan-marquee.ts +12 -2
  422. package/src/store/use-quick-measurement-hud.ts +3 -3
  423. package/src/store/use-stair-build-preview.ts +13 -5
  424. package/src/store/use-wall-move-ghosts.ts +4 -2
  425. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  426. package/src/store/use-wall-region-handles-selection.ts +139 -0
  427. package/src/store/view-layout.test.ts +175 -0
  428. package/src/store/view-layout.ts +196 -0
  429. package/src/test-utils/immediate-animation-frames.ts +16 -0
  430. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  431. package/src/test-utils/selection-harness.ts +82 -0
  432. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  433. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  434. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  435. package/src/components/tools/fence/fence-drafting.ts +0 -266
  436. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  437. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  438. package/src/lib/floorplan/stairs.ts +0 -489
  439. package/src/lib/level-duplication.test.ts +0 -144
  440. package/src/lib/level-duplication.ts +0 -195
@@ -9,6 +9,7 @@ import {
9
9
  CeilingNode as CeilingNodeSchema,
10
10
  type ColumnNode,
11
11
  calculateLevelMiters,
12
+ createSizedStairFlight,
12
13
  DEFAULT_ANGLE_STEP,
13
14
  type DoorNode,
14
15
  DoorNode as DoorNodeSchema,
@@ -27,6 +28,7 @@ import {
27
28
  isCurvedWall,
28
29
  type LevelNode,
29
30
  loadAssetUrl,
31
+ newZone,
30
32
  nodeRegistry,
31
33
  normalizeWallCurveOffset,
32
34
  type Point2D,
@@ -40,7 +42,6 @@ import {
40
42
  SlabNode as SlabNodeSchema,
41
43
  type StairNode,
42
44
  StairNode as StairNodeSchema,
43
- StairSegmentNode as StairSegmentNodeSchema,
44
45
  sampleWallCenterline,
45
46
  sceneRegistry,
46
47
  snapPointAlongAngleRay,
@@ -56,14 +57,13 @@ import {
56
57
  WindowNode as WindowNodeSchema,
57
58
  wallClosesRoom,
58
59
  wallRectangleCorners,
59
- ZoneNode as ZoneNodeSchema,
60
- type ZoneNode as ZoneNodeType,
61
60
  } from '@pascal-app/core'
62
61
  import { useSegmentDraftChain, useWallSnapIndicator } from '@pascal-app/editor'
63
62
  import { getSceneTheme, useViewer } from '@pascal-app/viewer'
64
63
  import { Command, Ruler } from 'lucide-react'
65
64
  import {
66
65
  type ComponentProps,
66
+ type CSSProperties,
67
67
  memo,
68
68
  type MouseEvent as ReactMouseEvent,
69
69
  type ReactNode,
@@ -78,11 +78,15 @@ import {
78
78
  import { createPortal } from 'react-dom'
79
79
  import { Vector3 } from 'three'
80
80
  import { useShallow } from 'zustand/react/shallow'
81
+ import {
82
+ hoverRoomFromHit,
83
+ resolvePlanRoomHit,
84
+ roomPickingEnabled,
85
+ } from '../../hooks/use-selected-room'
81
86
  import { resolveCeilingPlanPointSnap } from '../../lib/ceiling-plan-snap'
82
87
  import {
83
88
  alignFloorplanDraftPoint,
84
89
  buildFloorplanItemEntry,
85
- buildFloorplanStairEntry as buildSharedFloorplanStairEntry,
86
90
  collectLevelDescendants,
87
91
  FLOORPLAN_VIEW_ROTATION_DEG,
88
92
  floorplanLocalToWorldPoint,
@@ -91,29 +95,58 @@ import {
91
95
  type FloorplanNodeTransform as SharedFloorplanNodeTransform,
92
96
  worldToFloorplanLocalPoint,
93
97
  } from '../../lib/floorplan'
98
+ import { buildFloorplanContext } from '../../lib/floorplan/floorplan-readonly'
99
+ // Site-plan view. Owned under `lib/floorplan/site-plan/**`; this panel
100
+ // only chooses between it and the registry layer, and mounts the switch.
101
+ import {
102
+ FloorplanDrawingTypeSwitch,
103
+ useSitePlanAvailable,
104
+ } from '../../lib/floorplan/site-plan/drawing-type-switch'
105
+ import { FloorplanSitePlanLayer } from '../../lib/floorplan/site-plan/site-plan-layer'
94
106
  import { resolveGenericFloorplanGridEventPoint } from '../../lib/floorplan-grid-event-point'
95
107
  import type { EditorGridEvent } from '../../lib/grid-event-presentation'
96
108
  import { groundHeightAt } from '../../lib/ground-surface'
97
109
  import { guideEmitter } from '../../lib/guide-events'
98
110
  import {
99
111
  acceptsKeyboardPan,
112
+ advanceKeyboardPanMotion,
100
113
  clearKeyboardPanKeys,
114
+ createKeyboardPanMotion,
101
115
  isKeyboardPanKey,
102
116
  type KeyboardPanState,
103
- keyboardPanDirection,
104
117
  keyboardPanSpeed,
118
+ resetKeyboardPanMotion,
105
119
  setKeyboardPanKey,
120
+ syncKeyboardPanMotion,
106
121
  } from '../../lib/keyboard-pan'
107
122
  import { measurementHint, parseMeasurement } from '../../lib/measurement-parser'
108
123
  import { formatLinearMeasurement, linearUnitToMeters } from '../../lib/measurements'
124
+ import { snapRegisteredDraftPoint } from '../../lib/registered-draft-snap'
125
+ import { selectRoomFromHit } from '../../lib/room-selection-commands'
126
+ import { enterBuildingFromPlanHit } from '../../lib/selection-routing'
109
127
  import { sfxEmitter } from '../../lib/sfx-bus'
110
128
  import { SITE_BOUNDARY_DRAG_LABEL, siteBoundaryHandlesEnabled } from '../../lib/site-boundary'
111
129
  import { resolveSlabPlanPointSnap } from '../../lib/slab-plan-snap'
112
- import { cancelPendingZonePaint, focusedUnitNode, paintZoneMembership } from '../../lib/units'
130
+ import {
131
+ cancelPendingZonePaint,
132
+ focusedUnitNode,
133
+ paintZoneMembership,
134
+ zoneAtLevelPoint,
135
+ } from '../../lib/units'
113
136
  import { cn } from '../../lib/utils'
137
+ import { getWallDrawVariant } from '../../lib/wall-draw-variant'
138
+ import {
139
+ addWallPolygonDraftCorner,
140
+ commitWallPolygonDraft,
141
+ discardWallPolygonDraft,
142
+ startWallPolygonDraft,
143
+ wallPolygonDraftWalls,
144
+ } from '../../lib/wall-polygon-draft'
114
145
  import { snapBuildingLocalToWorldGrid } from '../../lib/world-grid-snap'
146
+ import { nextZoneName } from '../../lib/zone-name'
115
147
  import { subscribeNavigationSyncPose } from '../../store/navigation-sync-pose-store'
116
148
  import useAlignmentGuides from '../../store/use-alignment-guides'
149
+ import useDrawingView from '../../store/use-drawing-view'
117
150
  import type { GuideUiState, NavigationSyncPose } from '../../store/use-editor'
118
151
  import useEditor, {
119
152
  isAngleSnapActive,
@@ -132,11 +165,13 @@ import useInteractionScope, {
132
165
  import usePlacementPreview from '../../store/use-placement-preview'
133
166
  import { expandSessionSelectionForNode } from '../../store/use-session-groups'
134
167
  import { useStairBuildPreview } from '../../store/use-stair-build-preview'
168
+ import { activeViewLayout, isViewVisible, VIEW_2D, visibleScene } from '../../store/view-layout'
135
169
  import { FloorplanAlignmentGuideLayer } from '../editor-2d/floorplan-alignment-guide-layer'
136
170
  import { FloorplanCursorIndicatorOverlay as Editor2dFloorplanCursorIndicatorOverlay } from '../editor-2d/floorplan-cursor-indicator-overlay'
137
171
  import { FloorplanGroupActionMenu } from '../editor-2d/floorplan-group-action-menu'
138
172
  import { FloorplanSiteKeyHandler } from '../editor-2d/floorplan-hotkey-handlers'
139
173
  import { FloorplanMeasurementToolLayer } from '../editor-2d/floorplan-measurement-tool-layer'
174
+ import { FloorplanOpenWallEndsLayer } from '../editor-2d/floorplan-open-wall-ends-layer'
140
175
  import { FloorplanRegisteredToolLayer } from '../editor-2d/floorplan-registered-tool-layer'
141
176
  import { FloorplanRegistryActionMenu } from '../editor-2d/floorplan-registry-action-menu'
142
177
  import { FloorplanRegistryMoveOverlay } from '../editor-2d/floorplan-registry-move-overlay'
@@ -147,7 +182,7 @@ import {
147
182
  import { FloorplanSnapBeaconLayer } from '../editor-2d/floorplan-snap-beacon-layer'
148
183
  import { FloorplanWallMoveGhostLayer } from '../editor-2d/floorplan-wall-move-ghost-layer'
149
184
  import { FloorplanDraftLayer } from '../editor-2d/renderers/floorplan-draft-layer'
150
- import { FloorplanDraftWallMeasurement } from '../editor-2d/renderers/floorplan-draft-wall-measurement'
185
+ import { FloorplanDraftMeasurement } from '../editor-2d/renderers/floorplan-draft-measurement'
151
186
  import { FloorplanGeometryRenderer } from '../editor-2d/renderers/floorplan-geometry-renderer'
152
187
  import { FloorplanMarqueeLayer } from '../editor-2d/renderers/floorplan-marquee-layer'
153
188
  import { FloorplanPlacementPreviewLayer } from '../editor-2d/renderers/floorplan-placement-preview-layer'
@@ -155,10 +190,8 @@ import {
155
190
  FloorplanRegistryLayer,
156
191
  RotationAngleOverlay,
157
192
  } from '../editor-2d/renderers/floorplan-registry-layer'
158
- import { FloorplanStairLayer } from '../editor-2d/renderers/floorplan-stair-layer'
159
193
  import { FloorplanVoronoiLayer } from '../editor-2d/renderers/floorplan-voronoi-layer'
160
- import { buildSvgPolylinePath, formatPolygonPath, getArcPlanPoint } from '../editor-2d/svg-paths'
161
- import { snapFenceDraftPoint } from '../tools/fence/fence-drafting'
194
+ import { buildSvgPolylinePath, formatPolygonPath } from '../editor-2d/svg-paths'
162
195
  import { snapToHalf } from '../tools/item/placement-math'
163
196
  import {
164
197
  isBoxSelectPointerSuppressed,
@@ -188,15 +221,6 @@ import {
188
221
  getAngleToSegmentReference,
189
222
  getSegmentAngleReferenceAtPoint,
190
223
  } from '../tools/shared/segment-angle'
191
- import {
192
- DEFAULT_STAIR_ATTACHMENT_SIDE,
193
- DEFAULT_STAIR_FILL_TO_FLOOR,
194
- DEFAULT_STAIR_HEIGHT,
195
- DEFAULT_STAIR_LENGTH,
196
- DEFAULT_STAIR_STEP_COUNT,
197
- DEFAULT_STAIR_THICKNESS,
198
- DEFAULT_STAIR_WIDTH,
199
- } from '../tools/stair/stair-defaults'
200
224
  import {
201
225
  chainEndJoinsExistingWall,
202
226
  createWallOnCurrentLevel,
@@ -208,8 +232,6 @@ import {
208
232
  WALL_JOIN_SNAP_RADIUS,
209
233
  type WallPlanPoint,
210
234
  } from '../tools/wall/wall-drafting'
211
-
212
- import { PALETTE_COLORS } from '../ui/primitives/color-dot'
213
235
  import { FloorplanCompassButton } from '../viewer/floorplan-compass-button'
214
236
  import { resolveFloorplanBackgroundSelection } from './floorplan-background-selection'
215
237
  import {
@@ -232,6 +254,7 @@ import {
232
254
  import { useFloorplanBackgroundPlacement } from './use-floorplan-background-placement'
233
255
  import { useFloorplanHitTesting } from './use-floorplan-hit-testing'
234
256
  import { useFloorplanSceneData } from './use-floorplan-scene-data'
257
+ import { sceneSpan } from './views/scene-region'
235
258
 
236
259
  const FALLBACK_VIEW_SIZE = 12
237
260
  const FLOORPLAN_PADDING = 2
@@ -282,7 +305,6 @@ const FLOORPLAN_HOVER_TRANSITION = 'opacity 180ms cubic-bezier(0.2, 0, 0, 1)'
282
305
  const FLOORPLAN_WALL_HIT_STROKE_WIDTH = 18
283
306
  const FLOORPLAN_WALL_STROKE_WIDTH = '1'
284
307
  const FLOORPLAN_OPENING_HIT_STROKE_WIDTH = 16
285
- const noopFloorplanStairHandler = () => {}
286
308
  const FLOORPLAN_OPENING_STROKE_WIDTH = 0.05
287
309
  const FLOORPLAN_ENDPOINT_HIT_STROKE_WIDTH = 18
288
310
  const FLOORPLAN_ENDPOINT_HOVER_GLOW_STROKE_WIDTH = 16
@@ -2124,68 +2146,6 @@ function movePlanPointTowards(start: Point2D, end: Point2D, distance: number): P
2124
2146
  return interpolatePlanPoint(start, end, Math.min(1, distance / totalDistance))
2125
2147
  }
2126
2148
 
2127
- function getNormalizedFloorplanStairSweepAngle(stair: StairNode) {
2128
- const stairType = stair.stairType ?? 'straight'
2129
- const baseSweepAngle = stair.sweepAngle ?? (stairType === 'spiral' ? Math.PI * 2 : Math.PI / 2)
2130
-
2131
- if (Math.abs(baseSweepAngle) >= Math.PI * 2) {
2132
- return Math.sign(baseSweepAngle || 1) * (Math.PI * 2 - 0.001)
2133
- }
2134
-
2135
- return baseSweepAngle
2136
- }
2137
-
2138
- function getFloorplanSpiralLandingSweep(stair: StairNode, sweepAngle: number) {
2139
- if (
2140
- (stair.stairType ?? 'straight') !== 'spiral' ||
2141
- (stair.topLandingMode ?? 'none') !== 'integrated'
2142
- ) {
2143
- return 0
2144
- }
2145
-
2146
- const innerRadius = Math.max(0.05, stair.innerRadius ?? 0.9)
2147
- const width = Math.max(stair.width ?? 1, 0.4)
2148
- const landingDepth = Math.max(0.3, stair.topLandingDepth ?? Math.max(width * 0.9, 0.8))
2149
-
2150
- return (
2151
- Math.min(Math.PI * 0.75, landingDepth / Math.max(innerRadius + width / 2, 0.1)) *
2152
- Math.sign(sweepAngle || 1)
2153
- )
2154
- }
2155
-
2156
- function getFloorplanCurvedStairHitPolygon(stair: StairNode): Point2D[] {
2157
- const stairType = stair.stairType ?? 'straight'
2158
- const sweepAngle = getNormalizedFloorplanStairSweepAngle(stair)
2159
- const startAngle = -stair.rotation - sweepAngle / 2
2160
- const endAngle = startAngle + sweepAngle + getFloorplanSpiralLandingSweep(stair, sweepAngle)
2161
- const center = {
2162
- x: stair.position[0],
2163
- y: stair.position[2],
2164
- }
2165
- const innerRadius = Math.max(
2166
- stairType === 'spiral' ? 0.05 : 0.2,
2167
- stair.innerRadius ?? (stairType === 'spiral' ? 0.2 : 0.9),
2168
- )
2169
- const outerRadius = innerRadius + stair.width
2170
- const outerArcLength = Math.abs(sweepAngle) * outerRadius
2171
- const segmentCount = Math.max(
2172
- 24,
2173
- Math.ceil(Math.abs(sweepAngle) / (Math.PI / 24)),
2174
- Math.ceil(outerArcLength / 0.14),
2175
- )
2176
- const outerPoints: Point2D[] = []
2177
- const innerPoints: Point2D[] = []
2178
-
2179
- for (let index = 0; index <= segmentCount; index += 1) {
2180
- const t = index / segmentCount
2181
- const angle = startAngle + (endAngle - startAngle) * t
2182
- outerPoints.push(getArcPlanPoint(center, outerRadius, angle))
2183
- innerPoints.push(getArcPlanPoint(center, innerRadius, angle))
2184
- }
2185
-
2186
- return [...outerPoints, ...innerPoints.reverse()]
2187
- }
2188
-
2189
2149
  function isPointInsidePolygonWithHoles(
2190
2150
  point: Point2D,
2191
2151
  polygon: Point2D[],
@@ -4226,107 +4186,53 @@ const FloorplanPolygonHandleLayer = memo(function FloorplanPolygonHandleLayer({
4226
4186
  )
4227
4187
  })
4228
4188
 
4229
- // Static segment for the in-flight stair build preview. No per-render
4230
- // dependency (the geometry only moves / rotates), so it lives at module scope
4231
- // instead of a `useMemo`.
4232
- const FLOORPLAN_PREVIEW_STAIR_SEGMENT = StairSegmentNodeSchema.parse({
4233
- id: 'sseg_floorplan_preview',
4234
- segmentType: 'stair',
4235
- width: DEFAULT_STAIR_WIDTH,
4236
- length: DEFAULT_STAIR_LENGTH,
4237
- height: DEFAULT_STAIR_HEIGHT,
4238
- stepCount: DEFAULT_STAIR_STEP_COUNT,
4239
- attachmentSide: DEFAULT_STAIR_ATTACHMENT_SIDE,
4240
- fillToFloor: DEFAULT_STAIR_FILL_TO_FLOOR,
4241
- thickness: DEFAULT_STAIR_THICKNESS,
4242
- position: [0, 0, 0],
4243
- metadata: { isTransient: true, isFloorplanPreview: true },
4244
- })
4245
-
4246
- const EMPTY_FLOORPLAN_ID_SET: ReadonlySet<string> = new Set()
4247
-
4248
- type FloorplanStairLayerPalette = ComponentProps<typeof FloorplanStairLayer>['palette']
4249
-
4250
- // Leaf layer for the stair tool's in-flight 2D build preview. Subscribes to the
4251
- // `useStairBuildPreview` store directly so a per-`grid:move` point update (or an
4252
- // R/T rotation) re-renders ONLY this tiny layer — never the ~120-220ms
4253
- // `FloorplanPanel`. This mirrors how column / elevator placement stays smooth by
4254
- // routing preview state through a store + leaf. Committed stairs render through
4255
- // `FloorplanRegistryLayer`; this layer is non-interactive (noop handlers, empty
4256
- // hit sets), so it never participates in hover / select.
4257
4189
  function FloorplanStairBuildPreviewLayer({
4258
4190
  palette,
4259
- isDeleteMode,
4260
4191
  }: {
4261
- palette: FloorplanStairLayerPalette
4262
- isDeleteMode: boolean
4192
+ palette: FloorplanRenderContextValue['palette']
4263
4193
  }) {
4264
- const phase = useEditor((s) => s.phase)
4265
4194
  const mode = useEditor((s) => s.mode)
4266
4195
  const tool = useEditor((s) => s.tool)
4267
4196
  const point = useStairBuildPreview((s) => s.point)
4268
4197
  const rotation = useStairBuildPreview((s) => s.rotation)
4269
- const isActive = phase === 'structure' && mode === 'build' && tool === 'stair'
4198
+ const rise = useStairBuildPreview((s) => s.rise)
4199
+ const isActive = mode === 'build' && tool === 'stair'
4270
4200
 
4271
- const previewEntry = useMemo(() => {
4272
- if (!(isActive && point)) {
4201
+ const previewGeometry = useMemo(() => {
4202
+ if (!(isActive && point && rise && rise > 0)) {
4273
4203
  return null
4274
4204
  }
4205
+ const previewSegment = {
4206
+ ...createSizedStairFlight(rise),
4207
+ id: 'sseg_floorplan_preview' as const,
4208
+ parentId: 'stair_floorplan_preview' as const,
4209
+ }
4275
4210
  const previewStair = StairNodeSchema.parse({
4276
4211
  id: 'stair_floorplan_preview',
4277
4212
  name: 'Staircase preview',
4278
4213
  position: [point[0], 0, point[1]],
4279
4214
  rotation,
4280
- children: [FLOORPLAN_PREVIEW_STAIR_SEGMENT.id],
4215
+ children: [previewSegment.id],
4281
4216
  metadata: { isTransient: true, isFloorplanPreview: true },
4282
4217
  })
4283
- const entry = buildSharedFloorplanStairEntry(previewStair, [FLOORPLAN_PREVIEW_STAIR_SEGMENT])
4284
- if (!entry) {
4285
- return null
4286
- }
4287
- const hitPolygons =
4288
- (previewStair.stairType ?? 'straight') === 'straight'
4289
- ? entry.segments.map((segmentEntry) => segmentEntry.polygon)
4290
- : [getFloorplanCurvedStairHitPolygon(previewStair)]
4291
-
4292
- return {
4293
- ...entry,
4294
- hitPolygons,
4295
- segments: entry.segments.map((segmentEntry) => ({
4296
- ...segmentEntry,
4297
- innerPoints: formatPolygonPoints(segmentEntry.innerPolygon),
4298
- points: formatPolygonPoints(segmentEntry.polygon),
4299
- treadBars: segmentEntry.treadBars.map((polygon) => ({
4300
- points: formatPolygonPoints(polygon),
4301
- polygon,
4302
- })),
4303
- })),
4304
- }
4305
- }, [isActive, point, rotation])
4306
-
4307
- if (!previewEntry) {
4308
- return null
4309
- }
4218
+ const nodes = { [previewStair.id]: previewStair, [previewSegment.id]: previewSegment }
4219
+ return (
4220
+ nodeRegistry.get(previewStair.type)?.floorplan?.(
4221
+ previewStair,
4222
+ buildFloorplanContext(previewStair, nodes, {
4223
+ selected: false,
4224
+ highlighted: false,
4225
+ hovered: false,
4226
+ moving: true,
4227
+ unit: 'metric',
4228
+ palette,
4229
+ }),
4230
+ ) ?? null
4231
+ )
4232
+ }, [isActive, point, rotation, rise, palette])
4310
4233
 
4311
- return (
4312
- <FloorplanStairLayer
4313
- canFocusStairs={false}
4314
- canSelectStairs={false}
4315
- cursor={EDITOR_CURSOR}
4316
- highlightedIdSet={EMPTY_FLOORPLAN_ID_SET}
4317
- hitStrokeWidth={FLOORPLAN_OPENING_HIT_STROKE_WIDTH}
4318
- hoveredStairId={null}
4319
- isDeleteMode={isDeleteMode}
4320
- onStairDoubleClick={noopFloorplanStairHandler}
4321
- onStairHoverChange={noopFloorplanStairHandler}
4322
- onStairHoverEnter={noopFloorplanStairHandler}
4323
- onStairPointerDown={noopFloorplanStairHandler}
4324
- onStairSelect={noopFloorplanStairHandler}
4325
- palette={palette}
4326
- selectedIdSet={EMPTY_FLOORPLAN_ID_SET}
4327
- stairEntries={[previewEntry]}
4328
- />
4329
- )
4234
+ if (!previewGeometry) return null
4235
+ return <FloorplanGeometryRenderer geometry={previewGeometry} pointerEventsOverride="none" />
4330
4236
  }
4331
4237
 
4332
4238
  // Leaf overlay for the cursor-following draft preview: the cursor crosshair plus
@@ -4530,6 +4436,7 @@ function FloorplanLinearDraftLayer({
4530
4436
  const roofDraftEnd = useFloorplanDraftPreview((s) => s.roofDraftEnd)
4531
4437
  const roofDraftQuarterTurn = useFloorplanDraftPreview((s) => s.roofDraftQuarterTurn)
4532
4438
  const wallRectangleDraftStart = useFloorplanDraftPreview((s) => s.wallRectangleDraftStart)
4439
+ const wallPolygonDraftPoints = useFloorplanDraftPreview((s) => s.wallPolygonDraftPoints)
4533
4440
  // The live cursor is the rectangle's opposite corner; select it only while a
4534
4441
  // rectangle draft is open so idle moves don't re-render this layer.
4535
4442
  const wallRectangleDraftEnd = useFloorplanDraftPreview((s) =>
@@ -4599,6 +4506,20 @@ function FloorplanLinearDraftLayer({
4599
4506
  wallRectangleDraftStart,
4600
4507
  ])
4601
4508
 
4509
+ // An open Polygon room's placed sides — draft state, drawn like the
4510
+ // rectangle draft (mitered together) until the polygon is written.
4511
+ const polygonRoomDraft = useMemo(() => {
4512
+ if (!(levelId && isWallBuildActive && wallPolygonDraftPoints.length >= 2)) return null
4513
+ const draftWalls = wallPolygonDraftPoints.slice(1).map((end, index) =>
4514
+ getSharedFloorplanWall({
4515
+ ...buildDraftWall(levelId, wallPolygonDraftPoints[index]!, end),
4516
+ id: `wall_polygon_draft_${index}` as WallNode['id'],
4517
+ }),
4518
+ )
4519
+ const miterData = calculateLevelMiters(draftWalls)
4520
+ return draftWalls.map((wall) => formatPolygonPoints(getWallPlanFootprint(wall, miterData)))
4521
+ }, [isWallBuildActive, levelId, wallPolygonDraftPoints])
4522
+
4602
4523
  const draftPolygonPoints = useMemo(() => {
4603
4524
  if (isRoofBuildActive && roofDraftStart && roofDraftEnd) {
4604
4525
  const minX = Math.min(roofDraftStart[0], roofDraftEnd[0])
@@ -4831,7 +4752,7 @@ function FloorplanLinearDraftLayer({
4831
4752
  )}
4832
4753
 
4833
4754
  {draftWallMeasurement && (
4834
- <FloorplanDraftWallMeasurement
4755
+ <FloorplanDraftMeasurement
4835
4756
  labelBackground={isDark ? '#0f172a' : '#ffffff'}
4836
4757
  labelText={isDark ? '#e2e8f0' : '#171717'}
4837
4758
  measurement={draftWallMeasurement}
@@ -4841,6 +4762,22 @@ function FloorplanLinearDraftLayer({
4841
4762
  />
4842
4763
  )}
4843
4764
 
4765
+ {polygonRoomDraft?.map((points, index) => (
4766
+ <FloorplanDraftLayer
4767
+ anchorFill={draftStroke}
4768
+ draftAnchorPoints={EMPTY_DRAFT_ANCHOR_POINTS}
4769
+ draftFill={draftFill}
4770
+ draftPolygonPoints={points}
4771
+ draftStroke={draftStroke}
4772
+ key={`polygon-room-${index}`}
4773
+ linearDraftSegment={null}
4774
+ polygonDraftClosingSegment={null}
4775
+ polygonDraftPolygonPoints={null}
4776
+ polygonDraftPolylinePoints={null}
4777
+ unitsPerPixel={unitsPerPixel}
4778
+ />
4779
+ ))}
4780
+
4844
4781
  {rectangleDraft?.polygons.map((points, index) => (
4845
4782
  <FloorplanDraftLayer
4846
4783
  anchorFill={draftStroke}
@@ -4858,7 +4795,7 @@ function FloorplanLinearDraftLayer({
4858
4795
  ))}
4859
4796
 
4860
4797
  {rectangleDraft?.measurements.map((measurement, index) => (
4861
- <FloorplanDraftWallMeasurement
4798
+ <FloorplanDraftMeasurement
4862
4799
  key={index}
4863
4800
  labelBackground={isDark ? '#0f172a' : '#ffffff'}
4864
4801
  labelText={isDark ? '#e2e8f0' : '#171717'}
@@ -4962,6 +4899,12 @@ export function FloorplanPanel({
4962
4899
  const unit = useViewer((state) => state.unit)
4963
4900
  const metricNotation = useViewer((state) => state.metricNotation)
4964
4901
  const showGrid = useViewer((state) => state.showGrid)
4902
+ // Floor plan vs. site plan. Site plan hides the level geometry and draws
4903
+ // the lot instead — see `lib/floorplan/site-plan/site-plan-layer.tsx`. A
4904
+ // persisted site-plan choice on a scene with no lot reads as the floor plan.
4905
+ const sitePlanAvailable = useSitePlanAvailable()
4906
+ const storedDrawingType = useDrawingView((state) => state.drawingType)
4907
+ const activeDrawingType = sitePlanAvailable ? storedDrawingType : 'floor-plan'
4965
4908
  const showGuides = useViewer((state) => state.showGuides)
4966
4909
  const setShowGuides = useViewer((state) => state.setShowGuides)
4967
4910
  const selectedItem = useEditor((state) => state.selectedItem)
@@ -4971,8 +4914,21 @@ export function FloorplanPanel({
4971
4914
  // editor/index.tsx — subscribing here lets us re-fit the viewport when
4972
4915
  // the user closes and re-opens the 2D editor instead of restoring the
4973
4916
  // stale viewport from before they closed it.
4974
- const isFloorplanOpen = useEditor((state) => state.isFloorplanOpen)
4975
- const viewMode = useEditor((state) => state.viewMode)
4917
+ const isFloorplanOpen = useEditor((state) => isViewVisible(state, VIEW_2D))
4918
+ // The compass sits at the scene's bottom-left corner, the drawing switch
4919
+ // beside it. With the plan in the right pane both move to its bottom-right
4920
+ // corner (compass outermost), clear of the dock centred over the scene.
4921
+ const planOnRight = useEditor((state) => {
4922
+ const layout = activeViewLayout(state)
4923
+ return layout.split && layout.panes[1] === VIEW_2D
4924
+ })
4925
+ // `null` while no scene view is on screen: the compass has nothing to orient.
4926
+ const sceneLeft = useEditor((state) => sceneSpan(activeViewLayout(state))?.left ?? null)
4927
+ // Mirrored, the pair stays left of the helper toggle in the stage's corner.
4928
+ const compassStyle: CSSProperties = planOnRight
4929
+ ? { right: 56 }
4930
+ : { left: `calc(${(sceneLeft ?? 0) * 100}% + 12px)` }
4931
+ const shownScene = useEditor(visibleScene)
4976
4932
  // Mirror for callbacks that fire outside React's render (the per-frame
4977
4933
  // navigation-pose subscriber): when the 2D panel is hidden (`display:none` in
4978
4934
  // 3D mode) it must NOT re-render on every camera-zoom frame.
@@ -4990,8 +4946,6 @@ export function FloorplanPanel({
4990
4946
  const activeHandleDrag = useActiveHandleDrag()
4991
4947
  const setPhase = useEditor((state) => state.setPhase)
4992
4948
  const setMovingNode = useEditor((state) => state.setMovingNode)
4993
- const structureLayer = useEditor((state) => state.structureLayer)
4994
- const setStructureLayer = useEditor((state) => state.setStructureLayer)
4995
4949
  const setTool = useEditor((state) => state.setTool)
4996
4950
  const tool = useEditor((state) => state.tool)
4997
4951
  const deleteNode = useScene((state) => state.deleteNode)
@@ -5273,12 +5227,6 @@ export function FloorplanPanel({
5273
5227
  height: number
5274
5228
  } | null>(null)
5275
5229
 
5276
- useEffect(() => {
5277
- if (structureLayer === 'zones' && floorplanSelectionTool === 'marquee') {
5278
- setFloorplanSelectionTool('click')
5279
- }
5280
- }, [floorplanSelectionTool, setFloorplanSelectionTool, structureLayer])
5281
-
5282
5230
  useEffect(() => {
5283
5231
  setIsMacPlatform(navigator.platform.toUpperCase().includes('MAC'))
5284
5232
  }, [])
@@ -5792,21 +5740,21 @@ export function FloorplanPanel({
5792
5740
  )
5793
5741
 
5794
5742
  const isSiteEditActive = phase === 'site'
5795
- const isWallBuildActive = phase === 'structure' && mode === 'build' && tool === 'wall'
5796
- const isSlabBuildActive = phase === 'structure' && mode === 'build' && tool === 'slab'
5797
- const isCeilingBuildActive = phase === 'structure' && mode === 'build' && tool === 'ceiling'
5798
- const isZoneBuildActive = phase === 'structure' && mode === 'build' && tool === 'zone'
5799
- const isDoorBuildActive = phase === 'structure' && mode === 'build' && tool === 'door'
5800
- const isWindowBuildActive = phase === 'structure' && mode === 'build' && tool === 'window'
5743
+ const isWallBuildActive = mode === 'build' && tool === 'wall'
5744
+ const isSlabBuildActive = mode === 'build' && tool === 'slab'
5745
+ const isCeilingBuildActive = mode === 'build' && tool === 'ceiling'
5746
+ const isZoneBuildActive = mode === 'build' && tool === 'zone'
5747
+ const isDoorBuildActive = mode === 'build' && tool === 'door'
5748
+ const isWindowBuildActive = mode === 'build' && tool === 'window'
5801
5749
  const isPolygonBuildActive = isSlabBuildActive || isZoneBuildActive
5802
5750
  const isPolygonDraftBuildActive = isPolygonBuildActive || isCeilingBuildActive
5803
5751
  const isOpeningBuildActive = isDoorBuildActive || isWindowBuildActive
5804
5752
  const isOpeningMoveActive = movingOpeningType !== null
5805
5753
  const isOpeningPlacementActive = isOpeningBuildActive || isOpeningMoveActive
5806
- const isFenceBuildActive = phase === 'structure' && mode === 'build' && tool === 'fence'
5754
+ const isFenceBuildActive = mode === 'build' && tool === 'fence'
5807
5755
  const fenceContinuation = useEditor((state) => state.continuationByContext.fence)
5808
- const isRoofBuildActive = phase === 'structure' && mode === 'build' && tool === 'roof'
5809
- const isStairBuildActive = phase === 'structure' && mode === 'build' && tool === 'stair'
5756
+ const isRoofBuildActive = mode === 'build' && tool === 'roof'
5757
+ const isStairBuildActive = mode === 'build' && tool === 'stair'
5810
5758
  const isStairMoveActive = movingNode?.type === 'stair'
5811
5759
  const isRoofMoveActive = movingNode?.type === 'roof' || movingNode?.type === 'roof-segment'
5812
5760
  const isSlabMoveActive = movingNode?.type === 'slab'
@@ -5932,12 +5880,11 @@ export function FloorplanPanel({
5932
5880
  mode === 'select' &&
5933
5881
  floorplanSelectionTool === 'marquee' &&
5934
5882
  !movingNode &&
5935
- !isFenceEndpointMoveActive &&
5936
- structureLayer !== 'zones'
5883
+ !isFenceEndpointMoveActive
5937
5884
  const isScreenSelectionToolActive =
5938
5885
  mode === 'select' &&
5939
5886
  floorplanSelectionTool === 'click' &&
5940
- (phase === 'structure' || phase === 'furnish') &&
5887
+ phase === 'building' &&
5941
5888
  !movingNode &&
5942
5889
  !isFenceEndpointMoveActive &&
5943
5890
  !referenceScaleDraft &&
@@ -5947,8 +5894,7 @@ export function FloorplanPanel({
5947
5894
  mode === 'select' &&
5948
5895
  floorplanSelectionTool === 'click' &&
5949
5896
  !movingNode &&
5950
- !isFenceEndpointMoveActive &&
5951
- structureLayer !== 'zones'
5897
+ !isFenceEndpointMoveActive
5952
5898
  const canInteractElementFloorplanGeometry = isDeleteMode || canSelectElementFloorplanGeometry
5953
5899
  const canInteractFloorplanSlabs = isDeleteMode || canSelectElementFloorplanGeometry
5954
5900
  const canInteractWithGuides =
@@ -5960,42 +5906,7 @@ export function FloorplanPanel({
5960
5906
  mode === 'select' &&
5961
5907
  floorplanSelectionTool === 'click' &&
5962
5908
  !movingNode &&
5963
- !isFenceEndpointMoveActive &&
5964
- structureLayer === 'zones'
5965
- const canInteractFloorplanZones = isDeleteMode || canSelectFloorplanZones
5966
- const isFloorplanStructureContextActive = phase === 'structure' && structureLayer !== 'zones'
5967
- const isFloorplanFurnishContextActive = phase === 'furnish'
5968
- const isFloorplanItemContextActive =
5969
- isFloorplanFurnishContextActive || isFloorplanStructureContextActive
5970
- const canSelectFloorplanStairs =
5971
- (mode === 'select' &&
5972
- floorplanSelectionTool === 'click' &&
5973
- !movingNode &&
5974
- !isFenceEndpointMoveActive &&
5975
- isFloorplanStructureContextActive) ||
5976
- isDeleteMode
5977
- const canSelectFloorplanElevators = canSelectFloorplanStairs
5978
- const canSelectFloorplanSpawns = canSelectFloorplanStairs
5979
- const canSelectFloorplanItems =
5980
- (mode === 'select' &&
5981
- floorplanSelectionTool === 'click' &&
5982
- !movingNode &&
5983
- !isFenceEndpointMoveActive &&
5984
- isFloorplanItemContextActive) ||
5985
- isDeleteMode
5986
- const canFocusFloorplanStairs =
5987
- mode === 'select' &&
5988
- floorplanSelectionTool === 'click' &&
5989
- !movingNode &&
5990
- !isFenceEndpointMoveActive &&
5991
- isFloorplanStructureContextActive
5992
- const canFocusFloorplanSpawns = canFocusFloorplanStairs
5993
- const canFocusFloorplanItems =
5994
- mode === 'select' &&
5995
- floorplanSelectionTool === 'click' &&
5996
- !movingNode &&
5997
- !isFenceEndpointMoveActive &&
5998
- isFloorplanItemContextActive
5909
+ !isFenceEndpointMoveActive
5999
5910
  const visibleSitePolygon = displaySitePolygon
6000
5911
  const canUseSiteBoundaryVertexHandles =
6001
5912
  visibleSitePolygon !== null && siteBoundaryHandlesEnabled({ mode, phase })
@@ -7648,7 +7559,7 @@ export function FloorplanPanel({
7648
7559
  scheduleFloorplanZoomCommit()
7649
7560
  const userRotationDeg = latestFloorplanUserRotationDegRef.current
7650
7561
  floorplanZoomPoseRef.current = { localCenter, userRotationDeg, viewWidth: nextWidth }
7651
- if (useEditor.getState().viewMode === 'split') {
7562
+ if (visibleScene(useEditor.getState()) === 'split') {
7652
7563
  publishFloorplanNavigationPose(localCenter, userRotationDeg, nextWidth)
7653
7564
  }
7654
7565
  },
@@ -7731,28 +7642,24 @@ export function FloorplanPanel({
7731
7642
  right: false,
7732
7643
  })
7733
7644
  useEffect(() => {
7734
- if (viewMode !== '2d') return
7645
+ if (shownScene !== '2d') return
7735
7646
  const keys = keyboardPanKeysRef.current
7647
+ const motion = createKeyboardPanMotion()
7736
7648
  let frame: number | null = null
7737
- let lastTime: number | null = null
7738
7649
  const sceneRotationDeg = (userRotationDeg: number) =>
7739
7650
  FLOORPLAN_VIEW_ROTATION_DEG + userRotationDeg - buildingRotationDeg
7740
- const step = (time: number) => {
7741
- const { horizontal, vertical } = keyboardPanDirection(keys)
7651
+ const step = () => {
7742
7652
  const viewport = latestViewportRef.current ?? latestFittedViewportRef.current
7743
- if ((horizontal === 0 && vertical === 0) || !viewport) {
7653
+ if (!(viewport && advanceKeyboardPanMotion(motion, keys, performance.now()))) {
7744
7654
  frame = null
7745
- lastTime = null
7655
+ resetKeyboardPanMotion(motion)
7746
7656
  commitFloorplanPan()
7747
7657
  return
7748
7658
  }
7749
- const elapsed = lastTime === null ? 0 : Math.min((time - lastTime) / 1000, 0.05)
7750
- lastTime = time
7751
- const distance =
7752
- (keyboardPanSpeed(viewport.width) * elapsed) / Math.hypot(horizontal, vertical)
7659
+ const speed = keyboardPanSpeed(viewport.width)
7753
7660
  const next = {
7754
- centerX: viewport.centerX + horizontal * distance,
7755
- centerY: viewport.centerY - vertical * distance,
7661
+ centerX: viewport.centerX + motion.stepX * speed,
7662
+ centerY: viewport.centerY - motion.stepY * speed,
7756
7663
  width: viewport.width,
7757
7664
  }
7758
7665
  const userRotationDeg = latestFloorplanUserRotationDegRef.current
@@ -7773,12 +7680,15 @@ export function FloorplanPanel({
7773
7680
  }
7774
7681
  const onKeyDown = (event: KeyboardEvent) => {
7775
7682
  if (!(isKeyboardPanKey(event.code) && acceptsKeyboardPan(event))) return
7683
+ syncKeyboardPanMotion(motion, keys, performance.now())
7776
7684
  if (setKeyboardPanKey(keys, event.code, true)) start()
7777
7685
  event.preventDefault()
7778
7686
  event.stopPropagation()
7779
7687
  }
7780
7688
  const onKeyUp = (event: KeyboardEvent) => {
7781
- if (isKeyboardPanKey(event.code) && setKeyboardPanKey(keys, event.code, false)) {
7689
+ if (!isKeyboardPanKey(event.code)) return
7690
+ syncKeyboardPanMotion(motion, keys, performance.now())
7691
+ if (setKeyboardPanKey(keys, event.code, false)) {
7782
7692
  event.preventDefault()
7783
7693
  event.stopPropagation()
7784
7694
  }
@@ -7828,7 +7738,7 @@ export function FloorplanPanel({
7828
7738
  commitFloorplanPan,
7829
7739
  publishFloorplanNavigationPose,
7830
7740
  smoothFloorplanNavigationView,
7831
- viewMode,
7741
+ shownScene,
7832
7742
  ])
7833
7743
 
7834
7744
  useLayoutEffect(() => {
@@ -7880,7 +7790,10 @@ export function FloorplanPanel({
7880
7790
  }
7881
7791
  }, [isFloorplanOpen, stopFloorplanViewAnimation])
7882
7792
 
7793
+ // Clearing an open wall draft drops a Polygon room in progress (nothing of
7794
+ // it was written); paths that keep it commit first.
7883
7795
  const clearWallPlacementDraft = useCallback(() => {
7796
+ discardWallPolygonDraft()
7884
7797
  setDraftStart(null)
7885
7798
  setWallChainFirstVertex(null)
7886
7799
  wallConstructionOptionsRef.current = undefined
@@ -7937,10 +7850,9 @@ export function FloorplanPanel({
7937
7850
  setHoveredSiteHandleId(null)
7938
7851
  }, [clearSiteBoundaryLivePreview])
7939
7852
  const exitSiteEditingToSelect = useCallback(() => {
7940
- setPhase('structure')
7941
- setStructureLayer('elements')
7853
+ setPhase('building')
7942
7854
  setMode('select')
7943
- }, [setMode, setPhase, setStructureLayer])
7855
+ }, [setMode, setPhase])
7944
7856
 
7945
7857
  const clearDraft = useCallback(() => {
7946
7858
  clearWallPlacementDraft()
@@ -8002,10 +7914,8 @@ export function FloorplanPanel({
8002
7914
  }
8003
7915
 
8004
7916
  const { createNode, updateNode, nodes } = useScene.getState()
8005
- const zoneCount = Object.values(nodes).filter((node) => node.type === 'zone').length
8006
- const zone = ZoneNodeSchema.parse({
8007
- color: PALETTE_COLORS[zoneCount % PALETTE_COLORS.length],
8008
- name: `Zone ${zoneCount + 1}`,
7917
+ const zone = newZone({
7918
+ name: nextZoneName(nodes),
8009
7919
  polygon: points.map(([x, z]) => [x, z] as [number, number]),
8010
7920
  })
8011
7921
 
@@ -8287,13 +8197,13 @@ export function FloorplanPanel({
8287
8197
 
8288
8198
  if (
8289
8199
  isStairBuildActive &&
8290
- useEditor.getState().viewMode === '2d' &&
8200
+ visibleScene(useEditor.getState()) === '2d' &&
8291
8201
  (event.key === 'r' || event.key === 'R')
8292
8202
  ) {
8293
8203
  useStairBuildPreview.getState().rotateBy(Math.PI / 4)
8294
8204
  } else if (
8295
8205
  isStairBuildActive &&
8296
- useEditor.getState().viewMode === '2d' &&
8206
+ visibleScene(useEditor.getState()) === '2d' &&
8297
8207
  (event.key === 't' || event.key === 'T')
8298
8208
  ) {
8299
8209
  useStairBuildPreview.getState().rotateBy(-Math.PI / 4)
@@ -9004,7 +8914,7 @@ export function FloorplanPanel({
9004
8914
  // tools that subscribe to these grid events.
9005
8915
  const emitFloorplanGridEvent = useCallback(
9006
8916
  (
9007
- eventType: 'move' | 'click' | 'double-click',
8917
+ eventType: 'move' | 'click' | 'double-click' | 'pointerdown' | 'pointerup',
9008
8918
  planPoint: WallPlanPoint,
9009
8919
  nativeEvent: ReactMouseEvent<SVGSVGElement> | ReactPointerEvent<SVGSVGElement>,
9010
8920
  ) => {
@@ -9205,7 +9115,7 @@ export function FloorplanPanel({
9205
9115
  const nextUserRotationDeg = rotationState.initialUserRotationDeg + angleDeltaDeg
9206
9116
 
9207
9117
  applyFloorplanRotationImperatively(rotationState, nextUserRotationDeg)
9208
- if (useEditor.getState().viewMode === 'split') {
9118
+ if (visibleScene(useEditor.getState()) === 'split') {
9209
9119
  publishFloorplanNavigationPose(
9210
9120
  rotationState.viewportCenterLocal,
9211
9121
  nextUserRotationDeg,
@@ -9247,7 +9157,7 @@ export function FloorplanPanel({
9247
9157
  userRotationDeg: currentUserRotationDeg,
9248
9158
  viewWidth: currentViewport.width,
9249
9159
  }
9250
- if (useEditor.getState().viewMode === 'split') {
9160
+ if (visibleScene(useEditor.getState()) === 'split') {
9251
9161
  publishFloorplanNavigationPose(localCenter, currentUserRotationDeg, currentViewport.width)
9252
9162
  }
9253
9163
 
@@ -9339,6 +9249,14 @@ export function FloorplanPanel({
9339
9249
  }
9340
9250
 
9341
9251
  if (isFenceBuildActive) {
9252
+ if (useEditor.getState().getContinuation('fence') === 'freehand') {
9253
+ useAlignmentGuides.getState().clear()
9254
+ emitFloorplanGridEvent('move', planPoint, event)
9255
+ setCursorPoint((previousPoint) =>
9256
+ previousPoint && pointsEqual(previousPoint, planPoint) ? previousPoint : planPoint,
9257
+ )
9258
+ return
9259
+ }
9342
9260
  // Fence draft: grid snap (+ existing-wall/fence endpoint snap), then
9343
9261
  // Figma alignment — same endpoint-wins precedence as the wall branch.
9344
9262
  // While a draft is open the segment locks to 15° rays from its start.
@@ -9346,14 +9264,18 @@ export function FloorplanPanel({
9346
9264
  // there is no Shift hold-to-bypass. Alignment follows the magnetic snap
9347
9265
  // mode, not Alt (continuation is cycled through the HUD / C).
9348
9266
  const fenceAngleSnap = fenceDraftStart !== null && isAngleSnapActive()
9349
- const fenceSnapped = snapFenceDraftPoint({
9350
- point: planPoint,
9351
- walls,
9352
- fences,
9353
- start: fenceDraftStart ?? undefined,
9354
- angleSnap: fenceAngleSnap,
9355
- magnetic: isMagneticSnapActive(),
9356
- })
9267
+ const fenceSnapped = snapRegisteredDraftPoint(
9268
+ 'fence',
9269
+ {
9270
+ point: planPoint,
9271
+ walls,
9272
+ fences,
9273
+ start: fenceDraftStart ?? undefined,
9274
+ angleSnap: fenceAngleSnap,
9275
+ magnetic: isMagneticSnapActive(),
9276
+ },
9277
+ planPoint,
9278
+ )
9357
9279
  const fenceGridBase = snapWallPointToGrid(planPoint)
9358
9280
  const fenceLocked =
9359
9281
  fenceSnapped[0] !== fenceGridBase[0] || fenceSnapped[1] !== fenceGridBase[1]
@@ -9542,7 +9464,7 @@ export function FloorplanPanel({
9542
9464
  const wallAngleSnap = draftStart !== null && isAngleSnapActive()
9543
9465
  const wallSnap = snapWallDraftPointDetailed({
9544
9466
  point: planPoint,
9545
- walls,
9467
+ walls: [...walls, ...wallPolygonDraftWalls()],
9546
9468
  start: draftStart ?? undefined,
9547
9469
  angleSnap: wallAngleSnap,
9548
9470
  magnetic: isMagneticSnapActive(),
@@ -9649,7 +9571,7 @@ export function FloorplanPanel({
9649
9571
  const firstPoint = slabDraftPoints[0]
9650
9572
  if (firstPoint && slabDraftPoints.length >= 3 && isPointNearPlanPoint(point, firstPoint)) {
9651
9573
  // 2D-only view: the 3D tool can't commit, so close the polygon here.
9652
- if (useEditor.getState().viewMode === '2d') {
9574
+ if (visibleScene(useEditor.getState()) === '2d') {
9653
9575
  createSlabOnCurrentLevel(slabDraftPoints, slabPlacementBaseRef.current)
9654
9576
  }
9655
9577
  clearDraft()
@@ -9699,7 +9621,7 @@ export function FloorplanPanel({
9699
9621
  }
9700
9622
 
9701
9623
  // 2D-only view: the 3D tool can't commit, so create the slab here.
9702
- if (useEditor.getState().viewMode === '2d') {
9624
+ if (visibleScene(useEditor.getState()) === '2d') {
9703
9625
  createSlabOnCurrentLevel(nextPoints, slabPlacementBaseRef.current)
9704
9626
  }
9705
9627
  clearDraft()
@@ -9715,7 +9637,7 @@ export function FloorplanPanel({
9715
9637
 
9716
9638
  const firstPoint = ceilingDraftPoints[0]
9717
9639
  if (firstPoint && ceilingDraftPoints.length >= 3 && isPointNearPlanPoint(point, firstPoint)) {
9718
- if (useEditor.getState().viewMode === '2d') {
9640
+ if (visibleScene(useEditor.getState()) === '2d') {
9719
9641
  createCeilingOnCurrentLevel(ceilingDraftPoints)
9720
9642
  }
9721
9643
  clearCeilingPlacementDraft()
@@ -9748,7 +9670,7 @@ export function FloorplanPanel({
9748
9670
  return
9749
9671
  }
9750
9672
 
9751
- if (useEditor.getState().viewMode === '2d') {
9673
+ if (visibleScene(useEditor.getState()) === '2d') {
9752
9674
  createCeilingOnCurrentLevel(nextPoints)
9753
9675
  }
9754
9676
  clearCeilingPlacementDraft()
@@ -9815,6 +9737,14 @@ export function FloorplanPanel({
9815
9737
  useEditor.getState().toolDefaults.wall,
9816
9738
  )
9817
9739
  : undefined
9740
+ // 2D-only owns a Polygon room's draft; split view leaves it to the 3D tool.
9741
+ if (
9742
+ levelId &&
9743
+ visibleScene(useEditor.getState()) === '2d' &&
9744
+ getWallDrawVariant() === 'polygon'
9745
+ ) {
9746
+ startWallPolygonDraft(levelId as AnyNodeId, point, wallConstructionOptionsRef.current)
9747
+ }
9818
9748
  setDraftStart(point)
9819
9749
  setWallChainFirstVertex(point)
9820
9750
  setDraftEnd(point)
@@ -9840,8 +9770,22 @@ export function FloorplanPanel({
9840
9770
  // `display:none`, so the tool never commits. Mirror the slab /
9841
9771
  // ceiling 2D-only committers: create locally here, gated on the
9842
9772
  // view, so split / 3D keep their single-owner tool commit.
9843
- const viewIs2DOnly = useEditor.getState().viewMode === '2d'
9773
+ const viewIs2DOnly = visibleScene(useEditor.getState()) === '2d'
9844
9774
  let createdWall: WallNode | null = null
9775
+ // 2D-only Polygon room: the corner joins the draft; nothing is written
9776
+ // until it closes, seals or tees (then every wall at once).
9777
+ if (viewIs2DOnly && getWallDrawVariant() === 'polygon') {
9778
+ if (addWallPolygonDraftCorner(point) !== 'open') {
9779
+ commitWallPolygonDraft()
9780
+ clearWallPlacementDraft()
9781
+ setCursorPoint(null)
9782
+ return
9783
+ }
9784
+ setDraftStart(point)
9785
+ setDraftEnd(point)
9786
+ setCursorPoint(point)
9787
+ return
9788
+ }
9845
9789
  if (viewIs2DOnly) {
9846
9790
  createdWall = createWallOnCurrentLevel(
9847
9791
  draftStart,
@@ -10045,18 +9989,25 @@ export function FloorplanPanel({
10045
9989
  return
10046
9990
  }
10047
9991
 
9992
+ if (useInteractionScope.getState().scope.kind !== 'idle') return
10048
9993
  const modifierKeys = getSelectionModifierKeys(event)
10049
9994
 
9995
+ if (enterBuildingFromPlanHit(getFloorplanHitIdAtPoint(planPoint))) return
9996
+ if (roomPickingEnabled()) {
9997
+ const hitId = getFloorplanHitIdAtPoint(planPoint)
9998
+ if (selectRoomFromHit(resolvePlanRoomHit(hitId, planPoint), modifierKeys, hitId)) return
9999
+ }
10050
10000
  const backgroundSelection = resolveFloorplanBackgroundSelection({
10051
10001
  canSelectElementFloorplanGeometry,
10052
- canSelectFloorplanZones,
10002
+ canSelectFloorplanZones: canSelectFloorplanZones && !!useViewer.getState().focusedUnitId,
10053
10003
  currentSelectedIds: useViewer.getState().selection.selectedIds,
10054
10004
  expandIdsForNode: expandSessionSelectionForNode,
10055
- getFloorplanHitIdAtPoint,
10005
+ getFloorplanHitIdAtPoint: useViewer.getState().focusedUnitId
10006
+ ? (point) => zoneAtLevelPoint(point[0], point[1])?.id ?? null
10007
+ : getFloorplanHitIdAtPoint,
10056
10008
  isWallBuildActive,
10057
10009
  modifierKeys,
10058
10010
  planPoint,
10059
- structureLayer,
10060
10011
  })
10061
10012
 
10062
10013
  if (backgroundSelection.handled) {
@@ -10104,15 +10055,14 @@ export function FloorplanPanel({
10104
10055
  }
10105
10056
 
10106
10057
  if (backgroundSelection.kind === 'clear-zones') {
10107
- setSelection({ zoneId: null })
10108
- // Return to structure select (same as 3D grid click)
10109
- useEditor.getState().setStructureLayer('elements')
10110
- useEditor.getState().setMode('select')
10058
+ useEditor.getState().clearRoom()
10059
+ setSelection({ selectedIds: [], zoneId: null })
10111
10060
  return
10112
10061
  }
10113
10062
 
10114
10063
  if (!backgroundSelection.preserveSelection) {
10115
- setSelection({ selectedIds: [] })
10064
+ useEditor.getState().clearRoom()
10065
+ setSelection({ selectedIds: [], zoneId: null })
10116
10066
  }
10117
10067
  return
10118
10068
  }
@@ -10130,7 +10080,6 @@ export function FloorplanPanel({
10130
10080
  referenceScaleDraft,
10131
10081
  setSelectedReferenceId,
10132
10082
  setSelection,
10133
- structureLayer,
10134
10083
  getFloorplanHitIdAtPoint,
10135
10084
  unit,
10136
10085
  emitFloorplanGridEvent,
@@ -10150,6 +10099,49 @@ export function FloorplanPanel({
10150
10099
  },
10151
10100
  [handleBackgroundClick],
10152
10101
  )
10102
+ const handleSvgPointerDown = useCallback(
10103
+ (event: ReactPointerEvent<SVGSVGElement>) => {
10104
+ if (
10105
+ isFenceBuildActive &&
10106
+ useEditor.getState().getContinuation('fence') === 'freehand' &&
10107
+ event.button === 0
10108
+ ) {
10109
+ const point = getPlanPointFromClientPoint(event.clientX, event.clientY)
10110
+ if (point) {
10111
+ clearFencePlacementDraft()
10112
+ event.currentTarget.setPointerCapture(event.pointerId)
10113
+ emitFloorplanGridEvent('pointerdown', point, event)
10114
+ }
10115
+ }
10116
+ handlePointerDown(event)
10117
+ },
10118
+ [
10119
+ clearFencePlacementDraft,
10120
+ emitFloorplanGridEvent,
10121
+ getPlanPointFromClientPoint,
10122
+ handlePointerDown,
10123
+ isFenceBuildActive,
10124
+ ],
10125
+ )
10126
+ const handleSvgPointerUp = useCallback(
10127
+ (event: ReactPointerEvent<SVGSVGElement>) => {
10128
+ if (
10129
+ isFenceBuildActive &&
10130
+ useEditor.getState().getContinuation('fence') === 'freehand' &&
10131
+ event.button === 0
10132
+ ) {
10133
+ const point = getPlanPointFromClientPoint(event.clientX, event.clientY)
10134
+ if (point) emitFloorplanGridEvent('pointerup', point, event)
10135
+ }
10136
+ endFloorplanNavigation(event)
10137
+ },
10138
+ [
10139
+ emitFloorplanGridEvent,
10140
+ endFloorplanNavigation,
10141
+ getPlanPointFromClientPoint,
10142
+ isFenceBuildActive,
10143
+ ],
10144
+ )
10153
10145
  const handleBackgroundDoubleClick = useCallback(
10154
10146
  (event: ReactMouseEvent<SVGSVGElement>) => {
10155
10147
  if (!(isPolygonDraftBuildActive && !isRoofBuildActive)) {
@@ -10300,28 +10292,18 @@ export function FloorplanPanel({
10300
10292
  if (floorplanScreenSelectionOwnsInputDraggingRef.current) {
10301
10293
  useViewer.getState().setInputDragging(false)
10302
10294
  floorplanScreenSelectionOwnsInputDraggingRef.current = false
10295
+ useInteractionScope.getState().endIf((scope) => scope.kind === 'box-select')
10303
10296
  }
10304
10297
  }, [syncPreviewSelectedIds])
10305
10298
 
10306
10299
  const commitFloorplanScreenSelection = useCallback(
10307
10300
  (nextSelectedIds: string[], event: PointerEvent) => {
10308
10301
  const modifierKeys = getSelectionModifierKeys(event)
10309
- const shouldAppend = modifierKeys.meta || modifierKeys.ctrl || modifierKeys.shift
10310
-
10311
10302
  setSelectedReferenceId(null)
10312
10303
 
10313
- if (phase === 'structure' && structureLayer === 'zones') {
10314
- if (nextSelectedIds.length > 0) {
10315
- setSelection({ zoneId: nextSelectedIds[0] as ZoneNodeType['id'] })
10316
- } else if (!shouldAppend) {
10317
- setSelection({ zoneId: null })
10318
- }
10319
- return
10320
- }
10321
-
10322
10304
  addFloorplanSelection(nextSelectedIds, modifierKeys)
10323
10305
  },
10324
- [addFloorplanSelection, phase, setSelectedReferenceId, setSelection, structureLayer],
10306
+ [addFloorplanSelection, setSelectedReferenceId],
10325
10307
  )
10326
10308
 
10327
10309
  useEffect(() => {
@@ -10371,6 +10353,7 @@ export function FloorplanPanel({
10371
10353
  if (!state.isDragging && dragDistance >= SCREEN_RECTANGLE_SELECTION_DRAG_THRESHOLD_PX) {
10372
10354
  state.isDragging = true
10373
10355
  floorplanScreenSelectionOwnsInputDraggingRef.current = true
10356
+ useInteractionScope.getState().begin({ kind: 'box-select' })
10374
10357
  useViewer.getState().setInputDragging(true)
10375
10358
  markBoxSelectHandled()
10376
10359
 
@@ -10775,11 +10758,16 @@ export function FloorplanPanel({
10775
10758
  const hasFloorplanCursorIndicator =
10776
10759
  Boolean(movingOpeningType) ||
10777
10760
  (mode === 'build' && tool !== null) ||
10778
- (mode === 'select' && floorplanSelectionTool === 'marquee' && structureLayer !== 'zones') ||
10761
+ (mode === 'select' && floorplanSelectionTool === 'marquee') ||
10779
10762
  mode === 'delete'
10780
10763
 
10781
10764
  const handleSvgPointerMove = useCallback(
10782
10765
  (event: ReactPointerEvent<SVGSVGElement>) => {
10766
+ if (roomPickingEnabled() && !(event.target as Element).closest('.floorplan-registry-entry')) {
10767
+ const point = getPlanPointFromClientPoint(event.clientX, event.clientY)
10768
+ if (point)
10769
+ hoverRoomFromHit(resolvePlanRoomHit(null, point), getSelectionModifierKeys(event), null)
10770
+ }
10783
10771
  if (
10784
10772
  hasFloorplanCursorIndicator &&
10785
10773
  !isSpacePanPressed &&
@@ -10812,6 +10800,7 @@ export function FloorplanPanel({
10812
10800
  [
10813
10801
  handlePointerMove,
10814
10802
  hasFloorplanCursorIndicator,
10803
+ getPlanPointFromClientPoint,
10815
10804
  isSpacePanPressed,
10816
10805
  siteVertexDragState,
10817
10806
  setFloorplanCursorPosition,
@@ -10819,6 +10808,7 @@ export function FloorplanPanel({
10819
10808
  )
10820
10809
 
10821
10810
  const handleSvgPointerLeave = useCallback(() => {
10811
+ useEditor.getState().setHoveredRoom(null)
10822
10812
  setFloorplanCursorPosition(null)
10823
10813
  setHoveredGuideCorner(null)
10824
10814
  handlePointerLeave()
@@ -11129,8 +11119,7 @@ export function FloorplanPanel({
11129
11119
  )
11130
11120
  })()
11131
11121
 
11132
- setPhase('structure')
11133
- setStructureLayer('elements')
11122
+ setPhase('building')
11134
11123
  setMode('select')
11135
11124
 
11136
11125
  const nextSelection: Parameters<typeof setSelection>[0] = {
@@ -11147,16 +11136,7 @@ export function FloorplanPanel({
11147
11136
  }
11148
11137
 
11149
11138
  setSelection(nextSelection)
11150
- }, [
11151
- currentBuildingId,
11152
- floorplanLevels,
11153
- levelNode,
11154
- setMode,
11155
- setPhase,
11156
- setSelection,
11157
- setStructureLayer,
11158
- site,
11159
- ])
11139
+ }, [currentBuildingId, floorplanLevels, levelNode, setMode, setPhase, setSelection, site])
11160
11140
  const activeDraftAnchorPoint =
11161
11141
  referenceScaleDraft?.start ??
11162
11142
  draftStart ??
@@ -11236,8 +11216,10 @@ export function FloorplanPanel({
11236
11216
  pill, plus the group pill for multi-selections. */}
11237
11217
  <FloorplanRegistryActionMenu />
11238
11218
  <FloorplanGroupActionMenu />
11219
+ <FloorplanDrawingTypeSwitch className={planOnRight ? 'right-24 left-auto' : undefined} />
11239
11220
 
11240
11221
  {!isStudioWorkspace &&
11222
+ sceneLeft !== null &&
11241
11223
  (levelNode?.type === 'level' || hasAmbientBuildingLevel) &&
11242
11224
  (compassHost ? (
11243
11225
  createPortal(
@@ -11245,6 +11227,7 @@ export function FloorplanPanel({
11245
11227
  needleRef={compassNeedleRef}
11246
11228
  northRotationDeg={floorplanUserRotationDeg}
11247
11229
  onAlignNorth={alignFloorplanViewToNorth}
11230
+ style={compassStyle}
11248
11231
  />,
11249
11232
  compassHost,
11250
11233
  )
@@ -11385,7 +11368,7 @@ export function FloorplanPanel({
11385
11368
  // renderer, handle layers) must NOT render/reconcile while hidden —
11386
11369
  // otherwise every scene/selection change in pure 3D re-rendered the
11387
11370
  // whole floorplan tree (profiler: 150–200ms on a wall-endpoint drag).
11388
- // `isFloorplanOpen` is `viewMode !== '3d'`, so this still renders fully
11371
+ // `isFloorplanOpen` is "the plan is in a pane", so this still renders fully
11389
11372
  // in both 2D and split. Viewport state lives on the still-mounted
11390
11373
  // panel, so pan/zoom is preserved across the toggle.
11391
11374
  <svg
@@ -11395,11 +11378,11 @@ export function FloorplanPanel({
11395
11378
  onContextMenu={(event) => event.preventDefault()}
11396
11379
  onDoubleClick={isMarqueeSelectionToolActive ? undefined : handleBackgroundDoubleClick}
11397
11380
  onPointerCancel={endFloorplanNavigation}
11398
- onPointerDown={handlePointerDown}
11381
+ onPointerDown={handleSvgPointerDown}
11399
11382
  onPointerDownCapture={handleNavigationPointerDown}
11400
11383
  onPointerLeave={handleSvgPointerLeave}
11401
11384
  onPointerMove={handleSvgPointerMove}
11402
- onPointerUp={endFloorplanNavigation}
11385
+ onPointerUp={handleSvgPointerUp}
11403
11386
  ref={svgRef}
11404
11387
  style={{
11405
11388
  cursor:
@@ -11496,7 +11479,7 @@ export function FloorplanPanel({
11496
11479
  subscribes to the `useStairBuildPreview` store directly, so a
11497
11480
  per-`grid:move` cursor update re-renders only that tiny layer
11498
11481
  rather than this whole panel. */}
11499
- <FloorplanStairBuildPreviewLayer isDeleteMode={isDeleteMode} palette={palette} />
11482
+ <FloorplanStairBuildPreviewLayer palette={floorplanRegistryPalette} />
11500
11483
 
11501
11484
  <FloorplanReferenceScaleLayer
11502
11485
  draft={referenceScaleDraft}
@@ -11547,7 +11530,13 @@ export function FloorplanPanel({
11547
11530
  whose extent is derived from the current viewBox and
11548
11531
  would create a measure→fit→measure loop. */}
11549
11532
  <g ref={floorplanContentRef}>
11550
- <FloorplanRegistryLayer />
11533
+ {/* Site-plan mode swaps the level drawing for the lot /
11534
+ setback / footprint drawing, in SITE coordinates. */}
11535
+ {activeDrawingType === 'site-plan' ? (
11536
+ <FloorplanSitePlanLayer />
11537
+ ) : (
11538
+ <FloorplanRegistryLayer />
11539
+ )}
11551
11540
  {/* Faint footprint ghost of the node being placed by a
11552
11541
  registry placement tool (e.g. column), following the
11553
11542
  cursor. The 3D mesh preview is hidden in 2D, so this is
@@ -11561,6 +11550,7 @@ export function FloorplanPanel({
11561
11550
  `floorplan-wall-move-ghost-layer.tsx`. */}
11562
11551
  <FloorplanWallMoveGhostLayer />
11563
11552
  </g>
11553
+ {activeDrawingType !== 'site-plan' && <FloorplanOpenWallEndsLayer />}
11564
11554
  <FloorplanMeasurementToolLayer />
11565
11555
  <FloorplanRegisteredToolLayer />
11566
11556
  {floorplanSceneSlot}
@@ -11577,42 +11567,54 @@ export function FloorplanPanel({
11577
11567
  paint on top of node geometry. */}
11578
11568
  <FloorplanAlignmentGuideLayer />
11579
11569
 
11580
- <FloorplanSiteLayer
11581
- dimmed={selectedIds.length > 1 || previewSelectedIds.length > 1}
11582
- isHighlighted={isSiteBoundaryHighlighted}
11583
- palette={palette}
11584
- sitePolygon={visibleSitePolygon}
11585
- />
11570
+ {/* The property line, its vertex handles and its edge labels are
11571
+ drawn in BUILDING-LOCAL metres (the frame the level's walls
11572
+ live in). The site-plan drawing type draws the lot itself, in
11573
+ SITE metres, so mounting these there paints a second lot
11574
+ offset by the building's own position — the "two dashed
11575
+ boxes" — and it shifts whenever the house moves. */}
11576
+ {activeDrawingType !== 'site-plan' && (
11577
+ <FloorplanSiteLayer
11578
+ dimmed={selectedIds.length > 1 || previewSelectedIds.length > 1}
11579
+ isHighlighted={isSiteBoundaryHighlighted}
11580
+ palette={palette}
11581
+ sitePolygon={visibleSitePolygon}
11582
+ />
11583
+ )}
11586
11584
 
11587
- <FloorplanPolygonHandleLayer
11588
- edgeHandles={siteEdgeHandles}
11589
- hoveredHandleId={hoveredSiteHandleId}
11590
- midpointHandles={siteMidpointHandles}
11591
- onHandleHoverChange={setHoveredSiteHandleId}
11592
- onMidpointPointerDown={(nodeId, edgeIndex, event) =>
11593
- handleSiteMidpointPointerDown(nodeId as SiteNode['id'], edgeIndex, event)
11594
- }
11595
- onVertexDoubleClick={(nodeId, vertexIndex, event) =>
11596
- handleSiteVertexDoubleClick(nodeId as SiteNode['id'], vertexIndex, event)
11597
- }
11598
- onVertexPointerDown={(nodeId, vertexIndex, event) =>
11599
- handleSiteVertexPointerDown(nodeId as SiteNode['id'], vertexIndex, event)
11600
- }
11601
- palette={palette}
11602
- unitsPerPixel={floorplanUnitsPerPixel}
11603
- vertexHandles={siteVertexHandles}
11604
- />
11585
+ {activeDrawingType !== 'site-plan' && (
11586
+ <FloorplanPolygonHandleLayer
11587
+ edgeHandles={siteEdgeHandles}
11588
+ hoveredHandleId={hoveredSiteHandleId}
11589
+ midpointHandles={siteMidpointHandles}
11590
+ onHandleHoverChange={setHoveredSiteHandleId}
11591
+ onMidpointPointerDown={(nodeId, edgeIndex, event) =>
11592
+ handleSiteMidpointPointerDown(nodeId as SiteNode['id'], edgeIndex, event)
11593
+ }
11594
+ onVertexDoubleClick={(nodeId, vertexIndex, event) =>
11595
+ handleSiteVertexDoubleClick(nodeId as SiteNode['id'], vertexIndex, event)
11596
+ }
11597
+ onVertexPointerDown={(nodeId, vertexIndex, event) =>
11598
+ handleSiteVertexPointerDown(nodeId as SiteNode['id'], vertexIndex, event)
11599
+ }
11600
+ palette={palette}
11601
+ unitsPerPixel={floorplanUnitsPerPixel}
11602
+ vertexHandles={siteVertexHandles}
11603
+ />
11604
+ )}
11605
11605
 
11606
- <FloorplanSiteEdgeLabelLayer
11607
- labelBackground={isDark ? '#0f172a' : '#ffffff'}
11608
- labelText={isDark ? '#e2e8f0' : '#171717'}
11609
- palette={palette}
11610
- sceneRotationDeg={floorplanSceneRotationDeg}
11611
- shouldShow={shouldShowSiteEdgeLabels}
11612
- sitePolygon={visibleSitePolygon}
11613
- unit={unit}
11614
- unitsPerPixel={floorplanUnitsPerPixel}
11615
- />
11606
+ {activeDrawingType !== 'site-plan' && (
11607
+ <FloorplanSiteEdgeLabelLayer
11608
+ labelBackground={isDark ? '#0f172a' : '#ffffff'}
11609
+ labelText={isDark ? '#e2e8f0' : '#171717'}
11610
+ palette={palette}
11611
+ sceneRotationDeg={floorplanSceneRotationDeg}
11612
+ shouldShow={shouldShowSiteEdgeLabels}
11613
+ sitePolygon={visibleSitePolygon}
11614
+ unit={unit}
11615
+ unitsPerPixel={floorplanUnitsPerPixel}
11616
+ />
11617
+ )}
11616
11618
 
11617
11619
  {/* "Magnetic" wall-snap beacon — per-kind glyph at the active
11618
11620
  draft / endpoint-move snap point. Same store + coord space as