@pascal-app/editor 1.0.1 → 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 (498) hide show
  1. package/package.json +16 -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 +221 -128
  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 +37 -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 +536 -487
  27. package/src/components/editor/footprint-height-handle.tsx +204 -0
  28. package/src/components/editor/grid.tsx +31 -7
  29. package/src/components/editor/group-actions.ts +59 -11
  30. package/src/components/editor/group-floating-action-menu.tsx +19 -9
  31. package/src/components/editor/group-move-3d.ts +4 -6
  32. package/src/components/editor/group-rotate-handle.tsx +81 -35
  33. package/src/components/editor/group-selection-box-3d.tsx +25 -17
  34. package/src/components/editor/group-transform-shared.test.ts +126 -1
  35. package/src/components/editor/group-transform-shared.ts +79 -73
  36. package/src/components/editor/handles/handle-arrow.tsx +72 -2
  37. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  38. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  39. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  40. package/src/components/editor/handles/use-handle-drag.ts +272 -131
  41. package/src/components/editor/index.tsx +316 -176
  42. package/src/components/editor/join-walls-pill.tsx +44 -0
  43. package/src/components/editor/node-action-menu.tsx +70 -70
  44. package/src/components/editor/node-arrow-handles.tsx +167 -14
  45. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  46. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  47. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  48. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  49. package/src/components/editor/registry-action-contributions.tsx +132 -0
  50. package/src/components/editor/room-controls.tsx +847 -0
  51. package/src/components/editor/room-floor-highlight.tsx +128 -0
  52. package/src/components/editor/room-handles-2d.tsx +354 -0
  53. package/src/components/editor/room-handles.tsx +675 -0
  54. package/src/components/editor/room-highlight.tsx +225 -0
  55. package/src/components/editor/room-labels.test.ts +357 -0
  56. package/src/components/editor/room-labels.tsx +990 -0
  57. package/src/components/editor/room-transform-ghost.tsx +256 -0
  58. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  59. package/src/components/editor/selection-manager.tsx +451 -317
  60. package/src/components/editor/slab-hole-highlights.tsx +3 -0
  61. package/src/components/editor/snapshot-capture-overlay.tsx +4 -0
  62. package/src/components/editor/snapshot-capture.test.ts +52 -4
  63. package/src/components/editor/snapshot-capture.ts +30 -5
  64. package/src/components/editor/thumbnail-generator.test.ts +308 -0
  65. package/src/components/editor/thumbnail-generator.tsx +1035 -229
  66. package/src/components/editor/use-floorplan-background-placement.ts +31 -11
  67. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  68. package/src/components/editor/views/host-view.tsx +61 -0
  69. package/src/components/editor/views/plugins-view.tsx +670 -0
  70. package/src/components/editor/views/scene-region.tsx +59 -0
  71. package/src/components/editor/views/use-editor-views.ts +57 -0
  72. package/src/components/editor/views/view-bar.tsx +220 -0
  73. package/src/components/editor/wall-justification.test.ts +140 -0
  74. package/src/components/editor/wall-move-side-handles.tsx +114 -163
  75. package/src/components/editor/wall-opening-highlights.tsx +2 -3
  76. package/src/components/editor/wall-push-arrow.tsx +92 -0
  77. package/src/components/editor/wall-region-handles.tsx +509 -0
  78. package/src/components/editor/wall-snap-beacon-layer.tsx +3 -0
  79. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  80. package/src/components/editor-2d/floorplan-group-move.tsx +17 -24
  81. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  82. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  83. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +33 -12
  84. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  85. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +242 -126
  86. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  87. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  88. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  89. package/src/components/editor-2d/renderers/floorplan-draft-measurement.tsx +166 -0
  90. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  91. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +87 -8
  92. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +355 -245
  93. package/src/components/editor-2d/svg-paths.ts +12 -15
  94. package/src/components/procedural-items/inspector.tsx +40 -15
  95. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  96. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  97. package/src/components/systems/zone/zone-system.test.ts +47 -0
  98. package/src/components/systems/zone/zone-system.tsx +31 -66
  99. package/src/components/tools/__fixtures__/hosting.ts +10 -0
  100. package/src/components/tools/item/item-surface-placement.test.ts +3 -9
  101. package/src/components/tools/item/item-surface-strategy-resolver.test.ts +3 -9
  102. package/src/components/tools/item/placement-history.test.tsx +474 -0
  103. package/src/components/tools/item/placement-strategies.ts +63 -47
  104. package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +3 -3
  105. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  106. package/src/components/tools/item/use-draft-node.ts +207 -97
  107. package/src/components/tools/item/use-placement-coordinator.tsx +251 -111
  108. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  109. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  110. package/src/components/tools/registry/item-surface-move.test.ts +10 -2
  111. package/src/components/tools/registry/item-surface-move.ts +46 -24
  112. package/src/components/tools/registry/item-surface-resolver.test.ts +2 -2
  113. package/src/components/tools/registry/move-registry-node-tool.tsx +26 -10
  114. package/src/components/tools/select/box-select-tool.tsx +2 -12
  115. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  116. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  117. package/src/components/tools/select/select-candidates.test.ts +37 -4
  118. package/src/components/tools/select/select-candidates.ts +15 -11
  119. package/src/components/tools/shared/affordance-dispatch.test.ts +24 -0
  120. package/src/components/tools/shared/affordance-dispatch.ts +13 -0
  121. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  122. package/src/components/tools/shared/draft-measurement-label.tsx +38 -0
  123. package/src/components/tools/shared/pointer-support-cap.test.ts +27 -0
  124. package/src/components/tools/shared/pointer-support-cap.ts +17 -5
  125. package/src/components/tools/shared/surface-rejection.test.tsx +5 -4
  126. package/src/components/tools/shared/surface-rejection.tsx +14 -0
  127. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  128. package/src/components/tools/stair/stair-defaults.ts +0 -3
  129. package/src/components/tools/stair/stair-tool.tsx +90 -42
  130. package/src/components/tools/tool-manager.tsx +82 -23
  131. package/src/components/tools/wall/wall-drafting.test.ts +19 -4
  132. package/src/components/tools/zone/zone-tool.tsx +4 -12
  133. package/src/components/ui/action-menu/action-button.tsx +3 -0
  134. package/src/components/ui/action-menu/camera-actions.tsx +2 -1
  135. package/src/components/ui/action-menu/control-modes.tsx +2 -35
  136. package/src/components/ui/action-menu/measurement-control.tsx +3 -10
  137. package/src/components/ui/build-panel/build-panel.tsx +235 -0
  138. package/src/components/ui/command-palette/editor-commands.tsx +4 -30
  139. package/src/components/ui/command-palette/index.tsx +5 -24
  140. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  141. package/src/components/ui/controls/material-paint-panel.tsx +101 -91
  142. package/src/components/ui/controls/material-picker.tsx +8 -67
  143. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  144. package/src/components/ui/controls/select-control.tsx +65 -0
  145. package/src/components/ui/controls/slider-control.tsx +76 -31
  146. package/src/components/ui/floating-level-selector.tsx +9 -14
  147. package/src/components/ui/helpers/building-helper.tsx +4 -1
  148. package/src/components/ui/helpers/contextual-helper-panel.tsx +154 -70
  149. package/src/components/ui/helpers/helper-manager.test.tsx +69 -0
  150. package/src/components/ui/helpers/helper-manager.tsx +348 -19
  151. package/src/components/ui/helpers/item-helper.tsx +8 -0
  152. package/src/components/ui/helpers/registered-tool-helper.tsx +10 -33
  153. package/src/components/ui/icon-ref.tsx +45 -1
  154. package/src/components/ui/item-catalog/item-catalog.tsx +5 -30
  155. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  156. package/src/components/ui/node-icon-image.tsx +16 -0
  157. package/src/components/ui/panels/collections/collections-popover.tsx +20 -14
  158. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  159. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  160. package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
  161. package/src/components/ui/panels/mobile-selection-bar.tsx +2 -2
  162. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  163. package/src/components/ui/panels/multi-field-value.ts +6 -3
  164. package/src/components/ui/panels/multi-height-mode.tsx +27 -21
  165. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  166. package/src/components/ui/panels/multi-selection-panel.tsx +43 -4
  167. package/src/components/ui/panels/node-display.ts +8 -3
  168. package/src/components/ui/panels/panel-manager.tsx +103 -68
  169. package/src/components/ui/panels/panel-wrapper.tsx +78 -36
  170. package/src/components/ui/panels/parametric-field-control.tsx +1 -1
  171. package/src/components/ui/panels/parametric-inspector.tsx +7 -19
  172. package/src/components/ui/panels/reference-panel.tsx +2 -3
  173. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  174. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  175. package/src/components/ui/panels/room-name-options.ts +47 -0
  176. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  177. package/src/components/ui/panels/room-panel.tsx +725 -0
  178. package/src/components/ui/panels/shape-choice.tsx +58 -0
  179. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  180. package/src/components/ui/primitives/color-dot.tsx +36 -24
  181. package/src/components/ui/primitives/shortcut-token.tsx +4 -0
  182. package/src/components/ui/primitives/sidebar.tsx +2 -0
  183. package/src/components/ui/right-stack.test.ts +29 -0
  184. package/src/components/ui/right-stack.tsx +145 -0
  185. package/src/components/ui/sidebar/icon-rail.tsx +5 -0
  186. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +2 -2
  187. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +615 -372
  188. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +7 -7
  189. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +1 -1
  190. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  191. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  192. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  193. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  194. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  195. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  196. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  197. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  198. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  199. package/src/components/ui/sidebar/panels/site-panel/index.tsx +176 -211
  200. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  201. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  202. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  203. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  204. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  205. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  206. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  207. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  208. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  209. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  210. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +0 -27
  211. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  212. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  213. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  214. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  215. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +0 -2
  216. package/src/components/ui/sidebar/tab-bar.tsx +10 -0
  217. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  218. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  219. package/src/components/viewer/floorplan-preview-visibility.test.ts +35 -0
  220. package/src/components/viewer/floorplan-preview-visibility.ts +19 -0
  221. package/src/components/viewer/floorplan-preview.tsx +6 -18
  222. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  223. package/src/components/viewer/viewer-rooms.tsx +175 -0
  224. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  225. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  226. package/src/components/viewer/viewer-stage.tsx +2 -2
  227. package/src/components/viewer-zone-system.tsx +9 -9
  228. package/src/hooks/use-auto-save.integration.test.tsx +311 -0
  229. package/src/hooks/use-auto-save.ts +55 -14
  230. package/src/hooks/use-ceiling-events.test.ts +1 -0
  231. package/src/hooks/use-ceiling-events.ts +2 -0
  232. package/src/hooks/use-grid-events.test.ts +2 -0
  233. package/src/hooks/use-grid-events.ts +16 -11
  234. package/src/hooks/use-keyboard.test.ts +182 -0
  235. package/src/hooks/use-keyboard.ts +122 -91
  236. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  237. package/src/hooks/use-open-wall-ends.ts +238 -0
  238. package/src/hooks/use-placement-notice.test.ts +48 -0
  239. package/src/hooks/use-placement-notice.ts +42 -0
  240. package/src/hooks/use-selected-room.ts +143 -0
  241. package/src/index.tsx +306 -39
  242. package/src/lib/catalog-panel-model.ts +39 -0
  243. package/src/lib/catalog-selection.test.ts +27 -0
  244. package/src/lib/catalog-selection.ts +8 -0
  245. package/src/lib/ceiling-edit-session.test.ts +321 -0
  246. package/src/lib/ceiling-edit-session.ts +206 -0
  247. package/src/lib/ceiling-region-session.test.ts +179 -0
  248. package/src/lib/contextual-help.test.ts +27 -1
  249. package/src/lib/contextual-help.ts +16 -9
  250. package/src/lib/continuation.test.ts +19 -1
  251. package/src/lib/continuation.ts +25 -4
  252. package/src/lib/door-interaction.ts +14 -0
  253. package/src/lib/editor-level-display.test.ts +96 -0
  254. package/src/lib/editor-level-display.ts +48 -0
  255. package/src/lib/editor-views.ts +144 -0
  256. package/src/lib/export-geometry-wait.test.ts +90 -0
  257. package/src/lib/floor-edit-session.ts +153 -0
  258. package/src/lib/floor-footprints.test.ts +563 -0
  259. package/src/lib/floor-footprints.ts +395 -0
  260. package/src/lib/floor-foundation-support.test.ts +283 -0
  261. package/src/lib/floor-opening-draft.ts +218 -0
  262. package/src/lib/floor-openings.test.ts +216 -0
  263. package/src/lib/floor-region-geometry.ts +191 -0
  264. package/src/lib/floor-region-session.test.ts +485 -0
  265. package/src/lib/floor-region-session.ts +589 -0
  266. package/src/lib/floor-region-snap.ts +101 -0
  267. package/src/lib/floor-step-hover.test.ts +32 -0
  268. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  269. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  270. package/src/lib/floorplan/dimension-drive.ts +479 -0
  271. package/src/lib/floorplan/floorplan-export.test.ts +15 -9
  272. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  273. package/src/lib/floorplan/floorplan-extension.ts +48 -0
  274. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  275. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  276. package/src/lib/floorplan/floorplan-pdfkit-document.ts +339 -18
  277. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +138 -8
  278. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +56 -25
  279. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  280. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  281. package/src/lib/floorplan/geometry.ts +13 -6
  282. package/src/lib/floorplan/index.ts +0 -9
  283. package/src/lib/floorplan/items.ts +5 -2
  284. package/src/lib/floorplan/marks.test.ts +213 -0
  285. package/src/lib/floorplan/marks.ts +238 -0
  286. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  287. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  288. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  289. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  290. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  291. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  292. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  293. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  294. package/src/lib/floorplan/schedules.test.ts +189 -0
  295. package/src/lib/floorplan/schedules.ts +339 -0
  296. package/src/lib/floorplan/sheet-export.test.ts +100 -0
  297. package/src/lib/floorplan/sheet-export.ts +318 -0
  298. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +779 -0
  299. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  300. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  301. package/src/lib/floorplan/site-plan/coverage.ts +232 -0
  302. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  303. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  304. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  305. package/src/lib/floorplan/site-plan/front-edge.ts +156 -0
  306. package/src/lib/floorplan/site-plan/geometry.test.ts +275 -0
  307. package/src/lib/floorplan/site-plan/geometry.ts +557 -0
  308. package/src/lib/floorplan/site-plan/index.ts +71 -0
  309. package/src/lib/floorplan/site-plan/site-annotations.test.ts +366 -0
  310. package/src/lib/floorplan/site-plan/site-annotations.ts +1083 -0
  311. package/src/lib/floorplan/site-plan/site-parts.ts +379 -0
  312. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  313. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  314. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  315. package/src/lib/floorplan/site-plan/yards.test.ts +69 -0
  316. package/src/lib/floorplan/types.ts +1 -27
  317. package/src/lib/fresh-planar-placement.test.ts +36 -0
  318. package/src/lib/fresh-planar-placement.ts +191 -36
  319. package/src/lib/geometry-script/author.ts +152 -0
  320. package/src/lib/geometry-script/client.ts +152 -0
  321. package/src/lib/geometry-script/protocol.ts +12 -0
  322. package/src/lib/gesture-lifecycle.test.ts +595 -0
  323. package/src/lib/gesture-lifecycle.ts +290 -0
  324. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  325. package/src/lib/glb-export.test.ts +544 -16
  326. package/src/lib/glb-export.ts +256 -125
  327. package/src/lib/grid-frame.test.ts +19 -0
  328. package/src/lib/grid-frame.ts +18 -0
  329. package/src/lib/history.test.ts +6 -0
  330. package/src/lib/history.ts +7 -0
  331. package/src/lib/hud-title.ts +109 -0
  332. package/src/lib/hydration-gated-storage.ts +49 -0
  333. package/src/lib/ifc-export.test.ts +174 -0
  334. package/src/lib/ifc-export.ts +201 -0
  335. package/src/lib/ifc-parts.ts +15 -0
  336. package/src/lib/inspector-expanded.test.ts +44 -0
  337. package/src/lib/inspector-expanded.ts +36 -0
  338. package/src/lib/interaction/scope.ts +27 -1
  339. package/src/lib/keyboard-pan.test.ts +184 -0
  340. package/src/lib/keyboard-pan.ts +238 -0
  341. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  342. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  343. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  344. package/src/lib/lot/clean-ring.test.ts +186 -0
  345. package/src/lib/lot/clean-ring.ts +217 -0
  346. package/src/lib/lot/dossier.test.ts +202 -0
  347. package/src/lib/lot/dossier.ts +498 -0
  348. package/src/lib/lot/drop-in.test.ts +326 -0
  349. package/src/lib/lot/drop-in.ts +407 -0
  350. package/src/lib/lot/index.ts +43 -0
  351. package/src/lib/lot/lot-patch.test.ts +340 -0
  352. package/src/lib/lot/lot-patch.ts +394 -0
  353. package/src/lib/lot/parcel-provider.ts +43 -0
  354. package/src/lib/lot/terrain.test.ts +161 -0
  355. package/src/lib/lot/terrain.ts +266 -0
  356. package/src/lib/material-catalog-model.ts +74 -0
  357. package/src/lib/material-paint-panel-model.ts +80 -0
  358. package/src/lib/material-paint.ts +4 -0
  359. package/src/lib/mezzanine-actions.test.ts +342 -0
  360. package/src/lib/mezzanine-draft.test.ts +501 -0
  361. package/src/lib/mezzanine-draft.ts +278 -0
  362. package/src/lib/mezzanine-messages.ts +26 -0
  363. package/src/lib/mezzanine-selection.test.ts +207 -0
  364. package/src/lib/mezzanine-stairs.ts +46 -0
  365. package/src/lib/model-export.ts +3 -1
  366. package/src/lib/node-action-movement.ts +17 -6
  367. package/src/lib/node-display-label.ts +37 -0
  368. package/src/lib/orbit-floor-clearance.test.ts +165 -0
  369. package/src/lib/orbit-floor-clearance.ts +106 -0
  370. package/src/lib/overlay-transform.test.ts +28 -0
  371. package/src/lib/overlay-transform.ts +12 -0
  372. package/src/lib/paint-commit-route.test.ts +37 -0
  373. package/src/lib/paint-commit-route.ts +17 -0
  374. package/src/lib/paint-eyedropper.test.tsx +165 -0
  375. package/src/lib/paint-eyedropper.ts +99 -0
  376. package/src/lib/paint-pass-through.test.ts +111 -0
  377. package/src/lib/paint-pass-through.ts +76 -0
  378. package/src/lib/paint-region-hover.ts +25 -0
  379. package/src/lib/paint-region-mode.ts +154 -0
  380. package/src/lib/paint-regions.test.ts +166 -0
  381. package/src/lib/paint-regions.ts +192 -0
  382. package/src/lib/paint-scope.test.ts +317 -35
  383. package/src/lib/paint-scope.ts +266 -25
  384. package/src/lib/panel-rows.ts +96 -0
  385. package/src/lib/panel-tool-options.test.ts +34 -0
  386. package/src/lib/panel-tool-options.ts +46 -0
  387. package/src/lib/parametric-node-update.ts +37 -0
  388. package/src/lib/plate-paint-affected.test.ts +300 -0
  389. package/src/lib/plate-paint-affected.ts +177 -0
  390. package/src/lib/plugin-chat.ts +50 -0
  391. package/src/lib/plugin-panels.test.ts +25 -0
  392. package/src/lib/plugin-panels.ts +94 -0
  393. package/src/lib/print-shell-compiler.ts +1 -0
  394. package/src/lib/registered-draft-snap.ts +12 -0
  395. package/src/lib/roof-duplication.ts +10 -2
  396. package/src/lib/room-assembly-overlay.test.ts +275 -0
  397. package/src/lib/room-assembly-overlay.ts +266 -0
  398. package/src/lib/room-built-on.test.ts +379 -0
  399. package/src/lib/room-built-on.ts +138 -0
  400. package/src/lib/room-construction-commands.ts +712 -0
  401. package/src/lib/room-construction.test.ts +746 -0
  402. package/src/lib/room-construction.ts +385 -0
  403. package/src/lib/room-controls-phase6.test.ts +630 -0
  404. package/src/lib/room-divide-session.ts +523 -0
  405. package/src/lib/room-handle-drag.ts +1068 -0
  406. package/src/lib/room-name-catalog.test.ts +90 -0
  407. package/src/lib/room-name-catalog.ts +168 -0
  408. package/src/lib/room-push-dimensions.test.ts +311 -0
  409. package/src/lib/room-push-dimensions.ts +251 -0
  410. package/src/lib/room-selection-commands.ts +62 -0
  411. package/src/lib/room-selection.test.ts +973 -0
  412. package/src/lib/room-selection.ts +417 -0
  413. package/src/lib/room-structure-commands.ts +94 -0
  414. package/src/lib/room-structure.test.ts +921 -0
  415. package/src/lib/room-transform-session.ts +570 -0
  416. package/src/lib/room-zone-routing.test.ts +294 -0
  417. package/src/lib/room-zone-routing.ts +197 -0
  418. package/src/lib/scene-clipboard.test.ts +364 -0
  419. package/src/lib/scene-clipboard.ts +229 -48
  420. package/src/lib/scene-material-refs.test.ts +79 -0
  421. package/src/lib/scene-material-refs.ts +79 -0
  422. package/src/lib/scene.test.ts +87 -0
  423. package/src/lib/scene.ts +39 -45
  424. package/src/lib/selection-routing.test.ts +129 -45
  425. package/src/lib/selection-routing.ts +53 -74
  426. package/src/lib/session-writes.test.ts +120 -0
  427. package/src/lib/session-writes.ts +205 -0
  428. package/src/lib/sidebar-panel.ts +62 -0
  429. package/src/lib/site-boundary.test.ts +4 -6
  430. package/src/lib/site-boundary.ts +1 -1
  431. package/src/lib/snapping-mode.test.ts +17 -2
  432. package/src/lib/snapping-mode.ts +19 -7
  433. package/src/lib/spatial-drag-plane.test.ts +53 -0
  434. package/src/lib/spatial-drag-plane.ts +19 -0
  435. package/src/lib/spatial-pointer-input.test.ts +72 -0
  436. package/src/lib/spatial-pointer-input.ts +62 -0
  437. package/src/lib/spatial-snap.test.ts +47 -0
  438. package/src/lib/terrace-draft.test.ts +145 -0
  439. package/src/lib/terrace-draft.ts +213 -0
  440. package/src/lib/terrain-panel-model.ts +117 -0
  441. package/src/lib/tool-cursor-icon.test.ts +16 -0
  442. package/src/lib/tool-cursor-icon.ts +11 -0
  443. package/src/lib/typing-target.ts +9 -0
  444. package/src/lib/units.ts +77 -39
  445. package/src/lib/vector-edges.test.ts +58 -0
  446. package/src/lib/vector-edges.ts +400 -0
  447. package/src/lib/viewer-selection.test.ts +577 -0
  448. package/src/lib/viewer-selection.ts +569 -0
  449. package/src/lib/wall-draw-variant.test.ts +71 -0
  450. package/src/lib/wall-draw-variant.ts +91 -0
  451. package/src/lib/wall-paint-region-session.ts +246 -0
  452. package/src/lib/wall-paint-region.test.ts +373 -0
  453. package/src/lib/wall-polygon-draft.test.ts +262 -0
  454. package/src/lib/wall-polygon-draft.ts +286 -0
  455. package/src/lib/wall-push.test.ts +453 -0
  456. package/src/lib/wall-region-face.ts +352 -0
  457. package/src/lib/wall-region-handles.test.ts +304 -0
  458. package/src/lib/wall-region-handles.ts +368 -0
  459. package/src/lib/wall-region-list.test.ts +119 -0
  460. package/src/lib/wall-region-list.ts +136 -0
  461. package/src/lib/wall-region-snap.ts +110 -0
  462. package/src/lib/window-interaction.ts +14 -0
  463. package/src/lib/world-grid-snap.test.ts +48 -0
  464. package/src/lib/world-grid-snap.ts +9 -5
  465. package/src/lib/zone-content.ts +4 -1
  466. package/src/lib/zone-name.test.ts +17 -0
  467. package/src/lib/zone-name.ts +17 -0
  468. package/src/store/opens-in-select-mode.test.ts +223 -0
  469. package/src/store/paint-mode.test.ts +188 -0
  470. package/src/store/rotation-snapping.test.ts +53 -0
  471. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  472. package/src/store/tool-mode.test.ts +137 -6
  473. package/src/store/use-delete-confirmation.ts +28 -1
  474. package/src/store/use-drawing-view.test.ts +24 -3
  475. package/src/store/use-drawing-view.ts +29 -1
  476. package/src/store/use-editor.tsx +442 -323
  477. package/src/store/use-floorplan-draft-preview.ts +23 -1
  478. package/src/store/use-floorplan-marquee.ts +12 -2
  479. package/src/store/use-interaction-scope.ts +155 -6
  480. package/src/store/use-quick-measurement-hud.ts +3 -3
  481. package/src/store/use-stair-build-preview.ts +13 -5
  482. package/src/store/use-wall-move-ghosts.ts +4 -2
  483. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  484. package/src/store/use-wall-region-handles-selection.ts +139 -0
  485. package/src/store/view-layout.test.ts +175 -0
  486. package/src/store/view-layout.ts +196 -0
  487. package/src/test-utils/immediate-animation-frames.ts +16 -0
  488. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  489. package/src/test-utils/selection-harness.ts +82 -0
  490. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  491. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  492. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  493. package/src/components/tools/fence/fence-drafting.ts +0 -266
  494. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  495. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  496. package/src/lib/floorplan/stairs.ts +0 -489
  497. package/src/lib/level-duplication.test.ts +0 -144
  498. package/src/lib/level-duplication.ts +0 -195
