@pascal-app/editor 1.0.3 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (624) hide show
  1. package/README.md +106 -0
  2. package/package.json +42 -38
  3. package/src/components/editor/__bench__/room-label-placement.bench.ts +108 -0
  4. package/src/components/editor/action-menu-button.tsx +83 -0
  5. package/src/components/editor/bake-thumbnail.tsx +307 -5
  6. package/src/components/editor/capture-level-picker.tsx +72 -0
  7. package/src/components/editor/construction-reveal-audio.test.tsx +150 -0
  8. package/src/components/editor/construction-reveal-audio.tsx +40 -0
  9. package/src/components/editor/custom-camera-controls.tsx +275 -57
  10. package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
  11. package/src/components/editor/duplicate-node.ts +244 -0
  12. package/src/components/editor/editor-layout-mobile.tsx +37 -26
  13. package/src/components/editor/editor-layout-v2.tsx +132 -59
  14. package/src/components/editor/export-manager.tsx +47 -26
  15. package/src/components/editor/first-person/build-collider-world.test.ts +320 -0
  16. package/src/components/editor/first-person/build-collider-world.ts +28 -7
  17. package/src/components/editor/first-person/ground-holes.test.ts +128 -0
  18. package/src/components/editor/first-person/ground-holes.ts +130 -0
  19. package/src/components/editor/first-person/mechanism-targets.test.ts +162 -0
  20. package/src/components/editor/first-person/mechanism-targets.ts +88 -0
  21. package/src/components/editor/first-person-controls.tsx +220 -9
  22. package/src/components/editor/floating-action-menu.tsx +63 -207
  23. package/src/components/editor/floor-aware-camera-controls.test.ts +466 -0
  24. package/src/components/editor/floor-aware-camera-controls.ts +324 -0
  25. package/src/components/editor/floor-openings-overlay.tsx +243 -0
  26. package/src/components/editor/floor-region-controls.tsx +846 -0
  27. package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
  28. package/src/components/editor/floor-surface.test.ts +165 -0
  29. package/src/components/editor/floor-surface.ts +182 -0
  30. package/src/components/editor/floorplan-background-selection.test.ts +11 -2
  31. package/src/components/editor/floorplan-background-selection.ts +1 -3
  32. package/src/components/editor/floorplan-mode-coordinator.tsx +2 -2
  33. package/src/components/editor/floorplan-panel.tsx +414 -363
  34. package/src/components/editor/follow-pascal.test.tsx +912 -0
  35. package/src/components/editor/follow-pascal.tsx +354 -0
  36. package/src/components/editor/footprint-height-handle.tsx +204 -0
  37. package/src/components/editor/group-actions.ts +56 -2
  38. package/src/components/editor/group-move-3d.ts +2 -0
  39. package/src/components/editor/group-rotate-handle.tsx +2 -0
  40. package/src/components/editor/group-transform-shared.test.ts +460 -0
  41. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  42. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  43. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  44. package/src/components/editor/handles/use-handle-drag.ts +269 -197
  45. package/src/components/editor/index.tsx +409 -197
  46. package/src/components/editor/join-walls-pill.tsx +44 -0
  47. package/src/components/editor/node-action-menu.tsx +68 -70
  48. package/src/components/editor/node-arrow-handles.tsx +25 -6
  49. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  50. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  51. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  52. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  53. package/src/components/editor/registry-action-contributions.tsx +80 -1
  54. package/src/components/editor/riser-diagram-panel.tsx +14 -7
  55. package/src/components/editor/room-controls.tsx +847 -0
  56. package/src/components/editor/room-floor-highlight.tsx +128 -0
  57. package/src/components/editor/room-handles-2d.tsx +354 -0
  58. package/src/components/editor/room-handles.tsx +675 -0
  59. package/src/components/editor/room-highlight.tsx +233 -0
  60. package/src/components/editor/room-labels.test.ts +850 -0
  61. package/src/components/editor/room-labels.tsx +1890 -0
  62. package/src/components/editor/room-transform-ghost.tsx +256 -0
  63. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  64. package/src/components/editor/selection-manager.tsx +474 -419
  65. package/src/components/editor/slab-hole-highlights.tsx +5 -2
  66. package/src/components/editor/snapshot-capture-overlay.tsx +7 -2
  67. package/src/components/editor/snapshot-capture.test.ts +119 -60
  68. package/src/components/editor/snapshot-capture.ts +36 -5
  69. package/src/components/editor/thumbnail-generator.test.ts +654 -0
  70. package/src/components/editor/thumbnail-generator.tsx +1103 -223
  71. package/src/components/editor/use-floorplan-background-placement.ts +34 -12
  72. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  73. package/src/components/editor/views/host-view.tsx +61 -0
  74. package/src/components/editor/views/plugins-view.tsx +663 -0
  75. package/src/components/editor/views/scene-region.tsx +59 -0
  76. package/src/components/editor/views/use-editor-views.ts +57 -0
  77. package/src/components/editor/views/view-bar.test.tsx +54 -0
  78. package/src/components/editor/views/view-bar.tsx +343 -0
  79. package/src/components/editor/wall-justification.test.ts +140 -0
  80. package/src/components/editor/wall-move-side-handles.tsx +43 -142
  81. package/src/components/editor/wall-push-arrow.tsx +92 -0
  82. package/src/components/editor/wall-region-handles.tsx +509 -0
  83. package/src/components/editor/wall-snap-beacon-layer.tsx +8 -4
  84. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  85. package/src/components/editor-2d/floorplan-group-move.tsx +3 -0
  86. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  87. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  88. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +2 -2
  89. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  90. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +116 -106
  91. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  92. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  93. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  94. package/src/components/editor-2d/renderers/{floorplan-draft-wall-measurement.tsx → floorplan-draft-measurement.tsx} +8 -8
  95. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  96. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +38 -8
  97. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +319 -233
  98. package/src/components/editor-2d/renderers/floorplan-reveal.test.tsx +254 -0
  99. package/src/components/editor-2d/renderers/floorplan-reveal.ts +141 -0
  100. package/src/components/editor-2d/svg-paths.ts +12 -15
  101. package/src/components/point-ask/bubble.tsx +538 -0
  102. package/src/components/point-ask/hover-chip.tsx +91 -0
  103. package/src/components/point-ask/mode-pill.tsx +92 -0
  104. package/src/components/point-ask/optional-motion.ts +22 -0
  105. package/src/components/point-ask/outline-corners.tsx +93 -0
  106. package/src/components/point-ask/pins.tsx +711 -0
  107. package/src/components/point-ask/point-ask-layer.tsx +83 -0
  108. package/src/components/point-ask/point-ask-styles.ts +105 -0
  109. package/src/components/point-ask/region-marquee-styles.ts +15 -0
  110. package/src/components/point-ask/region-marquee.tsx +64 -0
  111. package/src/components/procedural-items/inspector.tsx +40 -15
  112. package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +12 -2
  113. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  114. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  115. package/src/components/systems/roof/roof-edit-system.tsx +1 -0
  116. package/src/components/systems/zone/zone-system.test.ts +47 -0
  117. package/src/components/systems/zone/zone-system.tsx +29 -68
  118. package/src/components/tools/distribution/connection-compatibility.test.ts +34 -0
  119. package/src/components/tools/distribution/connection-compatibility.ts +43 -0
  120. package/src/components/tools/distribution/distribution-public.ts +9 -0
  121. package/src/components/tools/distribution/distribution-run-contract.test.ts +26 -0
  122. package/src/components/tools/distribution/distribution-run-contract.ts +94 -0
  123. package/src/components/tools/distribution/distribution-run-tool.test.ts +241 -0
  124. package/src/components/tools/distribution/distribution-run-tool.test.tsx +125 -0
  125. package/src/components/tools/distribution/distribution-run-tool.tsx +1323 -0
  126. package/src/components/tools/distribution/draw-alignment.ts +35 -0
  127. package/src/components/tools/distribution/ports.test.ts +56 -0
  128. package/src/components/tools/distribution/ports.ts +378 -0
  129. package/src/components/tools/distribution/run-cursor.test.ts +135 -0
  130. package/src/components/tools/distribution/run-cursor.ts +36 -0
  131. package/src/components/tools/distribution/run-direction-feedback.test.ts +60 -0
  132. package/src/components/tools/distribution/run-direction-feedback.tsx +251 -0
  133. package/src/components/tools/distribution/run-port-snap.test.ts +79 -0
  134. package/src/components/tools/distribution/run-port-snap.ts +25 -0
  135. package/src/components/tools/distribution/run-screen-direction.test.ts +23 -0
  136. package/src/components/tools/distribution/run-screen-direction.ts +30 -0
  137. package/src/components/tools/elevator/elevator-tool.tsx +3 -6
  138. package/src/components/tools/elevator/move-elevator-tool.tsx +2 -3
  139. package/src/components/tools/item/item-surface-strategy-resolver.test.ts +67 -68
  140. package/src/components/tools/item/placement-history.test.tsx +512 -0
  141. package/src/components/tools/item/placement-strategies.test.ts +46 -1
  142. package/src/components/tools/item/placement-strategies.ts +11 -10
  143. package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +56 -56
  144. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  145. package/src/components/tools/item/use-draft-node.ts +166 -95
  146. package/src/components/tools/item/use-placement-coordinator.tsx +148 -95
  147. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  148. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  149. package/src/components/tools/registry/item-surface-move.test.ts +62 -63
  150. package/src/components/tools/registry/item-surface-move.ts +2 -0
  151. package/src/components/tools/registry/item-surface-resolver.test.ts +26 -26
  152. package/src/components/tools/registry/move-registry-node-tool.test.ts +268 -1
  153. package/src/components/tools/registry/move-registry-node-tool.tsx +66 -30
  154. package/src/components/tools/select/box-select-tool.tsx +2 -12
  155. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  156. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  157. package/src/components/tools/select/select-candidates.test.ts +37 -4
  158. package/src/components/tools/select/select-candidates.ts +15 -11
  159. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  160. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  161. package/src/components/tools/tool-manager.tsx +59 -24
  162. package/src/components/tools/wall/wall-drafting.test.ts +175 -47
  163. package/src/components/tools/zone/zone-tool.tsx +4 -12
  164. package/src/components/ui/action-menu/action-button.tsx +9 -1
  165. package/src/components/ui/action-menu/camera-actions.tsx +9 -4
  166. package/src/components/ui/action-menu/control-modes.tsx +15 -42
  167. package/src/components/ui/action-menu/follow-pascal-pill.tsx +32 -0
  168. package/src/components/ui/action-menu/follow-pascal-toggle.tsx +36 -0
  169. package/src/components/ui/action-menu/index.tsx +56 -33
  170. package/src/components/ui/action-menu/measurement-control.tsx +14 -19
  171. package/src/components/ui/action-menu/placement.ts +20 -0
  172. package/src/components/ui/action-menu/point-ask-button.tsx +52 -0
  173. package/src/components/ui/action-menu/view-toggles.tsx +47 -42
  174. package/src/components/ui/build-panel/build-panel.tsx +242 -0
  175. package/src/components/ui/command-palette/editor-commands.tsx +7 -42
  176. package/src/components/ui/command-palette/index.tsx +50 -39
  177. package/src/components/ui/command-palette/ranked-groups.test.ts +31 -0
  178. package/src/components/ui/controls/action-button.tsx +5 -2
  179. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  180. package/src/components/ui/controls/material-paint-panel.tsx +97 -26
  181. package/src/components/ui/controls/material-picker.tsx +4 -1
  182. package/src/components/ui/controls/metric-control.tsx +1 -1
  183. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  184. package/src/components/ui/controls/segmented-control.tsx +21 -8
  185. package/src/components/ui/controls/select-control.tsx +65 -0
  186. package/src/components/ui/controls/slider-control.tsx +1 -1
  187. package/src/components/ui/controls/toggle-control.tsx +3 -3
  188. package/src/components/ui/controls/tool-options-panel.tsx +1 -1
  189. package/src/components/ui/floating-level-selector.tsx +159 -59
  190. package/src/components/ui/helpers/building-helper.tsx +4 -1
  191. package/src/components/ui/helpers/contextual-helper-panel.tsx +181 -72
  192. package/src/components/ui/helpers/helper-manager.test.tsx +121 -0
  193. package/src/components/ui/helpers/helper-manager.tsx +331 -14
  194. package/src/components/ui/helpers/item-helper.tsx +17 -1
  195. package/src/components/ui/helpers/registered-tool-helper.tsx +8 -1
  196. package/src/components/ui/icon-ref.tsx +45 -1
  197. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  198. package/src/components/ui/node-icon-image.tsx +16 -0
  199. package/src/components/ui/panels/collections/collections-popover.tsx +28 -22
  200. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  201. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  202. package/src/components/ui/panels/mobile-panel-sheet.tsx +4 -4
  203. package/src/components/ui/panels/mobile-selection-bar.tsx +8 -5
  204. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  205. package/src/components/ui/panels/multi-field-value.ts +6 -3
  206. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  207. package/src/components/ui/panels/multi-selection-panel.tsx +161 -5
  208. package/src/components/ui/panels/node-display.ts +8 -3
  209. package/src/components/ui/panels/panel-manager.tsx +103 -68
  210. package/src/components/ui/panels/panel-wrapper.tsx +130 -62
  211. package/src/components/ui/panels/parametric-field-control.tsx +3 -3
  212. package/src/components/ui/panels/parametric-inspector.tsx +7 -7
  213. package/src/components/ui/panels/reference-panel.tsx +2 -3
  214. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  215. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  216. package/src/components/ui/panels/room-name-options.ts +47 -0
  217. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  218. package/src/components/ui/panels/room-panel.tsx +790 -0
  219. package/src/components/ui/panels/shape-choice.tsx +58 -0
  220. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  221. package/src/components/ui/primitives/badge.tsx +8 -0
  222. package/src/components/ui/primitives/color-dot.tsx +36 -24
  223. package/src/components/ui/primitives/sidebar.tsx +3 -1
  224. package/src/components/ui/right-stack.test.ts +29 -0
  225. package/src/components/ui/right-stack.tsx +164 -0
  226. package/src/components/ui/sidebar/app-sidebar.tsx +1 -1
  227. package/src/components/ui/sidebar/icon-rail.tsx +8 -3
  228. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +1 -1
  229. package/src/components/ui/sidebar/panels/items-panel/index.tsx +6 -6
  230. package/src/components/ui/sidebar/panels/settings-panel/animation-speed-setting.tsx +40 -0
  231. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +7 -8
  232. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.test.tsx +170 -0
  233. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.tsx +92 -0
  234. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +138 -27
  235. package/src/components/ui/sidebar/panels/settings-panel/interface-theme.test.tsx +20 -0
  236. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +5 -5
  237. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +4 -10
  238. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -11
  239. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  240. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  241. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  242. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  243. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  244. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  245. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  246. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  247. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  248. package/src/components/ui/sidebar/panels/site-panel/index.tsx +181 -223
  249. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  250. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  251. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  252. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  253. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  254. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  255. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  256. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  257. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  258. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  259. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +13 -37
  260. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  261. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  262. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  263. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  264. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +2 -4
  265. package/src/components/ui/sidebar/tab-bar.tsx +14 -4
  266. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  267. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  268. package/src/components/viewer/floorplan-preview.tsx +5 -6
  269. package/src/components/viewer/viewer-controls-bar.tsx +18 -6
  270. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  271. package/src/components/viewer/viewer-rooms.tsx +191 -0
  272. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  273. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  274. package/src/components/viewer/viewer-stage.tsx +2 -2
  275. package/src/components/viewer-zone-system.tsx +3 -8
  276. package/src/hooks/use-auto-save.integration.test.tsx +215 -43
  277. package/src/hooks/use-auto-save.ts +60 -9
  278. package/src/hooks/use-ceiling-events.test.ts +123 -118
  279. package/src/hooks/use-grid-events.test.ts +105 -105
  280. package/src/hooks/use-keyboard.test.ts +243 -19
  281. package/src/hooks/use-keyboard.ts +118 -83
  282. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  283. package/src/hooks/use-open-wall-ends.ts +238 -0
  284. package/src/hooks/use-placement-notice.test.ts +71 -0
  285. package/src/hooks/use-placement-notice.ts +73 -0
  286. package/src/hooks/use-selected-room.ts +143 -0
  287. package/src/index.tsx +312 -58
  288. package/src/lib/__fixtures__/owner-victor-seattle-parking.json +1 -0
  289. package/src/lib/__fixtures__/owner-villa-aurelia-basement.json +1 -0
  290. package/src/lib/add-storey.test.ts +177 -0
  291. package/src/lib/add-storey.ts +59 -0
  292. package/src/lib/below-grade.test.tsx +171 -0
  293. package/src/lib/below-grade.ts +133 -0
  294. package/src/lib/camera-facing.test.ts +24 -0
  295. package/src/lib/camera-facing.ts +24 -0
  296. package/src/lib/ceiling-edit-session.test.ts +321 -0
  297. package/src/lib/ceiling-edit-session.ts +206 -0
  298. package/src/lib/ceiling-region-session.test.ts +179 -0
  299. package/src/lib/continuation.test.ts +19 -1
  300. package/src/lib/continuation.ts +25 -4
  301. package/src/lib/direct-manipulation.ts +2 -0
  302. package/src/lib/door-interaction.ts +14 -0
  303. package/src/lib/download-project-file.ts +17 -0
  304. package/src/lib/editor-level-display.test.ts +96 -0
  305. package/src/lib/editor-level-display.ts +55 -0
  306. package/src/lib/editor-views.ts +178 -0
  307. package/src/lib/export-geometry-wait.test.ts +90 -0
  308. package/src/lib/export-texture-utils.ts +2 -4
  309. package/src/lib/floor-edit-session.ts +153 -0
  310. package/src/lib/floor-footprints.test.ts +563 -0
  311. package/src/lib/floor-footprints.ts +395 -0
  312. package/src/lib/floor-foundation-support.test.ts +284 -0
  313. package/src/lib/floor-opening-draft.ts +218 -0
  314. package/src/lib/floor-openings.test.ts +216 -0
  315. package/src/lib/floor-region-geometry.ts +191 -0
  316. package/src/lib/floor-region-session.test.ts +485 -0
  317. package/src/lib/floor-region-session.ts +589 -0
  318. package/src/lib/floor-region-snap.ts +101 -0
  319. package/src/lib/floor-step-hover.test.ts +32 -0
  320. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  321. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  322. package/src/lib/floorplan/dimension-drive.ts +479 -0
  323. package/src/lib/floorplan/floorplan-export.test.ts +12 -8
  324. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  325. package/src/lib/floorplan/floorplan-extension.ts +32 -0
  326. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  327. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  328. package/src/lib/floorplan/floorplan-pdfkit-document.ts +363 -27
  329. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +177 -8
  330. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +60 -25
  331. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  332. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  333. package/src/lib/floorplan/geometry.ts +13 -6
  334. package/src/lib/floorplan/index.ts +0 -9
  335. package/src/lib/floorplan/items.ts +5 -2
  336. package/src/lib/floorplan/marks.test.ts +213 -0
  337. package/src/lib/floorplan/marks.ts +238 -0
  338. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  339. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  340. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  341. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  342. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  343. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  344. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  345. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  346. package/src/lib/floorplan/pdfkit-standard-fonts.d.ts +5 -0
  347. package/src/lib/floorplan/schedules.test.ts +189 -0
  348. package/src/lib/floorplan/schedules.ts +339 -0
  349. package/src/lib/floorplan/sheet-export.test.ts +124 -0
  350. package/src/lib/floorplan/sheet-export.ts +318 -0
  351. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +768 -0
  352. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  353. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  354. package/src/lib/floorplan/site-plan/coverage.ts +230 -0
  355. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  356. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  357. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  358. package/src/lib/floorplan/site-plan/front-edge.ts +157 -0
  359. package/src/lib/floorplan/site-plan/geometry.test.ts +269 -0
  360. package/src/lib/floorplan/site-plan/geometry.ts +142 -0
  361. package/src/lib/floorplan/site-plan/index.ts +55 -0
  362. package/src/lib/floorplan/site-plan/site-annotations.test.ts +483 -0
  363. package/src/lib/floorplan/site-plan/site-annotations.ts +1084 -0
  364. package/src/lib/floorplan/site-plan/site-parts.ts +287 -0
  365. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  366. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  367. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  368. package/src/lib/floorplan/site-plan/yards.test.ts +70 -0
  369. package/src/lib/floorplan/types.ts +1 -27
  370. package/src/lib/follow-pascal-exterior.test-fixture.ts +17 -0
  371. package/src/lib/follow-pascal-exterior.test.ts +171 -0
  372. package/src/lib/follow-pascal-exterior.ts +39 -0
  373. package/src/lib/follow-pascal-presentation.test.ts +203 -0
  374. package/src/lib/follow-pascal-presentation.ts +129 -0
  375. package/src/lib/follow-pascal-sight.ts +84 -0
  376. package/src/lib/follow-pascal-targets.test.ts +406 -0
  377. package/src/lib/follow-pascal-targets.ts +198 -0
  378. package/src/lib/follow-pascal.test.ts +350 -0
  379. package/src/lib/follow-pascal.ts +284 -0
  380. package/src/lib/fresh-planar-placement.test.ts +148 -0
  381. package/src/lib/fresh-planar-placement.ts +114 -37
  382. package/src/lib/geometry-script/author.ts +157 -0
  383. package/src/lib/geometry-script/client.ts +159 -0
  384. package/src/lib/geometry-script/finish-scripts.ts +127 -0
  385. package/src/lib/geometry-script/protocol.ts +13 -0
  386. package/src/lib/gesture-lifecycle.test.ts +595 -0
  387. package/src/lib/gesture-lifecycle.ts +290 -0
  388. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  389. package/src/lib/glb-export.test.ts +636 -126
  390. package/src/lib/glb-export.ts +241 -120
  391. package/src/lib/history.test.ts +18 -7
  392. package/src/lib/history.ts +7 -0
  393. package/src/lib/hud-title.ts +109 -0
  394. package/src/lib/hydration-gated-storage.ts +49 -0
  395. package/src/lib/ifc-export.test.ts +174 -0
  396. package/src/lib/ifc-export.ts +201 -0
  397. package/src/lib/ifc-parts.ts +15 -0
  398. package/src/lib/inspector-expanded.test.ts +44 -0
  399. package/src/lib/inspector-expanded.ts +36 -0
  400. package/src/lib/interaction/scope.ts +22 -0
  401. package/src/lib/keyboard-pan.test.ts +129 -7
  402. package/src/lib/keyboard-pan.ts +161 -12
  403. package/src/lib/level-selection.test.ts +57 -0
  404. package/src/lib/level-selection.ts +16 -1
  405. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  406. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  407. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  408. package/src/lib/lot/clean-ring.test.ts +187 -0
  409. package/src/lib/lot/clean-ring.ts +217 -0
  410. package/src/lib/lot/dossier.test.ts +202 -0
  411. package/src/lib/lot/dossier.ts +498 -0
  412. package/src/lib/lot/drop-in.test.ts +326 -0
  413. package/src/lib/lot/drop-in.ts +392 -0
  414. package/src/lib/lot/index.ts +43 -0
  415. package/src/lib/lot/lot-patch.test.ts +340 -0
  416. package/src/lib/lot/lot-patch.ts +394 -0
  417. package/src/lib/lot/parcel-provider.ts +43 -0
  418. package/src/lib/lot/terrain.test.ts +161 -0
  419. package/src/lib/lot/terrain.ts +266 -0
  420. package/src/lib/material-catalog-model.ts +3 -1
  421. package/src/lib/material-paint-panel-model.ts +9 -5
  422. package/src/lib/material-paint.ts +4 -0
  423. package/src/lib/measurement-kind.test.ts +6 -8
  424. package/src/lib/measurements.test.ts +23 -0
  425. package/src/lib/mezzanine-actions.test.ts +342 -0
  426. package/src/lib/mezzanine-draft.test.ts +501 -0
  427. package/src/lib/mezzanine-draft.ts +278 -0
  428. package/src/lib/mezzanine-messages.ts +26 -0
  429. package/src/lib/mezzanine-selection.test.ts +207 -0
  430. package/src/lib/mezzanine-stairs.ts +46 -0
  431. package/src/lib/model-export.ts +3 -1
  432. package/src/lib/node-action-movement.ts +17 -6
  433. package/src/lib/node-display-label.ts +37 -0
  434. package/src/lib/orbit-floor-clearance.test.ts +201 -0
  435. package/src/lib/orbit-floor-clearance.ts +154 -0
  436. package/src/lib/paint-commit-route.test.ts +37 -0
  437. package/src/lib/paint-commit-route.ts +17 -0
  438. package/src/lib/paint-eyedropper.test.tsx +165 -0
  439. package/src/lib/paint-eyedropper.ts +99 -0
  440. package/src/lib/paint-pass-through.test.ts +122 -0
  441. package/src/lib/paint-pass-through.ts +78 -0
  442. package/src/lib/paint-region-hover.ts +25 -0
  443. package/src/lib/paint-region-mode.ts +154 -0
  444. package/src/lib/paint-regions.test.ts +166 -0
  445. package/src/lib/paint-regions.ts +192 -0
  446. package/src/lib/paint-scope.test.ts +317 -35
  447. package/src/lib/paint-scope.ts +267 -25
  448. package/src/lib/panel-rows.ts +21 -0
  449. package/src/lib/parametric-node-update.ts +16 -7
  450. package/src/lib/plate-paint-affected.test.ts +300 -0
  451. package/src/lib/plate-paint-affected.ts +177 -0
  452. package/src/lib/plugin-chat.ts +65 -0
  453. package/src/lib/plugin-panels.test.ts +25 -0
  454. package/src/lib/plugin-panels.ts +94 -0
  455. package/src/lib/point-ask/__fixtures__/consumer-browser.tsx +76 -0
  456. package/src/lib/point-ask/__fixtures__/house.ts +81 -0
  457. package/src/lib/point-ask/ask-created.test.ts +195 -0
  458. package/src/lib/point-ask/ask-flow.test.ts +380 -0
  459. package/src/lib/point-ask/build-context.test.ts +173 -0
  460. package/src/lib/point-ask/build-context.ts +94 -0
  461. package/src/lib/point-ask/capture-crop.ts +129 -0
  462. package/src/lib/point-ask/choreography.ts +69 -0
  463. package/src/lib/point-ask/compare.test.ts +145 -0
  464. package/src/lib/point-ask/compare.ts +73 -0
  465. package/src/lib/point-ask/describe-target.test.ts +165 -0
  466. package/src/lib/point-ask/describe-target.ts +234 -0
  467. package/src/lib/point-ask/dictation.ts +79 -0
  468. package/src/lib/point-ask/edge-marker.test.ts +61 -0
  469. package/src/lib/point-ask/edge-marker.ts +27 -0
  470. package/src/lib/point-ask/frame-loop.ts +25 -0
  471. package/src/lib/point-ask/history-marks.test.ts +94 -0
  472. package/src/lib/point-ask/history-marks.ts +39 -0
  473. package/src/lib/point-ask/host-api.ts +32 -0
  474. package/src/lib/point-ask/outline-box.test.ts +30 -0
  475. package/src/lib/point-ask/outline-box.ts +11 -0
  476. package/src/lib/point-ask/overlay-metrics.ts +6 -0
  477. package/src/lib/point-ask/pick.test.ts +220 -0
  478. package/src/lib/point-ask/pick.ts +121 -0
  479. package/src/lib/point-ask/pin-machine.test.ts +494 -0
  480. package/src/lib/point-ask/pin-machine.ts +217 -0
  481. package/src/lib/point-ask/placement.test.ts +203 -0
  482. package/src/lib/point-ask/placement.ts +141 -0
  483. package/src/lib/point-ask/pointer-tracker.ts +34 -0
  484. package/src/lib/point-ask/projector.ts +83 -0
  485. package/src/lib/point-ask/region.test.ts +157 -0
  486. package/src/lib/point-ask/region.ts +136 -0
  487. package/src/lib/point-ask/retry.test.ts +217 -0
  488. package/src/lib/point-ask/retry.ts +37 -0
  489. package/src/lib/point-ask/scene-commit.test.ts +312 -0
  490. package/src/lib/point-ask/scene-commit.ts +32 -0
  491. package/src/lib/point-ask/session.ts +1391 -0
  492. package/src/lib/point-ask/spring-keyframes.test.ts +45 -0
  493. package/src/lib/point-ask/spring-keyframes.ts +38 -0
  494. package/src/lib/point-ask/spring.test.ts +63 -0
  495. package/src/lib/point-ask/spring.ts +107 -0
  496. package/src/lib/point-ask/suggestions.test.ts +80 -0
  497. package/src/lib/point-ask/suggestions.ts +125 -0
  498. package/src/lib/point-ask/target-scope.test.ts +622 -0
  499. package/src/lib/point-ask/target-scope.ts +338 -0
  500. package/src/lib/point-ask/types.ts +48 -0
  501. package/src/lib/portable-export.test.ts +34 -1
  502. package/src/lib/portable-export.ts +119 -53
  503. package/src/lib/print-feature-thickness.ts +10 -1
  504. package/src/lib/print-roof-solids.test.ts +39 -1
  505. package/src/lib/print-roof-solids.ts +43 -10
  506. package/src/lib/print-shell-compiler-manifold-core.ts +1 -1
  507. package/src/lib/print-shell-compiler.ts +1 -0
  508. package/src/lib/registered-draft-snap.ts +12 -0
  509. package/src/lib/roof-duplication.ts +10 -2
  510. package/src/lib/room-assembly-overlay.test.ts +275 -0
  511. package/src/lib/room-assembly-overlay.ts +265 -0
  512. package/src/lib/room-built-on.test.ts +379 -0
  513. package/src/lib/room-built-on.ts +138 -0
  514. package/src/lib/room-construction-commands.ts +712 -0
  515. package/src/lib/room-construction.test.ts +746 -0
  516. package/src/lib/room-construction.ts +385 -0
  517. package/src/lib/room-controls-phase6.test.ts +630 -0
  518. package/src/lib/room-divide-session.ts +523 -0
  519. package/src/lib/room-handle-drag.ts +1068 -0
  520. package/src/lib/room-name-catalog.test.ts +90 -0
  521. package/src/lib/room-name-catalog.ts +168 -0
  522. package/src/lib/room-push-dimensions.test.ts +311 -0
  523. package/src/lib/room-push-dimensions.ts +251 -0
  524. package/src/lib/room-selection-commands.ts +62 -0
  525. package/src/lib/room-selection.test.ts +1017 -0
  526. package/src/lib/room-selection.ts +417 -0
  527. package/src/lib/room-structure-commands.ts +94 -0
  528. package/src/lib/room-structure.test.ts +921 -0
  529. package/src/lib/room-transform-session.ts +570 -0
  530. package/src/lib/room-zone-routing.test.ts +294 -0
  531. package/src/lib/room-zone-routing.ts +197 -0
  532. package/src/lib/scene-clipboard.test.ts +610 -1
  533. package/src/lib/scene-clipboard.ts +269 -58
  534. package/src/lib/scene-material-refs.test.ts +79 -0
  535. package/src/lib/scene-material-refs.ts +79 -0
  536. package/src/lib/scene.test.ts +24 -36
  537. package/src/lib/scene.ts +33 -57
  538. package/src/lib/selection-routing.test.ts +129 -45
  539. package/src/lib/selection-routing.ts +54 -75
  540. package/src/lib/session-writes.test.ts +120 -0
  541. package/src/lib/session-writes.ts +205 -0
  542. package/src/lib/sfx-bus.ts +13 -0
  543. package/src/lib/sfx-player.test.ts +70 -9
  544. package/src/lib/sfx-player.ts +6 -0
  545. package/src/lib/sidebar-panel.ts +62 -0
  546. package/src/lib/site-boundary.test.ts +4 -6
  547. package/src/lib/site-boundary.ts +1 -1
  548. package/src/lib/snapping-mode.test.ts +4 -0
  549. package/src/lib/snapping-mode.ts +8 -1
  550. package/src/lib/stair-levels.test.ts +25 -0
  551. package/src/lib/stair-levels.ts +22 -3
  552. package/src/lib/terrace-draft.test.ts +145 -0
  553. package/src/lib/terrace-draft.ts +214 -0
  554. package/src/lib/tool-cursor-icon.test.ts +16 -0
  555. package/src/lib/tool-cursor-icon.ts +11 -0
  556. package/src/lib/typing-target.ts +9 -0
  557. package/src/lib/units.ts +77 -39
  558. package/src/lib/vector-edges.test.ts +58 -0
  559. package/src/lib/vector-edges.ts +400 -0
  560. package/src/lib/viewer-selection.test.ts +577 -0
  561. package/src/lib/viewer-selection.ts +580 -0
  562. package/src/lib/wall-draw-variant.test.ts +71 -0
  563. package/src/lib/wall-draw-variant.ts +91 -0
  564. package/src/lib/wall-paint-region-session.ts +246 -0
  565. package/src/lib/wall-paint-region.test.ts +373 -0
  566. package/src/lib/wall-placement-pointer.test.ts +42 -0
  567. package/src/lib/wall-placement-pointer.ts +57 -0
  568. package/src/lib/wall-polygon-draft.test.ts +262 -0
  569. package/src/lib/wall-polygon-draft.ts +286 -0
  570. package/src/lib/wall-push.test.ts +453 -0
  571. package/src/lib/wall-region-face.ts +352 -0
  572. package/src/lib/wall-region-handles.test.ts +304 -0
  573. package/src/lib/wall-region-handles.ts +368 -0
  574. package/src/lib/wall-region-list.test.ts +119 -0
  575. package/src/lib/wall-region-list.ts +136 -0
  576. package/src/lib/wall-region-snap.ts +110 -0
  577. package/src/lib/window-changes.ts +35 -0
  578. package/src/lib/window-interaction.ts +14 -0
  579. package/src/lib/zone-content.ts +4 -1
  580. package/src/lib/zone-name.test.ts +17 -0
  581. package/src/lib/zone-name.ts +17 -0
  582. package/src/store/opens-in-select-mode.test.ts +223 -0
  583. package/src/store/paint-mode.test.ts +188 -0
  584. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  585. package/src/store/tool-mode.test.ts +137 -6
  586. package/src/store/use-audio.test.ts +146 -0
  587. package/src/store/use-audio.tsx +127 -3
  588. package/src/store/use-construction-reveal.test.ts +173 -0
  589. package/src/store/use-construction-reveal.ts +145 -0
  590. package/src/store/use-corner-snap-hint.test.ts +24 -0
  591. package/src/store/use-corner-snap-hint.ts +47 -0
  592. package/src/store/use-delete-confirmation.ts +28 -1
  593. package/src/store/use-drawing-view.test.ts +24 -3
  594. package/src/store/use-drawing-view.ts +29 -1
  595. package/src/store/use-editor.tsx +432 -321
  596. package/src/store/use-floorplan-draft-preview.ts +13 -0
  597. package/src/store/use-floorplan-marquee.ts +12 -2
  598. package/src/store/use-follow-pascal.test.ts +62 -0
  599. package/src/store/use-follow-pascal.ts +53 -0
  600. package/src/store/use-point-ask.ts +247 -0
  601. package/src/store/use-quick-measurement-hud.ts +3 -3
  602. package/src/store/use-stair-build-preview.ts +33 -5
  603. package/src/store/use-wall-move-ghosts.ts +4 -2
  604. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  605. package/src/store/use-wall-region-handles-selection.ts +139 -0
  606. package/src/store/view-layout.test.ts +201 -0
  607. package/src/store/view-layout.ts +199 -0
  608. package/src/test-utils/immediate-animation-frames.ts +16 -0
  609. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  610. package/src/test-utils/render-current-markup.tsx +16 -0
  611. package/src/test-utils/selection-harness.ts +82 -0
  612. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  613. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  614. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  615. package/src/components/tools/fence/fence-drafting.ts +0 -266
  616. package/src/components/tools/stair/stair-click-guard.test.ts +0 -58
  617. package/src/components/tools/stair/stair-click-guard.ts +0 -72
  618. package/src/components/tools/stair/stair-defaults.ts +0 -18
  619. package/src/components/tools/stair/stair-tool.tsx +0 -634
  620. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  621. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  622. package/src/lib/floorplan/stairs.ts +0 -489
  623. package/src/lib/level-duplication.test.ts +0 -144
  624. package/src/lib/level-duplication.ts +0 -195
