@pascal-app/editor 1.0.3 → 1.0.5

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 (624) hide show
  1. package/README.md +106 -0
  2. package/package.json +42 -38
  3. package/src/components/editor/__bench__/room-label-placement.bench.ts +108 -0
  4. package/src/components/editor/action-menu-button.tsx +83 -0
  5. package/src/components/editor/bake-thumbnail.tsx +307 -5
  6. package/src/components/editor/capture-level-picker.tsx +72 -0
  7. package/src/components/editor/construction-reveal-audio.test.tsx +150 -0
  8. package/src/components/editor/construction-reveal-audio.tsx +40 -0
  9. package/src/components/editor/custom-camera-controls.tsx +275 -57
  10. package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
  11. package/src/components/editor/duplicate-node.ts +244 -0
  12. package/src/components/editor/editor-layout-mobile.tsx +37 -26
  13. package/src/components/editor/editor-layout-v2.tsx +132 -59
  14. package/src/components/editor/export-manager.tsx +47 -26
  15. package/src/components/editor/first-person/build-collider-world.test.ts +320 -0
  16. package/src/components/editor/first-person/build-collider-world.ts +28 -7
  17. package/src/components/editor/first-person/ground-holes.test.ts +128 -0
  18. package/src/components/editor/first-person/ground-holes.ts +130 -0
  19. package/src/components/editor/first-person/mechanism-targets.test.ts +162 -0
  20. package/src/components/editor/first-person/mechanism-targets.ts +88 -0
  21. package/src/components/editor/first-person-controls.tsx +220 -9
  22. package/src/components/editor/floating-action-menu.tsx +63 -207
  23. package/src/components/editor/floor-aware-camera-controls.test.ts +466 -0
  24. package/src/components/editor/floor-aware-camera-controls.ts +324 -0
  25. package/src/components/editor/floor-openings-overlay.tsx +243 -0
  26. package/src/components/editor/floor-region-controls.tsx +846 -0
  27. package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
  28. package/src/components/editor/floor-surface.test.ts +165 -0
  29. package/src/components/editor/floor-surface.ts +182 -0
  30. package/src/components/editor/floorplan-background-selection.test.ts +11 -2
  31. package/src/components/editor/floorplan-background-selection.ts +1 -3
  32. package/src/components/editor/floorplan-mode-coordinator.tsx +2 -2
  33. package/src/components/editor/floorplan-panel.tsx +414 -363
  34. package/src/components/editor/follow-pascal.test.tsx +912 -0
  35. package/src/components/editor/follow-pascal.tsx +354 -0
  36. package/src/components/editor/footprint-height-handle.tsx +204 -0
  37. package/src/components/editor/group-actions.ts +56 -2
  38. package/src/components/editor/group-move-3d.ts +2 -0
  39. package/src/components/editor/group-rotate-handle.tsx +2 -0
  40. package/src/components/editor/group-transform-shared.test.ts +460 -0
  41. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  42. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  43. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  44. package/src/components/editor/handles/use-handle-drag.ts +269 -197
  45. package/src/components/editor/index.tsx +409 -197
  46. package/src/components/editor/join-walls-pill.tsx +44 -0
  47. package/src/components/editor/node-action-menu.tsx +68 -70
  48. package/src/components/editor/node-arrow-handles.tsx +25 -6
  49. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  50. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  51. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  52. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  53. package/src/components/editor/registry-action-contributions.tsx +80 -1
  54. package/src/components/editor/riser-diagram-panel.tsx +14 -7
  55. package/src/components/editor/room-controls.tsx +847 -0
  56. package/src/components/editor/room-floor-highlight.tsx +128 -0
  57. package/src/components/editor/room-handles-2d.tsx +354 -0
  58. package/src/components/editor/room-handles.tsx +675 -0
  59. package/src/components/editor/room-highlight.tsx +233 -0
  60. package/src/components/editor/room-labels.test.ts +850 -0
  61. package/src/components/editor/room-labels.tsx +1890 -0
  62. package/src/components/editor/room-transform-ghost.tsx +256 -0
  63. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  64. package/src/components/editor/selection-manager.tsx +474 -419
  65. package/src/components/editor/slab-hole-highlights.tsx +5 -2
  66. package/src/components/editor/snapshot-capture-overlay.tsx +7 -2
  67. package/src/components/editor/snapshot-capture.test.ts +119 -60
  68. package/src/components/editor/snapshot-capture.ts +36 -5
  69. package/src/components/editor/thumbnail-generator.test.ts +654 -0
  70. package/src/components/editor/thumbnail-generator.tsx +1103 -223
  71. package/src/components/editor/use-floorplan-background-placement.ts +34 -12
  72. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  73. package/src/components/editor/views/host-view.tsx +61 -0
  74. package/src/components/editor/views/plugins-view.tsx +663 -0
  75. package/src/components/editor/views/scene-region.tsx +59 -0
  76. package/src/components/editor/views/use-editor-views.ts +57 -0
  77. package/src/components/editor/views/view-bar.test.tsx +54 -0
  78. package/src/components/editor/views/view-bar.tsx +343 -0
  79. package/src/components/editor/wall-justification.test.ts +140 -0
  80. package/src/components/editor/wall-move-side-handles.tsx +43 -142
  81. package/src/components/editor/wall-push-arrow.tsx +92 -0
  82. package/src/components/editor/wall-region-handles.tsx +509 -0
  83. package/src/components/editor/wall-snap-beacon-layer.tsx +8 -4
  84. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  85. package/src/components/editor-2d/floorplan-group-move.tsx +3 -0
  86. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  87. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  88. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +2 -2
  89. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  90. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +116 -106
  91. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  92. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  93. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  94. package/src/components/editor-2d/renderers/{floorplan-draft-wall-measurement.tsx → floorplan-draft-measurement.tsx} +8 -8
  95. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  96. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +38 -8
  97. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +319 -233
  98. package/src/components/editor-2d/renderers/floorplan-reveal.test.tsx +254 -0
  99. package/src/components/editor-2d/renderers/floorplan-reveal.ts +141 -0
  100. package/src/components/editor-2d/svg-paths.ts +12 -15
  101. package/src/components/point-ask/bubble.tsx +538 -0
  102. package/src/components/point-ask/hover-chip.tsx +91 -0
  103. package/src/components/point-ask/mode-pill.tsx +92 -0
  104. package/src/components/point-ask/optional-motion.ts +22 -0
  105. package/src/components/point-ask/outline-corners.tsx +93 -0
  106. package/src/components/point-ask/pins.tsx +711 -0
  107. package/src/components/point-ask/point-ask-layer.tsx +83 -0
  108. package/src/components/point-ask/point-ask-styles.ts +105 -0
  109. package/src/components/point-ask/region-marquee-styles.ts +15 -0
  110. package/src/components/point-ask/region-marquee.tsx +64 -0
  111. package/src/components/procedural-items/inspector.tsx +40 -15
  112. package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +12 -2
  113. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  114. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  115. package/src/components/systems/roof/roof-edit-system.tsx +1 -0
  116. package/src/components/systems/zone/zone-system.test.ts +47 -0
  117. package/src/components/systems/zone/zone-system.tsx +29 -68
  118. package/src/components/tools/distribution/connection-compatibility.test.ts +34 -0
  119. package/src/components/tools/distribution/connection-compatibility.ts +43 -0
  120. package/src/components/tools/distribution/distribution-public.ts +9 -0
  121. package/src/components/tools/distribution/distribution-run-contract.test.ts +26 -0
  122. package/src/components/tools/distribution/distribution-run-contract.ts +94 -0
  123. package/src/components/tools/distribution/distribution-run-tool.test.ts +241 -0
  124. package/src/components/tools/distribution/distribution-run-tool.test.tsx +125 -0
  125. package/src/components/tools/distribution/distribution-run-tool.tsx +1323 -0
  126. package/src/components/tools/distribution/draw-alignment.ts +35 -0
  127. package/src/components/tools/distribution/ports.test.ts +56 -0
  128. package/src/components/tools/distribution/ports.ts +378 -0
  129. package/src/components/tools/distribution/run-cursor.test.ts +135 -0
  130. package/src/components/tools/distribution/run-cursor.ts +36 -0
  131. package/src/components/tools/distribution/run-direction-feedback.test.ts +60 -0
  132. package/src/components/tools/distribution/run-direction-feedback.tsx +251 -0
  133. package/src/components/tools/distribution/run-port-snap.test.ts +79 -0
  134. package/src/components/tools/distribution/run-port-snap.ts +25 -0
  135. package/src/components/tools/distribution/run-screen-direction.test.ts +23 -0
  136. package/src/components/tools/distribution/run-screen-direction.ts +30 -0
  137. package/src/components/tools/elevator/elevator-tool.tsx +3 -6
  138. package/src/components/tools/elevator/move-elevator-tool.tsx +2 -3
  139. package/src/components/tools/item/item-surface-strategy-resolver.test.ts +67 -68
  140. package/src/components/tools/item/placement-history.test.tsx +512 -0
  141. package/src/components/tools/item/placement-strategies.test.ts +46 -1
  142. package/src/components/tools/item/placement-strategies.ts +11 -10
  143. package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +56 -56
  144. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  145. package/src/components/tools/item/use-draft-node.ts +166 -95
  146. package/src/components/tools/item/use-placement-coordinator.tsx +148 -95
  147. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  148. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  149. package/src/components/tools/registry/item-surface-move.test.ts +62 -63
  150. package/src/components/tools/registry/item-surface-move.ts +2 -0
  151. package/src/components/tools/registry/item-surface-resolver.test.ts +26 -26
  152. package/src/components/tools/registry/move-registry-node-tool.test.ts +268 -1
  153. package/src/components/tools/registry/move-registry-node-tool.tsx +66 -30
  154. package/src/components/tools/select/box-select-tool.tsx +2 -12
  155. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  156. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  157. package/src/components/tools/select/select-candidates.test.ts +37 -4
  158. package/src/components/tools/select/select-candidates.ts +15 -11
  159. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  160. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  161. package/src/components/tools/tool-manager.tsx +59 -24
  162. package/src/components/tools/wall/wall-drafting.test.ts +175 -47
  163. package/src/components/tools/zone/zone-tool.tsx +4 -12
  164. package/src/components/ui/action-menu/action-button.tsx +9 -1
  165. package/src/components/ui/action-menu/camera-actions.tsx +9 -4
  166. package/src/components/ui/action-menu/control-modes.tsx +15 -42
  167. package/src/components/ui/action-menu/follow-pascal-pill.tsx +32 -0
  168. package/src/components/ui/action-menu/follow-pascal-toggle.tsx +36 -0
  169. package/src/components/ui/action-menu/index.tsx +56 -33
  170. package/src/components/ui/action-menu/measurement-control.tsx +14 -19
  171. package/src/components/ui/action-menu/placement.ts +20 -0
  172. package/src/components/ui/action-menu/point-ask-button.tsx +52 -0
  173. package/src/components/ui/action-menu/view-toggles.tsx +47 -42
  174. package/src/components/ui/build-panel/build-panel.tsx +242 -0
  175. package/src/components/ui/command-palette/editor-commands.tsx +7 -42
  176. package/src/components/ui/command-palette/index.tsx +50 -39
  177. package/src/components/ui/command-palette/ranked-groups.test.ts +31 -0
  178. package/src/components/ui/controls/action-button.tsx +5 -2
  179. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  180. package/src/components/ui/controls/material-paint-panel.tsx +97 -26
  181. package/src/components/ui/controls/material-picker.tsx +4 -1
  182. package/src/components/ui/controls/metric-control.tsx +1 -1
  183. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  184. package/src/components/ui/controls/segmented-control.tsx +21 -8
  185. package/src/components/ui/controls/select-control.tsx +65 -0
  186. package/src/components/ui/controls/slider-control.tsx +1 -1
  187. package/src/components/ui/controls/toggle-control.tsx +3 -3
  188. package/src/components/ui/controls/tool-options-panel.tsx +1 -1
  189. package/src/components/ui/floating-level-selector.tsx +159 -59
  190. package/src/components/ui/helpers/building-helper.tsx +4 -1
  191. package/src/components/ui/helpers/contextual-helper-panel.tsx +181 -72
  192. package/src/components/ui/helpers/helper-manager.test.tsx +121 -0
  193. package/src/components/ui/helpers/helper-manager.tsx +331 -14
  194. package/src/components/ui/helpers/item-helper.tsx +17 -1
  195. package/src/components/ui/helpers/registered-tool-helper.tsx +8 -1
  196. package/src/components/ui/icon-ref.tsx +45 -1
  197. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  198. package/src/components/ui/node-icon-image.tsx +16 -0
  199. package/src/components/ui/panels/collections/collections-popover.tsx +28 -22
  200. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  201. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  202. package/src/components/ui/panels/mobile-panel-sheet.tsx +4 -4
  203. package/src/components/ui/panels/mobile-selection-bar.tsx +8 -5
  204. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  205. package/src/components/ui/panels/multi-field-value.ts +6 -3
  206. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  207. package/src/components/ui/panels/multi-selection-panel.tsx +161 -5
  208. package/src/components/ui/panels/node-display.ts +8 -3
  209. package/src/components/ui/panels/panel-manager.tsx +103 -68
  210. package/src/components/ui/panels/panel-wrapper.tsx +130 -62
  211. package/src/components/ui/panels/parametric-field-control.tsx +3 -3
  212. package/src/components/ui/panels/parametric-inspector.tsx +7 -7
  213. package/src/components/ui/panels/reference-panel.tsx +2 -3
  214. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  215. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  216. package/src/components/ui/panels/room-name-options.ts +47 -0
  217. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  218. package/src/components/ui/panels/room-panel.tsx +790 -0
  219. package/src/components/ui/panels/shape-choice.tsx +58 -0
  220. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  221. package/src/components/ui/primitives/badge.tsx +8 -0
  222. package/src/components/ui/primitives/color-dot.tsx +36 -24
  223. package/src/components/ui/primitives/sidebar.tsx +3 -1
  224. package/src/components/ui/right-stack.test.ts +29 -0
  225. package/src/components/ui/right-stack.tsx +164 -0
  226. package/src/components/ui/sidebar/app-sidebar.tsx +1 -1
  227. package/src/components/ui/sidebar/icon-rail.tsx +8 -3
  228. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +1 -1
  229. package/src/components/ui/sidebar/panels/items-panel/index.tsx +6 -6
  230. package/src/components/ui/sidebar/panels/settings-panel/animation-speed-setting.tsx +40 -0
  231. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +7 -8
  232. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.test.tsx +170 -0
  233. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.tsx +92 -0
  234. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +138 -27
  235. package/src/components/ui/sidebar/panels/settings-panel/interface-theme.test.tsx +20 -0
  236. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +5 -5
  237. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +4 -10
  238. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -11
  239. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  240. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  241. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  242. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  243. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  244. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  245. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  246. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  247. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  248. package/src/components/ui/sidebar/panels/site-panel/index.tsx +181 -223
  249. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  250. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  251. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  252. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  253. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  254. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  255. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  256. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  257. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  258. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  259. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +13 -37
  260. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  261. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  262. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  263. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  264. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +2 -4
  265. package/src/components/ui/sidebar/tab-bar.tsx +14 -4
  266. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  267. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  268. package/src/components/viewer/floorplan-preview.tsx +5 -6
  269. package/src/components/viewer/viewer-controls-bar.tsx +18 -6
  270. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  271. package/src/components/viewer/viewer-rooms.tsx +191 -0
  272. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  273. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  274. package/src/components/viewer/viewer-stage.tsx +2 -2
  275. package/src/components/viewer-zone-system.tsx +3 -8
  276. package/src/hooks/use-auto-save.integration.test.tsx +215 -43
  277. package/src/hooks/use-auto-save.ts +60 -9
  278. package/src/hooks/use-ceiling-events.test.ts +123 -118
  279. package/src/hooks/use-grid-events.test.ts +105 -105
  280. package/src/hooks/use-keyboard.test.ts +243 -19
  281. package/src/hooks/use-keyboard.ts +118 -83
  282. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  283. package/src/hooks/use-open-wall-ends.ts +238 -0
  284. package/src/hooks/use-placement-notice.test.ts +71 -0
  285. package/src/hooks/use-placement-notice.ts +73 -0
  286. package/src/hooks/use-selected-room.ts +143 -0
  287. package/src/index.tsx +312 -58
  288. package/src/lib/__fixtures__/owner-victor-seattle-parking.json +1 -0
  289. package/src/lib/__fixtures__/owner-villa-aurelia-basement.json +1 -0
  290. package/src/lib/add-storey.test.ts +177 -0
  291. package/src/lib/add-storey.ts +59 -0
  292. package/src/lib/below-grade.test.tsx +171 -0
  293. package/src/lib/below-grade.ts +133 -0
  294. package/src/lib/camera-facing.test.ts +24 -0
  295. package/src/lib/camera-facing.ts +24 -0
  296. package/src/lib/ceiling-edit-session.test.ts +321 -0
  297. package/src/lib/ceiling-edit-session.ts +206 -0
  298. package/src/lib/ceiling-region-session.test.ts +179 -0
  299. package/src/lib/continuation.test.ts +19 -1
  300. package/src/lib/continuation.ts +25 -4
  301. package/src/lib/direct-manipulation.ts +2 -0
  302. package/src/lib/door-interaction.ts +14 -0
  303. package/src/lib/download-project-file.ts +17 -0
  304. package/src/lib/editor-level-display.test.ts +96 -0
  305. package/src/lib/editor-level-display.ts +55 -0
  306. package/src/lib/editor-views.ts +178 -0
  307. package/src/lib/export-geometry-wait.test.ts +90 -0
  308. package/src/lib/export-texture-utils.ts +2 -4
  309. package/src/lib/floor-edit-session.ts +153 -0
  310. package/src/lib/floor-footprints.test.ts +563 -0
  311. package/src/lib/floor-footprints.ts +395 -0
  312. package/src/lib/floor-foundation-support.test.ts +284 -0
  313. package/src/lib/floor-opening-draft.ts +218 -0
  314. package/src/lib/floor-openings.test.ts +216 -0
  315. package/src/lib/floor-region-geometry.ts +191 -0
  316. package/src/lib/floor-region-session.test.ts +485 -0
  317. package/src/lib/floor-region-session.ts +589 -0
  318. package/src/lib/floor-region-snap.ts +101 -0
  319. package/src/lib/floor-step-hover.test.ts +32 -0
  320. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  321. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  322. package/src/lib/floorplan/dimension-drive.ts +479 -0
  323. package/src/lib/floorplan/floorplan-export.test.ts +12 -8
  324. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  325. package/src/lib/floorplan/floorplan-extension.ts +32 -0
  326. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  327. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  328. package/src/lib/floorplan/floorplan-pdfkit-document.ts +363 -27
  329. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +177 -8
  330. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +60 -25
  331. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  332. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  333. package/src/lib/floorplan/geometry.ts +13 -6
  334. package/src/lib/floorplan/index.ts +0 -9
  335. package/src/lib/floorplan/items.ts +5 -2
  336. package/src/lib/floorplan/marks.test.ts +213 -0
  337. package/src/lib/floorplan/marks.ts +238 -0
  338. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  339. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  340. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  341. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  342. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  343. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  344. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  345. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  346. package/src/lib/floorplan/pdfkit-standard-fonts.d.ts +5 -0
  347. package/src/lib/floorplan/schedules.test.ts +189 -0
  348. package/src/lib/floorplan/schedules.ts +339 -0
  349. package/src/lib/floorplan/sheet-export.test.ts +124 -0
  350. package/src/lib/floorplan/sheet-export.ts +318 -0
  351. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +768 -0
  352. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  353. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  354. package/src/lib/floorplan/site-plan/coverage.ts +230 -0
  355. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  356. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  357. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  358. package/src/lib/floorplan/site-plan/front-edge.ts +157 -0
  359. package/src/lib/floorplan/site-plan/geometry.test.ts +269 -0
  360. package/src/lib/floorplan/site-plan/geometry.ts +142 -0
  361. package/src/lib/floorplan/site-plan/index.ts +55 -0
  362. package/src/lib/floorplan/site-plan/site-annotations.test.ts +483 -0
  363. package/src/lib/floorplan/site-plan/site-annotations.ts +1084 -0
  364. package/src/lib/floorplan/site-plan/site-parts.ts +287 -0
  365. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  366. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  367. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  368. package/src/lib/floorplan/site-plan/yards.test.ts +70 -0
  369. package/src/lib/floorplan/types.ts +1 -27
  370. package/src/lib/follow-pascal-exterior.test-fixture.ts +17 -0
  371. package/src/lib/follow-pascal-exterior.test.ts +171 -0
  372. package/src/lib/follow-pascal-exterior.ts +39 -0
  373. package/src/lib/follow-pascal-presentation.test.ts +203 -0
  374. package/src/lib/follow-pascal-presentation.ts +129 -0
  375. package/src/lib/follow-pascal-sight.ts +84 -0
  376. package/src/lib/follow-pascal-targets.test.ts +406 -0
  377. package/src/lib/follow-pascal-targets.ts +198 -0
  378. package/src/lib/follow-pascal.test.ts +350 -0
  379. package/src/lib/follow-pascal.ts +284 -0
  380. package/src/lib/fresh-planar-placement.test.ts +148 -0
  381. package/src/lib/fresh-planar-placement.ts +114 -37
  382. package/src/lib/geometry-script/author.ts +157 -0
  383. package/src/lib/geometry-script/client.ts +159 -0
  384. package/src/lib/geometry-script/finish-scripts.ts +127 -0
  385. package/src/lib/geometry-script/protocol.ts +13 -0
  386. package/src/lib/gesture-lifecycle.test.ts +595 -0
  387. package/src/lib/gesture-lifecycle.ts +290 -0
  388. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  389. package/src/lib/glb-export.test.ts +636 -126
  390. package/src/lib/glb-export.ts +241 -120
  391. package/src/lib/history.test.ts +18 -7
  392. package/src/lib/history.ts +7 -0
  393. package/src/lib/hud-title.ts +109 -0
  394. package/src/lib/hydration-gated-storage.ts +49 -0
  395. package/src/lib/ifc-export.test.ts +174 -0
  396. package/src/lib/ifc-export.ts +201 -0
  397. package/src/lib/ifc-parts.ts +15 -0
  398. package/src/lib/inspector-expanded.test.ts +44 -0
  399. package/src/lib/inspector-expanded.ts +36 -0
  400. package/src/lib/interaction/scope.ts +22 -0
  401. package/src/lib/keyboard-pan.test.ts +129 -7
  402. package/src/lib/keyboard-pan.ts +161 -12
  403. package/src/lib/level-selection.test.ts +57 -0
  404. package/src/lib/level-selection.ts +16 -1
  405. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  406. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  407. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  408. package/src/lib/lot/clean-ring.test.ts +187 -0
  409. package/src/lib/lot/clean-ring.ts +217 -0
  410. package/src/lib/lot/dossier.test.ts +202 -0
  411. package/src/lib/lot/dossier.ts +498 -0
  412. package/src/lib/lot/drop-in.test.ts +326 -0
  413. package/src/lib/lot/drop-in.ts +392 -0
  414. package/src/lib/lot/index.ts +43 -0
  415. package/src/lib/lot/lot-patch.test.ts +340 -0
  416. package/src/lib/lot/lot-patch.ts +394 -0
  417. package/src/lib/lot/parcel-provider.ts +43 -0
  418. package/src/lib/lot/terrain.test.ts +161 -0
  419. package/src/lib/lot/terrain.ts +266 -0
  420. package/src/lib/material-catalog-model.ts +3 -1
  421. package/src/lib/material-paint-panel-model.ts +9 -5
  422. package/src/lib/material-paint.ts +4 -0
  423. package/src/lib/measurement-kind.test.ts +6 -8
  424. package/src/lib/measurements.test.ts +23 -0
  425. package/src/lib/mezzanine-actions.test.ts +342 -0
  426. package/src/lib/mezzanine-draft.test.ts +501 -0
  427. package/src/lib/mezzanine-draft.ts +278 -0
  428. package/src/lib/mezzanine-messages.ts +26 -0
  429. package/src/lib/mezzanine-selection.test.ts +207 -0
  430. package/src/lib/mezzanine-stairs.ts +46 -0
  431. package/src/lib/model-export.ts +3 -1
  432. package/src/lib/node-action-movement.ts +17 -6
  433. package/src/lib/node-display-label.ts +37 -0
  434. package/src/lib/orbit-floor-clearance.test.ts +201 -0
  435. package/src/lib/orbit-floor-clearance.ts +154 -0
  436. package/src/lib/paint-commit-route.test.ts +37 -0
  437. package/src/lib/paint-commit-route.ts +17 -0
  438. package/src/lib/paint-eyedropper.test.tsx +165 -0
  439. package/src/lib/paint-eyedropper.ts +99 -0
  440. package/src/lib/paint-pass-through.test.ts +122 -0
  441. package/src/lib/paint-pass-through.ts +78 -0
  442. package/src/lib/paint-region-hover.ts +25 -0
  443. package/src/lib/paint-region-mode.ts +154 -0
  444. package/src/lib/paint-regions.test.ts +166 -0
  445. package/src/lib/paint-regions.ts +192 -0
  446. package/src/lib/paint-scope.test.ts +317 -35
  447. package/src/lib/paint-scope.ts +267 -25
  448. package/src/lib/panel-rows.ts +21 -0
  449. package/src/lib/parametric-node-update.ts +16 -7
  450. package/src/lib/plate-paint-affected.test.ts +300 -0
  451. package/src/lib/plate-paint-affected.ts +177 -0
  452. package/src/lib/plugin-chat.ts +65 -0
  453. package/src/lib/plugin-panels.test.ts +25 -0
  454. package/src/lib/plugin-panels.ts +94 -0
  455. package/src/lib/point-ask/__fixtures__/consumer-browser.tsx +76 -0
  456. package/src/lib/point-ask/__fixtures__/house.ts +81 -0
  457. package/src/lib/point-ask/ask-created.test.ts +195 -0
  458. package/src/lib/point-ask/ask-flow.test.ts +380 -0
  459. package/src/lib/point-ask/build-context.test.ts +173 -0
  460. package/src/lib/point-ask/build-context.ts +94 -0
  461. package/src/lib/point-ask/capture-crop.ts +129 -0
  462. package/src/lib/point-ask/choreography.ts +69 -0
  463. package/src/lib/point-ask/compare.test.ts +145 -0
  464. package/src/lib/point-ask/compare.ts +73 -0
  465. package/src/lib/point-ask/describe-target.test.ts +165 -0
  466. package/src/lib/point-ask/describe-target.ts +234 -0
  467. package/src/lib/point-ask/dictation.ts +79 -0
  468. package/src/lib/point-ask/edge-marker.test.ts +61 -0
  469. package/src/lib/point-ask/edge-marker.ts +27 -0
  470. package/src/lib/point-ask/frame-loop.ts +25 -0
  471. package/src/lib/point-ask/history-marks.test.ts +94 -0
  472. package/src/lib/point-ask/history-marks.ts +39 -0
  473. package/src/lib/point-ask/host-api.ts +32 -0
  474. package/src/lib/point-ask/outline-box.test.ts +30 -0
  475. package/src/lib/point-ask/outline-box.ts +11 -0
  476. package/src/lib/point-ask/overlay-metrics.ts +6 -0
  477. package/src/lib/point-ask/pick.test.ts +220 -0
  478. package/src/lib/point-ask/pick.ts +121 -0
  479. package/src/lib/point-ask/pin-machine.test.ts +494 -0
  480. package/src/lib/point-ask/pin-machine.ts +217 -0
  481. package/src/lib/point-ask/placement.test.ts +203 -0
  482. package/src/lib/point-ask/placement.ts +141 -0
  483. package/src/lib/point-ask/pointer-tracker.ts +34 -0
  484. package/src/lib/point-ask/projector.ts +83 -0
  485. package/src/lib/point-ask/region.test.ts +157 -0
  486. package/src/lib/point-ask/region.ts +136 -0
  487. package/src/lib/point-ask/retry.test.ts +217 -0
  488. package/src/lib/point-ask/retry.ts +37 -0
  489. package/src/lib/point-ask/scene-commit.test.ts +312 -0
  490. package/src/lib/point-ask/scene-commit.ts +32 -0
  491. package/src/lib/point-ask/session.ts +1391 -0
  492. package/src/lib/point-ask/spring-keyframes.test.ts +45 -0
  493. package/src/lib/point-ask/spring-keyframes.ts +38 -0
  494. package/src/lib/point-ask/spring.test.ts +63 -0
  495. package/src/lib/point-ask/spring.ts +107 -0
  496. package/src/lib/point-ask/suggestions.test.ts +80 -0
  497. package/src/lib/point-ask/suggestions.ts +125 -0
  498. package/src/lib/point-ask/target-scope.test.ts +622 -0
  499. package/src/lib/point-ask/target-scope.ts +338 -0
  500. package/src/lib/point-ask/types.ts +48 -0
  501. package/src/lib/portable-export.test.ts +34 -1
  502. package/src/lib/portable-export.ts +119 -53
  503. package/src/lib/print-feature-thickness.ts +10 -1
  504. package/src/lib/print-roof-solids.test.ts +39 -1
  505. package/src/lib/print-roof-solids.ts +43 -10
  506. package/src/lib/print-shell-compiler-manifold-core.ts +1 -1
  507. package/src/lib/print-shell-compiler.ts +1 -0
  508. package/src/lib/registered-draft-snap.ts +12 -0
  509. package/src/lib/roof-duplication.ts +10 -2
  510. package/src/lib/room-assembly-overlay.test.ts +275 -0
  511. package/src/lib/room-assembly-overlay.ts +265 -0
  512. package/src/lib/room-built-on.test.ts +379 -0
  513. package/src/lib/room-built-on.ts +138 -0
  514. package/src/lib/room-construction-commands.ts +712 -0
  515. package/src/lib/room-construction.test.ts +746 -0
  516. package/src/lib/room-construction.ts +385 -0
  517. package/src/lib/room-controls-phase6.test.ts +630 -0
  518. package/src/lib/room-divide-session.ts +523 -0
  519. package/src/lib/room-handle-drag.ts +1068 -0
  520. package/src/lib/room-name-catalog.test.ts +90 -0
  521. package/src/lib/room-name-catalog.ts +168 -0
  522. package/src/lib/room-push-dimensions.test.ts +311 -0
  523. package/src/lib/room-push-dimensions.ts +251 -0
  524. package/src/lib/room-selection-commands.ts +62 -0
  525. package/src/lib/room-selection.test.ts +1017 -0
  526. package/src/lib/room-selection.ts +417 -0
  527. package/src/lib/room-structure-commands.ts +94 -0
  528. package/src/lib/room-structure.test.ts +921 -0
  529. package/src/lib/room-transform-session.ts +570 -0
  530. package/src/lib/room-zone-routing.test.ts +294 -0
  531. package/src/lib/room-zone-routing.ts +197 -0
  532. package/src/lib/scene-clipboard.test.ts +610 -1
  533. package/src/lib/scene-clipboard.ts +269 -58
  534. package/src/lib/scene-material-refs.test.ts +79 -0
  535. package/src/lib/scene-material-refs.ts +79 -0
  536. package/src/lib/scene.test.ts +24 -36
  537. package/src/lib/scene.ts +33 -57
  538. package/src/lib/selection-routing.test.ts +129 -45
  539. package/src/lib/selection-routing.ts +54 -75
  540. package/src/lib/session-writes.test.ts +120 -0
  541. package/src/lib/session-writes.ts +205 -0
  542. package/src/lib/sfx-bus.ts +13 -0
  543. package/src/lib/sfx-player.test.ts +70 -9
  544. package/src/lib/sfx-player.ts +6 -0
  545. package/src/lib/sidebar-panel.ts +62 -0
  546. package/src/lib/site-boundary.test.ts +4 -6
  547. package/src/lib/site-boundary.ts +1 -1
  548. package/src/lib/snapping-mode.test.ts +4 -0
  549. package/src/lib/snapping-mode.ts +8 -1
  550. package/src/lib/stair-levels.test.ts +25 -0
  551. package/src/lib/stair-levels.ts +22 -3
  552. package/src/lib/terrace-draft.test.ts +145 -0
  553. package/src/lib/terrace-draft.ts +214 -0
  554. package/src/lib/tool-cursor-icon.test.ts +16 -0
  555. package/src/lib/tool-cursor-icon.ts +11 -0
  556. package/src/lib/typing-target.ts +9 -0
  557. package/src/lib/units.ts +77 -39
  558. package/src/lib/vector-edges.test.ts +58 -0
  559. package/src/lib/vector-edges.ts +400 -0
  560. package/src/lib/viewer-selection.test.ts +577 -0
  561. package/src/lib/viewer-selection.ts +580 -0
  562. package/src/lib/wall-draw-variant.test.ts +71 -0
  563. package/src/lib/wall-draw-variant.ts +91 -0
  564. package/src/lib/wall-paint-region-session.ts +246 -0
  565. package/src/lib/wall-paint-region.test.ts +373 -0
  566. package/src/lib/wall-placement-pointer.test.ts +42 -0
  567. package/src/lib/wall-placement-pointer.ts +57 -0
  568. package/src/lib/wall-polygon-draft.test.ts +262 -0
  569. package/src/lib/wall-polygon-draft.ts +286 -0
  570. package/src/lib/wall-push.test.ts +453 -0
  571. package/src/lib/wall-region-face.ts +352 -0
  572. package/src/lib/wall-region-handles.test.ts +304 -0
  573. package/src/lib/wall-region-handles.ts +368 -0
  574. package/src/lib/wall-region-list.test.ts +119 -0
  575. package/src/lib/wall-region-list.ts +136 -0
  576. package/src/lib/wall-region-snap.ts +110 -0
  577. package/src/lib/window-changes.ts +35 -0
  578. package/src/lib/window-interaction.ts +14 -0
  579. package/src/lib/zone-content.ts +4 -1
  580. package/src/lib/zone-name.test.ts +17 -0
  581. package/src/lib/zone-name.ts +17 -0
  582. package/src/store/opens-in-select-mode.test.ts +223 -0
  583. package/src/store/paint-mode.test.ts +188 -0
  584. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  585. package/src/store/tool-mode.test.ts +137 -6
  586. package/src/store/use-audio.test.ts +146 -0
  587. package/src/store/use-audio.tsx +127 -3
  588. package/src/store/use-construction-reveal.test.ts +173 -0
  589. package/src/store/use-construction-reveal.ts +145 -0
  590. package/src/store/use-corner-snap-hint.test.ts +24 -0
  591. package/src/store/use-corner-snap-hint.ts +47 -0
  592. package/src/store/use-delete-confirmation.ts +28 -1
  593. package/src/store/use-drawing-view.test.ts +24 -3
  594. package/src/store/use-drawing-view.ts +29 -1
  595. package/src/store/use-editor.tsx +432 -321
  596. package/src/store/use-floorplan-draft-preview.ts +13 -0
  597. package/src/store/use-floorplan-marquee.ts +12 -2
  598. package/src/store/use-follow-pascal.test.ts +62 -0
  599. package/src/store/use-follow-pascal.ts +53 -0
  600. package/src/store/use-point-ask.ts +247 -0
  601. package/src/store/use-quick-measurement-hud.ts +3 -3
  602. package/src/store/use-stair-build-preview.ts +33 -5
  603. package/src/store/use-wall-move-ghosts.ts +4 -2
  604. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  605. package/src/store/use-wall-region-handles-selection.ts +139 -0
  606. package/src/store/view-layout.test.ts +201 -0
  607. package/src/store/view-layout.ts +199 -0
  608. package/src/test-utils/immediate-animation-frames.ts +16 -0
  609. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  610. package/src/test-utils/render-current-markup.tsx +16 -0
  611. package/src/test-utils/selection-harness.ts +82 -0
  612. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  613. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  614. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  615. package/src/components/tools/fence/fence-drafting.ts +0 -266
  616. package/src/components/tools/stair/stair-click-guard.test.ts +0 -58
  617. package/src/components/tools/stair/stair-click-guard.ts +0 -72
  618. package/src/components/tools/stair/stair-defaults.ts +0 -18
  619. package/src/components/tools/stair/stair-tool.tsx +0 -634
  620. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  621. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  622. package/src/lib/floorplan/stairs.ts +0 -489
  623. package/src/lib/level-duplication.test.ts +0 -144
  624. package/src/lib/level-duplication.ts +0 -195
