@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,523 @@
1
+ import {
2
+ containsPoint,
3
+ createZoneDivisionContext,
4
+ DEFAULT_ANGLE_STEP,
5
+ divideZone,
6
+ generateId,
7
+ type Point,
8
+ type StructureNodes,
9
+ type StructurePlan,
10
+ snapPointAlongAngleRay,
11
+ snapPointToGrid,
12
+ snapZoneBoundary,
13
+ useScene,
14
+ type ZoneDivisionContext,
15
+ } from '@pascal-app/core'
16
+ import useEditor, {
17
+ isAngleSnapActive,
18
+ isGridSnapActive,
19
+ isMagneticSnapActive,
20
+ } from '../store/use-editor'
21
+ import useInteractionScope from '../store/use-interaction-scope'
22
+ import useWallSnapIndicator from '../store/use-wall-snap-indicator'
23
+ import { beginGesture, type GestureHandle } from './gesture-lifecycle'
24
+ import { applyRoomPlan } from './room-structure-commands'
25
+ import { sfxEmitter } from './sfx-bus'
26
+
27
+ let cached: {
28
+ nodes: StructureNodes
29
+ zoneId: string
30
+ revision: string
31
+ context: ZoneDivisionContext
32
+ } | null = null
33
+
34
+ // Everything the division context is built from: the room's own outline and
35
+ // every wall / separator on its level. Intent edits (a name, a finish) leave it
36
+ // alone; any boundary move changes it.
37
+ function topologyRevision(
38
+ nodes: StructureNodes,
39
+ zone: Extract<StructureNodes[string], { type: 'zone' }>,
40
+ ) {
41
+ return JSON.stringify([
42
+ zone.parentId,
43
+ zone.polygon,
44
+ zone.holes,
45
+ Object.values(nodes).flatMap((node) =>
46
+ node.parentId === zone.parentId && (node.type === 'wall' || node.type === 'separator')
47
+ ? [
48
+ [
49
+ node.id,
50
+ node.type,
51
+ node.start,
52
+ node.end,
53
+ node.type === 'wall' ? [node.curveOffset, node.thickness, node.justification] : null,
54
+ ],
55
+ ]
56
+ : [],
57
+ ),
58
+ ])
59
+ }
60
+
61
+ export function roomDivideContext(nodes: StructureNodes, zoneId: string) {
62
+ if (cached?.nodes === nodes && cached.zoneId === zoneId) return cached.context
63
+ const zone = nodes[zoneId]
64
+ if (zone?.type !== 'zone') throw Error('The selected room no longer exists.')
65
+ const revision = topologyRevision(nodes, zone)
66
+ const context =
67
+ cached?.zoneId === zoneId && cached.revision === revision
68
+ ? cached.context
69
+ : createZoneDivisionContext(nodes, zoneId)
70
+ cached = { nodes, zoneId, revision, context }
71
+ return context
72
+ }
73
+
74
+ // The topology the active session started on. Points, boundary ids and
75
+ // validity all refer to it, so the draft cannot outlive it.
76
+ let sessionRevision: string | null = null
77
+
78
+ /** Whether the room being divided is gone or its boundaries changed since the draft began. */
79
+ export function roomDivideTopologyChanged(nodes: StructureNodes) {
80
+ const scope = useInteractionScope.getState().scope
81
+ if (scope.kind !== 'room-divide') return false
82
+ const zone = nodes[scope.nodeId]
83
+ return zone?.type !== 'zone' || topologyRevision(nodes, zone) !== sessionRevision
84
+ }
85
+
86
+ // Short cursor-side wording, the way the wall tool labels a draft it cannot
87
+ // build. Keyed by the planner's conflict codes; anything else is generic.
88
+ const DIVIDE_MESSAGES: Record<string, string> = {
89
+ 'snap-distance': 'Move to an edge',
90
+ 'boundary-overlap': 'Already an edge',
91
+ 'short-cut': 'Too short',
92
+ 'outside-room': 'Leaves the room',
93
+ 'open-room': 'Room is open',
94
+ 'self-cross': 'Crosses itself',
95
+ 'self-intersection': 'Crosses itself',
96
+ 'small-island': 'Too small',
97
+ 'wall-clearance': 'Too close to a wall',
98
+ 'close-loop': 'Close the loop',
99
+ }
100
+ export function roomDivideMessage(code?: string) {
101
+ return (code && DIVIDE_MESSAGES[code]) || "Can't divide here"
102
+ }
103
+
104
+ // A floor pointer this close to the boundary lands on it; a boundary surface
105
+ // hit (a wall face in 3D) always does. Past it the point is free, so a path can
106
+ // turn inside the room and an island can start there.
107
+ const EDGE_CAPTURE = 0.5
108
+ // A pointer this close to an island's first point closes the loop.
109
+ const CLOSE_CAPTURE = 0.3
110
+ // `lines` squares a segment to the edge / aligns a point with earlier points
111
+ // within this much.
112
+ const LINES_TOLERANCE = 0.15
113
+ const MIN_SEGMENT = 0.05
114
+
115
+ type DivideScope = Extract<
116
+ ReturnType<typeof useInteractionScope.getState>['scope'],
117
+ { kind: 'room-divide' }
118
+ >
119
+
120
+ /** Plans a divide along `path`: an open cut between two boundary points, or a closed island. */
121
+ function planRoomDivide(
122
+ nodes: StructureNodes,
123
+ zoneId: string,
124
+ input: { path: Point[]; closed: boolean; startBoundaryId?: string; endBoundaryId?: string },
125
+ context: ZoneDivisionContext,
126
+ mintId: Parameters<typeof divideZone>[1]['mintId'],
127
+ ): StructurePlan {
128
+ return divideZone(nodes, { zoneId, ...input, mintId }, context)
129
+ }
130
+
131
+ // Previews mint throwaway ids — distinct ones, since a path mints one per segment.
132
+ function previewMint() {
133
+ let n = 0
134
+ return (kind: string) => `${kind}_preview_${n++}`
135
+ }
136
+
137
+ const distance = (a: Point, b: Point) => Math.hypot(a[0] - b[0], a[1] - b[1])
138
+
139
+ // Proper crossing only: touching at an endpoint is not a crossing.
140
+ function segmentsCross(a: Point, b: Point, c: Point, d: Point) {
141
+ const cross = (o: Point, p: Point, q: Point) =>
142
+ (p[0] - o[0]) * (q[1] - o[1]) - (p[1] - o[1]) * (q[0] - o[0])
143
+ const d1 = cross(c, d, a),
144
+ d2 = cross(c, d, b),
145
+ d3 = cross(a, b, c),
146
+ d4 = cross(a, b, d)
147
+ const eps = 1e-9
148
+ return (
149
+ ((d1 > eps && d2 < -eps) || (d1 < -eps && d2 > eps)) &&
150
+ ((d3 > eps && d4 < -eps) || (d3 < -eps && d4 > eps))
151
+ )
152
+ }
153
+
154
+ type Face = NonNullable<ZoneDivisionContext['face']>
155
+ const rings = (face: Face) => [face.referencePolygon, ...face.holes]
156
+ const inside = (face: Face, point: Point) =>
157
+ containsPoint([{ outer: face.referencePolygon, holes: face.holes }], point)
158
+
159
+ /** Why the segment `from → to` cannot join the path, or null when it can. */
160
+ function segmentConflict(face: Face, points: Point[], from: Point, to: Point) {
161
+ if (distance(from, to) < MIN_SEGMENT) return 'short-cut'
162
+ for (const ring of rings(face))
163
+ for (let i = 0; i < ring.length; i++)
164
+ if (segmentsCross(from, to, ring[i]!, ring[(i + 1) % ring.length]!)) return 'outside-room'
165
+ if (!inside(face, [(from[0] + to[0]) / 2, (from[1] + to[1]) / 2])) return 'outside-room'
166
+ // Earlier segments, skipping the one sharing `from`.
167
+ for (let i = 0; i < points.length - 2; i++)
168
+ if (segmentsCross(from, to, points[i]!, points[i + 1]!)) return 'self-cross'
169
+ return null
170
+ }
171
+
172
+ /** Aligns a free point with the x or z of an earlier point (`lines`). */
173
+ function alignToPoints(point: Point, points: Point[]): Point {
174
+ let [x, z] = point
175
+ let bestX = LINES_TOLERANCE,
176
+ bestZ = LINES_TOLERANCE
177
+ for (const other of points) {
178
+ if (Math.abs(other[0] - point[0]) < bestX) {
179
+ bestX = Math.abs(other[0] - point[0])
180
+ x = other[0]
181
+ }
182
+ if (Math.abs(other[1] - point[1]) < bestZ) {
183
+ bestZ = Math.abs(other[1] - point[1])
184
+ z = other[1]
185
+ }
186
+ }
187
+ return [x, z]
188
+ }
189
+
190
+ /**
191
+ * The wall tool's snap beacon where the live point locks on: the first point
192
+ * (closing an island) as a corner, a boundary (starting or finishing a cut)
193
+ * as a point on that wall. Cleared anywhere else.
194
+ */
195
+ function publishDivideSnap(
196
+ end: Point | null,
197
+ endKind?: DivideScope['endKind'],
198
+ endBoundaryId?: string,
199
+ ) {
200
+ const indicator = useWallSnapIndicator.getState()
201
+ if (end && endKind === 'close') indicator.set({ x: end[0], z: end[1], kind: 'endpoint' })
202
+ else if (end && endBoundaryId && (endKind === 'edge' || endKind === 'start'))
203
+ indicator.set({ x: end[0], z: end[1], kind: 'wall', wallIds: [endBoundaryId] })
204
+ else if (indicator.point) indicator.clear()
205
+ }
206
+
207
+ function invalidPreview(code?: string, end: Point | null = null) {
208
+ publishDivideSnap(null)
209
+ useInteractionScope.getState().update({
210
+ kind: 'room-divide',
211
+ end,
212
+ endKind: undefined,
213
+ endBoundaryId: undefined,
214
+ valid: false,
215
+ message: roomDivideMessage(code),
216
+ })
217
+ }
218
+
219
+ let lastPointer: { point: Point; boundaryId?: string } | null = null
220
+
221
+ // The last planner verdict. Snapping lands many pointer moves on the same
222
+ // candidate, and the planner is the expensive step, so an identical candidate
223
+ // on the same context reuses it.
224
+ let planMemo: { context: ZoneDivisionContext; key: string; conflict?: string } | null = null
225
+
226
+ function previewConflict(
227
+ nodes: StructureNodes,
228
+ zoneId: string,
229
+ input: Parameters<typeof planRoomDivide>[2],
230
+ context: ZoneDivisionContext,
231
+ ) {
232
+ const key = JSON.stringify(input)
233
+ if (planMemo?.context === context && planMemo.key === key) return planMemo.conflict
234
+ const conflict = planRoomDivide(nodes, zoneId, input, context, previewMint()).conflicts?.[0]?.code
235
+ planMemo = { context, key, conflict }
236
+ return conflict
237
+ }
238
+
239
+ let owner: GestureHandle | null = null
240
+
241
+ function resetSession() {
242
+ cached = null
243
+ lastPointer = null
244
+ planMemo = null
245
+ sessionRevision = null
246
+ publishDivideSnap(null)
247
+ }
248
+
249
+ /**
250
+ * Starts the draft under the gesture lifecycle owner: a mode, tool, level or
251
+ * selection change, the scope replaced, the room's boundary changing under it
252
+ * (undo, another tool) or its room deselected, and any history command end it.
253
+ */
254
+ export function startRoomDivide(zoneId: string, levelId: string) {
255
+ owner?.cancel()
256
+ resetSession()
257
+ const nodes: StructureNodes = useScene.getState().nodes
258
+ const zone = nodes[zoneId]
259
+ sessionRevision = zone?.type === 'zone' ? topologyRevision(nodes, zone) : null
260
+ const handle = beginGesture({
261
+ kind: 'room-divide',
262
+ scope: { kind: 'room-divide', nodeId: zoneId, levelId, points: [], end: null, valid: false },
263
+ stale: () =>
264
+ useEditor.getState().room?.zoneId !== zoneId ||
265
+ roomDivideTopologyChanged(useScene.getState().nodes),
266
+ onCancel: () => {
267
+ if (owner === handle) owner = null
268
+ resetSession()
269
+ },
270
+ })
271
+ owner = handle
272
+ }
273
+
274
+ /** Ends the draft (committed or dropped). Idempotent. */
275
+ export function cancelRoomDivide() {
276
+ const current = owner
277
+ owner = null
278
+ resetSession()
279
+ if (current?.active) current.cancel()
280
+ else useInteractionScope.getState().endIf((scope) => scope.kind === 'room-divide')
281
+ }
282
+ /** Whether a plan point lies inside the room being divided (off its boundary). */
283
+ export function roomDivideContains(point: Point) {
284
+ const scope = useInteractionScope.getState().scope
285
+ if (scope.kind !== 'room-divide') return false
286
+ try {
287
+ const face = roomDivideContext(useScene.getState().nodes, scope.nodeId).face
288
+ return !!face && inside(face, point)
289
+ } catch {
290
+ return false
291
+ }
292
+ }
293
+
294
+ /**
295
+ * Moves the live point. The first point snaps onto the room's boundary (or,
296
+ * away from it, starts an island inside the room); later points are free,
297
+ * placed by the active snapping mode — the wall tool's modes, cycled with
298
+ * Shift — until the pointer reaches the boundary again (finishing the cut) or
299
+ * an island's first point (closing the loop).
300
+ */
301
+ export function previewRoomDivide(point: Point, boundaryId?: string) {
302
+ const scope = useInteractionScope.getState().scope
303
+ if (scope.kind !== 'room-divide') return
304
+ lastPointer = { point, boundaryId }
305
+ const hover: Point | null = point.every(Number.isFinite) ? point : null
306
+ try {
307
+ const nodes = useScene.getState().nodes
308
+ const context = roomDivideContext(nodes, scope.nodeId)
309
+ const face = context.face
310
+ if (!face) return invalidPreview('open-room', hover)
311
+ const { points, startBoundaryId } = scope
312
+ const from = points.at(-1) ?? null
313
+ const step = isGridSnapActive() ? useEditor.getState().gridSnapStep : 0
314
+
315
+ const first = points[0]
316
+ const island = !!first && points.length >= 3 && !startBoundaryId
317
+ const close = () => {
318
+ const conflict = previewConflict(nodes, scope.nodeId, { path: points, closed: true }, context)
319
+ return update(scope, first!, 'close', undefined, conflict)
320
+ }
321
+ if (island && distance(point, first!) <= CLOSE_CAPTURE) return close()
322
+
323
+ const destination = snapZoneBoundary(nodes, scope.nodeId, point, step, context, boundaryId)
324
+ if (destination && (boundaryId || destination.distance <= EDGE_CAPTURE)) {
325
+ if (!from) return update(scope, destination.point, 'start', destination.boundaryId)
326
+ if (!startBoundaryId) return invalidPreview('close-loop', destination.point)
327
+ const target = lockToEdge(nodes, scope, context, destination, from) ?? destination.point
328
+ const path = [...points, target]
329
+ const conflict = previewConflict(
330
+ nodes,
331
+ scope.nodeId,
332
+ { path, closed: false, startBoundaryId, endBoundaryId: destination.boundaryId },
333
+ context,
334
+ )
335
+ return update(scope, target, 'edge', destination.boundaryId, conflict)
336
+ }
337
+
338
+ let target = point
339
+ if (from && isAngleSnapActive())
340
+ target = snapPointAlongAngleRay(from, point, DEFAULT_ANGLE_STEP) as Point
341
+ else if (step > 0) target = snapPointToGrid(point, step) as Point
342
+ else if (isMagneticSnapActive()) target = alignToPoints(point, points)
343
+ // The snapping mode can land the point on the island's start by itself.
344
+ if (island && distance(target, first!) < 1e-6) return close()
345
+ if (!inside(face, target)) return invalidPreview(from ? 'outside-room' : 'snap-distance', hover)
346
+ const conflict = from ? segmentConflict(face, points, from, target) : null
347
+ return update(scope, target, from ? 'point' : 'start', undefined, conflict ?? undefined)
348
+ } catch {
349
+ invalidPreview(undefined, hover)
350
+ }
351
+ }
352
+
353
+ function update(
354
+ scope: DivideScope,
355
+ end: Point,
356
+ endKind: NonNullable<DivideScope['endKind']>,
357
+ endBoundaryId?: string,
358
+ conflict?: string | null,
359
+ ) {
360
+ const valid = !conflict
361
+ const message = conflict ? roomDivideMessage(conflict) : undefined
362
+ publishDivideSnap(end, endKind, endBoundaryId)
363
+ const moved = scope.end?.[0] !== end[0] || scope.end?.[1] !== end[1]
364
+ // Unchanged preview: skip the store write and every overlay re-render.
365
+ if (
366
+ !moved &&
367
+ scope.endKind === endKind &&
368
+ scope.endBoundaryId === endBoundaryId &&
369
+ scope.valid === valid &&
370
+ scope.message === message
371
+ )
372
+ return
373
+ // The wall draft's tick: the live point stepped to a new snapped spot.
374
+ if (moved && scope.points.length) sfxEmitter.emit('sfx:grid-snap')
375
+ useInteractionScope.getState().update({
376
+ kind: 'room-divide',
377
+ end,
378
+ endKind,
379
+ endBoundaryId,
380
+ valid,
381
+ message,
382
+ })
383
+ }
384
+
385
+ /**
386
+ * Where the last segment meets the boundary under the snapping mode: `lines`
387
+ * squares it to the edge (to the start edge for a straight cut), `angles`
388
+ * locks it to 15° rays from the previous point. Null keeps the pointer's own
389
+ * boundary point.
390
+ */
391
+ function lockToEdge(
392
+ nodes: StructureNodes,
393
+ scope: DivideScope,
394
+ context: ZoneDivisionContext,
395
+ destination: NonNullable<ReturnType<typeof snapZoneBoundary>>,
396
+ from: Point,
397
+ ): Point | null {
398
+ // Where the ray from `from` along `direction` crosses the boundary the
399
+ // pointer is on — kept only if it really lands on that boundary.
400
+ const lock = (direction: Point): Point | null => {
401
+ const along = destination.direction
402
+ const denominator = direction[0] * along[1] - direction[1] * along[0]
403
+ if (Math.abs(denominator) < 1e-8) return null
404
+ const reach =
405
+ ((destination.point[0] - from[0]) * along[1] - (destination.point[1] - from[1]) * along[0]) /
406
+ denominator
407
+ if (reach <= 0) return null
408
+ const crossing: Point = [from[0] + direction[0] * reach, from[1] + direction[1] * reach]
409
+ const snapped = snapZoneBoundary(nodes, scope.nodeId, crossing, 0, context)
410
+ return snapped && snapped.boundaryId === destination.boundaryId && snapped.distance < 1e-6
411
+ ? snapped.point
412
+ : null
413
+ }
414
+ const target = destination.point
415
+ if (isMagneticSnapActive()) {
416
+ const straight = scope.points.length === 1
417
+ const anchor = straight ? snapZoneBoundary(nodes, scope.nodeId, from, 0, context) : destination
418
+ if (!anchor) return null
419
+ const tangent = anchor.direction
420
+ const offset = (target[0] - from[0]) * tangent[0] + (target[1] - from[1]) * tangent[1]
421
+ if (Math.abs(offset) > LINES_TOLERANCE) return null
422
+ const normal: Point = [-tangent[1], tangent[0]]
423
+ const side = (target[0] - from[0]) * normal[0] + (target[1] - from[1]) * normal[1] < 0 ? -1 : 1
424
+ return lock([normal[0] * side, normal[1] * side])
425
+ }
426
+ if (isAngleSnapActive()) {
427
+ const angle = Math.atan2(target[1] - from[1], target[0] - from[0])
428
+ const locked = Math.round(angle / DEFAULT_ANGLE_STEP) * DEFAULT_ANGLE_STEP
429
+ return lock([Math.cos(locked), Math.sin(locked)])
430
+ }
431
+ return null
432
+ }
433
+
434
+ function commit(scope: DivideScope, context: ZoneDivisionContext) {
435
+ const closed = scope.endKind === 'close'
436
+ const path = closed ? scope.points : [...scope.points, scope.end!]
437
+ const plan = planRoomDivide(
438
+ useScene.getState().nodes,
439
+ scope.nodeId,
440
+ {
441
+ path,
442
+ closed,
443
+ startBoundaryId: scope.startBoundaryId,
444
+ endBoundaryId: closed ? undefined : scope.endBoundaryId,
445
+ },
446
+ context,
447
+ generateId,
448
+ )
449
+ if (plan.conflicts?.length) {
450
+ invalidPreview(plan.conflicts[0]?.code, scope.end)
451
+ return false
452
+ }
453
+ // Out of the lifecycle before the write: the boundary it changes must not read as stale.
454
+ const current = owner
455
+ owner = null
456
+ try {
457
+ if (current) current.finish(() => applyRoomPlan(plan))
458
+ else applyRoomPlan(plan)
459
+ } finally {
460
+ resetSession()
461
+ useInteractionScope.getState().endIf((live) => live.kind === 'room-divide')
462
+ }
463
+ sfxEmitter.emit('sfx:structure-build')
464
+ return true
465
+ }
466
+
467
+ /**
468
+ * Places the live point: starts the path, adds a point, or — on the boundary
469
+ * or an island's first point — commits the division. True once committed.
470
+ */
471
+ export function clickRoomDivide() {
472
+ const scope = useInteractionScope.getState().scope
473
+ if (scope.kind !== 'room-divide' || !scope.end || !scope.valid) return false
474
+ try {
475
+ const context = roomDivideContext(useScene.getState().nodes, scope.nodeId)
476
+ if (!context.face) {
477
+ invalidPreview('open-room')
478
+ return false
479
+ }
480
+ if (scope.endKind === 'edge' || scope.endKind === 'close') return commit(scope, context)
481
+ if (!scope.points.length) sfxEmitter.emit('sfx:structure-build-start')
482
+ useInteractionScope.getState().update({
483
+ kind: 'room-divide',
484
+ points: [...scope.points, scope.end],
485
+ startBoundaryId: scope.points.length ? scope.startBoundaryId : scope.endBoundaryId,
486
+ end: null,
487
+ endKind: undefined,
488
+ endBoundaryId: undefined,
489
+ valid: false,
490
+ message: undefined,
491
+ })
492
+ return false
493
+ } catch {
494
+ invalidPreview()
495
+ return false
496
+ }
497
+ }
498
+
499
+ /** Backspace: drops the last placed point and re-reads the pointer. */
500
+ export function removeLastRoomDividePoint() {
501
+ const scope = useInteractionScope.getState().scope
502
+ if (scope.kind !== 'room-divide' || !scope.points.length) return false
503
+ const points = scope.points.slice(0, -1)
504
+ useInteractionScope.getState().update({
505
+ kind: 'room-divide',
506
+ points,
507
+ startBoundaryId: points.length ? scope.startBoundaryId : undefined,
508
+ end: null,
509
+ endKind: undefined,
510
+ endBoundaryId: undefined,
511
+ valid: false,
512
+ message: undefined,
513
+ })
514
+ if (lastPointer) previewRoomDivide(lastPointer.point, lastPointer.boundaryId)
515
+ return true
516
+ }
517
+
518
+ /** Enter: commits an open path whose live point sits on the boundary. */
519
+ export function finishRoomDivide() {
520
+ const scope = useInteractionScope.getState().scope
521
+ if (scope.kind !== 'room-divide' || scope.endKind !== 'edge' || !scope.valid) return false
522
+ return clickRoomDivide()
523
+ }