@@ -0,0 +1,262 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
2
+ import {
3
+ type AnyNode,
4
+ type AnyNodeId,
5
+ clearSceneHistory,
6
+ emitter,
7
+ GROUND_SUPPORT_ID,
8
+ LevelNode,
9
+ subscribeSceneCommits,
10
+ useScene,
11
+ WallNode,
12
+ } from '@pascal-app/core'
13
+ import { useViewer } from '@pascal-app/viewer'
14
+ import useEditor from '../store/use-editor'
15
+ import { useFloorplanDraftPreview } from '../store/use-floorplan-draft-preview'
16
+ import { sceneLayout, visibleScene } from '../store/view-layout'
17
+ import { runUndo } from './history'
18
+ import { sfxEmitter } from './sfx-bus'
19
+ import {
20
+ addWallPolygonDraftCorner,
21
+ commitWallPolygonDraft,
22
+ discardWallPolygonDraft,
23
+ isWallPolygonDraftOpen,
24
+ startWallPolygonDraft,
25
+ wallPolygonDraftWalls,
26
+ } from './wall-polygon-draft'
27
+
28
+ // Node updates batch their dirty marks on the next frame.
29
+ ;(
30
+ globalThis as { requestAnimationFrame?: (callback: () => void) => number }
31
+ ).requestAnimationFrame ??= (callback) => {
32
+ callback()
33
+ return 0
34
+ }
35
+ ;(globalThis as { cancelAnimationFrame?: (id: number) => void }).cancelAnimationFrame ??= () => {}
36
+
37
+ const level = LevelNode.parse({ id: 'level_polygon', children: ['wall_existing'] })
38
+ const existing = WallNode.parse({
39
+ id: 'wall_existing',
40
+ parentId: level.id,
41
+ start: [10, 0],
42
+ end: [14, 0],
43
+ })
44
+ const levelId = level.id as AnyNodeId
45
+
46
+ const history = () => {
47
+ const { pastStates, futureStates } = useScene.temporal.getState()
48
+ return { past: pastStates.length, future: futureStates.length }
49
+ }
50
+ const walls = () =>
51
+ Object.values(useScene.getState().nodes).filter((node): node is WallNode => node.type === 'wall')
52
+
53
+ let commits = 0
54
+ let sceneWrites = 0
55
+ let stopCommits = () => {}
56
+ let stopWrites = () => {}
57
+
58
+ beforeEach(() => {
59
+ useScene.setState({
60
+ nodes: { [level.id]: level, [existing.id]: existing } as Record<AnyNodeId, AnyNode>,
61
+ rootNodeIds: [levelId],
62
+ collections: {},
63
+ materials: {},
64
+ readOnly: false,
65
+ })
66
+ clearSceneHistory()
67
+ useViewer.getState().setSelection({ levelId: level.id })
68
+ commits = 0
69
+ sceneWrites = 0
70
+ stopCommits = subscribeSceneCommits(() => {
71
+ commits += 1
72
+ })
73
+ stopWrites = useScene.subscribe((state, previous) => {
74
+ if (state.nodes !== previous.nodes) sceneWrites += 1
75
+ })
76
+ })
77
+
78
+ afterEach(() => {
79
+ discardWallPolygonDraft()
80
+ stopCommits()
81
+ stopWrites()
82
+ })
83
+
84
+ /** Three placed sides of an open rectangle room, not closed yet. */
85
+ function drawThreeSides() {
86
+ startWallPolygonDraft(levelId, [0, 0])
87
+ expect(addWallPolygonDraftCorner([4, 0])).toBe('open')
88
+ expect(addWallPolygonDraftCorner([4, 3])).toBe('open')
89
+ expect(addWallPolygonDraftCorner([0, 3])).toBe('open')
90
+ }
91
+
92
+ describe('Polygon room draft', () => {
93
+ test('an open polygon writes nothing: no scene change, history, commit or autosave trigger', () => {
94
+ const before = useScene.getState().nodes
95
+ drawThreeSides()
96
+ expect(useScene.getState().nodes).toBe(before)
97
+ expect(sceneWrites).toBe(0)
98
+ expect(commits).toBe(0)
99
+ expect(history()).toEqual({ past: 0, future: 0 })
100
+ // It exists only as draft corners, drawn as ghosts and used as snap targets.
101
+ expect(useFloorplanDraftPreview.getState().wallPolygonDraftPoints).toHaveLength(4)
102
+ expect(wallPolygonDraftWalls()).toHaveLength(3)
103
+ })
104
+
105
+ test('each placed corner plays the start tick; closing plays the build cue once', () => {
106
+ const played: string[] = []
107
+ const listen = (name: string) => () => played.push(name)
108
+ const onStart = listen('start')
109
+ const onBuild = listen('build')
110
+ sfxEmitter.on('sfx:structure-build-start', onStart)
111
+ sfxEmitter.on('sfx:structure-build', onBuild)
112
+ try {
113
+ drawThreeSides()
114
+ expect(played).toEqual(['start', 'start', 'start'])
115
+ expect(addWallPolygonDraftCorner([0.05, 0.05])).toBe('closed')
116
+ commitWallPolygonDraft()
117
+ expect(played).toEqual(['start', 'start', 'start', 'build'])
118
+ } finally {
119
+ sfxEmitter.off('sfx:structure-build-start', onStart)
120
+ sfxEmitter.off('sfx:structure-build', onBuild)
121
+ }
122
+ })
123
+
124
+ test('Esc just drops the draft', () => {
125
+ const before = useScene.getState().nodes
126
+ drawThreeSides()
127
+ discardWallPolygonDraft()
128
+ expect(isWallPolygonDraftOpen()).toBe(false)
129
+ expect(useFloorplanDraftPreview.getState().wallPolygonDraftPoints).toEqual([])
130
+ expect(useScene.getState().nodes).toBe(before)
131
+ expect({ sceneWrites, commits, ...history() }).toEqual({
132
+ sceneWrites: 0,
133
+ commits: 0,
134
+ past: 0,
135
+ future: 0,
136
+ })
137
+ })
138
+
139
+ test('closing on the first corner writes every wall as one undo step and one commit', () => {
140
+ const before = useScene.getState().nodes
141
+ drawThreeSides()
142
+ expect(addWallPolygonDraftCorner([0.05, 0.05])).toBe('closed')
143
+ const created = commitWallPolygonDraft()
144
+
145
+ expect(created).toHaveLength(4)
146
+ expect(walls()).toHaveLength(5)
147
+ expect(history()).toEqual({ past: 1, future: 0 })
148
+ expect(commits).toBe(1)
149
+ // The closing side ends exactly on the first corner.
150
+ expect(created.some((wall) => wall.end[0] === 0 && wall.end[1] === 0)).toBe(true)
151
+
152
+ runUndo()
153
+ expect(useScene.getState().nodes).toEqual(before)
154
+ })
155
+
156
+ test('a corner that tees into an existing wall ends the polygon and splits that wall', () => {
157
+ startWallPolygonDraft(levelId, [12, 3])
158
+ expect(addWallPolygonDraftCorner([12, 0])).toBe('joined')
159
+ commitWallPolygonDraft()
160
+ // The existing wall is split at the tee; one new wall joins it.
161
+ expect(walls()).toHaveLength(3)
162
+ expect(history()).toEqual({ past: 1, future: 0 })
163
+ expect(commits).toBe(1)
164
+ })
165
+
166
+ test('a double-click keeps an open polygon as drawn', () => {
167
+ drawThreeSides()
168
+ expect(commitWallPolygonDraft()).toHaveLength(3)
169
+ expect(walls()).toHaveLength(4)
170
+ expect(history()).toEqual({ past: 1, future: 0 })
171
+ })
172
+
173
+ test("a collaborator's edit during the draft survives Esc and survives the commit", () => {
174
+ drawThreeSides()
175
+ // Stands in for a remote patch landing mid-draft.
176
+ useScene.getState().updateNode(existing.id, { name: 'Renamed remotely' })
177
+ discardWallPolygonDraft()
178
+ expect(useScene.getState().nodes[existing.id]?.name).toBe('Renamed remotely')
179
+
180
+ drawThreeSides()
181
+ useScene.getState().updateNode(existing.id, { name: 'Renamed again' })
182
+ addWallPolygonDraftCorner([0, 0])
183
+ commitWallPolygonDraft()
184
+ expect(useScene.getState().nodes[existing.id]?.name).toBe('Renamed again')
185
+ expect(walls()).toHaveLength(5)
186
+ })
187
+
188
+ test('a level change drops the polygon and resets the drafting tool', () => {
189
+ let toolCancels = 0
190
+ const onCancel = () => {
191
+ toolCancels += 1
192
+ }
193
+ emitter.on('tool:cancel', onCancel)
194
+ try {
195
+ drawThreeSides()
196
+ useViewer.getState().setSelection({ levelId: 'level_other' })
197
+ expect(isWallPolygonDraftOpen()).toBe(false)
198
+ expect(toolCancels).toBe(1)
199
+ expect(sceneWrites).toBe(0)
200
+ } finally {
201
+ emitter.off('tool:cancel', onCancel)
202
+ }
203
+ })
204
+
205
+ test('a view switch drops the polygon: its owning view changes, so ownership starts over', () => {
206
+ let toolCancels = 0
207
+ const onCancel = () => {
208
+ toolCancels += 1
209
+ }
210
+ emitter.on('tool:cancel', onCancel)
211
+ const layout = useEditor.getState().viewLayouts.edit
212
+ try {
213
+ drawThreeSides()
214
+ useEditor
215
+ .getState()
216
+ .setViewLayout(sceneLayout(visibleScene(useEditor.getState()) === '2d' ? '3d' : '2d'))
217
+ expect(isWallPolygonDraftOpen()).toBe(false)
218
+ expect(toolCancels).toBe(1)
219
+ // The other view's click after that finds no polygon and ends its chain
220
+ // instead of drafting on as if one were open.
221
+ expect(addWallPolygonDraftCorner([6, 6])).toBe('ended')
222
+ expect(sceneWrites).toBe(0)
223
+ } finally {
224
+ emitter.off('tool:cancel', onCancel)
225
+ useEditor.setState({ viewLayouts: layout ? { edit: layout } : {} })
226
+ }
227
+ })
228
+
229
+ test('sides split by a later crossing side keep the polygon construction settings', () => {
230
+ // A raised ground draft: the construction plane sets each wall's height
231
+ // and support offset.
232
+ startWallPolygonDraft(levelId, [0, 0], {
233
+ preferredSupportSlabId: GROUND_SUPPORT_ID,
234
+ flatConstructionBase: true,
235
+ constructionElevation: 2,
236
+ constructionHeight: 2,
237
+ })
238
+ addWallPolygonDraftCorner([4, 0])
239
+ addWallPolygonDraftCorner([4, 3])
240
+ // Crosses the first side at (3, 0): that side splits into two pieces.
241
+ addWallPolygonDraftCorner([2, -1])
242
+ const created = commitWallPolygonDraft()
243
+ const drawn = walls().filter((wall) => wall.id !== existing.id)
244
+ expect(drawn.length).toBeGreaterThan(3)
245
+ expect(new Set(created.map((wall) => wall.id))).toEqual(new Set(drawn.map((wall) => wall.id)))
246
+ for (const wall of drawn) {
247
+ expect(wall.height).toBe(2)
248
+ expect(wall.supportOffset).toBe(2)
249
+ }
250
+ })
251
+
252
+ test('commit and discard are no-ops without an open draft', () => {
253
+ expect(commitWallPolygonDraft()).toEqual([])
254
+ discardWallPolygonDraft()
255
+ expect({ sceneWrites, commits, ...history() }).toEqual({
256
+ sceneWrites: 0,
257
+ commits: 0,
258
+ past: 0,
259
+ future: 0,
260
+ })
261
+ })
262
+ })
@@ -0,0 +1,286 @@
1
+ import {
2
+ type AnyNode,
3
+ type AnyNodeId,
4
+ emitter,
5
+ planWallInsertion,
6
+ resolveWallConstruction,
7
+ runAsSingleSceneHistoryStep,
8
+ useScene,
9
+ type WallConstructionOptions,
10
+ type WallNode,
11
+ wallClosesRoom,
12
+ } from '@pascal-app/core'
13
+ import { useViewer } from '@pascal-app/viewer'
14
+ import {
15
+ chainEndJoinsExistingWall,
16
+ WALL_CONNECT_SNAP_RADIUS,
17
+ WALL_JOIN_SNAP_RADIUS,
18
+ type WallPlanPoint,
19
+ } from '../components/tools/wall/wall-snap-geometry'
20
+ import useEditor, { isMagneticSnapActive } from '../store/use-editor'
21
+ import { useFloorplanDraftPreview } from '../store/use-floorplan-draft-preview'
22
+ import { visibleScene } from '../store/view-layout'
23
+ import { sfxEmitter } from './sfx-bus'
24
+
25
+ /**
26
+ * A Polygon room is draft state until it completes: its corners live in
27
+ * `useFloorplanDraftPreview.wallPolygonDraftPoints` (drawn by the 3D tool and
28
+ * the plan as ghosts) and no wall is written. Completing it — closing on the
29
+ * first corner, sealing or teeing into existing walls, or a double-click —
30
+ * writes every wall in one `applyNodeChanges` batch: one undo step, one scene
31
+ * commit, and room detection derives the floor and ceiling from it. Esc, a
32
+ * tool / shape / level change or a history command just drops the draft; the
33
+ * scene, history, autosave and collaboration never saw it.
34
+ *
35
+ * One draft at a time: the 3D wall tool owns it, or the plan in 2D-only view.
36
+ */
37
+
38
+ type Draft = {
39
+ levelId: AnyNodeId
40
+ construction?: WallConstructionOptions
41
+ stopGuards: () => void
42
+ }
43
+
44
+ let draft: Draft | null = null
45
+
46
+ const points = () => useFloorplanDraftPreview.getState().wallPolygonDraftPoints
47
+ const setPoints = (next: readonly WallPlanPoint[]) =>
48
+ useFloorplanDraftPreview.getState().setWallPolygonDraftPoints(next)
49
+
50
+ export function isWallPolygonDraftOpen(): boolean {
51
+ return draft !== null
52
+ }
53
+
54
+ /** Starts a Polygon room at its first corner, replacing any open one. */
55
+ export function startWallPolygonDraft(
56
+ levelId: AnyNodeId,
57
+ start: WallPlanPoint,
58
+ construction?: WallConstructionOptions,
59
+ ): void {
60
+ discardWallPolygonDraft()
61
+ const abandon = () => {
62
+ discardWallPolygonDraft()
63
+ emitter.emit('tool:cancel')
64
+ }
65
+ // The corners are in this level's frame, and the view that started the
66
+ // polygon owns its clicks (the 3D tool, or the plan in 2D-only view — a
67
+ // hidden canvas keeps listening). A level or view switch abandons it and
68
+ // tells the drafting tools to reset their chain, so ownership starts over.
69
+ const scene = visibleScene(useEditor.getState())
70
+ const stops = [
71
+ useViewer.subscribe((state) => {
72
+ if (state.selection.levelId !== levelId) abandon()
73
+ }),
74
+ useEditor.subscribe((state) => {
75
+ if (visibleScene(state) !== scene) abandon()
76
+ }),
77
+ ]
78
+ draft = {
79
+ levelId,
80
+ construction,
81
+ stopGuards: () => {
82
+ for (const stop of stops) stop()
83
+ },
84
+ }
85
+ setPoints([start])
86
+ }
87
+
88
+ /** Drops the open polygon; nothing was written, so nothing is restored. */
89
+ export function discardWallPolygonDraft(): void {
90
+ const current = draft
91
+ draft = null
92
+ current?.stopGuards()
93
+ if (points().length) setPoints([])
94
+ }
95
+
96
+ const levelWallsOf = (nodes: Record<string, AnyNode>, levelId: AnyNodeId) =>
97
+ Object.values(nodes).filter(
98
+ (node): node is WallNode => node.type === 'wall' && node.parentId === levelId,
99
+ )
100
+
101
+ /** Whether `point` lies on the straight segment `wall` (within 1 mm). */
102
+ function liesOnWall(point: WallPlanPoint, wall: WallNode): boolean {
103
+ const [ax, az] = wall.start
104
+ const [bx, bz] = wall.end
105
+ const dx = bx - ax
106
+ const dz = bz - az
107
+ const lengthSquared = dx * dx + dz * dz
108
+ if (lengthSquared === 0) return false
109
+ const t = ((point[0] - ax) * dx + (point[1] - az) * dz) / lengthSquared
110
+ if (t < -1e-6 || t > 1 + 1e-6) return false
111
+ return Math.hypot(ax + t * dx - point[0], az + t * dz - point[1]) <= 1e-3
112
+ }
113
+
114
+ /**
115
+ * The polygon's walls planned against a scratch copy of the scene, the way
116
+ * the chain used to write them one by one: each side joins the previous one,
117
+ * splits the walls it tees into and skips what existing walls already cover.
118
+ * `draftWalls` are every surviving piece of the polygon's own sides — a later
119
+ * side that crosses an earlier one splits it into new walls, and those pieces
120
+ * are still the polygon's (they take its construction settings); `last` is
121
+ * the final side, for the tee / seal checks.
122
+ */
123
+ function planPolygon(
124
+ nodes: Record<AnyNodeId, AnyNode>,
125
+ levelId: AnyNodeId,
126
+ corners: WallPlanPoint[],
127
+ ) {
128
+ const virtual = { ...nodes }
129
+ const draftIds = new Set<AnyNodeId>()
130
+ let last: WallNode | undefined
131
+ const joinRadius = isMagneticSnapActive() ? WALL_JOIN_SNAP_RADIUS : WALL_CONNECT_SNAP_RADIUS
132
+ const wallDefaults = useEditor.getState().toolDefaults.wall ?? {}
133
+ for (let index = 0; index < corners.length - 1; index++) {
134
+ const result = planWallInsertion(virtual, {
135
+ levelId,
136
+ start: corners[index]!,
137
+ end: corners[index + 1]!,
138
+ joinRadius,
139
+ wallDefaults,
140
+ })
141
+ if (!result.ok) continue
142
+ const draftBefore = [...draftIds].flatMap((id) => {
143
+ const node = virtual[id]
144
+ return node?.type === 'wall' ? [node] : []
145
+ })
146
+ for (const id of result.plan.changes.delete) delete virtual[id]
147
+ for (const { node, parentId } of result.plan.changes.create) {
148
+ virtual[node.id] = { ...node, ...(parentId ? { parentId } : {}) } as AnyNode
149
+ }
150
+ for (const { id, data } of result.plan.changes.update) {
151
+ if (virtual[id]) virtual[id] = { ...virtual[id], ...data } as AnyNode
152
+ }
153
+ for (const id of result.plan.changes.delete) draftIds.delete(id)
154
+ const insertedIds = new Set(result.plan.insertedWalls.map((wall) => wall.id))
155
+ for (const { node } of result.plan.changes.create) {
156
+ if (node.type !== 'wall') continue
157
+ const descends =
158
+ insertedIds.has(node.id) ||
159
+ draftBefore.some((side) => liesOnWall(node.start, side) && liesOnWall(node.end, side))
160
+ if (descends) draftIds.add(node.id as AnyNodeId)
161
+ }
162
+ const final = result.plan.insertedWalls.at(-1)
163
+ if (final) last = virtual[final.id] as WallNode | undefined
164
+ }
165
+ const draftWalls = [...draftIds].flatMap((id) => {
166
+ const node = virtual[id]
167
+ return node?.type === 'wall' ? [node] : []
168
+ })
169
+ return { virtual, draftWalls, last }
170
+ }
171
+
172
+ /**
173
+ * The open polygon's sides as wall-shaped values — snap targets for the draft
174
+ * (so the last click can land exactly on the first corner) and the ghosts.
175
+ * Never written to the scene.
176
+ */
177
+ export function wallPolygonDraftWalls(): WallNode[] {
178
+ const current = draft
179
+ if (!current) return []
180
+ const corners = points()
181
+ return corners.slice(1).map((end, index) => ({
182
+ object: 'node',
183
+ id: `wall_polygon_draft_${index}` as WallNode['id'],
184
+ type: 'wall',
185
+ name: 'Draft wall',
186
+ parentId: current.levelId,
187
+ visible: true,
188
+ metadata: {},
189
+ children: [],
190
+ start: corners[index]!,
191
+ end,
192
+ frontSide: 'unknown',
193
+ backSide: 'unknown',
194
+ }))
195
+ }
196
+
197
+ /**
198
+ * How a corner left the polygon: still open, closed on its first corner,
199
+ * ended on existing walls, or `ended` — there was no open polygon to add to
200
+ * (it was committed or abandoned elsewhere), so the caller's chain ends too.
201
+ */
202
+ export type WallPolygonCornerResult = 'open' | 'closed' | 'joined' | 'ended'
203
+
204
+ /**
205
+ * Adds a corner. A corner within the join radius of the first one closes the
206
+ * polygon (snapped exactly onto it); a corner that tees into an existing wall
207
+ * or seals a room against the wall network ends it, as the chain did.
208
+ */
209
+ export function addWallPolygonDraftCorner(corner: WallPlanPoint): WallPolygonCornerResult {
210
+ const current = draft
211
+ const corners = points()
212
+ const first = corners[0]
213
+ if (!(current && first)) return 'ended'
214
+ const closes =
215
+ corners.length >= 3 &&
216
+ (corner[0] - first[0]) ** 2 + (corner[1] - first[1]) ** 2 <=
217
+ WALL_JOIN_SNAP_RADIUS * WALL_JOIN_SNAP_RADIUS
218
+ const next = [...corners, closes ? first : corner]
219
+ setPoints(next)
220
+ if (closes) return 'closed'
221
+
222
+ const { virtual, draftWalls, last } = planPolygon(
223
+ useScene.getState().nodes,
224
+ current.levelId,
225
+ next,
226
+ )
227
+ const levelWalls = last ? levelWallsOf(virtual, current.levelId) : []
228
+ const joins =
229
+ !!last &&
230
+ (chainEndJoinsExistingWall(
231
+ corner,
232
+ levelWalls,
233
+ draftWalls.map((wall) => wall.id),
234
+ ) ||
235
+ wallClosesRoom(levelWalls, last))
236
+ // Each placed corner ticks like the Zone tool's; a close or a join is
237
+ // answered by the build cue of the commit that follows.
238
+ if (!joins) sfxEmitter.emit('sfx:structure-build-start')
239
+ return joins ? 'joined' : 'open'
240
+ }
241
+
242
+ /**
243
+ * Writes the polygon — one history step, one scene commit — and closes the
244
+ * draft. Returns the walls it created (none for a draft without a side).
245
+ */
246
+ export function commitWallPolygonDraft(): WallNode[] {
247
+ const current = draft
248
+ if (!current) return []
249
+ const corners = points()
250
+ discardWallPolygonDraft()
251
+ if (corners.length < 2) return []
252
+
253
+ const scene = useScene.getState()
254
+ if (scene.readOnly) return []
255
+ const { virtual, draftWalls } = planPolygon(scene.nodes, current.levelId, corners)
256
+ if (!draftWalls.length) return []
257
+ const construction = resolveWallConstruction(
258
+ scene.nodes,
259
+ current.levelId,
260
+ draftWalls,
261
+ current.construction,
262
+ )
263
+ const finalized = new Map(construction.walls.map((wall) => [wall.id, wall]))
264
+ const created = Object.values(virtual).filter((node) => !scene.nodes[node.id])
265
+ const update = Object.values(virtual)
266
+ .filter((node) => scene.nodes[node.id] && node !== scene.nodes[node.id])
267
+ .map((node) => ({ id: node.id, data: node as Partial<AnyNode> }))
268
+ const source = construction.sourceSupportUpdate
269
+ if (source) {
270
+ const existing = update.find((operation) => operation.id === source.id)
271
+ if (existing) existing.data = { ...existing.data, ...source.data }
272
+ else update.push(source)
273
+ }
274
+ runAsSingleSceneHistoryStep(useScene, () =>
275
+ scene.applyNodeChanges({
276
+ create: created.map((node) => ({
277
+ node: finalized.get(node.id as WallNode['id']) ?? node,
278
+ parentId: node.parentId as AnyNodeId,
279
+ })),
280
+ update,
281
+ delete: Object.keys(scene.nodes).filter((id) => !virtual[id as AnyNodeId]) as AnyNodeId[],
282
+ }),
283
+ )
284
+ sfxEmitter.emit('sfx:structure-build')
285
+ return construction.walls
286
+ }