@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
@@ -0,0 +1,128 @@
1
+ 'use client'
2
+
3
+ import { type Point, useScene } from '@pascal-app/core'
4
+ import { useEffect, useMemo } from 'react'
5
+ import {
6
+ BufferGeometry,
7
+ DoubleSide,
8
+ Float32BufferAttribute,
9
+ Path,
10
+ Shape,
11
+ ShapeGeometry,
12
+ Vector2,
13
+ } from 'three'
14
+ import { MeshBasicNodeMaterial } from 'three/webgpu'
15
+ import { getRoomSelectionIndex } from '../../hooks/use-selected-room'
16
+ import { EDITOR_LAYER } from '../../lib/constants'
17
+
18
+ const HIGHLIGHT_COLOR = '#818cf8'
19
+ /** Half width (m) of the outline ribbon. */
20
+ const OUTLINE_HALF_WIDTH = 0.025
21
+ /** Above the plane it marks, so the floor under it never z-fights. */
22
+ const LIFT = 0.004
23
+
24
+ const fillMaterial = new MeshBasicNodeMaterial({
25
+ color: HIGHLIGHT_COLOR,
26
+ depthTest: false,
27
+ depthWrite: false,
28
+ opacity: 0.2,
29
+ side: DoubleSide,
30
+ transparent: true,
31
+ })
32
+ const edgeMaterial = new MeshBasicNodeMaterial({
33
+ color: HIGHLIGHT_COLOR,
34
+ depthTest: false,
35
+ depthWrite: false,
36
+ opacity: 0.95,
37
+ side: DoubleSide,
38
+ transparent: true,
39
+ })
40
+ const noRaycast = () => {}
41
+
42
+ /** Flat quads along each ring edge: a line WebGPU draws at a real width. */
43
+ function outlineRibbon(rings: readonly (readonly Point[])[], halfWidth: number) {
44
+ const positions: number[] = []
45
+ for (const ring of rings)
46
+ for (let i = 0; i < ring.length; i++) {
47
+ const a = ring[i]!
48
+ const b = ring[(i + 1) % ring.length]!
49
+ const length = Math.hypot(b[0] - a[0], b[1] - a[1])
50
+ if (length < 1e-9) continue
51
+ const nx = (-(b[1] - a[1]) / length) * halfWidth
52
+ const nz = ((b[0] - a[0]) / length) * halfWidth
53
+ const [p0, p1, p2, p3] = [
54
+ [a[0] + nx, a[1] + nz],
55
+ [b[0] + nx, b[1] + nz],
56
+ [b[0] - nx, b[1] - nz],
57
+ [a[0] - nx, a[1] - nz],
58
+ ] as const
59
+ for (const [x, z] of [p0, p1, p2, p0, p2, p3]) positions.push(x, 0, z)
60
+ }
61
+ return new BufferGeometry().setAttribute('position', new Float32BufferAttribute(positions, 3))
62
+ }
63
+
64
+ /**
65
+ * A room's clear floor drawn on top of every wall (depth test off): a subtle
66
+ * fill and a strong outline, holes included, at `elevation` in the level
67
+ * frame — the plane a draft in that room lands on (Divide on the floor, Add
68
+ * mezzanine on the mezzanine's plane), so the user always sees whether the
69
+ * pointer is inside the room without any wall being cut away.
70
+ */
71
+ export function RoomFloorHighlight3D({
72
+ levelId,
73
+ zoneId,
74
+ elevation,
75
+ renderOrder = 998,
76
+ }: {
77
+ levelId: string
78
+ zoneId: string
79
+ elevation: number
80
+ /** The fill's order; the outline draws one above it. */
81
+ renderOrder?: number
82
+ }) {
83
+ const nodes = useScene((s) => s.nodes)
84
+ const geometry = useMemo(() => {
85
+ const record = getRoomSelectionIndex(levelId)
86
+ .update(nodes)
87
+ .find((room) => room.zoneId === zoneId)
88
+ if (!record) return null
89
+ const shapes = record.clearPolygon.map(({ outer, holes }) => {
90
+ const shape = new Shape(outer.map(([x, z]) => new Vector2(x, z)))
91
+ shape.holes = holes.map((hole) => new Path(hole.map(([x, z]) => new Vector2(x, z))))
92
+ return shape
93
+ })
94
+ return {
95
+ fill: new ShapeGeometry(shapes).rotateX(Math.PI / 2),
96
+ outline: outlineRibbon(
97
+ record.clearPolygon.flatMap(({ outer, holes }) => [outer, ...holes]),
98
+ OUTLINE_HALF_WIDTH,
99
+ ),
100
+ }
101
+ }, [nodes, levelId, zoneId])
102
+ useEffect(
103
+ () => () => {
104
+ geometry?.fill.dispose()
105
+ geometry?.outline.dispose()
106
+ },
107
+ [geometry],
108
+ )
109
+ if (!geometry) return null
110
+ return (
111
+ <group position-y={elevation + LIFT}>
112
+ <mesh
113
+ geometry={geometry.fill}
114
+ layers={EDITOR_LAYER}
115
+ material={fillMaterial}
116
+ raycast={noRaycast}
117
+ renderOrder={renderOrder}
118
+ />
119
+ <mesh
120
+ geometry={geometry.outline}
121
+ layers={EDITOR_LAYER}
122
+ material={edgeMaterial}
123
+ raycast={noRaycast}
124
+ renderOrder={renderOrder + 1}
125
+ />
126
+ </group>
127
+ )
128
+ }
@@ -0,0 +1,354 @@
1
+ 'use client'
2
+
3
+ import { type FloorplanGeometry, type Point, roomDrawnFloor, useScene } from '@pascal-app/core'
4
+ import { useViewer } from '@pascal-app/viewer'
5
+ import { Minus, Plus } from 'lucide-react'
6
+ import { type PointerEvent as ReactPointerEvent, useMemo, useState } from 'react'
7
+ import { useSelectedRoom } from '../../hooks/use-selected-room'
8
+ import { formatLinearMeasurement, MEASUREMENT_FLOORPLAN_COLOR } from '../../lib/measurements'
9
+ import {
10
+ commitRoomFloorElevation,
11
+ roomRelativeFloorHeight,
12
+ stepRoomFloorElevation,
13
+ } from '../../lib/room-construction-commands'
14
+ import {
15
+ type MezzanineEdgeHandle,
16
+ mezzanineEdgeHandles,
17
+ type RoomHandleDrag,
18
+ roomPushHandles,
19
+ runFloorplanMezzanineEdge,
20
+ runFloorplanRoomPush,
21
+ useRoomHandleDrag,
22
+ type WallPushHandle,
23
+ } from '../../lib/room-handle-drag'
24
+ import type { RoomDimension } from '../../lib/room-push-dimensions'
25
+ import useEditor from '../../store/use-editor'
26
+ import useInteractionScope from '../../store/use-interaction-scope'
27
+ import { useFloorplanRender } from '../editor-2d/floorplan-render-context'
28
+ import { FloorplanGeometryRenderer } from '../editor-2d/renderers/floorplan-geometry-renderer'
29
+ import { suppressBoxSelectForPointer } from '../tools/select/box-select-state'
30
+ import { ActionMenuButton } from './action-menu-button'
31
+ import { formatMeasurement } from './measurement-pill'
32
+
33
+ /**
34
+ * The selected room while its controls may show: select mode, nothing in
35
+ * progress, no element drilled into, the scene editable.
36
+ */
37
+ export function useRoomControls() {
38
+ const room = useSelectedRoom()
39
+ const enabled = useEditor((s) => s.phase !== 'site' && s.mode === 'select')
40
+ const idle = useInteractionScope((s) => s.scope.kind === 'idle')
41
+ const readOnly = useScene((s) => s.readOnly)
42
+ const sole = useViewer((s) => s.selection.selectedIds.length === 0)
43
+ return enabled && idle && sole && !readOnly ? room : null
44
+ }
45
+
46
+ /** A room floor's step on the plan's Floor height stepper, m. */
47
+ export const FLOOR_HEIGHT_STEP = 0.05
48
+
49
+ function signed(text: string, value: number) {
50
+ return `${value > 0.0005 ? '+' : value < -0.0005 ? '−' : ''}${text}`
51
+ }
52
+
53
+ /**
54
+ * The plan has no vertical drag: the room pill carries the floor's height
55
+ * above the floor it is measured from, in 5 cm steps, shown even at 0 — the
56
+ * 3D elevation handle's clamp and commit, one undo step a click.
57
+ */
58
+ export function RoomFloorHeightStepper({ zoneId }: { zoneId: string }) {
59
+ const nodes = useScene((s) => s.nodes)
60
+ const unit = useViewer((s) => s.unit)
61
+ const metricNotation = useViewer((s) => s.metricNotation)
62
+ // A drawn-slab floor's height is the slab's: the room has no height to step.
63
+ if (roomDrawnFloor(nodes, zoneId)) return null
64
+ const height = roomRelativeFloorHeight(nodes, zoneId)
65
+ const down = stepRoomFloorElevation(nodes, zoneId, -FLOOR_HEIGHT_STEP)
66
+ const up = stepRoomFloorElevation(nodes, zoneId, FLOOR_HEIGHT_STEP)
67
+ const step = (next: number | null) => (event: { stopPropagation: () => void }) => {
68
+ event.stopPropagation()
69
+ if (next !== null)
70
+ commitRoomFloorElevation(zoneId, next, (meters) =>
71
+ formatLinearMeasurement(meters, unit, metricNotation),
72
+ )
73
+ }
74
+ return (
75
+ <div
76
+ className="pointer-events-auto flex items-center gap-1 rounded-lg border border-border bg-background/95 py-1 pr-1 pl-3 shadow-xl backdrop-blur-md"
77
+ data-room-floor-height
78
+ onPointerDown={(event) => event.stopPropagation()}
79
+ onPointerUp={(event) => event.stopPropagation()}
80
+ >
81
+ <span className="mr-1 text-muted-foreground text-xs">Floor height</span>
82
+ <ActionMenuButton
83
+ disabled={down === null}
84
+ disabledReason="The floor can't go lower here"
85
+ label="Lower the floor"
86
+ onClick={step(down)}
87
+ >
88
+ <Minus className="h-4 w-4" />
89
+ </ActionMenuButton>
90
+ <span
91
+ className="min-w-14 text-center font-medium text-foreground text-xs tabular-nums"
92
+ data-room-floor-height-value
93
+ >
94
+ {signed(formatMeasurement(Math.abs(height), unit, metricNotation), height)}
95
+ </span>
96
+ <ActionMenuButton
97
+ disabled={up === null}
98
+ disabledReason="The floor can't go higher here"
99
+ label="Raise the floor"
100
+ onClick={step(up)}
101
+ >
102
+ <Plus className="h-4 w-4" />
103
+ </ActionMenuButton>
104
+ </div>
105
+ )
106
+ }
107
+
108
+ // The plan's push arrow: the wall arrows' shape and indigo, in plan metres.
109
+ const SHAFT = 0.1
110
+ const HEAD = 0.12
111
+ const SHAFT_HALF = 0.04
112
+ const HEAD_HALF = 0.1
113
+ const INSET = 0.03
114
+ const ARROW_PATH = `M ${INSET},${-SHAFT_HALF} L ${INSET + SHAFT},${-SHAFT_HALF} L ${INSET + SHAFT},${-HEAD_HALF} L ${INSET + SHAFT + HEAD},0 L ${INSET + SHAFT},${HEAD_HALF} L ${INSET + SHAFT},${SHAFT_HALF} L ${INSET},${SHAFT_HALF} Z`
115
+ const ARROW_FILL = '#8381ed'
116
+ const ARROW_HOVER_FILL = '#a5b4fc'
117
+
118
+ function PlanArrow({
119
+ position,
120
+ outward,
121
+ kind,
122
+ onPointerDown,
123
+ }: {
124
+ position: Point
125
+ outward: Point
126
+ kind: 'room-push' | 'mezzanine-edge'
127
+ onPointerDown: (event: ReactPointerEvent<SVGPathElement>) => void
128
+ }) {
129
+ const [hover, setHover] = useState(false)
130
+ const angle = (Math.atan2(outward[1], outward[0]) * 180) / Math.PI
131
+ return (
132
+ <g
133
+ data-room-arrow={kind}
134
+ onClick={(event) => event.stopPropagation()}
135
+ transform={`translate(${position[0]} ${position[1]}) rotate(${angle})`}
136
+ >
137
+ <path d={ARROW_PATH} fill={hover ? ARROW_HOVER_FILL : ARROW_FILL} pointerEvents="none" />
138
+ <path
139
+ d={ARROW_PATH}
140
+ fill="transparent"
141
+ onPointerDown={onPointerDown}
142
+ onPointerEnter={() => setHover(true)}
143
+ onPointerLeave={() => setHover(false)}
144
+ pointerEvents="fill"
145
+ style={{ cursor: 'grab' }}
146
+ />
147
+ </g>
148
+ )
149
+ }
150
+
151
+ function startArrow(event: ReactPointerEvent<SVGPathElement>, run: () => unknown) {
152
+ if (event.button !== 0) return
153
+ event.preventDefault()
154
+ event.stopPropagation()
155
+ suppressBoxSelectForPointer(event)
156
+ run()
157
+ }
158
+
159
+ /**
160
+ * The selected room's arrows on the plan: one push/pull chevron per boundary
161
+ * span (on the far face, along the normal the handle supplies) and, for a
162
+ * mezzanine, one per plate edge. Mounted after every node's handles so a
163
+ * wall outline never takes the press. The drags are the 3D arrows' own.
164
+ */
165
+ export function RoomHandles2D({ levelId }: { levelId: string | null }) {
166
+ const room = useRoomControls()
167
+ const nodes = useScene((s) => s.nodes)
168
+ const onLevel = !!room && room.key.levelId === levelId
169
+ const handles = useMemo(
170
+ () => (onLevel && room ? roomPushHandles(nodes, room.spans) : []),
171
+ [nodes, onLevel, room],
172
+ )
173
+ const edges = useMemo(
174
+ () => (onLevel && room?.mezzanine ? mezzanineEdgeHandles(nodes, room.zoneId) : []),
175
+ [nodes, onLevel, room],
176
+ )
177
+ if (!(onLevel && room)) return null
178
+ const push = (handle: WallPushHandle) => (event: ReactPointerEvent<SVGPathElement>) =>
179
+ startArrow(event, () =>
180
+ runFloorplanRoomPush({
181
+ handle,
182
+ levelId: room.key.levelId,
183
+ zoneId: room.zoneId,
184
+ clientX: event.clientX,
185
+ clientY: event.clientY,
186
+ }),
187
+ )
188
+ const edge = (handle: MezzanineEdgeHandle) => (event: ReactPointerEvent<SVGPathElement>) =>
189
+ startArrow(event, () =>
190
+ runFloorplanMezzanineEdge({
191
+ handle,
192
+ levelId: room.key.levelId,
193
+ zoneId: room.zoneId,
194
+ clientX: event.clientX,
195
+ clientY: event.clientY,
196
+ }),
197
+ )
198
+ return (
199
+ <g data-room-handles-2d>
200
+ {handles.map((handle) => (
201
+ <PlanArrow
202
+ key={handle.key}
203
+ kind="room-push"
204
+ onPointerDown={push(handle)}
205
+ outward={handle.outward}
206
+ position={handle.position}
207
+ />
208
+ ))}
209
+ {edges.map((handle) => (
210
+ <PlanArrow
211
+ key={handle.key}
212
+ kind="mezzanine-edge"
213
+ onPointerDown={edge(handle)}
214
+ outward={handle.outward}
215
+ position={handle.position}
216
+ />
217
+ ))}
218
+ </g>
219
+ )
220
+ }
221
+
222
+ const READOUT_HEIGHT_PX = 24
223
+ const READOUT_CHAR_PX = 6.6
224
+ const READOUT_PAD_PX = 12
225
+
226
+ /** A drag's readout pill on the plan, upright and screen-sized, at `at`. */
227
+ function PlanReadout({ at, text, refused }: { at: Point; text: string; refused: boolean }) {
228
+ const context = useFloorplanRender()
229
+ const upp = context?.unitsPerPixel ?? 0.01
230
+ const width = text.length * READOUT_CHAR_PX + READOUT_PAD_PX * 2
231
+ return (
232
+ <g
233
+ data-room-drag-readout
234
+ pointerEvents="none"
235
+ transform={`translate(${at[0]} ${at[1]}) rotate(${-(context?.sceneRotationDeg ?? 0)}) scale(${upp})`}
236
+ >
237
+ <rect
238
+ height={READOUT_HEIGHT_PX}
239
+ rx={READOUT_HEIGHT_PX / 2}
240
+ style={{ fill: 'var(--background)', fillOpacity: 0.92, stroke: 'var(--border)' }}
241
+ width={width}
242
+ x={-width / 2}
243
+ y={-READOUT_HEIGHT_PX - 14}
244
+ />
245
+ <text
246
+ dominantBaseline="central"
247
+ fontSize={12}
248
+ style={{ fill: refused ? '#ef4444' : 'var(--foreground)' }}
249
+ fontWeight={500}
250
+ textAnchor="middle"
251
+ y={-READOUT_HEIGHT_PX / 2 - 14}
252
+ >
253
+ {text}
254
+ </text>
255
+ </g>
256
+ )
257
+ }
258
+
259
+ /**
260
+ * A push's live room widths on the plan, drawn as the measurement tool draws a
261
+ * distance: indigo line, end dots, outlined label along the line.
262
+ */
263
+ function PlanRoomDimensions({ dimensions }: { dimensions: RoomDimension[] }) {
264
+ const context = useFloorplanRender()
265
+ const unit = useViewer((s) => s.unit)
266
+ const metricNotation = useViewer((s) => s.metricNotation)
267
+ const geometry = useMemo<FloorplanGeometry>(
268
+ () => ({
269
+ kind: 'group',
270
+ children: dimensions.flatMap(({ from, to, distance }): FloorplanGeometry[] => [
271
+ {
272
+ kind: 'line',
273
+ x1: from[0],
274
+ y1: from[1],
275
+ x2: to[0],
276
+ y2: to[1],
277
+ stroke: MEASUREMENT_FLOORPLAN_COLOR,
278
+ strokeWidth: 2,
279
+ strokeLinecap: 'round',
280
+ vectorEffect: 'non-scaling-stroke',
281
+ },
282
+ ...[from, to].map(
283
+ ([cx, cy]): FloorplanGeometry => ({
284
+ kind: 'circle',
285
+ cx,
286
+ cy,
287
+ r: 0.045,
288
+ fill: MEASUREMENT_FLOORPLAN_COLOR,
289
+ }),
290
+ ),
291
+ {
292
+ kind: 'dimension-label',
293
+ appearance: 'outlined',
294
+ cx: (from[0] + to[0]) / 2,
295
+ cy: (from[1] + to[1]) / 2,
296
+ text: formatLinearMeasurement(distance, unit, metricNotation),
297
+ angle: Math.atan2(to[1] - from[1], to[0] - from[0]),
298
+ offsetPx: 14,
299
+ },
300
+ ]),
301
+ }),
302
+ [dimensions, unit, metricNotation],
303
+ )
304
+ if (!dimensions.length) return null
305
+ return (
306
+ <g data-room-dimensions-2d>
307
+ <FloorplanGeometryRenderer
308
+ geometry={geometry}
309
+ pointerEventsOverride="none"
310
+ sceneRotationDeg={context?.sceneRotationDeg ?? 0}
311
+ screenUnitsPerPixel={context?.unitsPerPixel}
312
+ />
313
+ </g>
314
+ )
315
+ }
316
+
317
+ type PlanDrag = Extract<RoomHandleDrag, { kind: 'push' | 'mezzanine-edge' }>
318
+
319
+ /**
320
+ * The room drag in flight on the plan: the signed distance or the refusal at
321
+ * the arrow's new spot, and a mezzanine's outline as the push would leave it —
322
+ * the plan twin of `RoomHandleDragPreview3D` (the pushed walls and floors
323
+ * themselves draw from the live overrides and the wall ghosts).
324
+ */
325
+ export function RoomHandleDragPreview2D({ levelId }: { levelId: string | null }) {
326
+ const drag = useRoomHandleDrag((s) => s.drag)
327
+ const unit = useViewer((s) => s.unit)
328
+ const metricNotation = useViewer((s) => s.metricNotation)
329
+ if (!drag || drag.levelId !== levelId || drag.kind === 'elevation') return null
330
+ const plan = drag as PlanDrag
331
+ const at: Point = [
332
+ plan.anchor[0] + plan.outward[0] * plan.distance,
333
+ plan.anchor[1] + plan.outward[1] * plan.distance,
334
+ ]
335
+ const text =
336
+ plan.message ??
337
+ signed(formatMeasurement(Math.abs(plan.distance), unit, metricNotation), plan.distance)
338
+ return (
339
+ <g data-room-drag-preview-2d pointerEvents="none">
340
+ {plan.kind === 'mezzanine-edge' && plan.outline.length >= 3 && (
341
+ <polygon
342
+ fill={plan.message ? '#ef4444' : ARROW_FILL}
343
+ fillOpacity={0.2}
344
+ points={plan.outline.map(([x, z]) => `${x},${z}`).join(' ')}
345
+ stroke={plan.message ? '#ef4444' : ARROW_FILL}
346
+ strokeWidth={1.5}
347
+ vectorEffect="non-scaling-stroke"
348
+ />
349
+ )}
350
+ <PlanRoomDimensions dimensions={plan.dimensions} />
351
+ <PlanReadout at={at} refused={!!plan.message} text={text} />
352
+ </g>
353
+ )
354
+ }