@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,43 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { createTerrainField, quantize, terrainContours } from '@pascal-app/core'
3
+
4
+ /** A 20 × 20 m field at 1 m spacing rising 2 m from x = −10 to x = +10 (a plane). */
5
+ function ramp() {
6
+ const field = createTerrainField({ origin: [-10, -10], spacing: 1, cols: 21, rows: 21 })
7
+ const heights = new Int16Array(field.heights)
8
+ for (let row = 0; row < 21; row++)
9
+ for (let col = 0; col < 21; col++) heights[row * 21 + col] = quantize(field, (col / 20) * 2)
10
+ return { ...field, heights }
11
+ }
12
+
13
+ const LOT: [number, number][] = [
14
+ [-8, -8],
15
+ [8, -8],
16
+ [8, 8],
17
+ [-8, 8],
18
+ ]
19
+
20
+ describe('terrainContours', () => {
21
+ test('a plane rising along x: straight north–south lines at every interval, clipped to the lot, every fifth an index', () => {
22
+ const contours = terrainContours(ramp(), 0.3048, LOT) // 1 ft interval on a 2 m rise → levels 0.30 … 1.83
23
+ expect(contours.length).toBeGreaterThanOrEqual(5)
24
+ for (const c of contours) {
25
+ // a level line of a plane rising in x is a vertical line x = const
26
+ const xs = c.points.map((p) => p[0])
27
+ expect(Math.max(...xs) - Math.min(...xs)).toBeLessThan(1e-6)
28
+ expect(Math.abs(xs[0]! - (-10 + (c.levelM / 2) * 20))).toBeLessThan(1e-6)
29
+ // clipped to the lot: no point outside z ∈ [−8, 8] by more than a cell
30
+ for (const p of c.points) expect(Math.abs(p[1])).toBeLessThanOrEqual(9)
31
+ }
32
+ expect(contours.some((c) => c.index)).toBe(true)
33
+ // one polyline per level: the chaining joined the cell segments
34
+ const levels = new Set(contours.map((c) => c.levelM.toFixed(4)))
35
+ expect(levels.size).toBe(contours.length)
36
+ })
37
+
38
+ test('no interval, or flat ground, gives no lines', () => {
39
+ expect(terrainContours(ramp(), 0, LOT)).toEqual([])
40
+ const flat = createTerrainField({ origin: [-10, -10], spacing: 1, cols: 21, rows: 21 })
41
+ expect(terrainContours(flat, 0.3048, LOT)).toEqual([])
42
+ })
43
+ })
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Site-plan contributors — how plugin packages put their own kinds on the
3
+ * site plan without the editor depending on them.
4
+ *
5
+ * The site-plan layer draws only what `buildSitePlanDrawing` returns (it
6
+ * replaces the registry layer, so `def.floorplan` is not consulted in site
7
+ * view). A plugin whose kinds live in SITE metres (utilities: poles, runs,
8
+ * service points) registers a contributor at bootstrap; its primitives are
9
+ * appended to every site-plan drawing, on screen and on sheets alike. A
10
+ * plugin that locates the house's utility services also passes `services`,
11
+ * and the site plan runs them schematically to the street.
12
+ */
13
+ import type { AnyNodeId, FloorplanGeometry, SceneSnapshot } from '@pascal-app/core'
14
+
15
+ export type SitePlanContributor = (
16
+ scene: SceneSnapshot,
17
+ ) => { primitives: FloorplanGeometry[] } | null
18
+
19
+ export type SitePlanServiceRole = 'electric' | 'water' | 'sewer'
20
+
21
+ /**
22
+ * Where one service leaves the house, in the storey's plan frame. A point
23
+ * hosted by a wall with no position of its own sits `wallT` along `wallId`.
24
+ */
25
+ export type SitePlanServicePoint = {
26
+ role: SitePlanServiceRole | 'ac' | 'pole'
27
+ position: readonly [number, number, number]
28
+ wallId?: AnyNodeId
29
+ wallT?: number
30
+ }
31
+
32
+ export type SitePlanServices = {
33
+ /** The storey's service points; the first point of each role is drawn. */
34
+ points?: (scene: SceneSnapshot, levelId: AnyNodeId) => readonly SitePlanServicePoint[]
35
+ /** The storey's electric service entrance, when the plugin decides it. */
36
+ entrance?: (scene: SceneSnapshot, levelId: AnyNodeId) => 'overhead' | 'underground' | null
37
+ /** Services the plugin already draws as its own run, so the site plan does not route them. */
38
+ drawnRuns?: (scene: SceneSnapshot) => readonly SitePlanServiceRole[]
39
+ }
40
+
41
+ type Registration = { draw: SitePlanContributor | null; services?: SitePlanServices }
42
+
43
+ const contributors = new Map<string, Registration>()
44
+ const listeners = new Set<() => void>()
45
+
46
+ export function registerSitePlanContributor(
47
+ key: string,
48
+ contributor: SitePlanContributor | null,
49
+ services?: SitePlanServices,
50
+ ): void {
51
+ contributors.set(key, { draw: contributor, services })
52
+ for (const listener of listeners) listener()
53
+ }
54
+
55
+ /** Whether any plugin has put its kinds on the site plan. */
56
+ export function hasSitePlanContributors(): boolean {
57
+ return contributors.size > 0
58
+ }
59
+
60
+ export function subscribeSitePlanContributors(listener: () => void): () => void {
61
+ listeners.add(listener)
62
+ return () => {
63
+ listeners.delete(listener)
64
+ }
65
+ }
66
+
67
+ export function sitePlanContributions(scene: SceneSnapshot): FloorplanGeometry[] {
68
+ const out: FloorplanGeometry[] = []
69
+ for (const [key, { draw }] of contributors) {
70
+ if (!draw) continue
71
+ try {
72
+ const result = draw(scene)
73
+ if (result) out.push(...result.primitives)
74
+ } catch (error) {
75
+ if (typeof console !== 'undefined')
76
+ console.warn(`[site-plan] contributor ${key} failed`, error)
77
+ }
78
+ }
79
+ return out
80
+ }
81
+
82
+ /** Every registered `services` hook's answer, in registration order; a throwing hook is skipped. */
83
+ export function sitePlanServiceAnswers<T>(
84
+ ask: (services: SitePlanServices) => T | null | undefined,
85
+ ): T[] {
86
+ const out: T[] = []
87
+ for (const [key, { services }] of contributors) {
88
+ if (!services) continue
89
+ try {
90
+ const answer = ask(services)
91
+ if (answer !== null && answer !== undefined) out.push(answer)
92
+ } catch (error) {
93
+ if (typeof console !== 'undefined') console.warn(`[site-plan] services ${key} failed`, error)
94
+ }
95
+ }
96
+ return out
97
+ }
@@ -0,0 +1,230 @@
1
+ /**
2
+ * Lot coverage and impervious surface — the ONE computation the site plan
3
+ * (A1.0) and the cover's PROJECT DATA both print, so the two sheets can never
4
+ * disagree again (QA 2026-09-23: the cover said 24.6 %, the site plan 45.6 %
5
+ * — the site plan had counted the house's own floor platform and garage pad
6
+ * as "porches" on top of the footprint).
7
+ *
8
+ * The two figures a Florida plans examiner asks for (Alachua County / the
9
+ * City of Gainesville ask for both on a new single-family site plan; the
10
+ * zoning district caps the first, the stormwater review reads the second):
11
+ *
12
+ * BUILDING COVERAGE the lot area under roofed structure: the building's
13
+ * footprint to the OUTSIDE FACE of its exterior walls —
14
+ * every storey at or above grade projected, the attached
15
+ * garage included — plus the roofed porches, patios and
16
+ * decks (a slab at least half under a roof outline).
17
+ * Eaves and the roof overhang are not counted.
18
+ * IMPERVIOUS SURFACE building coverage plus the paving ON THE LOT (an apron
19
+ * drawn out into the right-of-way is not counted): the driveway, the
20
+ * walks, and the uncovered patios, landings and decks
21
+ * (a slatted deck over pervious ground is counted —
22
+ * the conservative reading; some jurisdictions exclude
23
+ * it, verify).
24
+ *
25
+ * Both are divided by the LOT AREA: the county parcel record when the lot
26
+ * drop-in resolved one (`site.parcel.lotAreaSqFt`), else the drawn lot
27
+ * polygon. Pure: reads a scene snapshot, touches no store.
28
+ */
29
+ import { footprintOutline, levelFootprintLoops, type SceneSnapshot } from '@pascal-app/core'
30
+ import { METRES_PER_FOOT, type Pt, pointInPolygon, polygonArea, polygonBounds } from './geometry'
31
+ import {
32
+ aboveGradeLevels,
33
+ findBuilding,
34
+ findLowestLevel,
35
+ findSite,
36
+ type OutdoorPart,
37
+ outdoorParts,
38
+ roofOutlineRings,
39
+ } from './site-parts'
40
+
41
+ const SQFT_PER_SQM = 1 / (METRES_PER_FOOT * METRES_PER_FOOT)
42
+
43
+ export type ImperviousRowKey = 'building' | 'porches' | 'driveway' | 'walks' | 'patios'
44
+
45
+ export interface ImperviousRow {
46
+ key: ImperviousRowKey
47
+ label: string
48
+ sqFt: number
49
+ /** What the row holds, e.g. "2 covered: porch, rear deck". */
50
+ note: string
51
+ }
52
+
53
+ export interface SiteCoverage {
54
+ /** Lot area, SF, and where it came from; null without a lot. */
55
+ lotSqFt: number | null
56
+ lotSource: 'parcel' | 'polygon' | null
57
+ /** The building's footprint to the outside face of its walls (garage included), SF. */
58
+ buildingSqFt: number
59
+ /** Roofed porches, patios and decks, SF. */
60
+ coveredOutdoorSqFt: number
61
+ /** Building coverage = footprint + roofed porches, SF. */
62
+ buildingCoverageSqFt: number
63
+ /** Building coverage ÷ lot, 0–1; null without a lot or a building. */
64
+ buildingCoverageRatio: number | null
65
+ drivewaySqFt: number
66
+ walksSqFt: number
67
+ /** Uncovered patios, landings and decks, SF. */
68
+ openOutdoorSqFt: number
69
+ /** Everything impervious, SF. */
70
+ imperviousSqFt: number
71
+ imperviousRatio: number | null
72
+ /** The IMPERVIOUS AREA table's rows, in print order (total and % are the caller's). */
73
+ rows: ImperviousRow[]
74
+ /** One line each: what the two figures are measured to — printed with them. */
75
+ basis: { building: string; impervious: string; lot: string }
76
+ /** The parts, for the drawing: the walls' outline and every outdoor slab (site metres). */
77
+ outline: Pt[][]
78
+ parts: OutdoorPart[]
79
+ }
80
+
81
+ const sf = (m2: number) => m2 * SQFT_PER_SQM
82
+
83
+ /**
84
+ * The part of a slab's area on the lot: a driveway apron drawn out into the
85
+ * right-of-way is the road authority's paving, not the lot's. Whole when
86
+ * every corner is on the lot, else sampled on a fine grid.
87
+ */
88
+ function onLotArea(ring: readonly Pt[], lot: readonly Pt[]): number {
89
+ const area = polygonArea(ring)
90
+ if (lot.length < 3) return area
91
+ const c = ring.reduce<[number, number]>(
92
+ (s, p) => [s[0] + p[0] / ring.length, s[1] + p[1] / ring.length],
93
+ [0, 0],
94
+ )
95
+ // a corner ON the lot line (the generator's driveway mouth) counts as on the lot
96
+ const onLot = (p: Pt) =>
97
+ pointInPolygon(lot, p[0] + (c[0] - p[0]) * 1e-3, p[1] + (c[1] - p[1]) * 1e-3)
98
+ if (ring.every(onLot)) return area
99
+ const b = polygonBounds(ring)
100
+ const N = 24
101
+ let inside = 0
102
+ let kept = 0
103
+ for (let i = 0; i < N; i++) {
104
+ for (let j = 0; j < N; j++) {
105
+ const x = b.minX + ((i + 0.5) / N) * (b.maxX - b.minX)
106
+ const y = b.minY + ((j + 0.5) / N) * (b.maxY - b.minY)
107
+ if (!pointInPolygon(ring, x, y)) continue
108
+ inside++
109
+ if (pointInPolygon(lot, x, y)) kept++
110
+ }
111
+ }
112
+ return inside > 0 ? (area * kept) / inside : area
113
+ }
114
+
115
+ /** "12.3 %" — the one way both sheets print a ratio. */
116
+ export function formatCoveragePercent(ratio: number | null): string {
117
+ return ratio === null ? '—' : `${(ratio * 100).toFixed(1)} %`
118
+ }
119
+
120
+ /** "2,739 SF" — the one way both sheets print an area. */
121
+ export function formatSqFt(sqFt: number): string {
122
+ return `${Math.round(sqFt).toLocaleString('en-US')} SF`
123
+ }
124
+
125
+ export function computeSiteCoverage(scene: Pick<SceneSnapshot, 'nodes'>): SiteCoverage {
126
+ const snapshot = scene as SceneSnapshot
127
+ const site = findSite(snapshot)
128
+ const building = findBuilding(snapshot, site)
129
+ const ground = findLowestLevel(snapshot, building)
130
+ const storeys = aboveGradeLevels(snapshot, building)
131
+ const levels = storeys.length > 0 ? storeys : ground ? [ground] : []
132
+
133
+ // the footprint: every storey's wall bands, unioned, outer rings only
134
+ const loops = levels.flatMap((level) => levelFootprintLoops(snapshot.nodes, level, building))
135
+ const outline = loops.length > 0 ? footprintOutline(loops) : []
136
+ const buildingM2 = outline.reduce((s, ring) => s + polygonArea(ring), 0)
137
+
138
+ const roofs = levels.flatMap((level) => roofOutlineRings(snapshot, level, building))
139
+ const parts = outdoorParts(snapshot, ground, building, outline, roofs)
140
+ const lotRing = (site?.polygon?.points ?? []) as Pt[]
141
+ const sum = (list: readonly OutdoorPart[]) =>
142
+ list.reduce((s, p) => s + onLotArea(p.ring, lotRing), 0)
143
+ const porches = parts.filter((p) => p.kind !== 'driveway' && p.kind !== 'walk')
144
+ const covered = porches.filter((p) => p.covered)
145
+ const open = porches.filter((p) => !p.covered)
146
+ const driveway = parts.filter((p) => p.kind === 'driveway')
147
+ const walks = parts.filter((p) => p.kind === 'walk')
148
+
149
+ const lotPoly = lotRing
150
+ const parcelSqFt =
151
+ typeof site?.parcel?.lotAreaSqFt === 'number' && site.parcel.lotAreaSqFt > 0
152
+ ? site.parcel.lotAreaSqFt
153
+ : null
154
+ const polygonSqFt = lotPoly.length >= 3 ? sf(polygonArea(lotPoly)) : 0
155
+ const lotSqFt = parcelSqFt ?? (polygonSqFt > 0 ? polygonSqFt : null)
156
+ const lotSource = parcelSqFt !== null ? 'parcel' : polygonSqFt > 0 ? 'polygon' : null
157
+
158
+ const buildingSqFt = sf(buildingM2)
159
+ const coveredOutdoorSqFt = sf(sum(covered))
160
+ const buildingCoverageSqFt = buildingSqFt + coveredOutdoorSqFt
161
+ const drivewaySqFt = sf(sum(driveway))
162
+ const walksSqFt = sf(sum(walks))
163
+ const openOutdoorSqFt = sf(sum(open))
164
+ const imperviousSqFt = buildingCoverageSqFt + drivewaySqFt + walksSqFt + openOutdoorSqFt
165
+ const ratio = (x: number) =>
166
+ lotSqFt !== null && lotSqFt > 0 && buildingSqFt > 0 ? x / lotSqFt : null
167
+
168
+ const names = (list: readonly OutdoorPart[]) => list.map((p) => p.label.toLowerCase()).join(', ')
169
+ const rows: ImperviousRow[] = [
170
+ {
171
+ key: 'building',
172
+ label: 'BUILDING (INCL. GARAGE)',
173
+ sqFt: buildingSqFt,
174
+ note: 'outside face of the exterior walls',
175
+ },
176
+ {
177
+ key: 'porches',
178
+ label: 'COVERED PORCHES / PATIOS',
179
+ sqFt: coveredOutdoorSqFt,
180
+ note: covered.length > 0 ? names(covered) : 'none',
181
+ },
182
+ {
183
+ key: 'driveway',
184
+ label: 'DRIVEWAY',
185
+ sqFt: drivewaySqFt,
186
+ note: driveway.length > 0 ? 'concrete' : 'none drawn',
187
+ },
188
+ {
189
+ key: 'walks',
190
+ label: 'WALKS',
191
+ sqFt: walksSqFt,
192
+ note: walks.length > 0 ? 'concrete' : 'none drawn',
193
+ },
194
+ {
195
+ key: 'patios',
196
+ label: 'OPEN PATIOS / DECKS / LANDINGS',
197
+ sqFt: openOutdoorSqFt,
198
+ note: open.length > 0 ? names(open) : 'none',
199
+ },
200
+ ]
201
+
202
+ return {
203
+ lotSqFt,
204
+ lotSource,
205
+ buildingSqFt,
206
+ coveredOutdoorSqFt,
207
+ buildingCoverageSqFt,
208
+ buildingCoverageRatio: ratio(buildingCoverageSqFt),
209
+ drivewaySqFt,
210
+ walksSqFt,
211
+ openOutdoorSqFt,
212
+ imperviousSqFt,
213
+ imperviousRatio: ratio(imperviousSqFt),
214
+ rows,
215
+ basis: {
216
+ building:
217
+ 'Building coverage is the roofed area: the footprint to the outside face of the exterior walls (garage included, every storey projected) plus the roofed porches and patios; eaves excluded.',
218
+ impervious:
219
+ 'Impervious surface is the building coverage plus the driveway, walks and the open patios, landings and decks (decks counted — verify the local rule).',
220
+ lot:
221
+ lotSource === 'parcel'
222
+ ? 'Lot area is the county parcel record.'
223
+ : lotSource === 'polygon'
224
+ ? 'Lot area is the drawn lot polygon (not a survey).'
225
+ : 'No lot area.',
226
+ },
227
+ outline,
228
+ parts,
229
+ }
230
+ }
@@ -0,0 +1,15 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { isSitePlanAvailable } from './drawing-type-switch'
3
+
4
+ describe('isSitePlanAvailable', () => {
5
+ test('a site with neither a parcel nor setbacks has no site plan', () => {
6
+ expect(isSitePlanAvailable(null, false)).toBe(false)
7
+ expect(isSitePlanAvailable({}, false)).toBe(false)
8
+ })
9
+
10
+ test('a resolved lot or a contributing plugin offers it', () => {
11
+ expect(isSitePlanAvailable({ parcel: { apn: '06075' } }, false)).toBe(true)
12
+ expect(isSitePlanAvailable({ setbacks: { front: 6, side: 1.5, rear: 3 } }, false)).toBe(true)
13
+ expect(isSitePlanAvailable({}, true)).toBe(true)
14
+ })
15
+ })
@@ -0,0 +1,86 @@
1
+ 'use client'
2
+
3
+ import { type SiteNode, useScene } from '@pascal-app/core'
4
+ import { useEffect, useSyncExternalStore } from 'react'
5
+ import useDrawingView, { EDITOR_DRAWING_TYPE_OPTIONS } from '../../../store/use-drawing-view'
6
+ import { cn } from '../../utils'
7
+ import { hasSitePlanContributors, subscribeSitePlanContributors } from './contributors'
8
+
9
+ /**
10
+ * The site plan only has something to draw for a resolved lot (a parcel or
11
+ * setbacks on the site) or when a plugin contributes site-plan content; every
12
+ * other scene would get the default 30 m square.
13
+ */
14
+ export function isSitePlanAvailable(
15
+ site: Pick<SiteNode, 'parcel' | 'setbacks'> | null,
16
+ pluginContributes: boolean,
17
+ ): boolean {
18
+ return pluginContributes || Boolean(site?.parcel || site?.setbacks)
19
+ }
20
+
21
+ export function useSitePlanAvailable(): boolean {
22
+ const pluginContributes = useSyncExternalStore(
23
+ subscribeSitePlanContributors,
24
+ hasSitePlanContributors,
25
+ hasSitePlanContributors,
26
+ )
27
+ const siteHasLot = useScene((state) => {
28
+ for (const id of state.rootNodeIds) {
29
+ const node = state.nodes[id]
30
+ if (node?.type === 'site') return isSitePlanAvailable(node as SiteNode, false)
31
+ }
32
+ return false
33
+ })
34
+ return pluginContributes || siteHasLot
35
+ }
36
+
37
+ /**
38
+ * Floor plan ⇄ Site plan switch for the 2D editor.
39
+ *
40
+ * Sits with the other floating plan controls in the floor-plan viewport
41
+ * (bottom-left, beside the compass). Theme tokens only. Hidden, and a
42
+ * persisted site-plan choice reset to the floor plan, when the scene has no
43
+ * site plan to show.
44
+ */
45
+ export function FloorplanDrawingTypeSwitch({ className }: { className?: string }) {
46
+ const drawingType = useDrawingView((s) => s.drawingType)
47
+ const setDrawingType = useDrawingView((s) => s.setDrawingType)
48
+ const available = useSitePlanAvailable()
49
+
50
+ useEffect(() => {
51
+ if (!available && drawingType === 'site-plan') setDrawingType('floor-plan')
52
+ }, [available, drawingType, setDrawingType])
53
+
54
+ if (!available) return null
55
+
56
+ return (
57
+ <div
58
+ className={cn(
59
+ 'pointer-events-auto absolute bottom-3 left-14 z-30 flex items-center gap-0.5 rounded-full border border-border bg-card/90 p-0.5 shadow-sm backdrop-blur-md',
60
+ className,
61
+ )}
62
+ >
63
+ {EDITOR_DRAWING_TYPE_OPTIONS.map((option) => (
64
+ <button
65
+ aria-pressed={drawingType === option.id}
66
+ className={cn(
67
+ 'rounded-full px-2.5 py-1 font-medium text-xs transition',
68
+ drawingType === option.id
69
+ ? 'bg-primary text-primary-foreground'
70
+ : 'text-muted-foreground hover:text-foreground',
71
+ )}
72
+ key={option.id}
73
+ onClick={(event) => {
74
+ event.preventDefault()
75
+ event.stopPropagation()
76
+ setDrawingType(option.id)
77
+ }}
78
+ onPointerDown={(event) => event.stopPropagation()}
79
+ type="button"
80
+ >
81
+ {option.label}
82
+ </button>
83
+ ))}
84
+ </div>
85
+ )
86
+ }
@@ -0,0 +1,103 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { detectFrontEdgeFromRoads, streetCore } from './front-edge'
3
+ import type { Pt } from './geometry'
4
+
5
+ // 20 m × 30 m lot centred on the origin, y south. Edge 0 = north side,
6
+ // 1 = east, 2 = south, 3 = west.
7
+ const LOT: Pt[] = [
8
+ [-10, -15],
9
+ [10, -15],
10
+ [10, 15],
11
+ [-10, 15],
12
+ ]
13
+ const road = (name: string, ...pts: Pt[]) => ({ name, centerline: pts })
14
+
15
+ describe('streetCore', () => {
16
+ test('drops the number, the direction and the suffix', () => {
17
+ expect(streetCore('2600 Castro Way')).toBe('castro')
18
+ expect(streetCore('S Castro')).toBe('castro')
19
+ expect(streetCore('W Cass St.')).toBe('cass')
20
+ expect(streetCore('1200 W Cass St')).toBe('cass')
21
+ expect(streetCore('North Troy Street')).toBe('troy')
22
+ // OSM spells the quadrant out
23
+ expect(streetCore('Northeast 109th Street')).toBe('109th')
24
+ expect(streetCore('1247 NE 109th St')).toBe('109th')
25
+ expect(streetCore('')).toBe('')
26
+ expect(streetCore(null)).toBe('')
27
+ })
28
+ })
29
+
30
+ describe('detectFrontEdgeFromRoads', () => {
31
+ test('the nearest parallel street on the outside picks its edge', () => {
32
+ const roads = [road('Castro Way', [-60, -25], [60, -25])]
33
+ const m = detectFrontEdgeFromRoads(LOT, roads)
34
+ expect(m?.index).toBe(0)
35
+ expect(m?.name).toBe('Castro Way')
36
+ expect(m?.distance).toBeCloseTo(10, 9)
37
+ expect(m?.named).toBe(false)
38
+ })
39
+
40
+ test('a road perpendicular to an edge never fronts it', () => {
41
+ // runs north-south past the middle of the north edge, 25 m out
42
+ const roads = [road('Stab St', [0, -60], [0, -25])]
43
+ expect(detectFrontEdgeFromRoads(LOT, roads)).toBeNull()
44
+ })
45
+
46
+ test('a road through the lot (inside the edge) is rejected', () => {
47
+ const roads = [road('Driveway', [-60, -5], [60, -5])]
48
+ expect(detectFrontEdgeFromRoads(LOT, roads)).toBeNull()
49
+ })
50
+
51
+ test('corner lot: the addressed street wins over a closer cross street', () => {
52
+ const roads = [
53
+ road('Castro Way', [-60, -30], [60, -30]), // north, 15 m off
54
+ road('Elm St', [18, -60], [18, 60]), // east, 8 m off — closer
55
+ ]
56
+ expect(detectFrontEdgeFromRoads(LOT, roads)?.index).toBe(1) // closest wins with no address
57
+ const named = detectFrontEdgeFromRoads(LOT, roads, '2600 Castro Way')
58
+ expect(named?.index).toBe(0)
59
+ expect(named?.named).toBe(true)
60
+ // an address on a street that is not mapped falls back to the closest
61
+ expect(detectFrontEdgeFromRoads(LOT, roads, '9 Nowhere Ln')?.index).toBe(1)
62
+ })
63
+
64
+ test('through lot: a street of the same number but another type is not the addressed street', () => {
65
+ const roads = [
66
+ road('Northwest 34th Terrace', [-18, -60], [-18, 60]), // west, 8 m off — closer
67
+ road('Northwest 34th Street', [25, -60], [25, 60]), // east, 15 m off
68
+ ]
69
+ const m = detectFrontEdgeFromRoads(LOT, roads, '4121 NW 34th St')
70
+ expect(m?.index).toBe(1)
71
+ expect(m?.name).toBe('Northwest 34th Street')
72
+ expect(m?.named).toBe(true)
73
+ expect(m?.streetNames).toEqual({ '1': 'Northwest 34th Street', '3': 'Northwest 34th Terrace' })
74
+ // addressed on the Terrace, the Terrace fronts it
75
+ expect(detectFrontEdgeFromRoads(LOT, roads, '4120 NW 34th Ter')?.index).toBe(3)
76
+ })
77
+
78
+ test('a bent road counts by its nearest segment; slight skew within 30° passes', () => {
79
+ const roads = [road('Bend Rd', [-80, -40], [-30, -26], [30, -24], [80, -40])]
80
+ expect(detectFrontEdgeFromRoads(LOT, roads)?.index).toBe(0)
81
+ })
82
+
83
+ test('winding does not matter', () => {
84
+ const cw = [...LOT].reverse() as Pt[]
85
+ const roads = [road('Castro Way', [-60, -25], [60, -25])]
86
+ const m = detectFrontEdgeFromRoads(cw, roads)
87
+ // reversed ring: the north edge is now points[2] → points[3]
88
+ expect(m?.index).toBe(2)
89
+ })
90
+
91
+ test('no roads, no answer', () => {
92
+ expect(detectFrontEdgeFromRoads(LOT, [])).toBeNull()
93
+ expect(
94
+ detectFrontEdgeFromRoads(
95
+ [
96
+ [0, 0],
97
+ [1, 0],
98
+ ],
99
+ [road('x', [0, -5], [1, -5])],
100
+ ),
101
+ ).toBeNull()
102
+ })
103
+ })