@@ -23,12 +23,20 @@ import {
23
23
  resolveBuildingForLevel,
24
24
  resolveSelectionProxyId,
25
25
  resumeSceneHistory,
26
+ type SlabNode,
26
27
  useInteractive,
27
28
  useLiveNodeOverrides,
28
29
  useLiveTransforms,
29
30
  useScene,
30
31
  } from '@pascal-app/core'
31
- import { beginPerfAction, cancelPerfAction, commitPerfAction, useViewer } from '@pascal-app/viewer'
32
+ import {
33
+ beginPerfAction,
34
+ cancelPerfAction,
35
+ commitPerfAction,
36
+ useRevealPlanActive,
37
+ useRevealWaiting,
38
+ useViewer,
39
+ } from '@pascal-app/viewer'
32
40
  import {
33
41
  type ComponentProps,
34
42
  memo,
@@ -43,6 +51,14 @@ import {
43
51
  } from 'react'
44
52
  import { useShallow } from 'zustand/react/shallow'
45
53
  import { markToolCancelConsumed } from '../../../hooks/use-keyboard'
54
+ import {
55
+ hoverRoomFromHit,
56
+ resolvePlanRoomHit,
57
+ roomPickingEnabled,
58
+ selectionModifiersFromEvent,
59
+ useRoomRecords,
60
+ } from '../../../hooks/use-selected-room'
61
+ import { useCeilingEditSession } from '../../../lib/ceiling-edit-session'
46
62
  import { ROTATE_HANDLE_DRAG_LABEL } from '../../../lib/contextual-help'
47
63
  import {
48
64
  canDirectRotateNode,
@@ -53,6 +69,7 @@ import {
53
69
  snapDirectRotationDelta,
54
70
  } from '../../../lib/direct-manipulation'
55
71
  import { createEditorApi } from '../../../lib/editor-api'
72
+ import { isRoomOwnedPlate, roomOwnedPlateDrillTarget } from '../../../lib/floor-footprints'
56
73
  import {
57
74
  type FloorplanAnnotationVisibility,
58
75
  filterFloorplanAnnotationGeometry,
@@ -86,7 +103,15 @@ import {
86
103
  isIdle,
87
104
  tangentReshapeScope,
88
105
  } from '../../../lib/interaction/scope'
89
- import { emitCanvasNodeSelection } from '../../../lib/selection-routing'
106
+ import { runFloorplanWallPush, WALL_PUSH_AFFORDANCE } from '../../../lib/room-handle-drag'
107
+ import { selectRoom, selectRoomFromHit } from '../../../lib/room-selection-commands'
108
+ import { clickZoneArea, hoverZoneArea } from '../../../lib/room-zone-routing'
109
+ import {
110
+ emitCanvasNodeSelection,
111
+ enterBuildingFromPlanHit,
112
+ resolveCanvasBuildingId,
113
+ } from '../../../lib/selection-routing'
114
+ import { createSessionWrites, type SessionWrites } from '../../../lib/session-writes'
90
115
  import { sfxEmitter } from '../../../lib/sfx-bus'
91
116
  import { clearSurfacePlanSnapFeedback } from '../../../lib/surface-plan-snap'
92
117
  import { paintZoneMembership } from '../../../lib/units'
@@ -101,8 +126,13 @@ import useInteractionScope, {
101
126
  useMovingNode,
102
127
  } from '../../../store/use-interaction-scope'
103
128
  import { expandSessionSelectionForNode } from '../../../store/use-session-groups'
129
+ import { isViewVisible, VIEW_2D } from '../../../store/view-layout'
130
+ import { FloorRegionControls2D } from '../../editor/floor-region-controls'
104
131
  import { startGroupPickUp } from '../../editor/group-actions'
105
132
  import { classifyParticipant } from '../../editor/group-transform-shared'
133
+ import { RoomHandleDragPreview2D, RoomHandles2D } from '../../editor/room-handles-2d'
134
+ import { RoomHighlight2D } from '../../editor/room-highlight'
135
+ import { RoomLabels2D } from '../../editor/room-labels'
106
136
  import { suppressBoxSelectForPointer } from '../../tools/select/box-select-state'
107
137
  import {
108
138
  FloorplanGroupSelectionBox,
@@ -120,6 +150,7 @@ import {
120
150
  resolveSvgAnnotationCollisions,
121
151
  svgAnnotationLabelId,
122
152
  } from './floorplan-annotation-layout'
153
+ import { FloorplanDimensionEditOverlay } from './floorplan-dimension-edit-overlay'
123
154
  import { FloorplanDimensionRenderer } from './floorplan-dimension-renderer'
124
155
  import { FloorplanGeometryRenderer } from './floorplan-geometry-renderer'
125
156
  import {
@@ -127,6 +158,7 @@ import {
127
158
  resolveFloorplanLabelAngle,
128
159
  updateSvgFloorplanLabelOrientations,
129
160
  } from './floorplan-label-angle'
161
+ import { useFloorplanReveal } from './floorplan-reveal'
130
162
 
131
163
  /**
132
164
  * Registry-driven floor-plan layer.
@@ -179,22 +211,19 @@ const ScaleAwareFloorplanGroupSelectionBox = memo(function ScaleAwareFloorplanGr
179
211
  return <FloorplanGroupSelectionBox {...props} unitsPerPixel={unitsPerPixel} />
180
212
  })
181
213
 
182
- /**
183
- * Snapshot of node fields captured at drag-start, used by the single-undo
184
- * dance to revert untracked before re-applying as a single tracked
185
- * change. The dispatcher only knows about the `affectedIds` the
186
- * affordance declares; it captures whatever fields exist on each node by
187
- * cloning the full record minus the registry-managed `id` / `type`.
188
- */
189
- type NodeSnapshot = { id: AnyNodeId; data: Record<string, unknown> }
190
-
191
214
  type ActiveDrag = {
192
215
  pointerId: number
193
216
  captureTarget: Element
194
217
  /** Key for the visual `active` flag — e.g. `${nodeId}:${endpoint}`. */
195
218
  handleId: string
196
219
  session: FloorplanAffordanceSession
197
- snapshots: NodeSnapshot[]
220
+ /**
221
+ * What the session itself wrote to the scene during `apply()` (legacy
222
+ * sessions only; override-only sessions write nothing). Cancel and commit
223
+ * take back exactly these writes, so a collaborator's edit to the same node
224
+ * mid-drag survives.
225
+ */
226
+ writes: SessionWrites
198
227
  historyPaused: boolean
199
228
  /**
200
229
  * Last plan point handed to `session.apply` (the grab point until the first
@@ -218,7 +247,6 @@ type ActiveDrag = {
218
247
  }
219
248
 
220
249
  type FloorplanAffordanceCancelEffects = {
221
- restoreSnapshots: (snapshots: NodeSnapshot[]) => void
222
250
  resumeHistory: () => void
223
251
  clearPreview: (id: AnyNodeId) => void
224
252
  clearSnapFeedback: () => void
@@ -242,7 +270,7 @@ export function cancelFloorplanAffordanceDrag(
242
270
  drag.captureTarget.releasePointerCapture?.(drag.pointerId)
243
271
  }
244
272
 
245
- effects.restoreSnapshots(drag.snapshots)
273
+ drag.writes.revert()
246
274
  if (drag.historyPaused) {
247
275
  effects.resumeHistory()
248
276
  drag.historyPaused = false
@@ -378,6 +406,7 @@ type CacheEntry = {
378
406
  deps: NodeDeps
379
407
  base: FloorplanGeometry | null
380
408
  overlay: FloorplanGeometry | null
409
+ handles: FloorplanGeometry | null
381
410
  node: AnyNode
382
411
  }
383
412
 
@@ -433,7 +462,7 @@ type FloorplanLevelDataHook = (args: {
433
462
  nodes: Record<string, AnyNode>
434
463
  }) => unknown
435
464
 
436
- type FloorplanRenderPass = 'base' | 'overlay'
465
+ type FloorplanRenderPass = 'base' | 'overlay' | 'handles'
437
466
 
438
467
  const POINTER_CURSOR_STYLE = { cursor: 'pointer' } as const
439
468
  // Group members advertise the drag-to-move-the-selection gesture.
@@ -441,18 +470,16 @@ const MOVE_CURSOR_STYLE = { cursor: 'move' } as const
441
470
  const NO_POINTER_EVENTS_STYLE = { pointerEvents: 'none' } as const
442
471
 
443
472
  export function isFloorplanOpeningPlacementState({
444
- phase,
445
473
  mode,
446
474
  tool,
447
475
  movingNodeHasWallOpeningPlacement,
448
476
  }: {
449
- phase: string
450
477
  mode: string
451
478
  tool: string | null
452
479
  movingNodeHasWallOpeningPlacement: boolean
453
480
  }): boolean {
454
481
  return (
455
- (phase === 'structure' && mode === 'build' && (tool === 'door' || tool === 'window')) ||
482
+ (mode === 'build' && (tool === 'door' || tool === 'window')) ||
456
483
  movingNodeHasWallOpeningPlacement
457
484
  )
458
485
  }
@@ -471,12 +498,11 @@ export function floorplanEntryYieldsToTool(state: {
471
498
  }
472
499
 
473
500
  function floorplanEntryYieldsToToolNow(): boolean {
474
- const { phase, mode, tool } = useEditor.getState()
501
+ const { mode, tool } = useEditor.getState()
475
502
  const movingNode = getMovingNode()
476
503
  return floorplanEntryYieldsToTool({
477
504
  mode,
478
505
  openingPlacement: isFloorplanOpeningPlacementState({
479
- phase,
480
506
  mode,
481
507
  tool,
482
508
  movingNodeHasWallOpeningPlacement:
@@ -486,21 +512,6 @@ function floorplanEntryYieldsToToolNow(): boolean {
486
512
  })
487
513
  }
488
514
 
489
- function snapshotNode(node: AnyNode): NodeSnapshot {
490
- // Shallow-clone every non-id, non-type field. Arrays / vec tuples are
491
- // deep-cloned to detach from the live store reference.
492
- const data: Record<string, unknown> = {}
493
- for (const [key, value] of Object.entries(node)) {
494
- if (key === 'id' || key === 'type' || key === 'object' || key === 'parentId') continue
495
- data[key] = Array.isArray(value) ? [...(value as unknown[])] : value
496
- }
497
- return { id: node.id, data }
498
- }
499
-
500
- function snapshotsToUpdates(snapshots: NodeSnapshot[]) {
501
- return snapshots.map((s) => ({ id: s.id, data: s.data }))
502
- }
503
-
504
515
  // Stable empty sentinel used by per-entry builders while the floor plan is
505
516
  // hidden; committed scene edits still flow through `useScene`.
506
517
  const EMPTY_LIVE_OVERRIDES: Map<string, LiveNodeOverrides> = new Map()
@@ -564,6 +575,7 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
564
575
  }, [selectedLevelId, ambientBuildingSourceId, nodes])
565
576
 
566
577
  const levelId = selectedLevelId ?? ambientLevelId
578
+ useRoomRecords(levelId)
567
579
  const isAmbient = !selectedLevelId && !!ambientLevelId
568
580
  const activeBuildingId = useMemo(
569
581
  () => (levelId ? resolveBuildingForLevel(levelId as AnyNodeId, nodes) : null),
@@ -584,19 +596,17 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
584
596
  // `handleClickStop` / `handleSelect`, so the tool never sees it. Pass
585
597
  // clicks through in that case (`floorplanEntryYieldsToToolNow`).
586
598
  const editorMode = useEditor((s) => s.mode)
587
- const structureLayer = useEditor((s) => s.structureLayer)
588
599
  const floorplanSelectionTool = useEditor((s) => s.floorplanSelectionTool)
589
600
  const endpointReshape = useEndpointReshape()
590
601
  const isMarqueeSelectionActive =
591
602
  editorMode === 'select' &&
592
603
  floorplanSelectionTool === 'marquee' &&
593
- structureLayer !== 'zones' &&
594
604
  !movingNode &&
595
605
  !endpointReshape
596
606
  // While the floor plan is not on screen (pure 3D view), per-entry live
597
607
  // selectors freeze to `undefined` so drag publishes do not re-render the
598
608
  // hidden floor-plan tree.
599
- const floorplanVisible = useEditor((s) => s.viewMode !== '3d')
609
+ const floorplanVisible = useEditor((s) => isViewVisible(s, VIEW_2D))
600
610
  const drawingType = useDrawingView((s) => s.drawingType)
601
611
  const annotationVisibility = useFloorplanAnnotationVisibility((s) => s.visibility)
602
612
  const wallDimensionReference = useFloorplanAnnotationVisibility((s) => s.wallDimensionReference)
@@ -697,6 +707,17 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
697
707
  swallowNextClick(200)
698
708
  return
699
709
  }
710
+ // A zone's fill or label takes the same rule as its pill (`clickZoneArea`).
711
+ if (clickedNode?.type === 'zone') {
712
+ clickZoneArea(clickedNode.id, {
713
+ meta: false,
714
+ ctrl: false,
715
+ shift: options.shouldToggle,
716
+ alt: options.isolateMember,
717
+ })
718
+ swallowNextClick(200)
719
+ return
720
+ }
700
721
  const currentSelectedIds = useViewer.getState().selection.selectedIds
701
722
  let nextSelectedIds: string[]
702
723
  if (options.shouldToggle) {
@@ -754,13 +775,13 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
754
775
  )
755
776
 
756
777
  const handleClickStop = useCallback((event: React.MouseEvent<SVGGElement>) => {
757
- if (floorplanEntryYieldsToToolNow()) return
778
+ if (floorplanEntryYieldsToToolNow() || !isIdle(useInteractionScope.getState().scope)) return
758
779
  event.stopPropagation()
759
780
  }, [])
760
781
 
761
782
  const startDirectMoveDrag = useCallback(
762
783
  (id: AnyNodeId, event: ReactPointerEvent<SVGGElement>): boolean => {
763
- if (event.button !== 0) return false
784
+ if (event.button !== 0 || event.altKey) return false
764
785
 
765
786
  const node = useScene.getState().nodes[id]
766
787
  if (!node || !isRegistryMovable(node.type)) return false
@@ -1015,7 +1036,35 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
1015
1036
  // the stores at event time. Commit clears the interaction scope before
1016
1037
  // React paints the next frame; a render-time `undefined` handler leaves
1017
1038
  // a short dead zone where the first post-placement selection is lost.
1018
- if (floorplanEntryYieldsToToolNow()) return
1039
+ if (floorplanEntryYieldsToToolNow() || !isIdle(useInteractionScope.getState().scope)) return
1040
+ if (event.button === 0 && enterBuildingFromPlanHit(id)) {
1041
+ event.stopPropagation()
1042
+ swallowNextClick(200)
1043
+ return
1044
+ }
1045
+ const point = clientToPlan(event.clientX, event.clientY)
1046
+ if (
1047
+ event.button === 0 &&
1048
+ point &&
1049
+ roomPickingEnabled() &&
1050
+ selectRoomFromHit(resolvePlanRoomHit(id, point), selectionModifiersFromEvent(event), id)
1051
+ ) {
1052
+ event.stopPropagation()
1053
+ swallowNextClick(200)
1054
+ return
1055
+ }
1056
+ // A room-owned plate past its room: the same routing as the 3D click.
1057
+ const plate = useScene.getState().nodes[id]
1058
+ if (event.button === 0 && point && roomPickingEnabled() && isRoomOwnedPlate(plate)) {
1059
+ const footprint = roomOwnedPlateDrillTarget(useScene.getState().nodes, plate as SlabNode)
1060
+ if (footprint) handleSelect(footprint.id as AnyNodeId, event)
1061
+ else {
1062
+ const hit = resolvePlanRoomHit(id, point)
1063
+ if (hit) selectRoom(hit)
1064
+ event.stopPropagation()
1065
+ }
1066
+ return
1067
+ }
1019
1068
  if (startDirectMoveDrag(id, event)) return
1020
1069
  if (startDirectRotateDrag(id, event)) return
1021
1070
  if (startGroupMoveDrag(id, event)) return
@@ -1241,6 +1290,16 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
1241
1290
  if (event.button !== 0) return
1242
1291
  if (movingNode) return
1243
1292
 
1293
+ // A wall push arrow runs the same push drag as the 3D arrows (its own
1294
+ // gesture, preview and commit), not a registry affordance session.
1295
+ if (affordance === WALL_PUSH_AFFORDANCE) {
1296
+ event.preventDefault()
1297
+ event.stopPropagation()
1298
+ suppressBoxSelectForPointer(event)
1299
+ runFloorplanWallPush(payload, event.clientX, event.clientY)
1300
+ return
1301
+ }
1302
+
1244
1303
  const sceneNodes = useScene.getState().nodes
1245
1304
  const node = sceneNodes[nodeId]
1246
1305
  if (!node) return
@@ -1266,13 +1325,9 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
1266
1325
  sceneApi: createSceneApi(useScene),
1267
1326
  })
1268
1327
 
1269
- const snapshots: NodeSnapshot[] = []
1270
- for (const id of session.affectedIds) {
1271
- const n = sceneNodes[id]
1272
- if (n) snapshots.push(snapshotNode(n))
1273
- }
1274
-
1275
1328
  pauseSceneHistory(useScene)
1329
+ const writes = createSessionWrites()
1330
+ writes.watch(session.affectedIds)
1276
1331
 
1277
1332
  // Rotation readout setup. The wedge radius tracks the grab distance
1278
1333
  // from the pivot (≈ the handle's orbit), nudged inward so the swept
@@ -1304,7 +1359,7 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
1304
1359
  captureTarget,
1305
1360
  handleId,
1306
1361
  session,
1307
- snapshots,
1362
+ writes,
1308
1363
  historyPaused: true,
1309
1364
  lastPlanPoint: initialPlanPoint,
1310
1365
  rotation,
@@ -1337,8 +1392,6 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
1337
1392
  cancelFloorplanAffordanceDrag(
1338
1393
  dragRef,
1339
1394
  {
1340
- restoreSnapshots: (snapshots) =>
1341
- useScene.getState().updateNodes(snapshotsToUpdates(snapshots)),
1342
1395
  resumeHistory: () => resumeSceneHistory(useScene),
1343
1396
  clearPreview: (id) => {
1344
1397
  useLiveNodeOverrides.getState().clear(id)
@@ -1364,15 +1417,17 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
1364
1417
  if (!planPoint) return
1365
1418
 
1366
1419
  drag.lastPlanPoint = planPoint
1367
- drag.session.apply({
1368
- planPoint,
1369
- modifiers: {
1370
- shiftKey: event.shiftKey,
1371
- altKey: event.altKey,
1372
- ctrlKey: event.ctrlKey,
1373
- metaKey: event.metaKey,
1374
- },
1375
- })
1420
+ drag.writes.record(() =>
1421
+ drag.session.apply({
1422
+ planPoint,
1423
+ modifiers: {
1424
+ shiftKey: event.shiftKey,
1425
+ altKey: event.altKey,
1426
+ ctrlKey: event.ctrlKey,
1427
+ metaKey: event.metaKey,
1428
+ },
1429
+ }),
1430
+ )
1376
1431
 
1377
1432
  // Live rotation readout. Sweep from the bearing at grab to the
1378
1433
  // current pointer bearing around the pivot — the same measurement
@@ -1409,11 +1464,11 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
1409
1464
  // Sessions with a `commit` hook own their atomic write (e.g.
1410
1465
  // affordances that publish to `useLiveNodeOverrides` during
1411
1466
  // `apply()` and never touch scene mid-drag). Mirrors the move
1412
- // overlay's `session.commit` path — revert untracked (no-op when
1413
- // the session never wrote to scene), resume history, then let
1414
- // the session do the tracked write.
1467
+ // overlay's `session.commit` path — take back what the session wrote
1468
+ // (nothing, for those), resume history, then let the session do the
1469
+ // tracked write.
1415
1470
  if (commitValid && drag.session.commit) {
1416
- useScene.getState().updateNodes(snapshotsToUpdates(drag.snapshots))
1471
+ drag.writes.revert()
1417
1472
  if (drag.historyPaused) {
1418
1473
  resumeSceneHistory(useScene)
1419
1474
  drag.historyPaused = false
@@ -1430,37 +1485,23 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
1430
1485
  }
1431
1486
 
1432
1487
  // Legacy compatibility for sessions that still wrote preview state into
1433
- // `useScene` during `apply()`: capture the final state BEFORE the revert
1434
- // so we know what to re-apply post-resume. New sessions should provide a
1435
- // `commit()` hook and preview through live overrides/transforms instead.
1436
- const sceneNodes = useScene.getState().nodes
1437
- const finalUpdates: Array<{ id: AnyNodeId; data: Record<string, unknown> }> = []
1438
- for (const snap of drag.snapshots) {
1439
- const current = sceneNodes[snap.id]
1440
- if (!current) continue
1441
- const data: Record<string, unknown> = {}
1442
- let changed = false
1443
- for (const [key, before] of Object.entries(snap.data)) {
1444
- const after = (current as unknown as Record<string, unknown>)[key]
1445
- if (!deepEqual(before, after)) {
1446
- data[key] = Array.isArray(after) ? [...(after as unknown[])] : after
1447
- changed = true
1448
- }
1449
- }
1450
- if (changed) finalUpdates.push({ id: snap.id, data })
1451
- }
1488
+ // `useScene` during `apply()`: the fields they wrote are re-applied as
1489
+ // one tracked change after the untracked revert. New sessions should
1490
+ // provide a `commit()` hook and preview through live overrides/transforms.
1491
+ const changes = drag.writes.changes()
1492
+ const changed = changes.create.length + changes.update.length + changes.delete.length > 0
1452
1493
 
1453
- if (commitValid && finalUpdates.length > 0) {
1494
+ if (commitValid && changed) {
1454
1495
  // Legacy single-undo dance (mirrors the old 3D move-endpoint-tool):
1455
- // 1. Revert to baseline while history is still paused (untracked).
1496
+ // 1. Take back the session's writes while history is still paused.
1456
1497
  // 2. Resume history.
1457
- // 3. Re-apply the final state — recorded as one tracked change.
1458
- useScene.getState().updateNodes(snapshotsToUpdates(drag.snapshots))
1498
+ // 3. Re-apply them — recorded as one tracked change.
1499
+ drag.writes.revert()
1459
1500
  if (drag.historyPaused) {
1460
1501
  resumeSceneHistory(useScene)
1461
1502
  drag.historyPaused = false
1462
1503
  }
1463
- useScene.getState().updateNodes(finalUpdates)
1504
+ useScene.getState().applyNodeChanges(changes)
1464
1505
  commitPerfAction()
1465
1506
  sfxEmitter.emit('sfx:structure-build')
1466
1507
  } else {
@@ -1468,7 +1509,7 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
1468
1509
  // resume without committing. Also clear any live overrides
1469
1510
  // the session published (no-op when the session writes to
1470
1511
  // scene directly).
1471
- useScene.getState().updateNodes(snapshotsToUpdates(drag.snapshots))
1512
+ drag.writes.revert()
1472
1513
  if (drag.historyPaused) {
1473
1514
  resumeSceneHistory(useScene)
1474
1515
  drag.historyPaused = false
@@ -1504,15 +1545,17 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
1504
1545
  ) {
1505
1546
  return
1506
1547
  }
1507
- drag.session.apply({
1508
- planPoint: drag.lastPlanPoint,
1509
- modifiers: {
1510
- shiftKey: event.shiftKey,
1511
- altKey: event.altKey,
1512
- ctrlKey: event.ctrlKey,
1513
- metaKey: event.metaKey,
1514
- },
1515
- })
1548
+ drag.writes.record(() =>
1549
+ drag.session.apply({
1550
+ planPoint: drag.lastPlanPoint,
1551
+ modifiers: {
1552
+ shiftKey: event.shiftKey,
1553
+ altKey: event.altKey,
1554
+ ctrlKey: event.ctrlKey,
1555
+ metaKey: event.metaKey,
1556
+ },
1557
+ }),
1558
+ )
1516
1559
  }
1517
1560
 
1518
1561
  window.addEventListener('pointermove', onPointerMove)
@@ -1542,6 +1585,48 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
1542
1585
 
1543
1586
  const palette = renderCtx?.palette
1544
1587
 
1588
+ const renderEntries = (pass: FloorplanRenderPass) =>
1589
+ entries.map((entry) => (
1590
+ <FloorplanRegistryEntry
1591
+ activeDragId={handleIdForNode(activeDragId, entry.id)}
1592
+ annotationVisibility={annotationVisibility}
1593
+ floorplanMode={floorplanMode}
1594
+ floorplanVisible={floorplanVisible}
1595
+ geometryCacheRef={geometryCacheRef}
1596
+ hatchPatternId={renderCtx?.hatchPatternId}
1597
+ hoveredHandleId={handleIdForNode(hoveredHandleId, entry.id)}
1598
+ interactiveElevators={interactiveElevators}
1599
+ isMarqueeSelectionActive={isMarqueeSelectionActive}
1600
+ key={`${pass}-${entry.id}`}
1601
+ levelDataCacheRef={levelDataCacheRef}
1602
+ levelNodeIdsByType={floorplanData.levelNodeIdsByType}
1603
+ moving={movingNode?.id === entry.id}
1604
+ node={entry.node}
1605
+ nodeId={entry.id}
1606
+ nodes={nodes}
1607
+ onClickStop={handleClickStop}
1608
+ onEntryPointerDown={handleEntryPointerDown}
1609
+ onGroupMovePointerDown={handleGroupMoveHandlePointerDown}
1610
+ onHandleHoverChange={setHoveredHandleId}
1611
+ onHandleDoubleClick={commitAffordanceAction}
1612
+ onHandlePointerDown={startAffordanceDrag}
1613
+ palette={palette}
1614
+ unitFocus={unitFocus}
1615
+ pass={pass}
1616
+ sceneRotationDeg={sceneRotationDeg}
1617
+ setMovingNode={setMovingNode}
1618
+ setMovingNodeOrigin={setMovingNodeOrigin}
1619
+ siblingEpoch={entry.dependsOnSiblingInputs ? (siblingEpochs.get(entry.id) ?? 0) : 0}
1620
+ unit={unit}
1621
+ metricNotation={metricNotation}
1622
+ wallDimensionReference={effectiveWallDimensionReference}
1623
+ visibilityRootId={
1624
+ entry.visibilityRootId ?? (entry.ctxOverrides ? undefined : (levelId as AnyNodeId))
1625
+ }
1626
+ ctxOverrides={entry.ctxOverrides}
1627
+ />
1628
+ ))
1629
+
1545
1630
  return (
1546
1631
  // The outer wrapper stops `click` events that escape an entry's
1547
1632
  // `onClick={handleClickStop}`. The base+overlay split means
@@ -1567,48 +1652,10 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
1567
1652
  strokes, hatches). Lower-rank kinds (zones) paint first so
1568
1653
  higher-rank kinds (slabs, then walls / items / shelves) layer
1569
1654
  on top in the expected document-order z-stack. */}
1570
- <g className="floorplan-registry-base">
1571
- {entries.map((entry) => (
1572
- <FloorplanRegistryEntry
1573
- activeDragId={handleIdForNode(activeDragId, entry.id)}
1574
- annotationVisibility={annotationVisibility}
1575
- floorplanMode={floorplanMode}
1576
- floorplanVisible={floorplanVisible}
1577
- geometryCacheRef={geometryCacheRef}
1578
- hatchPatternId={renderCtx?.hatchPatternId}
1579
- hoveredHandleId={handleIdForNode(hoveredHandleId, entry.id)}
1580
- interactiveElevators={interactiveElevators}
1581
- isMarqueeSelectionActive={isMarqueeSelectionActive}
1582
- key={`base-${entry.id}`}
1583
- levelDataCacheRef={levelDataCacheRef}
1584
- levelNodeIdsByType={floorplanData.levelNodeIdsByType}
1585
- moving={movingNode?.id === entry.id}
1586
- node={entry.node}
1587
- nodeId={entry.id}
1588
- nodes={nodes}
1589
- onClickStop={handleClickStop}
1590
- onEntryPointerDown={handleEntryPointerDown}
1591
- onGroupMovePointerDown={handleGroupMoveHandlePointerDown}
1592
- onHandleHoverChange={setHoveredHandleId}
1593
- onHandleDoubleClick={commitAffordanceAction}
1594
- onHandlePointerDown={startAffordanceDrag}
1595
- palette={palette}
1596
- unitFocus={unitFocus}
1597
- pass="base"
1598
- sceneRotationDeg={sceneRotationDeg}
1599
- setMovingNode={setMovingNode}
1600
- setMovingNodeOrigin={setMovingNodeOrigin}
1601
- siblingEpoch={entry.dependsOnSiblingInputs ? (siblingEpochs.get(entry.id) ?? 0) : 0}
1602
- unit={unit}
1603
- metricNotation={metricNotation}
1604
- wallDimensionReference={effectiveWallDimensionReference}
1605
- visibilityRootId={
1606
- entry.visibilityRootId ?? (entry.ctxOverrides ? undefined : (levelId as AnyNodeId))
1607
- }
1608
- ctxOverrides={entry.ctxOverrides}
1609
- />
1610
- ))}
1611
- </g>
1655
+ <g className="floorplan-registry-base">{renderEntries('base')}</g>
1656
+ <RoomHighlight2D levelId={levelId} />
1657
+ <RoomLabels2D levelId={levelId} />
1658
+ <FloorRegionControls2D levelId={levelId} />
1612
1659
  {/* Overlay pass — interactive handles (vertex / midpoint / edge /
1613
1660
  move) and labels (text / dimensions). Painted after every base
1614
1661
  entry so polygon-editor chrome on a selected slab stays above
@@ -1616,49 +1663,16 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
1616
1663
  slab + wall geometry sitting on top of the zone. Each overlay
1617
1664
  still routes through the same selection-handling `<g>` so a
1618
1665
  click on a zone's name selects the zone. */}
1619
- <g className="floorplan-registry-overlay">
1620
- {entries.map((entry) => (
1621
- <FloorplanRegistryEntry
1622
- activeDragId={handleIdForNode(activeDragId, entry.id)}
1623
- annotationVisibility={annotationVisibility}
1624
- floorplanMode={floorplanMode}
1625
- floorplanVisible={floorplanVisible}
1626
- geometryCacheRef={geometryCacheRef}
1627
- hatchPatternId={renderCtx?.hatchPatternId}
1628
- hoveredHandleId={handleIdForNode(hoveredHandleId, entry.id)}
1629
- interactiveElevators={interactiveElevators}
1630
- isMarqueeSelectionActive={isMarqueeSelectionActive}
1631
- key={`overlay-${entry.id}`}
1632
- levelDataCacheRef={levelDataCacheRef}
1633
- levelNodeIdsByType={floorplanData.levelNodeIdsByType}
1634
- moving={movingNode?.id === entry.id}
1635
- node={entry.node}
1636
- nodeId={entry.id}
1637
- nodes={nodes}
1638
- onClickStop={handleClickStop}
1639
- onEntryPointerDown={handleEntryPointerDown}
1640
- onGroupMovePointerDown={handleGroupMoveHandlePointerDown}
1641
- onHandleHoverChange={setHoveredHandleId}
1642
- onHandleDoubleClick={commitAffordanceAction}
1643
- onHandlePointerDown={startAffordanceDrag}
1644
- palette={palette}
1645
- unitFocus={unitFocus}
1646
- pass="overlay"
1647
- sceneRotationDeg={sceneRotationDeg}
1648
- setMovingNode={setMovingNode}
1649
- setMovingNodeOrigin={setMovingNodeOrigin}
1650
- siblingEpoch={entry.dependsOnSiblingInputs ? (siblingEpochs.get(entry.id) ?? 0) : 0}
1651
- unit={unit}
1652
- metricNotation={metricNotation}
1653
- wallDimensionReference={effectiveWallDimensionReference}
1654
- visibilityRootId={
1655
- entry.visibilityRootId ?? (entry.ctxOverrides ? undefined : (levelId as AnyNodeId))
1656
- }
1657
- ctxOverrides={entry.ctxOverrides}
1658
- />
1659
- ))}
1660
- </g>
1666
+ <g className="floorplan-registry-overlay">{renderEntries('overlay')}</g>
1667
+ {/* Handles pass — every grabbable handle above every node's overlay,
1668
+ so a neighbour's outline never covers a selected wall's corner. */}
1669
+ <g className="floorplan-registry-handles">{renderEntries('handles')}</g>
1670
+ <RoomHandles2D levelId={levelId} />
1671
+ <RoomHandleDragPreview2D levelId={levelId} />
1661
1672
  <FloorplanAnnotationLayoutResolver active={floorplanVisible} />
1673
+ {/* Click-to-type dimension labels. Portals its HTML input to
1674
+ document.body, so mounting it inside the SVG tree is safe. */}
1675
+ <FloorplanDimensionEditOverlay />
1662
1676
  {/* Dashed group bbox — shows what a group drag carries along while a
1663
1677
  multi-selection exists, rides the live delta mid-drag, and doubles
1664
1678
  as the group's whole-area drag handle. */}
@@ -2021,18 +2035,37 @@ const FloorplanRegistryEntry = memo(function FloorplanRegistryEntry({
2021
2035
  wallDimensionReference,
2022
2036
  visibilityRootId,
2023
2037
  }: FloorplanRegistryEntryProps): React.ReactElement | null {
2038
+ // A node an agent just built is drawn on its construction-reveal turn, as in 3D.
2039
+ const revealWaiting = useRevealWaiting(nodeId)
2040
+ const revealActive = useRevealPlanActive(nodeId)
2041
+ const entryRef = useRef<SVGGElement>(null)
2024
2042
  const selected = useViewer((state) => state.selection.selectedIds.includes(nodeId))
2025
2043
  const highlighted = useViewer((state) => state.previewSelectedIds.includes(nodeId))
2026
- const suppressHandles = useViewer(
2044
+ const multiSelected = useViewer(
2027
2045
  (state) =>
2028
2046
  state.selection.selectedIds.length > 1 && state.selection.selectedIds.includes(nodeId),
2029
2047
  )
2048
+ // A ceiling's vertex / edge handles belong to its Edit ceiling session.
2049
+ const ceilingWithoutSession = useCeilingEditSession(
2050
+ (state) => node.type === 'ceiling' && state.session?.ceilingId !== nodeId,
2051
+ )
2052
+ const suppressHandles = multiSelected || ceilingWithoutSession
2030
2053
  const selectedLevelId = useViewer((state) => state.selection.levelId)
2031
2054
  const selectionProxyId = resolveSelectionProxyId(
2032
2055
  node,
2033
2056
  nodes as Record<string, AnyNode | undefined>,
2034
2057
  )
2035
- const hovered = useViewer((state) => state.hoveredId === selectionProxyId)
2058
+ const phase = useEditor((state) => state.phase)
2059
+ const hoverTargetId =
2060
+ phase === 'site' ? (resolveCanvasBuildingId(node, nodes) ?? selectionProxyId) : selectionProxyId
2061
+ // What a chat chip points at (`pointedIds`) lights as a hover does, as it does in 3D: the node
2062
+ // itself, or what a hover on it would light.
2063
+ const hovered = useViewer(
2064
+ (state) =>
2065
+ state.hoveredId === hoverTargetId ||
2066
+ state.pointedIds.includes(nodeId) ||
2067
+ state.pointedIds.includes(hoverTargetId),
2068
+ )
2036
2069
  const setHoveredId = useViewer((state) => state.setHoveredId)
2037
2070
  const referencedAnnotationRole = useViewer((state) =>
2038
2071
  floorplanEntryReferencedAnnotationRole(node, nodes, new Set(state.selection.selectedIds)),
@@ -2041,7 +2074,7 @@ const FloorplanRegistryEntry = memo(function FloorplanRegistryEntry({
2041
2074
  state.activeRotateNodeId === nodeId ? nodeId : null,
2042
2075
  )
2043
2076
  const groupMoveCursor =
2044
- suppressHandles && classifyParticipant(node, selectedLevelId, nodes) !== null
2077
+ multiSelected && classifyParticipant(node, selectedLevelId, nodes) !== null
2045
2078
  const live = useLiveTransforms((s) => (floorplanVisible ? s.transforms.get(nodeId) : undefined))
2046
2079
  const liveOverride = useLiveNodeOverrides((s) =>
2047
2080
  floorplanVisible ? s.overrides.get(nodeId) : undefined,
@@ -2066,25 +2099,55 @@ const FloorplanRegistryEntry = memo(function FloorplanRegistryEntry({
2066
2099
 
2067
2100
  // Mirror the sidebar tree nodes' hover wiring — `useViewer.hoveredId` drives
2068
2101
  // the highlight halo in 3D as well as registry floor-plan hover strokes.
2069
- const handlePointerEnter = useCallback(() => {
2070
- const currentNode = useScene.getState().nodes[nodeId]
2071
- setHoveredId(
2072
- currentNode
2073
- ? resolveSelectionProxyId(
2074
- currentNode,
2075
- useScene.getState().nodes as Record<string, AnyNode | undefined>,
2076
- )
2077
- : nodeId,
2078
- )
2079
- }, [nodeId, setHoveredId])
2102
+ const handlePointerEnter = useCallback(
2103
+ (event: ReactPointerEvent<SVGGElement>) => {
2104
+ if (floorplanEntryYieldsToToolNow() || !isIdle(useInteractionScope.getState().scope)) return
2105
+ const currentNode = useScene.getState().nodes[nodeId]
2106
+ if (currentNode && useEditor.getState().phase === 'site') {
2107
+ const buildingId = resolveCanvasBuildingId(currentNode, useScene.getState().nodes)
2108
+ if (buildingId) {
2109
+ setHoveredId(buildingId)
2110
+ return
2111
+ }
2112
+ }
2113
+ if (currentNode?.type === 'zone' && roomPickingEnabled()) {
2114
+ hoverZoneArea(nodeId, selectionModifiersFromEvent(event))
2115
+ return
2116
+ }
2117
+ const point = clientToPlan(event.clientX, event.clientY)
2118
+ if (
2119
+ point &&
2120
+ roomPickingEnabled() &&
2121
+ hoverRoomFromHit(
2122
+ resolvePlanRoomHit(nodeId, point),
2123
+ selectionModifiersFromEvent(event),
2124
+ nodeId,
2125
+ )
2126
+ )
2127
+ return
2128
+ setHoveredId(
2129
+ currentNode
2130
+ ? resolveSelectionProxyId(
2131
+ currentNode,
2132
+ useScene.getState().nodes as Record<string, AnyNode | undefined>,
2133
+ )
2134
+ : nodeId,
2135
+ )
2136
+ },
2137
+ [nodeId, setHoveredId],
2138
+ )
2080
2139
 
2081
2140
  const handlePointerLeave = useCallback(() => {
2141
+ useEditor.getState().setHoveredRoom(null)
2082
2142
  const node = useScene.getState().nodes[nodeId]
2083
2143
  const targetId = node
2084
- ? resolveSelectionProxyId(
2085
- node,
2086
- useScene.getState().nodes as Record<string, AnyNode | undefined>,
2087
- )
2144
+ ? useEditor.getState().phase === 'site'
2145
+ ? (resolveCanvasBuildingId(node, useScene.getState().nodes) ??
2146
+ resolveSelectionProxyId(node, useScene.getState().nodes))
2147
+ : resolveSelectionProxyId(
2148
+ node,
2149
+ useScene.getState().nodes as Record<string, AnyNode | undefined>,
2150
+ )
2088
2151
  : nodeId
2089
2152
  if (useViewer.getState().hoveredId === targetId) setHoveredId(null)
2090
2153
  }, [nodeId, setHoveredId])
@@ -2160,7 +2223,7 @@ const FloorplanRegistryEntry = memo(function FloorplanRegistryEntry({
2160
2223
  wallDimensionReference,
2161
2224
  visibilityRootId,
2162
2225
  })
2163
- const rawGeometry = cacheEntry ? (pass === 'base' ? cacheEntry.base : cacheEntry.overlay) : null
2226
+ const rawGeometry = cacheEntry ? cacheEntry[pass] : null
2164
2227
  const visibleGeometry = rawGeometry
2165
2228
  ? filterFloorplanAnnotationGeometry(rawGeometry, presentationVisibility)
2166
2229
  : null
@@ -2168,10 +2231,11 @@ const FloorplanRegistryEntry = memo(function FloorplanRegistryEntry({
2168
2231
  // dimension chrome (all of which live in the overlay pass) while keeping
2169
2232
  // its highlighted body geometry.
2170
2233
  const geometry =
2171
- visibleGeometry && suppressHandles && pass === 'overlay'
2234
+ visibleGeometry && suppressHandles && pass !== 'base'
2172
2235
  ? stripHandleChrome(visibleGeometry)
2173
2236
  : visibleGeometry
2174
- if (!geometry) return null
2237
+ useFloorplanReveal(nodeId, entryRef, cacheEntry, revealWaiting, revealActive)
2238
+ if (revealWaiting || !geometry) return null
2175
2239
 
2176
2240
  const entryClick = isMarqueeSelectionActive ? undefined : onClickStop
2177
2241
  const entryPointerDown = isMarqueeSelectionActive ? undefined : handlePointerDown
@@ -2180,9 +2244,11 @@ const FloorplanRegistryEntry = memo(function FloorplanRegistryEntry({
2180
2244
  <g
2181
2245
  className="floorplan-registry-entry"
2182
2246
  data-node-id={nodeId}
2247
+ ref={entryRef}
2183
2248
  onClick={entryClick}
2184
2249
  onPointerDown={entryPointerDown}
2185
2250
  onPointerEnter={handlePointerEnter}
2251
+ onPointerMove={handlePointerEnter}
2186
2252
  onPointerLeave={handlePointerLeave}
2187
2253
  style={groupMoveCursor ? MOVE_CURSOR_STYLE : POINTER_CURSOR_STYLE}
2188
2254
  >
@@ -2401,6 +2467,7 @@ export function buildFloorplanEntryGeometry({
2401
2467
  selected,
2402
2468
  unit,
2403
2469
  metricNotation,
2470
+ roomLabelOverlay: true,
2404
2471
  wallDimensionReference,
2405
2472
  highlighted,
2406
2473
  hovered,
@@ -2426,6 +2493,7 @@ export function buildFloorplanEntryGeometry({
2426
2493
  automaticDimensions,
2427
2494
  metricNotation,
2428
2495
  purpose: 'edit',
2496
+ roomLabelOverlay: true,
2429
2497
  wallDimensionReference,
2430
2498
  }),
2431
2499
  viewState: palette
@@ -2467,10 +2535,13 @@ export function buildFloorplanEntryGeometry({
2467
2535
  transform: ctxOverrides.outputTransform,
2468
2536
  }
2469
2537
  : unprojectedGeometry
2470
- const { base, overlay } = geometry
2538
+ const { base, overlay: overlayWithHandles } = geometry
2471
2539
  ? splitFloorplanOverlay(geometry)
2472
2540
  : { base: null, overlay: null }
2473
- const entry: CacheEntry = { deps, base, overlay, node: effectiveNode }
2541
+ const { rest: overlay, handles } = overlayWithHandles
2542
+ ? splitFloorplanHandles(overlayWithHandles)
2543
+ : { rest: null, handles: null }
2544
+ const entry: CacheEntry = { deps, base, overlay, handles, node: effectiveNode }
2474
2545
  geometryCache.set(nodeId, entry)
2475
2546
  return entry
2476
2547
  }
@@ -2619,6 +2690,7 @@ export const InteractiveGeometry = memo(function InteractiveGeometry({
2619
2690
  }
2620
2691
  case 'endpoint-handle': {
2621
2692
  if (!palette) return <></>
2693
+ const endpointUnitsPerPixel = g.screenSized ? unitsPerPixel : handleUnitsPerPixel
2622
2694
  const handleId = makeHandleId(nodeId, g.payload)
2623
2695
  const doubleClickAffordance = floorplanHandleDoubleClickAffordance(g)
2624
2696
  const isHovered = hoveredHandleId === handleId
@@ -2644,10 +2716,10 @@ export const InteractiveGeometry = memo(function InteractiveGeometry({
2644
2716
  : palette.endpointHandleFill
2645
2717
  const outerRadius =
2646
2718
  (isActive ? ENDPOINT_HANDLE_ACTIVE_RADIUS_PX : ENDPOINT_HANDLE_SELECTED_RADIUS_PX) *
2647
- handleUnitsPerPixel
2719
+ endpointUnitsPerPixel
2648
2720
  const dotRadius =
2649
2721
  (isActive ? ENDPOINT_HANDLE_ACTIVE_DOT_RADIUS_PX : ENDPOINT_HANDLE_DOT_RADIUS_PX) *
2650
- handleUnitsPerPixel
2722
+ endpointUnitsPerPixel
2651
2723
  return (
2652
2724
  <g
2653
2725
  key={keyHint}
@@ -2663,7 +2735,7 @@ export const InteractiveGeometry = memo(function InteractiveGeometry({
2663
2735
  r={outerRadius}
2664
2736
  stroke={hoverStroke}
2665
2737
  strokeOpacity={isActive ? 0.24 : 0.16}
2666
- strokeWidth={ENDPOINT_HOVER_GLOW_STROKE_WIDTH_PX * handleUnitsPerPixel}
2738
+ strokeWidth={ENDPOINT_HOVER_GLOW_STROKE_WIDTH_PX * endpointUnitsPerPixel}
2667
2739
  style={{ opacity: isHovered || isActive ? 1 : 0, transition: HOVER_TRANSITION }}
2668
2740
  vectorEffect="non-scaling-stroke"
2669
2741
  />
@@ -2675,7 +2747,7 @@ export const InteractiveGeometry = memo(function InteractiveGeometry({
2675
2747
  r={outerRadius}
2676
2748
  stroke={hoverStroke}
2677
2749
  strokeOpacity={isActive ? 0.72 : 0.52}
2678
- strokeWidth={ENDPOINT_HOVER_RING_STROKE_WIDTH_PX * handleUnitsPerPixel}
2750
+ strokeWidth={ENDPOINT_HOVER_RING_STROKE_WIDTH_PX * endpointUnitsPerPixel}
2679
2751
  style={{ opacity: isHovered || isActive ? 1 : 0, transition: HOVER_TRANSITION }}
2680
2752
  vectorEffect="non-scaling-stroke"
2681
2753
  />
@@ -2923,12 +2995,13 @@ export const InteractiveGeometry = memo(function InteractiveGeometry({
2923
2995
  // body (matches the 3D `HANDLE_OFFSET`).
2924
2996
  const bi = 0.03 // base inset
2925
2997
  const arrowD = `M ${bi},${-sh} L ${bi + sl},${-sh} L ${bi + sl},${-hh} L ${bi + sl + hl},0 L ${bi + sl},${hh} L ${bi + sl},${sh} L ${bi},${sh} Z`
2926
- // Indigo palette to match the 3D `WallMoveSideHandles` arrows
2998
+ // Indigo palette to match the 3D wall push arrows
2927
2999
  // (`ARROW_COLOR` / `ARROW_HOVER_COLOR`) and the corner-sphere
2928
3000
  // accent in `floating-action-menu.tsx`.
2929
3001
  const fill = isHovered ? '#a5b4fc' : '#8381ed'
2930
3002
  const angleDeg = (g.angle * 180) / Math.PI
2931
- const cursor = g.affordance ? 'ew-resize' : 'move'
3003
+ const cursor =
3004
+ g.affordance === WALL_PUSH_AFFORDANCE ? 'grab' : g.affordance ? 'ew-resize' : 'move'
2932
3005
  const affordance = g.affordance
2933
3006
  const payload = g.payload
2934
3007
  // No hover-grow: a scaling transform would enlarge the hit area
@@ -3515,6 +3588,42 @@ export function splitFloorplanOverlay(g: FloorplanGeometry): {
3515
3588
  return { base: g, overlay: null }
3516
3589
  }
3517
3590
 
3591
+ /** The kinds a pointer grabs: they paint and hit-test above every node's outline. */
3592
+ const HANDLE_KINDS = new Set<FloorplanGeometry['kind']>([
3593
+ 'endpoint-handle',
3594
+ 'midpoint-handle',
3595
+ 'edge-handle',
3596
+ 'move-handle',
3597
+ 'move-arrow',
3598
+ 'rotate-arrow',
3599
+ ])
3600
+
3601
+ /**
3602
+ * Splits an overlay tree into its grabbable handles and the rest (labels,
3603
+ * dimensions, outlines). The handles render in a pass of their own after
3604
+ * every node's overlay: an outline painted later (a neighbouring wall's body)
3605
+ * would otherwise cover a selected wall's corner handle and take its press —
3606
+ * the 2D twin of the 3D handle hit priority.
3607
+ */
3608
+ export function splitFloorplanHandles(g: FloorplanGeometry): {
3609
+ rest: FloorplanGeometry | null
3610
+ handles: FloorplanGeometry | null
3611
+ } {
3612
+ if (HANDLE_KINDS.has(g.kind)) return { rest: null, handles: g }
3613
+ if (g.kind !== 'group') return { rest: g, handles: null }
3614
+ const rest: FloorplanGeometry[] = []
3615
+ const handles: FloorplanGeometry[] = []
3616
+ for (const child of g.children) {
3617
+ const split = splitFloorplanHandles(child)
3618
+ if (split.rest) rest.push(split.rest)
3619
+ if (split.handles) handles.push(split.handles)
3620
+ }
3621
+ return {
3622
+ rest: rest.length ? { ...g, children: rest } : null,
3623
+ handles: handles.length ? { ...g, children: handles } : null,
3624
+ }
3625
+ }
3626
+
3518
3627
  /**
3519
3628
  * Per-node edit chrome hidden while a multi-selection is active: the group is
3520
3629
  * manipulated as one rigid piece (drag to move, R/T to rotate), so individual
@@ -3703,29 +3812,6 @@ function depsValueEqual(a: unknown, b: unknown): boolean {
3703
3812
  */
3704
3813
  export { floorplanLayerRank }
3705
3814
 
3706
- function deepEqual(a: unknown, b: unknown): boolean {
3707
- if (a === b) return true
3708
- if (Array.isArray(a) && Array.isArray(b)) {
3709
- if (a.length !== b.length) return false
3710
- for (let i = 0; i < a.length; i++) {
3711
- if (!deepEqual(a[i], b[i])) return false
3712
- }
3713
- return true
3714
- }
3715
- if (typeof a === 'object' && typeof b === 'object' && a !== null && b !== null) {
3716
- const aKeys = Object.keys(a as Record<string, unknown>)
3717
- const bKeys = Object.keys(b as Record<string, unknown>)
3718
- if (aKeys.length !== bKeys.length) return false
3719
- for (const key of aKeys) {
3720
- if (!deepEqual((a as Record<string, unknown>)[key], (b as Record<string, unknown>)[key])) {
3721
- return false
3722
- }
3723
- }
3724
- return true
3725
- }
3726
- return false
3727
- }
3728
-
3729
3815
  const ROTATION_WEDGE_COLOR = '#8381ed'
3730
3816
  const ROTATION_WEDGE_SEGMENTS = 48
3731
3817