@@ -0,0 +1,846 @@
1
+ 'use client'
2
+
3
+ import {
4
+ containsPoint,
5
+ emitter,
6
+ type MultiPolygon,
7
+ resolveCeilingHeight,
8
+ sceneRegistry,
9
+ useScene,
10
+ } from '@pascal-app/core'
11
+ import { getSceneTheme, setSurfaceRaycastLayers, useViewer } from '@pascal-app/viewer'
12
+ import { useFrame, useThree } from '@react-three/fiber'
13
+ import { Pentagon, Square } from 'lucide-react'
14
+ import { useEffect, useMemo, useRef } from 'react'
15
+ import {
16
+ BufferGeometry,
17
+ type Camera,
18
+ DoubleSide,
19
+ Float32BufferAttribute,
20
+ type Group,
21
+ Matrix4,
22
+ type Object3D,
23
+ type OrthographicCamera,
24
+ type PerspectiveCamera,
25
+ Plane,
26
+ Raycaster,
27
+ ShapeUtils,
28
+ Vector2,
29
+ Vector3,
30
+ } from 'three'
31
+ import { LineBasicNodeMaterial, MeshBasicNodeMaterial } from 'three/webgpu'
32
+ import { markToolCancelConsumed } from '../../hooks/use-keyboard'
33
+ import { getRoomSelectionIndex } from '../../hooks/use-selected-room'
34
+ import { EDITOR_LAYER } from '../../lib/constants'
35
+ import {
36
+ cancelOpeningDraft,
37
+ openingDraftActive,
38
+ useOpeningDraft,
39
+ } from '../../lib/floor-opening-draft'
40
+ import { clipFloorRegion, floorRegionRoomAt } from '../../lib/floor-region-geometry'
41
+ import {
42
+ cancelFloorRegion,
43
+ currentFloorRegionSnap,
44
+ describeCeilingRegionRoom,
45
+ describeFloorRegionRoom,
46
+ dropFloorRegionDraft,
47
+ type FloorRegionDraft,
48
+ type FloorRegionRoom,
49
+ finishFloorRegion,
50
+ floorRegionDraftMessage,
51
+ floorRegionDraftOutline,
52
+ floorRegionEditable,
53
+ floorRegionGesture,
54
+ moveFloorRegion,
55
+ pressFloorRegion,
56
+ releaseFloorRegion,
57
+ removeLastFloorRegionPoint,
58
+ setFloorRegionHover,
59
+ useFloorRegionDraft,
60
+ } from '../../lib/floor-region-session'
61
+ import { type FloorRegionPoint, snapFloorRegionPoint } from '../../lib/floor-region-snap'
62
+ import { clientToPlan } from '../../lib/floorplan/plan-coords'
63
+ import { formatAreaLabel } from '../../lib/measurements'
64
+ import {
65
+ cancelMezzanineDraft,
66
+ mezzanineDraftActive,
67
+ useMezzanineDraft,
68
+ } from '../../lib/mezzanine-draft'
69
+ import { usePaintRegionMode } from '../../lib/paint-region-mode'
70
+ import { roomFloorElevation } from '../../lib/room-handle-drag'
71
+ import {
72
+ cancelTerraceDraft,
73
+ terraceDraftActive,
74
+ toggleTerraceShape,
75
+ useTerraceDraft,
76
+ } from '../../lib/terrace-draft'
77
+ import { isTypingTarget } from '../../lib/typing-target'
78
+ import useEditor from '../../store/use-editor'
79
+ import { useFloorplanRender } from '../editor-2d/floorplan-render-context'
80
+ import { usePaintTint } from '../tools/paint-region/use-paint-tint'
81
+ import { CursorSphere } from '../tools/shared/cursor-sphere'
82
+ import {
83
+ DRAFT_LABEL_Y_OFFSET,
84
+ DraftMeasurementLabel,
85
+ } from '../tools/shared/draft-measurement-label'
86
+ import { swallowNextClick } from './handles/use-handle-drag'
87
+ import { RoomFloorHighlight3D } from './room-floor-highlight'
88
+
89
+ // An outline on a room's floor, in 3D and on the plan: the paint tool's
90
+ // rectangle and polygon sub-modes ("Paint part of the floor"), and the armed
91
+ // "Add mezzanine" tool, which draws in its host room on the mezzanine's plane.
92
+ // The session (`floor-region-session`) owns the draft; this file turns
93
+ // pointers into plan points on a room's floor and draws the draft in the wall
94
+ // draft's colours.
95
+
96
+ const DRAFT_COLOR = '#818cf8'
97
+ const DRAFT_COLOR_LIGHT_PLAN = '#6366f1'
98
+ const INVALID_COLOR = '#ef4444'
99
+ /** How close (screen pixels) a click lands to the first point to close the polygon, in 3D and on the plan. */
100
+ const CLOSE_RADIUS_PX = 10
101
+ /** The 3D close radius (m) when the camera cannot say how big a pixel is. */
102
+ const CLOSE_RADIUS_3D_FALLBACK = 0.2
103
+ const PREVIEW_LIFT = 0.012
104
+
105
+ type FloorRegionMode = 'rectangle' | 'polygon'
106
+ type FloorRegionHit = { room: FloorRegionRoom; point: FloorRegionPoint }
107
+
108
+ function activeMode(): FloorRegionMode | null {
109
+ if (!floorRegionEditable()) return null
110
+ const opening = useOpeningDraft.getState()
111
+ if (opening.host) return opening.shape
112
+ const mezzanine = useMezzanineDraft.getState()
113
+ if (mezzanine.host) return mezzanine.shape
114
+ const terrace = useTerraceDraft.getState()
115
+ if (terrace.host) return terrace.shape
116
+ if (useEditor.getState().mode !== 'material-paint') return null
117
+ const mode = usePaintRegionMode.getState().mode
118
+ return mode === 'rectangle' || mode === 'polygon' ? mode : null
119
+ }
120
+
121
+ function useFloorRegionMode(): FloorRegionMode | null {
122
+ const editorMode = useEditor((s) => s.mode)
123
+ const regionMode = usePaintRegionMode((s) => s.mode)
124
+ const mezzanine = useMezzanineDraft((s) => (s.host ? s.shape : null))
125
+ const terrace = useTerraceDraft((s) => (s.host ? s.shape : null))
126
+ const opening = useOpeningDraft((s) => (s.host ? s.shape : null))
127
+ const readOnly = useScene((s) => s.readOnly)
128
+ if (readOnly) return null
129
+ if (opening) return opening
130
+ if (mezzanine) return mezzanine
131
+ if (terrace) return terrace
132
+ if (editorMode !== 'material-paint') return null
133
+ return regionMode === 'rectangle' || regionMode === 'polygon' ? regionMode : null
134
+ }
135
+
136
+ function useSceneIsDark() {
137
+ return useViewer((s) => getSceneTheme(s.sceneTheme).appearance === 'dark')
138
+ }
139
+
140
+ // The mezzanine tool owns Escape, Backspace, Enter (and the terrace tool T) while
141
+ // armed, draft or not, so none of them reaches the selected room (Backspace
142
+ // would delete it).
143
+ function floorRegionKey(event: KeyboardEvent) {
144
+ const draft = useFloorRegionDraft.getState().draft
145
+ const terrace = terraceDraftActive()
146
+ const opening = openingDraftActive()
147
+ const mezzanine = mezzanineDraftActive() || terrace || opening
148
+ if (!(draft || mezzanine) || event.defaultPrevented || isTypingTarget(event.target)) return
149
+ if (event.key === 'Escape') {
150
+ if (opening) cancelOpeningDraft()
151
+ else if (terrace) cancelTerraceDraft()
152
+ else if (mezzanine) cancelMezzanineDraft()
153
+ else cancelFloorRegion()
154
+ } else if (event.key === 'Backspace') {
155
+ if (!(removeLastFloorRegionPoint() || mezzanine)) return
156
+ } else if (event.key === 'Enter') {
157
+ if (!(finishFloorRegion() || mezzanine)) return
158
+ } else if (
159
+ terrace &&
160
+ (event.key === 't' || event.key === 'T') &&
161
+ !(event.metaKey || event.ctrlKey || event.altKey)
162
+ )
163
+ toggleTerraceShape()
164
+ else return
165
+ event.preventDefault()
166
+ event.stopPropagation()
167
+ }
168
+
169
+ // One keyboard listener per target however many surfaces bind to it (split
170
+ // view mounts the plan and the 3D controller together).
171
+ const keyboardOwners = new WeakMap<EventTarget, number>()
172
+ function acquireKeyboard(target: EventTarget) {
173
+ const owners = keyboardOwners.get(target) ?? 0
174
+ if (owners === 0) target.addEventListener('keydown', floorRegionKey as EventListener, true)
175
+ keyboardOwners.set(target, owners + 1)
176
+ let released = false
177
+ return () => {
178
+ if (released) return
179
+ released = true
180
+ const left = (keyboardOwners.get(target) ?? 1) - 1
181
+ keyboardOwners.set(target, left)
182
+ if (left === 0) target.removeEventListener('keydown', floorRegionKey as EventListener, true)
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Binds a surface to the floor region draft. `resolve` answers where the
188
+ * pointer is on a room floor: while a draft is open, on that draft's floor;
189
+ * otherwise the room under the pointer, or null when the floor is not what
190
+ * the pointer is on (another agent's surface keeps the event).
191
+ */
192
+ export function bindFloorRegionPointer(
193
+ surface: EventTarget,
194
+ resolve: (event: PointerEvent) => FloorRegionHit | null,
195
+ closeRadius: () => number,
196
+ keyboard: EventTarget = window,
197
+ ) {
198
+ let pressed: number | null = null
199
+ const capture = surface as Element
200
+ // Moves are coalesced to one update per animation frame; a press drops the
201
+ // pending move and acts on its own position at once.
202
+ let frame = 0
203
+ let pending: PointerEvent | null = null
204
+ const flush = () => {
205
+ frame = 0
206
+ const event = pending
207
+ pending = null
208
+ if (!(event && activeMode())) return
209
+ const hit = resolve(event)
210
+ const settings = currentFloorRegionSnap(event.altKey)
211
+ const draft = useFloorRegionDraft.getState().draft
212
+ // A refused box stays put until the next press; the marker shows meanwhile.
213
+ if (draft && !(draft.kind === 'rectangle' && draft.refusal)) {
214
+ setFloorRegionHover(null)
215
+ if (hit) moveFloorRegion(hit.point, settings, closeRadius())
216
+ return
217
+ }
218
+ setFloorRegionHover(
219
+ hit
220
+ ? {
221
+ levelId: hit.room.levelId,
222
+ elevation: hit.room.elevation,
223
+ point: snapFloorRegionPoint(hit.point, settings, hit.room.targets).point,
224
+ ...(hit.room.ceilingId ? { ceiling: true } : {}),
225
+ }
226
+ : null,
227
+ )
228
+ }
229
+ const dropPending = () => {
230
+ if (frame) cancelAnimationFrame(frame)
231
+ frame = 0
232
+ pending = null
233
+ }
234
+ const move = (event: PointerEvent) => {
235
+ if (!activeMode()) return
236
+ pending = event
237
+ if (!frame) frame = requestAnimationFrame(flush) || 0
238
+ }
239
+ const down = (event: PointerEvent) => {
240
+ const mode = activeMode()
241
+ if (event.button !== 0 || !mode || useViewer.getState().cameraDragging) return
242
+ const hit = resolve(event)
243
+ if (!hit) return
244
+ dropPending()
245
+ event.preventDefault()
246
+ event.stopPropagation()
247
+ setFloorRegionHover(null)
248
+ pressFloorRegion(mode, hit.room, hit.point, currentFloorRegionSnap(event.altKey), closeRadius())
249
+ pressed = event.pointerId
250
+ // A box dragged off the surface keeps following the pointer. The capture is
251
+ // the gesture owner's: released on release, cancel or any lifecycle end.
252
+ if ('setPointerCapture' in capture)
253
+ floorRegionGesture()?.capturePointer(capture, event.pointerId)
254
+ }
255
+ const up = (event: PointerEvent) => {
256
+ if (pressed !== event.pointerId) return
257
+ pressed = null
258
+ floorRegionGesture()?.releasePointer(event.pointerId)
259
+ // The last move lands before the release, so the box ends where the pointer let go.
260
+ if (frame) cancelAnimationFrame(frame)
261
+ flush()
262
+ event.preventDefault()
263
+ event.stopPropagation()
264
+ // The click the browser synthesizes from this release belongs to the draft:
265
+ // it must not deselect what the commit just selected.
266
+ swallowNextClick()
267
+ useViewer.getState().setInputDragging(true)
268
+ releaseFloorRegion(closeRadius())
269
+ setTimeout(() => useViewer.getState().setInputDragging(false), 0)
270
+ }
271
+ // The system took the pointer (a touch turned into a scroll, a lost window):
272
+ // a box ends uncommitted; a polygon keeps its placed points.
273
+ const cancel = (event: PointerEvent) => {
274
+ if (pressed !== event.pointerId) return
275
+ pressed = null
276
+ dropPending()
277
+ floorRegionGesture()?.releasePointer(event.pointerId)
278
+ if (useFloorRegionDraft.getState().draft?.kind === 'rectangle') dropFloorRegionDraft()
279
+ }
280
+ const leave = () => {
281
+ const draft = useFloorRegionDraft.getState().draft
282
+ if (!draft || (draft.kind === 'rectangle' && draft.refusal)) setFloorRegionHover(null)
283
+ }
284
+ const onMove = (event: Event) => move(event as PointerEvent)
285
+ const onDown = (event: Event) => down(event as PointerEvent)
286
+ surface.addEventListener('pointermove', onMove, true)
287
+ surface.addEventListener('pointerdown', onDown, true)
288
+ surface.addEventListener('pointerleave', leave)
289
+ keyboard.addEventListener('pointerup', up as EventListener, true)
290
+ keyboard.addEventListener('pointercancel', cancel as EventListener, true)
291
+ const releaseKeyboard = acquireKeyboard(keyboard)
292
+ return () => {
293
+ dropPending()
294
+ if (pressed !== null) floorRegionGesture()?.releasePointer(pressed)
295
+ surface.removeEventListener('pointermove', onMove, true)
296
+ surface.removeEventListener('pointerdown', onDown, true)
297
+ surface.removeEventListener('pointerleave', leave)
298
+ keyboard.removeEventListener('pointerup', up as EventListener, true)
299
+ keyboard.removeEventListener('pointercancel', cancel as EventListener, true)
300
+ releaseKeyboard()
301
+ }
302
+ }
303
+
304
+ /**
305
+ * The draft's own lifetime lives with the gesture lifecycle owner (sub-mode,
306
+ * mode, level or selection change, its room gone, its scope replaced, any
307
+ * history command). Here: Escape (`tool:cancel`, consumed so the paint mode
308
+ * stays), the hover marker on a level switch, and the surface unmounting.
309
+ */
310
+ function useFloorRegionLifetime(mode: FloorRegionMode | null) {
311
+ useEffect(() => {
312
+ if (!mode) setFloorRegionHover(null)
313
+ }, [mode])
314
+ useEffect(() => {
315
+ const stopViewer = useViewer.subscribe((state, previous) => {
316
+ if (state.selection.levelId !== previous.selection.levelId) setFloorRegionHover(null)
317
+ })
318
+ const onToolCancel = () => {
319
+ if (terraceDraftActive()) cancelTerraceDraft()
320
+ else if (mezzanineDraftActive()) cancelMezzanineDraft()
321
+ else if (useFloorRegionDraft.getState().draft) cancelFloorRegion()
322
+ else return
323
+ markToolCancelConsumed()
324
+ }
325
+ emitter.on('tool:cancel', onToolCancel)
326
+ editingSurfaces++
327
+ return () => {
328
+ stopViewer()
329
+ emitter.off('tool:cancel', onToolCancel)
330
+ cancelFloorRegion()
331
+ setFloorRegionHover(null)
332
+ editingSurfaces--
333
+ // Before its first press the mezzanine tool owns no draft, only its armed
334
+ // state and scope; the last surface going away (capture mode, the editor
335
+ // closing) must not leave them behind.
336
+ if (editingSurfaces === 0 && mezzanineDraftActive()) cancelMezzanineDraft()
337
+ if (editingSurfaces === 0 && terraceDraftActive()) cancelTerraceDraft()
338
+ }
339
+ }, [])
340
+ }
341
+
342
+ /** The mounted 3D / 2D drawing surfaces; the mezzanine tool lives only while one is. */
343
+ let editingSurfaces = 0
344
+
345
+ /** The room whose clear floor holds `point` on `levelId`, as a draft room. */
346
+ export function resolveFloorRegionRoom(levelId: string, point: FloorRegionPoint) {
347
+ const nodes = useScene.getState().nodes
348
+ const record = floorRegionRoomAt(getRoomSelectionIndex(levelId).update(nodes), point, (room) =>
349
+ roomFloorElevation(nodes, room.zoneId),
350
+ )
351
+ return record ? describeFloorRegionRoom(nodes, record) : null
352
+ }
353
+
354
+ // A cutaway hides walls without unregistering them, and an invisible wall must
355
+ // not answer a ray the user aimed at the floor behind it.
356
+ function visibleSurface(object: Object3D) {
357
+ for (let node: Object3D | null = object; node; node = node.parent)
358
+ if (!node.visible || node.userData.wallHidden === true) return false
359
+ return true
360
+ }
361
+
362
+ /** Distance (level space) from `origin` to the nearest visible wall of the level along the ray. */
363
+ function nearestWallDistance(
364
+ raycaster: Raycaster,
365
+ level: Object3D,
366
+ levelId: string,
367
+ origin: Vector3,
368
+ ) {
369
+ const nodes = useScene.getState().nodes
370
+ let nearest = Number.POSITIVE_INFINITY
371
+ for (const id of sceneRegistry.byType.wall ?? []) {
372
+ if (nodes[id as keyof typeof nodes]?.parentId !== levelId) continue
373
+ const object = sceneRegistry.nodes.get(id)
374
+ const hit = object
375
+ ? raycaster
376
+ .intersectObject(object, true)
377
+ .find((candidate) => visibleSurface(candidate.object))
378
+ : undefined
379
+ if (!hit) continue
380
+ nearest = Math.min(nearest, origin.distanceTo(level.worldToLocal(hit.point.clone())))
381
+ }
382
+ return nearest
383
+ }
384
+
385
+ const UP = new Vector3(0, 1, 0)
386
+
387
+ /**
388
+ * The 3D pointer on a room floor. With a draft open (or the mezzanine tool
389
+ * armed), the ray meets that room's plane whatever stands in front of it.
390
+ * Otherwise it meets each room's floor plane (at its own elevation); the
391
+ * nearest landing inside a room's clear floor wins, unless a visible wall of
392
+ * the level is nearer along the ray — then the wall's paint gesture owns the
393
+ * press.
394
+ */
395
+ function resolveFloorRegion3D(
396
+ event: PointerEvent,
397
+ element: HTMLElement,
398
+ camera: Camera,
399
+ ): FloorRegionHit | null {
400
+ const draft = useFloorRegionDraft.getState().draft
401
+ const fixed =
402
+ draft?.room ??
403
+ useOpeningDraft.getState().host ??
404
+ useMezzanineDraft.getState().host ??
405
+ useTerraceDraft.getState().host
406
+ const levelId = fixed?.levelId ?? useViewer.getState().selection.levelId
407
+ const level = levelId ? sceneRegistry.nodes.get(levelId) : null
408
+ if (!(levelId && level)) return null
409
+ level.updateWorldMatrix(true, false)
410
+ const bounds = element.getBoundingClientRect()
411
+ const raycaster = new Raycaster()
412
+ raycaster.setFromCamera(
413
+ new Vector2(
414
+ ((event.clientX - bounds.left) / bounds.width) * 2 - 1,
415
+ 1 - ((event.clientY - bounds.top) / bounds.height) * 2,
416
+ ),
417
+ camera,
418
+ )
419
+ setSurfaceRaycastLayers(raycaster.layers)
420
+ const ray = raycaster.ray.clone().applyMatrix4(new Matrix4().copy(level.matrixWorld).invert())
421
+ const onFloor = (elevation: number) =>
422
+ ray.intersectPlane(new Plane(UP, -elevation), new Vector3())
423
+ if (fixed) {
424
+ const hit = onFloor(fixed.elevation)
425
+ return hit ? { room: fixed, point: [hit.x, hit.z] } : null
426
+ }
427
+ const nodes = useScene.getState().nodes
428
+ let best: {
429
+ room: () => FloorRegionRoom
430
+ point: FloorRegionPoint
431
+ distance: number
432
+ } | null = null
433
+ for (const record of getRoomSelectionIndex(levelId).update(nodes)) {
434
+ const hit = onFloor(roomFloorElevation(nodes, record.zoneId))
435
+ if (!hit) continue
436
+ const point: FloorRegionPoint = [hit.x, hit.z]
437
+ if (!containsPoint(record.clearPolygon, point)) continue
438
+ const distance = ray.origin.distanceTo(hit)
439
+ if (!best || distance < best.distance)
440
+ best = { room: () => describeFloorRegionRoom(nodes, record), point, distance }
441
+ }
442
+ // Painting also draws on a ceiling seen from below: its underside, where it draws.
443
+ if (useEditor.getState().mode === 'material-paint' && ray.direction.y > 1e-6)
444
+ for (const id of sceneRegistry.byType.ceiling ?? []) {
445
+ const ceiling = nodes[id as keyof typeof nodes]
446
+ if (ceiling?.type !== 'ceiling' || ceiling.parentId !== levelId) continue
447
+ const object = sceneRegistry.nodes.get(id)
448
+ if (!(object && visibleSurface(object))) continue
449
+ const hit = onFloor(resolveCeilingHeight(ceiling, nodes) - 0.01)
450
+ if (!hit) continue
451
+ const point: FloorRegionPoint = [hit.x, hit.z]
452
+ if (!containsPoint([{ outer: ceiling.polygon, holes: ceiling.holes }], point)) continue
453
+ const distance = ray.origin.distanceTo(hit)
454
+ if (!best || distance < best.distance)
455
+ best = { room: () => describeCeilingRegionRoom(nodes, ceiling), point, distance }
456
+ }
457
+ if (!best || nearestWallDistance(raycaster, level, levelId, ray.origin) < best.distance)
458
+ return null
459
+ return { room: best.room(), point: best.point }
460
+ }
461
+
462
+ function ringPairs(ring: readonly FloorRegionPoint[], closed: boolean, y: number) {
463
+ const out: number[] = []
464
+ const count = closed ? ring.length : ring.length - 1
465
+ for (let i = 0; i < count; i++) {
466
+ const a = ring[i]!
467
+ const b = ring[(i + 1) % ring.length]!
468
+ out.push(a[0], y, a[1], b[0], y, b[1])
469
+ }
470
+ return out
471
+ }
472
+
473
+ function fillPositions(pieces: MultiPolygon, y: number) {
474
+ const out: number[] = []
475
+ for (const { outer, holes } of pieces) {
476
+ const contour = outer.map(([x, z]) => new Vector2(x, z))
477
+ const holeRings = holes.map((hole) => hole.map(([x, z]) => new Vector2(x, z)))
478
+ const all = [...contour, ...holeRings.flat()]
479
+ for (const triangle of ShapeUtils.triangulateShape(contour, holeRings))
480
+ for (const index of triangle) out.push(all[index]!.x, y, all[index]!.y)
481
+ }
482
+ return out
483
+ }
484
+
485
+ function geometryOf(positions: number[]) {
486
+ const geometry = new BufferGeometry()
487
+ geometry.setAttribute('position', new Float32BufferAttribute(positions, 3))
488
+ return geometry
489
+ }
490
+
491
+ const noRaycast = () => {}
492
+
493
+ /** What the draft shows: its outline, the clipped pieces, the label and its colour. */
494
+ function useDraftView(draft: FloorRegionDraft | null) {
495
+ const unit = useViewer((s) => s.unit)
496
+ return useMemo(() => {
497
+ if (!draft) return null
498
+ const outline = floorRegionDraftOutline(draft)
499
+ const clip = outline.length >= 3 ? clipFloorRegion(outline, draft.room.clear) : null
500
+ const message = floorRegionDraftMessage(draft)
501
+ const xs = outline.map((point) => point[0])
502
+ const zs = outline.map((point) => point[1])
503
+ const anchor: FloorRegionPoint | null = outline.length
504
+ ? [(Math.min(...xs) + Math.max(...xs)) / 2, (Math.min(...zs) + Math.max(...zs)) / 2]
505
+ : null
506
+ return {
507
+ outline,
508
+ closed: draft.kind === 'rectangle' || outline.length >= 3,
509
+ clip,
510
+ message,
511
+ label: message ?? (clip ? formatAreaLabel(clip.area, unit) : null),
512
+ anchor: message && draft.kind === 'polygon' ? (draft.cursor ?? anchor) : anchor,
513
+ points: draft.kind === 'polygon' ? draft.points : [],
514
+ cursor: draft.kind === 'polygon' ? draft.cursor : draft.end,
515
+ fill: draft.room.purpose?.fill ?? null,
516
+ }
517
+ }, [draft, unit])
518
+ }
519
+
520
+ function FloorRegionPreview3D({ mode }: { mode: FloorRegionMode }) {
521
+ const draft = useFloorRegionDraft((s) => s.draft)
522
+ const hover = useFloorRegionDraft((s) => s.hover)
523
+ const mezzanineHost = useMezzanineDraft((s) => s.host)
524
+ const openingHost = useOpeningDraft((s) => s.host)
525
+ const host = openingHost ?? mezzanineHost
526
+ const view = useDraftView(draft)
527
+ const isDark = useSceneIsDark()
528
+ const tint = usePaintTint(DRAFT_COLOR)
529
+ const color = view?.fill ?? tint
530
+ const levelId = draft?.room.levelId ?? hover?.levelId ?? host?.levelId ?? null
531
+ const elevation = draft?.room.elevation ?? hover?.elevation ?? host?.elevation ?? 0
532
+ // A ceiling's outline hangs just under it, a floor's just over it.
533
+ const onCeiling = !!(draft?.room.ceilingId ?? hover?.ceiling)
534
+ const lift = elevation + (onCeiling ? -PREVIEW_LIFT : PREVIEW_LIFT)
535
+ const root = useRef<Group>(null)
536
+ const materials = useMemo(
537
+ () => ({
538
+ fill: new MeshBasicNodeMaterial({
539
+ depthTest: false,
540
+ depthWrite: false,
541
+ opacity: 0.3,
542
+ side: DoubleSide,
543
+ transparent: true,
544
+ }),
545
+ line: new LineBasicNodeMaterial({ depthTest: false, depthWrite: false }),
546
+ }),
547
+ [],
548
+ )
549
+ useEffect(
550
+ () => () => {
551
+ materials.fill.dispose()
552
+ materials.line.dispose()
553
+ },
554
+ [materials],
555
+ )
556
+ const invalid = !!view?.message
557
+ useEffect(() => {
558
+ materials.fill.color.set(color)
559
+ materials.line.color.set(invalid ? INVALID_COLOR : DRAFT_COLOR)
560
+ }, [materials, color, invalid])
561
+ const geometry = useMemo(
562
+ () =>
563
+ view
564
+ ? {
565
+ fill: view.clip ? geometryOf(fillPositions(view.clip.pieces, lift)) : null,
566
+ line: geometryOf(ringPairs(view.outline, view.closed, lift)),
567
+ }
568
+ : null,
569
+ [view, lift],
570
+ )
571
+ useEffect(
572
+ () => () => {
573
+ geometry?.fill?.dispose()
574
+ geometry?.line.dispose()
575
+ },
576
+ [geometry],
577
+ )
578
+ useFrame(() => {
579
+ const level = levelId ? sceneRegistry.nodes.get(levelId) : null
580
+ if (!root.current) return
581
+ root.current.visible = !!level
582
+ if (level) {
583
+ level.updateWorldMatrix(true, false)
584
+ root.current.matrix.copy(level.matrixWorld)
585
+ }
586
+ })
587
+ if (!levelId) return null
588
+ const Icon = mode === 'polygon' ? Pentagon : Square
589
+ const cursor = view?.cursor ?? (draft ? null : (hover?.point ?? null))
590
+ return (
591
+ <group matrixAutoUpdate={false} ref={root}>
592
+ {/* Where a mezzanine can go: the host's clear floor on the mezzanine's
593
+ plane, drawn over its walls like Divide's room, under the draft. */}
594
+ {host && (
595
+ <RoomFloorHighlight3D
596
+ elevation={host.elevation}
597
+ levelId={host.levelId}
598
+ renderOrder={98}
599
+ zoneId={host.zoneId}
600
+ />
601
+ )}
602
+ {geometry?.fill && (
603
+ <mesh
604
+ geometry={geometry.fill}
605
+ layers={EDITOR_LAYER}
606
+ material={materials.fill}
607
+ raycast={noRaycast}
608
+ renderOrder={100}
609
+ />
610
+ )}
611
+ {geometry && (
612
+ <lineSegments
613
+ geometry={geometry.line}
614
+ layers={EDITOR_LAYER}
615
+ material={materials.line}
616
+ raycast={noRaycast}
617
+ renderOrder={101}
618
+ />
619
+ )}
620
+ {view?.points.map((point, index) => (
621
+ <CursorSphere
622
+ color={DRAFT_COLOR}
623
+ height={0}
624
+ key={index}
625
+ position={[point[0], elevation, point[1]]}
626
+ showTooltip={false}
627
+ />
628
+ ))}
629
+ {cursor && (
630
+ <CursorSphere
631
+ color={invalid ? INVALID_COLOR : DRAFT_COLOR}
632
+ height={0}
633
+ position={[cursor[0], elevation, cursor[1]]}
634
+ tooltipContent={<Icon className="h-5 w-5 text-white" />}
635
+ />
636
+ )}
637
+ {view?.label && view.anchor && (
638
+ <DraftMeasurementLabel
639
+ color={invalid ? INVALID_COLOR : isDark ? '#ffffff' : '#111111'}
640
+ label={view.label}
641
+ position={[view.anchor[0], elevation + DRAFT_LABEL_Y_OFFSET, view.anchor[1]]}
642
+ shadowColor={isDark ? '#111111' : '#ffffff'}
643
+ />
644
+ )}
645
+ </group>
646
+ )
647
+ }
648
+
649
+ /**
650
+ * The 3D close radius: CLOSE_RADIUS_PX at the polygon's first point's depth,
651
+ * so closing feels the same zoomed in or out.
652
+ */
653
+ export function floorRegionCloseRadius3D(
654
+ camera: Camera,
655
+ viewportHeight: number,
656
+ pointWorld: Vector3 | null,
657
+ ): number {
658
+ if (!(pointWorld && viewportHeight > 0)) return CLOSE_RADIUS_3D_FALLBACK
659
+ let perPixel: number
660
+ if ((camera as OrthographicCamera).isOrthographicCamera) {
661
+ const ortho = camera as OrthographicCamera
662
+ perPixel = (ortho.top - ortho.bottom) / ortho.zoom / viewportHeight
663
+ } else {
664
+ const perspective = camera as PerspectiveCamera
665
+ const distance = perspective.getWorldPosition(new Vector3()).distanceTo(pointWorld)
666
+ perPixel = (2 * distance * Math.tan((perspective.fov * Math.PI) / 360)) / viewportHeight
667
+ }
668
+ return Number.isFinite(perPixel) && perPixel > 0
669
+ ? CLOSE_RADIUS_PX * perPixel
670
+ : CLOSE_RADIUS_3D_FALLBACK
671
+ }
672
+
673
+ function firstPointWorld(): Vector3 | null {
674
+ const draft = useFloorRegionDraft.getState().draft
675
+ const first = draft?.kind === 'polygon' ? draft.points[0] : undefined
676
+ const level = draft ? sceneRegistry.nodes.get(draft.room.levelId) : undefined
677
+ if (!(draft && first && level)) return null
678
+ level.updateWorldMatrix(true, false)
679
+ return level.localToWorld(new Vector3(first[0], draft.room.elevation, first[1]))
680
+ }
681
+
682
+ export function FloorRegionControls3D() {
683
+ const mode = useFloorRegionMode()
684
+ const { gl, camera } = useThree()
685
+ useFloorRegionLifetime(mode)
686
+ useEffect(() => {
687
+ if (!mode) return
688
+ return bindFloorRegionPointer(
689
+ gl.domElement,
690
+ (event) =>
691
+ event.target === gl.domElement ? resolveFloorRegion3D(event, gl.domElement, camera) : null,
692
+ () =>
693
+ floorRegionCloseRadius3D(
694
+ camera,
695
+ gl.domElement.getBoundingClientRect().height,
696
+ firstPointWorld(),
697
+ ),
698
+ )
699
+ }, [gl, camera, mode])
700
+ return mode ? <FloorRegionPreview3D mode={mode} /> : null
701
+ }
702
+
703
+ /**
704
+ * The plan pointer on a room floor of `levelId` (the draft's floor while one
705
+ * is open, the host room while the mezzanine tool is armed).
706
+ */
707
+ export function resolveFloorRegionPlanPoint(
708
+ levelId: string | null,
709
+ clientX: number,
710
+ clientY: number,
711
+ ): FloorRegionHit | null {
712
+ const point = clientToPlan(clientX, clientY)
713
+ if (!(point && levelId)) return null
714
+ const plan: FloorRegionPoint = [point[0], point[1]]
715
+ const fixed =
716
+ useFloorRegionDraft.getState().draft?.room ??
717
+ useOpeningDraft.getState().host ??
718
+ useMezzanineDraft.getState().host ??
719
+ useTerraceDraft.getState().host
720
+ if (fixed) return fixed.levelId === levelId ? { room: fixed, point: plan } : null
721
+ const room = resolveFloorRegionRoom(levelId, plan)
722
+ return room ? { room, point: plan } : null
723
+ }
724
+
725
+ const MARKER_GLOW_PX = 10
726
+ const MARKER_CORE_PX = 3
727
+
728
+ function PlanMarker({ at, color, upp }: { at: FloorRegionPoint; color: string; upp: number }) {
729
+ return (
730
+ <>
731
+ <circle cx={at[0]} cy={at[1]} fill={color} fillOpacity={0.25} r={MARKER_GLOW_PX * upp} />
732
+ <circle cx={at[0]} cy={at[1]} fill={color} fillOpacity={0.9} r={MARKER_CORE_PX * upp} />
733
+ </>
734
+ )
735
+ }
736
+
737
+ function ringsPath(pieces: MultiPolygon) {
738
+ return pieces
739
+ .flatMap(({ outer, holes }) => [outer, ...holes])
740
+ .map((ring) => `M ${ring.map((point) => point.join(' ')).join(' L ')} Z`)
741
+ .join(' ')
742
+ }
743
+
744
+ function FloorRegionPreview2D({ levelId }: { levelId: string | null }) {
745
+ const context = useFloorplanRender()
746
+ const draft = useFloorRegionDraft((s) => s.draft)
747
+ const hover = useFloorRegionDraft((s) => s.hover)
748
+ const view = useDraftView(draft && draft.room.levelId === levelId ? draft : null)
749
+ const isDark = useSceneIsDark()
750
+ const upp = context?.unitsPerPixel ?? 0.01
751
+ const outline = isDark ? DRAFT_COLOR : DRAFT_COLOR_LIGHT_PLAN
752
+ const tint = usePaintTint(outline)
753
+ const fill = view?.fill ?? tint
754
+ const mezzanineHost = useMezzanineDraft((s) => (s.host?.levelId === levelId ? s.host : null))
755
+ const openingHost = useOpeningDraft((s) => (s.host?.levelId === levelId ? s.host : null))
756
+ const host = openingHost ?? mezzanineHost
757
+ const hostArea = host && (
758
+ <path
759
+ d={ringsPath(host.clear)}
760
+ data-mezzanine-host
761
+ fill={outline}
762
+ fillOpacity={0.12}
763
+ fillRule="evenodd"
764
+ stroke={outline}
765
+ strokeLinejoin="round"
766
+ strokeWidth={2 * upp}
767
+ />
768
+ )
769
+ if (!view) {
770
+ if (draft || !(hostArea || (hover && hover.levelId === levelId))) return null
771
+ return (
772
+ <g data-floor-region-preview pointerEvents="none">
773
+ {hostArea}
774
+ {hover && hover.levelId === levelId && (
775
+ <PlanMarker at={hover.point} color={outline} upp={upp} />
776
+ )}
777
+ </g>
778
+ )
779
+ }
780
+ const stroke = view.message ? INVALID_COLOR : outline
781
+ const path = `M ${view.outline.map((point) => point.join(' ')).join(' L ')}${view.closed ? ' Z' : ''}`
782
+ return (
783
+ <g data-floor-region-preview pointerEvents="none">
784
+ {hostArea}
785
+ {view.clip && (
786
+ <path d={ringsPath(view.clip.pieces)} fill={fill} fillOpacity={0.3} fillRule="evenodd" />
787
+ )}
788
+ {view.outline.length > 1 && (
789
+ <path
790
+ d={path}
791
+ fill="none"
792
+ stroke={stroke}
793
+ strokeLinecap="round"
794
+ strokeLinejoin="round"
795
+ strokeWidth={2 * upp}
796
+ />
797
+ )}
798
+ {view.points.map((point, index) => (
799
+ <PlanMarker at={point} color={outline} key={index} upp={upp} />
800
+ ))}
801
+ {view.cursor && <PlanMarker at={view.cursor} color={stroke} upp={upp} />}
802
+ {view.label && view.anchor && (
803
+ <g
804
+ transform={`translate(${view.anchor[0]} ${view.anchor[1]}) rotate(${-(context?.sceneRotationDeg ?? 0)}) scale(${upp})`}
805
+ >
806
+ <text
807
+ dominantBaseline="middle"
808
+ fill={view.message ? INVALID_COLOR : (context?.palette.measurementStroke ?? outline)}
809
+ fontSize={12}
810
+ fontWeight={600}
811
+ paintOrder="stroke"
812
+ stroke={isDark ? '#0f172a' : '#ffffff'}
813
+ strokeLinejoin="round"
814
+ strokeWidth={3}
815
+ textAnchor="middle"
816
+ y={view.message ? -16 : 0}
817
+ >
818
+ {view.label}
819
+ </text>
820
+ </g>
821
+ )}
822
+ </g>
823
+ )
824
+ }
825
+
826
+ export function FloorRegionControls2D({ levelId }: { levelId: string | null }) {
827
+ const mode = useFloorRegionMode()
828
+ const context = useFloorplanRender()
829
+ const ref = useRef<SVGGElement>(null)
830
+ const upp = useRef(context?.unitsPerPixel ?? 0.01)
831
+ upp.current = context?.unitsPerPixel ?? 0.01
832
+ useFloorRegionLifetime(mode)
833
+ useEffect(() => {
834
+ const svg = ref.current?.ownerSVGElement
835
+ if (!(svg && mode)) return
836
+ return bindFloorRegionPointer(
837
+ svg,
838
+ (event) => {
839
+ if (!(event.target instanceof Element) || event.target.closest('foreignObject')) return null
840
+ return resolveFloorRegionPlanPoint(levelId, event.clientX, event.clientY)
841
+ },
842
+ () => CLOSE_RADIUS_PX * upp.current,
843
+ )
844
+ }, [levelId, mode])
845
+ return <g ref={ref}>{mode && <FloorRegionPreview2D levelId={levelId} />}</g>
846
+ }