@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,589 @@
1
+ import {
2
+ type AnyNode,
3
+ type AnyNodeId,
4
+ area,
5
+ type CeilingNode,
6
+ ceilingPaintRegions,
7
+ ceilingRegionsOwner,
8
+ type MultiPolygon,
9
+ resolveCeilingHeight,
10
+ union,
11
+ useScene,
12
+ } from '@pascal-app/core'
13
+ import { create } from 'zustand'
14
+ import useEditor, { isGridSnapActive, isMagneticSnapActive } from '../store/use-editor'
15
+ import {
16
+ CROSSING_MESSAGE,
17
+ clipFloorRegion,
18
+ floorRegionAxisAngle,
19
+ floorRegionEdgeCrosses,
20
+ floorRegionRectangle,
21
+ MIN_FLOOR_REGION_AREA,
22
+ OUTSIDE_MESSAGE,
23
+ } from './floor-region-geometry'
24
+ import {
25
+ type FloorRegionPoint,
26
+ type FloorRegionSnapSettings,
27
+ type FloorRegionSnapTargets,
28
+ floorRegionSnapTargets,
29
+ snapFloorRegionPoint,
30
+ } from './floor-region-snap'
31
+ import { beginGesture, type GestureHandle } from './gesture-lifecycle'
32
+ import { hasActivePaintMaterial } from './material-paint'
33
+ import { isPaintErasing, usePaintRegionMode } from './paint-region-mode'
34
+ import { addCeilingRegion, addFloorRegion } from './paint-regions'
35
+ import { roomFloorElevation } from './room-handle-drag'
36
+
37
+ // An outline drawn on one room's floor: a rectangle (press, drag, release; a
38
+ // persistent tool also takes two clicks) or a polygon (click points, close on
39
+ // the first point or Enter). What the outline becomes is the draft's purpose:
40
+ // "Paint part of the floor" (the default) clips it to the room and paints it
41
+ // as one `zone.floor.regions` entry; on a ceiling (`ceilingId`) the outline is
42
+ // drawn on its underside and paints one ceiling region; "Add mezzanine" (`mezzanine-draft`)
43
+ // builds a mezzanine from it. Either way one commit is one undo step. Surfaces
44
+ // (3D canvas, 2D plan) resolve pointers into plan points and call the actions
45
+ // below.
46
+
47
+ export const PICK_MATERIAL_MESSAGE = 'Pick a material first'
48
+
49
+ export type FloorDraftCommit = { ok: true } | { ok: false; message: string }
50
+
51
+ /** What a floor outline is drawn for, and how it commits. */
52
+ export type FloorDraftPurpose = {
53
+ /** The lifecycle handle a draft runs under; `onCancel` drops the draft. */
54
+ own: (room: FloorRegionRoom, onCancel: () => void) => GestureHandle
55
+ /**
56
+ * The tool outlives its drafts: a draft that ends uncommitted keeps the
57
+ * handle, and a refused commit stays on the draft, red and labelled. Paint
58
+ * drafts are one gesture each and refusals go to the HUD notice.
59
+ */
60
+ persistent?: boolean
61
+ /** Whether a new draft may start; says why not itself. */
62
+ canStart?: () => boolean
63
+ commit: (room: FloorRegionRoom, polygon: readonly FloorRegionPoint[]) => FloorDraftCommit
64
+ /** Why a polygon point cannot be placed at `point`, or null. */
65
+ pointMessage?: (room: FloorRegionRoom, point: FloorRegionPoint) => string | null
66
+ /** Preview fill; the paint tint when unset. */
67
+ fill?: string
68
+ }
69
+
70
+ export type FloorRegionRoom = {
71
+ zoneId: string
72
+ levelId: string
73
+ /** Where the outline may go, level XZ: the room's clear floor for paint. */
74
+ clear: MultiPolygon
75
+ /** Elevation the 3D pointer plane and the preview use. */
76
+ elevation: number
77
+ /** Rectangle frame: the room's longest edge direction. */
78
+ angle: number
79
+ targets: FloorRegionSnapTargets
80
+ /** Painting part of the floor when unset. */
81
+ purpose?: FloorDraftPurpose
82
+ /** The ceiling whose underside the outline is drawn on, when it is not a floor. */
83
+ ceilingId?: string
84
+ }
85
+
86
+ export type FloorRegionDraft =
87
+ | {
88
+ kind: 'rectangle'
89
+ room: FloorRegionRoom
90
+ start: FloorRegionPoint
91
+ end: FloorRegionPoint
92
+ /** Why the released box was refused; the box stays put until the next press. */
93
+ refusal?: string | null
94
+ /**
95
+ * A persistent tool's click without a drag placed the first corner: the
96
+ * box follows the pointer and the next click sets the opposite corner.
97
+ */
98
+ anchored?: boolean
99
+ }
100
+ | {
101
+ kind: 'polygon'
102
+ room: FloorRegionRoom
103
+ points: FloorRegionPoint[]
104
+ cursor: FloorRegionPoint | null
105
+ /** Why the live edge cannot be placed (red preview), or null. */
106
+ message: string | null
107
+ /** Why closing was refused; kept until the points change. */
108
+ refusal?: string | null
109
+ }
110
+
111
+ export type FloorRegionHover = {
112
+ levelId: string
113
+ elevation: number
114
+ point: FloorRegionPoint
115
+ /** Over a ceiling's underside rather than a floor. */
116
+ ceiling?: boolean
117
+ }
118
+
119
+ export const useFloorRegionDraft = create<{
120
+ draft: FloorRegionDraft | null
121
+ hover: FloorRegionHover | null
122
+ }>(() => ({ draft: null, hover: null }))
123
+
124
+ /** The room a floor region is drawn in, read from its zone and room geometry. */
125
+ export function describeFloorRegionRoom(
126
+ nodes: Record<string, AnyNode>,
127
+ room: { zoneId: string; key: { levelId: string }; clearPolygon: MultiPolygon },
128
+ ): FloorRegionRoom {
129
+ const zone = nodes[room.zoneId]
130
+ const regions = zone?.type === 'zone' ? (zone.floor?.regions ?? []) : []
131
+ return {
132
+ zoneId: room.zoneId,
133
+ levelId: room.key.levelId,
134
+ clear: room.clearPolygon,
135
+ elevation: roomFloorElevation(nodes, room.zoneId),
136
+ angle: floorRegionAxisAngle(room.clearPolygon),
137
+ targets: floorRegionSnapTargets(
138
+ room.clearPolygon,
139
+ regions.map((region) => region.polygon),
140
+ ),
141
+ }
142
+ }
143
+
144
+ /** The ceilings a ceiling region covers: an automatic ceiling's room may be cut into several. */
145
+ function regionCeilings(nodes: Record<string, AnyNode>, ceiling: CeilingNode): CeilingNode[] {
146
+ const owner = ceilingRegionsOwner(ceiling)
147
+ if (owner.kind === 'ceiling') return [ceiling]
148
+ return Object.values(nodes).filter(
149
+ (node): node is CeilingNode =>
150
+ node.type === 'ceiling' &&
151
+ node.parentId === ceiling.parentId &&
152
+ node.boundary === 'auto' &&
153
+ node.zoneId === owner.id,
154
+ )
155
+ }
156
+
157
+ /**
158
+ * A ceiling's underside as a draft room: its outline minus holes (every part of
159
+ * an automatic ceiling's room), at the plane the ceiling draws at.
160
+ */
161
+ export function describeCeilingRegionRoom(
162
+ nodes: Record<string, AnyNode>,
163
+ ceiling: CeilingNode,
164
+ ): FloorRegionRoom {
165
+ const clear = union(
166
+ regionCeilings(nodes, ceiling).map((part) => ({ outer: part.polygon, holes: part.holes })),
167
+ )
168
+ const owner = ceilingRegionsOwner(ceiling)
169
+ return {
170
+ zoneId: owner.id,
171
+ levelId: ceiling.parentId ?? '',
172
+ clear,
173
+ elevation: resolveCeilingHeight(ceiling, nodes) - 0.01,
174
+ angle: floorRegionAxisAngle(clear),
175
+ targets: floorRegionSnapTargets(
176
+ clear,
177
+ ceilingPaintRegions(ceiling, nodes).map((region) => region.polygon),
178
+ ),
179
+ purpose: PAINT_CEILING_REGION,
180
+ ceilingId: ceiling.id,
181
+ }
182
+ }
183
+
184
+ /** The snapping the active context asks for: `lines` (magnetic), `grid`, or `off`. */
185
+ export function currentFloorRegionSnap(free: boolean): FloorRegionSnapSettings {
186
+ return {
187
+ mode: isMagneticSnapActive() ? 'lines' : isGridSnapActive() ? 'grid' : 'off',
188
+ step: useEditor.getState().gridSnapStep,
189
+ free,
190
+ }
191
+ }
192
+
193
+ function snap(room: FloorRegionRoom, raw: FloorRegionPoint, settings: FloorRegionSnapSettings) {
194
+ return snapFloorRegionPoint(raw, settings, room.targets).point
195
+ }
196
+
197
+ function samePoint(a: FloorRegionPoint, b: FloorRegionPoint) {
198
+ return Math.hypot(a[0] - b[0], a[1] - b[1]) < 1e-6
199
+ }
200
+
201
+ /** The outline the draft currently describes (the live pointer included). */
202
+ export function floorRegionDraftOutline(draft: FloorRegionDraft): FloorRegionPoint[] {
203
+ if (draft.kind === 'rectangle')
204
+ return floorRegionRectangle(draft.start, draft.end, draft.room.angle)
205
+ const { points, cursor } = draft
206
+ return cursor && !(points.length && samePoint(points.at(-1)!, cursor))
207
+ ? [...points, cursor]
208
+ : [...points]
209
+ }
210
+
211
+ /** Whether a polygon click at `point` closes the ring on its first point. */
212
+ export function closesOnFirst(
213
+ points: readonly FloorRegionPoint[],
214
+ point: FloorRegionPoint,
215
+ closeRadius: number,
216
+ ) {
217
+ const first = points[0]
218
+ return (
219
+ points.length >= 3 &&
220
+ !!first &&
221
+ Math.hypot(first[0] - point[0], first[1] - point[1]) <= closeRadius
222
+ )
223
+ }
224
+
225
+ /** Why the live polygon edge to `cursor` cannot be placed, or null. */
226
+ export function polygonLiveMessage(
227
+ points: readonly FloorRegionPoint[],
228
+ cursor: FloorRegionPoint | null,
229
+ closeRadius: number,
230
+ room?: FloorRegionRoom,
231
+ ): string | null {
232
+ if (!cursor) return null
233
+ const closing = closesOnFirst(points, cursor, closeRadius)
234
+ if (points.length && floorRegionEdgeCrosses(points, closing ? points[0]! : cursor, closing))
235
+ return CROSSING_MESSAGE
236
+ return (!closing && room && purposeOf(room).pointMessage?.(room, cursor)) || null
237
+ }
238
+
239
+ /** Why the draft is red: the live edge, or the refused commit. */
240
+ export function floorRegionDraftMessage(draft: FloorRegionDraft): string | null {
241
+ return (draft.kind === 'polygon' ? draft.message : null) ?? draft.refusal ?? null
242
+ }
243
+
244
+ function setNotice(notice: string | null) {
245
+ if (usePaintRegionMode.getState().notice !== notice)
246
+ usePaintRegionMode.getState().setNotice(notice)
247
+ }
248
+
249
+ /** The paint the region takes, or null (no material, or the eraser). */
250
+ function activePaint() {
251
+ const editor = useEditor.getState()
252
+ if (isPaintErasing() || !hasActivePaintMaterial(editor.activePaintMaterial)) return null
253
+ return {
254
+ material: editor.activePaintMaterial.material,
255
+ materialPreset: editor.activePaintMaterial.materialPreset,
256
+ }
257
+ }
258
+
259
+ /**
260
+ * Commits the draft's outline and ends the draft; a refused commit keeps it. A
261
+ * throwing write cancels the draft, so it can never stay half-alive.
262
+ */
263
+ function commitDraft(room: FloorRegionRoom, polygon: readonly FloorRegionPoint[]) {
264
+ let settled = false
265
+ try {
266
+ const result = purposeOf(room).commit(room, polygon)
267
+ settled = true
268
+ if (result.ok) endDraft(room)
269
+ return result
270
+ } finally {
271
+ if (!settled) cancelFloorRegion()
272
+ }
273
+ }
274
+
275
+ /** Clips `polygon` to the room and paints it; the HUD notice says why when it cannot. */
276
+ export function commitFloorRegion(room: FloorRegionRoom, polygon: readonly FloorRegionPoint[]) {
277
+ const paint = activePaint()
278
+ if (!paint) {
279
+ setNotice(PICK_MATERIAL_MESSAGE)
280
+ return false
281
+ }
282
+ const clip = clipFloorRegion(polygon, room.clear)
283
+ if (!clip) {
284
+ setNotice(OUTSIDE_MESSAGE)
285
+ return false
286
+ }
287
+ const result = addFloorRegion(room.zoneId, clip.polygon, paint)
288
+ setNotice(result.ok ? null : result.message)
289
+ return result.ok
290
+ }
291
+
292
+ export const FLOOR_PAINT_REGION_HANDLE = 'paint-floor-region'
293
+
294
+ /** "Paint part of the floor": each draft is its own gesture in the paint sub-mode. */
295
+ const PAINT_FLOOR_REGION: FloorDraftPurpose = {
296
+ own: (room, onCancel) =>
297
+ beginGesture({
298
+ kind: 'floor-paint-region',
299
+ scope: { kind: 'handle-drag', nodeId: room.zoneId, handle: FLOOR_PAINT_REGION_HANDLE },
300
+ paintSubMode: true,
301
+ stale: () => floorRegionRoomGone(useScene.getState().nodes) || !floorRegionEditable(),
302
+ onCancel,
303
+ }),
304
+ canStart: () => {
305
+ if (activePaint()) return true
306
+ setNotice(PICK_MATERIAL_MESSAGE)
307
+ return false
308
+ },
309
+ commit: (room, polygon) =>
310
+ commitFloorRegion(room, polygon)
311
+ ? { ok: true }
312
+ : { ok: false, message: usePaintRegionMode.getState().notice ?? '' },
313
+ }
314
+
315
+ /** Clips `polygon` to the ceiling and paints it as one region of its owner. */
316
+ export function commitCeilingRegion(room: FloorRegionRoom, polygon: readonly FloorRegionPoint[]) {
317
+ const paint = activePaint()
318
+ if (!paint) {
319
+ setNotice(PICK_MATERIAL_MESSAGE)
320
+ return false
321
+ }
322
+ const clip = clipFloorRegion(polygon, room.clear)
323
+ if (!(clip && room.ceilingId)) {
324
+ setNotice(OUTSIDE_MESSAGE)
325
+ return false
326
+ }
327
+ const result = addCeilingRegion(room.ceilingId, clip.polygon, paint)
328
+ setNotice(result.ok ? null : result.message)
329
+ return result.ok
330
+ }
331
+
332
+ export const CEILING_PAINT_REGION_HANDLE = 'paint-ceiling-region'
333
+
334
+ /** "Paint part of the ceiling": the floor paint gesture, drawn on a ceiling's underside. */
335
+ const PAINT_CEILING_REGION: FloorDraftPurpose = {
336
+ own: (room, onCancel) =>
337
+ beginGesture({
338
+ kind: 'ceiling-paint-region',
339
+ scope: {
340
+ kind: 'handle-drag',
341
+ nodeId: room.ceilingId ?? room.zoneId,
342
+ handle: CEILING_PAINT_REGION_HANDLE,
343
+ },
344
+ paintSubMode: true,
345
+ stale: () => floorRegionRoomGone(useScene.getState().nodes) || !floorRegionEditable(),
346
+ onCancel,
347
+ }),
348
+ canStart: PAINT_FLOOR_REGION.canStart,
349
+ commit: (room, polygon) =>
350
+ commitCeilingRegion(room, polygon)
351
+ ? { ok: true }
352
+ : { ok: false, message: usePaintRegionMode.getState().notice ?? '' },
353
+ }
354
+
355
+ function purposeOf(room: FloorRegionRoom) {
356
+ return room.purpose ?? PAINT_FLOOR_REGION
357
+ }
358
+
359
+ // The draft's lifecycle handle: a press, or a whole polygon across clicks.
360
+ let owner: GestureHandle | null = null
361
+
362
+ function clearDraft() {
363
+ if (useFloorRegionDraft.getState().draft) useFloorRegionDraft.setState({ draft: null })
364
+ }
365
+
366
+ function ownDraft(room: FloorRegionRoom) {
367
+ if (owner?.active) return owner
368
+ const handle = purposeOf(room).own(room, () => {
369
+ if (owner === handle) owner = null
370
+ clearDraft()
371
+ })
372
+ owner = handle
373
+ return handle
374
+ }
375
+
376
+ /** Ends the draft normally (committed, or nothing left to draw); a persistent tool stays. */
377
+ function endDraft(room: FloorRegionRoom) {
378
+ const current = owner
379
+ owner = null
380
+ clearDraft()
381
+ if (!purposeOf(room).persistent) current?.end()
382
+ }
383
+
384
+ /** The draft's lifecycle handle, for the surface's pointer capture. */
385
+ export function floorRegionGesture(): GestureHandle | null {
386
+ return owner?.active ? owner : null
387
+ }
388
+
389
+ export function cancelFloorRegion() {
390
+ const current = owner
391
+ owner = null
392
+ if (current?.active) current.cancel()
393
+ else clearDraft()
394
+ }
395
+
396
+ /** Drops the draft in hand: a paint draft is cancelled, a persistent tool stays armed. */
397
+ export function dropFloorRegionDraft() {
398
+ const draft = useFloorRegionDraft.getState().draft
399
+ if (!(draft && purposeOf(draft.room).persistent)) return cancelFloorRegion()
400
+ owner = null
401
+ clearDraft()
402
+ }
403
+
404
+ export function setFloorRegionHover(hover: FloorRegionHover | null) {
405
+ const current = useFloorRegionDraft.getState().hover
406
+ if (
407
+ current === hover ||
408
+ (current &&
409
+ hover &&
410
+ current.levelId === hover.levelId &&
411
+ current.elevation === hover.elevation &&
412
+ samePoint(current.point, hover.point))
413
+ )
414
+ return
415
+ useFloorRegionDraft.setState({ hover })
416
+ }
417
+
418
+ /**
419
+ * A press on a room's floor. Rectangle: the first corner (the opposite one
420
+ * once a click anchored the first). Polygon: previews the point the release
421
+ * will place (starting a draft in this room if none).
422
+ * Returns whether the press belongs to the region draft.
423
+ */
424
+ export function pressFloorRegion(
425
+ mode: 'rectangle' | 'polygon',
426
+ room: FloorRegionRoom,
427
+ raw: FloorRegionPoint,
428
+ settings: FloorRegionSnapSettings,
429
+ closeRadius: number,
430
+ ): boolean {
431
+ const current = useFloorRegionDraft.getState().draft
432
+ if (!(current || (purposeOf(room).canStart?.() ?? true))) return true
433
+ const draftRoom = mode === 'polygon' && current?.kind === 'polygon' ? current.room : room
434
+ if (current && current.room !== draftRoom) cancelFloorRegion()
435
+ ownDraft(draftRoom)
436
+ const point = snap(draftRoom, raw, settings)
437
+ if (mode === 'rectangle') {
438
+ if (current?.kind === 'rectangle' && current.anchored && !current.refusal) {
439
+ useFloorRegionDraft.setState({ draft: { ...current, end: point } })
440
+ return true
441
+ }
442
+ useFloorRegionDraft.setState({
443
+ draft: { kind: 'rectangle', room: draftRoom, start: point, end: point },
444
+ })
445
+ return true
446
+ }
447
+ const points = current?.kind === 'polygon' ? current.points : []
448
+ useFloorRegionDraft.setState({
449
+ draft: {
450
+ kind: 'polygon',
451
+ room: draftRoom,
452
+ points,
453
+ cursor: point,
454
+ message: polygonLiveMessage(points, point, closeRadius, draftRoom),
455
+ refusal: current?.kind === 'polygon' ? current.refusal : null,
456
+ },
457
+ })
458
+ return true
459
+ }
460
+
461
+ /** The pointer moved: the rectangle's opposite corner, or the polygon's live point. */
462
+ export function moveFloorRegion(
463
+ raw: FloorRegionPoint,
464
+ settings: FloorRegionSnapSettings,
465
+ closeRadius: number,
466
+ ) {
467
+ const draft = useFloorRegionDraft.getState().draft
468
+ if (!draft) return
469
+ const point = snap(draft.room, raw, settings)
470
+ if (draft.kind === 'rectangle') {
471
+ if (!(draft.refusal || samePoint(point, draft.end)))
472
+ useFloorRegionDraft.setState({ draft: { ...draft, end: point } })
473
+ return
474
+ }
475
+ if (draft.cursor && samePoint(point, draft.cursor)) return
476
+ useFloorRegionDraft.setState({
477
+ draft: {
478
+ ...draft,
479
+ cursor: point,
480
+ message: polygonLiveMessage(draft.points, point, closeRadius, draft.room),
481
+ },
482
+ })
483
+ }
484
+
485
+ function finishPolygon(
486
+ draft: Extract<FloorRegionDraft, { kind: 'polygon' }>,
487
+ points: FloorRegionPoint[],
488
+ ) {
489
+ const result = commitDraft(draft.room, points)
490
+ if (!result.ok)
491
+ useFloorRegionDraft.setState({
492
+ draft: {
493
+ ...draft,
494
+ points,
495
+ cursor: null,
496
+ message: null,
497
+ refusal: purposeOf(draft.room).persistent ? result.message : null,
498
+ },
499
+ })
500
+ }
501
+
502
+ /**
503
+ * The press was released. Rectangle: commits the box (a click without a drag
504
+ * commits nothing, or anchors the first corner in a persistent tool). Polygon: places the point, or closes and commits the ring
505
+ * on the first point. A crossing edge is refused and stays red.
506
+ */
507
+ export function releaseFloorRegion(closeRadius: number) {
508
+ const draft = useFloorRegionDraft.getState().draft
509
+ if (!draft) return
510
+ if (draft.kind === 'rectangle') {
511
+ if (draft.refusal) return
512
+ const box = floorRegionDraftOutline(draft)
513
+ const persistent = purposeOf(draft.room).persistent
514
+ // A click without a drag paints nothing; in a persistent tool it places
515
+ // the first corner and the next click the opposite one, like a Rectangle
516
+ // room. A persistent tool refuses a real box outside the room with a
517
+ // label; paint just drops it.
518
+ const nothing = persistent
519
+ ? area([{ outer: box, holes: [] }]) < MIN_FLOOR_REGION_AREA
520
+ : !clipFloorRegion(box, draft.room.clear)
521
+ if (nothing && persistent && !draft.anchored) {
522
+ useFloorRegionDraft.setState({ draft: { ...draft, anchored: true } })
523
+ return
524
+ }
525
+ if (nothing) return endDraft(draft.room)
526
+ const result = commitDraft(draft.room, box)
527
+ if (result.ok) return
528
+ if (persistent) useFloorRegionDraft.setState({ draft: { ...draft, refusal: result.message } })
529
+ else endDraft(draft.room)
530
+ return
531
+ }
532
+ const { points, cursor } = draft
533
+ if (!cursor) return
534
+ const message = polygonLiveMessage(points, cursor, closeRadius, draft.room)
535
+ if (message) {
536
+ useFloorRegionDraft.setState({ draft: { ...draft, message } })
537
+ return
538
+ }
539
+ if (closesOnFirst(points, cursor, closeRadius)) return finishPolygon(draft, points)
540
+ if (points.length && samePoint(points.at(-1)!, cursor)) return
541
+ useFloorRegionDraft.setState({
542
+ draft: { ...draft, points: [...points, cursor], message: null, refusal: null },
543
+ })
544
+ }
545
+
546
+ /** Enter: closes the polygon on its first point (three points or more). */
547
+ export function finishFloorRegion() {
548
+ const draft = useFloorRegionDraft.getState().draft
549
+ if (draft?.kind !== 'polygon' || draft.points.length < 3) return false
550
+ const { points } = draft
551
+ if (floorRegionEdgeCrosses(points, points[0]!, true)) {
552
+ useFloorRegionDraft.setState({ draft: { ...draft, message: CROSSING_MESSAGE } })
553
+ return true
554
+ }
555
+ finishPolygon(draft, points)
556
+ return true
557
+ }
558
+
559
+ /** Backspace: drops the polygon's last point; the draft ends with its first point. */
560
+ export function removeLastFloorRegionPoint() {
561
+ const draft = useFloorRegionDraft.getState().draft
562
+ if (draft?.kind !== 'polygon') return false
563
+ const points = draft.points.slice(0, -1)
564
+ if (!points.length) {
565
+ endDraft(draft.room)
566
+ return true
567
+ }
568
+ useFloorRegionDraft.setState({
569
+ draft: { ...draft, points, message: null, refusal: null },
570
+ })
571
+ return true
572
+ }
573
+
574
+ /** Whether the draft's room is gone (undo, delete, another level). */
575
+ export function floorRegionRoomGone(nodes: Record<string, AnyNode>) {
576
+ const draft = useFloorRegionDraft.getState().draft
577
+ if (!draft) return false
578
+ if (draft.room.ceilingId) {
579
+ const ceiling = nodes[draft.room.ceilingId as AnyNodeId]
580
+ return ceiling?.type !== 'ceiling' || ceiling.parentId !== draft.room.levelId
581
+ }
582
+ const zone = nodes[draft.room.zoneId as AnyNodeId]
583
+ return zone?.type !== 'zone' || zone.parentId !== draft.room.levelId
584
+ }
585
+
586
+ /** Whether the scene can take a region now. */
587
+ export function floorRegionEditable() {
588
+ return !useScene.getState().readOnly
589
+ }
@@ -0,0 +1,101 @@
1
+ import type { MultiPolygon } from '@pascal-app/core'
2
+
3
+ // Snapping for a floor region's points ("Paint part of the floor"): the
4
+ // polygon snap context's three modes. `grid` rounds to the grid step, `lines`
5
+ // pulls onto the room's corners and edges and the room's existing region
6
+ // vertices, `off` keeps the raw point. Alt (free) always keeps the raw point.
7
+
8
+ export type FloorRegionPoint = [number, number]
9
+ export type FloorRegionSnapMode = 'grid' | 'lines' | 'off'
10
+
11
+ export type FloorRegionSnapSettings = {
12
+ mode: FloorRegionSnapMode
13
+ /** Grid step in metres (`gridSnapStep`). */
14
+ step: number
15
+ /** Alt held: no snapping at all. */
16
+ free?: boolean
17
+ }
18
+
19
+ export type FloorRegionSnapTargets = {
20
+ vertices: FloorRegionPoint[]
21
+ edges: [FloorRegionPoint, FloorRegionPoint][]
22
+ }
23
+
24
+ export type FloorRegionSnapResult = {
25
+ point: FloorRegionPoint
26
+ snap: 'grid' | 'vertex' | 'edge' | null
27
+ }
28
+
29
+ export const FLOOR_REGION_SNAP_RADIUS = 0.1
30
+
31
+ /** Corners and edges of the room's clear floor, plus the vertices and edges of its regions. */
32
+ export function floorRegionSnapTargets(
33
+ clear: MultiPolygon,
34
+ regions: readonly (readonly FloorRegionPoint[])[] = [],
35
+ ): FloorRegionSnapTargets {
36
+ const vertices: FloorRegionPoint[] = []
37
+ const edges: [FloorRegionPoint, FloorRegionPoint][] = []
38
+ const rings = [...clear.flatMap(({ outer, holes }) => [outer, ...holes]), ...regions]
39
+ for (const ring of rings) {
40
+ for (let i = 0; i < ring.length; i++) {
41
+ const a = ring[i]!
42
+ const b = ring[(i + 1) % ring.length]!
43
+ vertices.push([a[0], a[1]])
44
+ edges.push([
45
+ [a[0], a[1]],
46
+ [b[0], b[1]],
47
+ ])
48
+ }
49
+ }
50
+ return { vertices, edges }
51
+ }
52
+
53
+ function closestOnSegment(
54
+ p: FloorRegionPoint,
55
+ a: FloorRegionPoint,
56
+ b: FloorRegionPoint,
57
+ ): FloorRegionPoint {
58
+ const dx = b[0] - a[0]
59
+ const dz = b[1] - a[1]
60
+ const lengthSq = dx * dx + dz * dz
61
+ const t = lengthSq
62
+ ? Math.max(0, Math.min(1, ((p[0] - a[0]) * dx + (p[1] - a[1]) * dz) / lengthSq))
63
+ : 0
64
+ return [a[0] + t * dx, a[1] + t * dz]
65
+ }
66
+
67
+ export function snapFloorRegionPoint(
68
+ raw: FloorRegionPoint,
69
+ settings: FloorRegionSnapSettings,
70
+ targets: FloorRegionSnapTargets,
71
+ radius = FLOOR_REGION_SNAP_RADIUS,
72
+ ): FloorRegionSnapResult {
73
+ if (settings.free || settings.mode === 'off') return { point: [raw[0], raw[1]], snap: null }
74
+ if (settings.mode === 'grid') {
75
+ const step = settings.step
76
+ if (!(step > 0)) return { point: [raw[0], raw[1]], snap: null }
77
+ const round = (value: number) => Number((Math.round(value / step) * step).toFixed(6))
78
+ return { point: [round(raw[0]), round(raw[1])], snap: 'grid' }
79
+ }
80
+ // Corners win over edges: a pointer near a corner lands on the corner.
81
+ let best: FloorRegionPoint | null = null
82
+ let bestDistance = radius
83
+ for (const vertex of targets.vertices) {
84
+ const distance = Math.hypot(vertex[0] - raw[0], vertex[1] - raw[1])
85
+ if (distance <= bestDistance) {
86
+ best = vertex
87
+ bestDistance = distance
88
+ }
89
+ }
90
+ if (best) return { point: [best[0], best[1]], snap: 'vertex' }
91
+ bestDistance = radius
92
+ for (const [a, b] of targets.edges) {
93
+ const candidate = closestOnSegment(raw, a, b)
94
+ const distance = Math.hypot(candidate[0] - raw[0], candidate[1] - raw[1])
95
+ if (distance <= bestDistance) {
96
+ best = candidate
97
+ bestDistance = distance
98
+ }
99
+ }
100
+ return best ? { point: best, snap: 'edge' } : { point: [raw[0], raw[1]], snap: null }
101
+ }