@pascal-app/editor 1.0.4 → 1.0.6

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 (309) hide show
  1. package/README.md +106 -0
  2. package/package.json +40 -38
  3. package/src/components/editor/__bench__/room-label-placement.bench.ts +108 -0
  4. package/src/components/editor/bake-thumbnail.tsx +6 -15
  5. package/src/components/editor/construction-reveal-audio.test.tsx +150 -0
  6. package/src/components/editor/construction-reveal-audio.tsx +40 -0
  7. package/src/components/editor/custom-camera-controls.tsx +81 -8
  8. package/src/components/editor/editor-layout-mobile.tsx +1 -1
  9. package/src/components/editor/editor-layout-v2.tsx +1 -1
  10. package/src/components/editor/export-manager.tsx +14 -19
  11. package/src/components/editor/first-person/build-collider-world.test.ts +269 -0
  12. package/src/components/editor/first-person/build-collider-world.ts +22 -5
  13. package/src/components/editor/first-person/ground-holes.test.ts +128 -0
  14. package/src/components/editor/first-person/ground-holes.ts +130 -0
  15. package/src/components/editor/first-person/mechanism-targets.test.ts +89 -2
  16. package/src/components/editor/first-person/mechanism-targets.ts +52 -0
  17. package/src/components/editor/first-person-controls.tsx +19 -11
  18. package/src/components/editor/floor-aware-camera-controls.test.ts +24 -0
  19. package/src/components/editor/floorplan-mode-coordinator.tsx +2 -2
  20. package/src/components/editor/floorplan-panel.tsx +72 -23
  21. package/src/components/editor/follow-pascal.test.tsx +912 -0
  22. package/src/components/editor/follow-pascal.tsx +354 -0
  23. package/src/components/editor/group-actions.ts +2 -0
  24. package/src/components/editor/group-move-3d.ts +2 -0
  25. package/src/components/editor/group-rotate-handle.tsx +2 -0
  26. package/src/components/editor/group-transform-shared.test.ts +460 -0
  27. package/src/components/editor/index.tsx +166 -79
  28. package/src/components/editor/riser-diagram-panel.tsx +14 -7
  29. package/src/components/editor/room-highlight.tsx +13 -5
  30. package/src/components/editor/room-labels.test.ts +621 -128
  31. package/src/components/editor/room-labels.tsx +1090 -190
  32. package/src/components/editor/selection-manager.tsx +38 -117
  33. package/src/components/editor/slab-hole-highlights.tsx +2 -2
  34. package/src/components/editor/snapshot-capture-overlay.tsx +3 -2
  35. package/src/components/editor/snapshot-capture.test.ts +69 -58
  36. package/src/components/editor/snapshot-capture.ts +7 -1
  37. package/src/components/editor/thumbnail-generator.test.ts +350 -4
  38. package/src/components/editor/thumbnail-generator.tsx +128 -54
  39. package/src/components/editor/use-floorplan-background-placement.ts +3 -1
  40. package/src/components/editor/views/plugins-view.tsx +1 -8
  41. package/src/components/editor/views/view-bar.test.tsx +54 -0
  42. package/src/components/editor/views/view-bar.tsx +140 -17
  43. package/src/components/editor/wall-snap-beacon-layer.tsx +5 -4
  44. package/src/components/editor-2d/floorplan-group-move.tsx +3 -0
  45. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +2 -2
  46. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +3 -4
  47. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +1 -1
  48. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +29 -7
  49. package/src/components/editor-2d/renderers/floorplan-reveal.test.tsx +254 -0
  50. package/src/components/editor-2d/renderers/floorplan-reveal.ts +141 -0
  51. package/src/components/point-ask/bubble.tsx +538 -0
  52. package/src/components/point-ask/hover-chip.tsx +91 -0
  53. package/src/components/point-ask/mode-pill.tsx +92 -0
  54. package/src/components/point-ask/optional-motion.ts +22 -0
  55. package/src/components/point-ask/outline-corners.tsx +93 -0
  56. package/src/components/point-ask/pins.tsx +711 -0
  57. package/src/components/point-ask/point-ask-layer.tsx +83 -0
  58. package/src/components/point-ask/point-ask-styles.ts +105 -0
  59. package/src/components/point-ask/region-marquee-styles.ts +15 -0
  60. package/src/components/point-ask/region-marquee.tsx +64 -0
  61. package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +12 -2
  62. package/src/components/systems/roof/roof-edit-system.tsx +1 -0
  63. package/src/components/tools/distribution/connection-compatibility.test.ts +34 -0
  64. package/src/components/tools/distribution/connection-compatibility.ts +43 -0
  65. package/src/components/tools/distribution/distribution-public.ts +9 -0
  66. package/src/components/tools/distribution/distribution-run-contract.test.ts +26 -0
  67. package/src/components/tools/distribution/distribution-run-contract.ts +94 -0
  68. package/src/components/tools/distribution/distribution-run-tool.test.ts +241 -0
  69. package/src/components/tools/distribution/distribution-run-tool.test.tsx +125 -0
  70. package/src/components/tools/distribution/distribution-run-tool.tsx +1323 -0
  71. package/src/components/tools/distribution/draw-alignment.ts +35 -0
  72. package/src/components/tools/distribution/ports.test.ts +56 -0
  73. package/src/components/tools/distribution/ports.ts +378 -0
  74. package/src/components/tools/distribution/run-cursor.test.ts +135 -0
  75. package/src/components/tools/distribution/run-cursor.ts +36 -0
  76. package/src/components/tools/distribution/run-direction-feedback.test.ts +60 -0
  77. package/src/components/tools/distribution/run-direction-feedback.tsx +251 -0
  78. package/src/components/tools/distribution/run-port-snap.test.ts +79 -0
  79. package/src/components/tools/distribution/run-port-snap.ts +25 -0
  80. package/src/components/tools/distribution/run-screen-direction.test.ts +23 -0
  81. package/src/components/tools/distribution/run-screen-direction.ts +30 -0
  82. package/src/components/tools/elevator/elevator-tool.tsx +3 -6
  83. package/src/components/tools/elevator/move-elevator-tool.tsx +2 -3
  84. package/src/components/tools/item/item-surface-strategy-resolver.test.ts +67 -68
  85. package/src/components/tools/item/placement-history.test.tsx +38 -0
  86. package/src/components/tools/item/placement-strategies.test.ts +46 -1
  87. package/src/components/tools/item/placement-strategies.ts +3 -6
  88. package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +56 -56
  89. package/src/components/tools/item/use-draft-node.test.tsx +94 -94
  90. package/src/components/tools/item/use-placement-coordinator.tsx +55 -49
  91. package/src/components/tools/registry/item-surface-move.test.ts +62 -63
  92. package/src/components/tools/registry/item-surface-move.ts +2 -0
  93. package/src/components/tools/registry/item-surface-resolver.test.ts +26 -26
  94. package/src/components/tools/registry/move-registry-node-tool.test.ts +268 -1
  95. package/src/components/tools/registry/move-registry-node-tool.tsx +63 -29
  96. package/src/components/tools/tool-manager.tsx +14 -5
  97. package/src/components/tools/wall/wall-drafting.test.ts +170 -57
  98. package/src/components/ui/action-menu/action-button.tsx +6 -1
  99. package/src/components/ui/action-menu/camera-actions.tsx +7 -3
  100. package/src/components/ui/action-menu/control-modes.tsx +13 -7
  101. package/src/components/ui/action-menu/follow-pascal-pill.tsx +32 -0
  102. package/src/components/ui/action-menu/follow-pascal-toggle.tsx +36 -0
  103. package/src/components/ui/action-menu/index.tsx +56 -33
  104. package/src/components/ui/action-menu/measurement-control.tsx +11 -9
  105. package/src/components/ui/action-menu/placement.ts +20 -0
  106. package/src/components/ui/action-menu/point-ask-button.tsx +52 -0
  107. package/src/components/ui/action-menu/view-toggles.tsx +47 -42
  108. package/src/components/ui/build-panel/build-panel.tsx +10 -3
  109. package/src/components/ui/command-palette/editor-commands.tsx +3 -12
  110. package/src/components/ui/command-palette/index.tsx +45 -15
  111. package/src/components/ui/command-palette/ranked-groups.test.ts +31 -0
  112. package/src/components/ui/controls/action-button.tsx +5 -2
  113. package/src/components/ui/controls/metric-control.tsx +1 -1
  114. package/src/components/ui/controls/segmented-control.tsx +21 -8
  115. package/src/components/ui/controls/select-control.tsx +1 -1
  116. package/src/components/ui/controls/slider-control.tsx +1 -1
  117. package/src/components/ui/controls/toggle-control.tsx +3 -3
  118. package/src/components/ui/controls/tool-options-panel.tsx +1 -1
  119. package/src/components/ui/floating-level-selector.tsx +150 -45
  120. package/src/components/ui/helpers/contextual-helper-panel.tsx +32 -7
  121. package/src/components/ui/helpers/helper-manager.test.tsx +54 -2
  122. package/src/components/ui/helpers/helper-manager.tsx +7 -1
  123. package/src/components/ui/helpers/item-helper.tsx +9 -1
  124. package/src/components/ui/panels/collections/collections-popover.tsx +8 -8
  125. package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
  126. package/src/components/ui/panels/mobile-selection-bar.tsx +6 -3
  127. package/src/components/ui/panels/multi-selection-panel.tsx +120 -3
  128. package/src/components/ui/panels/panel-wrapper.tsx +53 -27
  129. package/src/components/ui/panels/parametric-field-control.tsx +2 -2
  130. package/src/components/ui/panels/parametric-inspector.tsx +3 -3
  131. package/src/components/ui/panels/room-panel.tsx +65 -0
  132. package/src/components/ui/primitives/badge.tsx +8 -0
  133. package/src/components/ui/primitives/sidebar.tsx +1 -1
  134. package/src/components/ui/right-stack.tsx +21 -2
  135. package/src/components/ui/sidebar/app-sidebar.tsx +1 -1
  136. package/src/components/ui/sidebar/icon-rail.tsx +3 -3
  137. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +1 -1
  138. package/src/components/ui/sidebar/panels/items-panel/index.tsx +6 -6
  139. package/src/components/ui/sidebar/panels/settings-panel/animation-speed-setting.tsx +40 -0
  140. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +7 -8
  141. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.test.tsx +170 -0
  142. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.tsx +92 -0
  143. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +107 -23
  144. package/src/components/ui/sidebar/panels/settings-panel/interface-theme.test.tsx +20 -0
  145. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +4 -10
  146. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -11
  147. package/src/components/ui/sidebar/panels/site-panel/index.tsx +5 -12
  148. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +13 -10
  149. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +2 -2
  150. package/src/components/ui/sidebar/tab-bar.tsx +4 -4
  151. package/src/components/viewer/viewer-controls-bar.tsx +18 -6
  152. package/src/components/viewer/viewer-rooms.tsx +16 -0
  153. package/src/hooks/use-auto-save.integration.test.tsx +96 -38
  154. package/src/hooks/use-auto-save.ts +26 -1
  155. package/src/hooks/use-ceiling-events.test.ts +123 -118
  156. package/src/hooks/use-grid-events.test.ts +105 -105
  157. package/src/hooks/use-keyboard.test.ts +65 -16
  158. package/src/hooks/use-keyboard.ts +29 -12
  159. package/src/hooks/use-placement-notice.test.ts +24 -1
  160. package/src/hooks/use-placement-notice.ts +37 -6
  161. package/src/index.tsx +71 -24
  162. package/src/lib/__fixtures__/owner-victor-seattle-parking.json +1 -0
  163. package/src/lib/__fixtures__/owner-villa-aurelia-basement.json +1 -0
  164. package/src/lib/add-storey.test.ts +177 -0
  165. package/src/lib/add-storey.ts +59 -0
  166. package/src/lib/below-grade.test.tsx +171 -0
  167. package/src/lib/below-grade.ts +133 -0
  168. package/src/lib/camera-facing.test.ts +24 -0
  169. package/src/lib/camera-facing.ts +24 -0
  170. package/src/lib/direct-manipulation.ts +2 -0
  171. package/src/lib/download-project-file.ts +17 -0
  172. package/src/lib/editor-level-display.ts +9 -2
  173. package/src/lib/editor-views.ts +35 -1
  174. package/src/lib/export-texture-utils.ts +2 -4
  175. package/src/lib/floor-foundation-support.test.ts +1 -0
  176. package/src/lib/floorplan/floorplan-pdfkit-document.ts +31 -16
  177. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +39 -0
  178. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +4 -0
  179. package/src/lib/floorplan/pdfkit-standard-fonts.d.ts +5 -0
  180. package/src/lib/floorplan/sheet-export.test.ts +24 -0
  181. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +34 -45
  182. package/src/lib/floorplan/site-plan/coverage.ts +2 -4
  183. package/src/lib/floorplan/site-plan/front-edge.ts +2 -1
  184. package/src/lib/floorplan/site-plan/geometry.test.ts +9 -15
  185. package/src/lib/floorplan/site-plan/geometry.ts +1 -416
  186. package/src/lib/floorplan/site-plan/index.ts +0 -16
  187. package/src/lib/floorplan/site-plan/site-annotations.test.ts +117 -0
  188. package/src/lib/floorplan/site-plan/site-annotations.ts +3 -2
  189. package/src/lib/floorplan/site-plan/site-parts.ts +14 -106
  190. package/src/lib/floorplan/site-plan/yards.test.ts +2 -1
  191. package/src/lib/follow-pascal-exterior.test-fixture.ts +17 -0
  192. package/src/lib/follow-pascal-exterior.test.ts +171 -0
  193. package/src/lib/follow-pascal-exterior.ts +39 -0
  194. package/src/lib/follow-pascal-presentation.test.ts +203 -0
  195. package/src/lib/follow-pascal-presentation.ts +129 -0
  196. package/src/lib/follow-pascal-sight.ts +84 -0
  197. package/src/lib/follow-pascal-targets.test.ts +406 -0
  198. package/src/lib/follow-pascal-targets.ts +198 -0
  199. package/src/lib/follow-pascal.test.ts +350 -0
  200. package/src/lib/follow-pascal.ts +284 -0
  201. package/src/lib/fresh-planar-placement.test.ts +112 -0
  202. package/src/lib/fresh-planar-placement.ts +57 -3
  203. package/src/lib/geometry-script/author.ts +6 -1
  204. package/src/lib/geometry-script/client.ts +8 -1
  205. package/src/lib/geometry-script/finish-scripts.ts +127 -0
  206. package/src/lib/geometry-script/protocol.ts +1 -0
  207. package/src/lib/glb-export.test.ts +187 -162
  208. package/src/lib/glb-export.ts +4 -1
  209. package/src/lib/history.test.ts +12 -7
  210. package/src/lib/interaction/scope.ts +4 -0
  211. package/src/lib/level-selection.test.ts +57 -0
  212. package/src/lib/level-selection.ts +16 -1
  213. package/src/lib/lot/clean-ring.test.ts +2 -1
  214. package/src/lib/lot/drop-in.ts +9 -24
  215. package/src/lib/lot/lot-patch.test.ts +1 -1
  216. package/src/lib/lot/lot-patch.ts +2 -2
  217. package/src/lib/measurement-kind.test.ts +6 -8
  218. package/src/lib/measurements.test.ts +23 -0
  219. package/src/lib/orbit-floor-clearance.test.ts +36 -0
  220. package/src/lib/orbit-floor-clearance.ts +48 -0
  221. package/src/lib/paint-pass-through.test.ts +11 -0
  222. package/src/lib/paint-pass-through.ts +4 -2
  223. package/src/lib/paint-scope.ts +1 -0
  224. package/src/lib/parametric-node-update.ts +16 -7
  225. package/src/lib/plugin-chat.ts +15 -0
  226. package/src/lib/point-ask/__fixtures__/consumer-browser.tsx +76 -0
  227. package/src/lib/point-ask/__fixtures__/house.ts +81 -0
  228. package/src/lib/point-ask/ask-created.test.ts +195 -0
  229. package/src/lib/point-ask/ask-flow.test.ts +380 -0
  230. package/src/lib/point-ask/build-context.test.ts +173 -0
  231. package/src/lib/point-ask/build-context.ts +94 -0
  232. package/src/lib/point-ask/capture-crop.ts +129 -0
  233. package/src/lib/point-ask/choreography.ts +69 -0
  234. package/src/lib/point-ask/compare.test.ts +145 -0
  235. package/src/lib/point-ask/compare.ts +73 -0
  236. package/src/lib/point-ask/describe-target.test.ts +165 -0
  237. package/src/lib/point-ask/describe-target.ts +234 -0
  238. package/src/lib/point-ask/dictation.ts +79 -0
  239. package/src/lib/point-ask/edge-marker.test.ts +61 -0
  240. package/src/lib/point-ask/edge-marker.ts +27 -0
  241. package/src/lib/point-ask/frame-loop.ts +25 -0
  242. package/src/lib/point-ask/history-marks.test.ts +94 -0
  243. package/src/lib/point-ask/history-marks.ts +39 -0
  244. package/src/lib/point-ask/host-api.ts +32 -0
  245. package/src/lib/point-ask/outline-box.test.ts +30 -0
  246. package/src/lib/point-ask/outline-box.ts +11 -0
  247. package/src/lib/point-ask/overlay-metrics.ts +6 -0
  248. package/src/lib/point-ask/pick.test.ts +220 -0
  249. package/src/lib/point-ask/pick.ts +121 -0
  250. package/src/lib/point-ask/pin-machine.test.ts +494 -0
  251. package/src/lib/point-ask/pin-machine.ts +217 -0
  252. package/src/lib/point-ask/placement.test.ts +203 -0
  253. package/src/lib/point-ask/placement.ts +141 -0
  254. package/src/lib/point-ask/pointer-tracker.ts +34 -0
  255. package/src/lib/point-ask/projector.ts +83 -0
  256. package/src/lib/point-ask/region.test.ts +157 -0
  257. package/src/lib/point-ask/region.ts +136 -0
  258. package/src/lib/point-ask/retry.test.ts +217 -0
  259. package/src/lib/point-ask/retry.ts +37 -0
  260. package/src/lib/point-ask/scene-commit.test.ts +312 -0
  261. package/src/lib/point-ask/scene-commit.ts +32 -0
  262. package/src/lib/point-ask/session.ts +1391 -0
  263. package/src/lib/point-ask/spring-keyframes.test.ts +45 -0
  264. package/src/lib/point-ask/spring-keyframes.ts +38 -0
  265. package/src/lib/point-ask/spring.test.ts +63 -0
  266. package/src/lib/point-ask/spring.ts +107 -0
  267. package/src/lib/point-ask/suggestions.test.ts +80 -0
  268. package/src/lib/point-ask/suggestions.ts +125 -0
  269. package/src/lib/point-ask/target-scope.test.ts +622 -0
  270. package/src/lib/point-ask/target-scope.ts +338 -0
  271. package/src/lib/point-ask/types.ts +48 -0
  272. package/src/lib/portable-export.test.ts +34 -1
  273. package/src/lib/portable-export.ts +119 -53
  274. package/src/lib/print-feature-thickness.ts +10 -1
  275. package/src/lib/print-roof-solids.test.ts +39 -1
  276. package/src/lib/print-roof-solids.ts +43 -10
  277. package/src/lib/print-shell-compiler-manifold-core.ts +1 -1
  278. package/src/lib/room-assembly-overlay.ts +2 -3
  279. package/src/lib/room-selection.test.ts +45 -1
  280. package/src/lib/scene-clipboard.test.ts +246 -1
  281. package/src/lib/scene-clipboard.ts +44 -14
  282. package/src/lib/selection-routing.ts +1 -1
  283. package/src/lib/sfx-bus.ts +13 -0
  284. package/src/lib/sfx-player.test.ts +70 -9
  285. package/src/lib/sfx-player.ts +6 -0
  286. package/src/lib/stair-levels.test.ts +25 -0
  287. package/src/lib/stair-levels.ts +22 -3
  288. package/src/lib/terrace-draft.ts +2 -1
  289. package/src/lib/viewer-selection.ts +22 -11
  290. package/src/lib/wall-placement-pointer.test.ts +42 -0
  291. package/src/lib/wall-placement-pointer.ts +57 -0
  292. package/src/lib/window-changes.ts +35 -0
  293. package/src/store/use-audio.test.ts +146 -0
  294. package/src/store/use-audio.tsx +127 -3
  295. package/src/store/use-construction-reveal.test.ts +173 -0
  296. package/src/store/use-construction-reveal.ts +145 -0
  297. package/src/store/use-corner-snap-hint.test.ts +24 -0
  298. package/src/store/use-corner-snap-hint.ts +47 -0
  299. package/src/store/use-follow-pascal.test.ts +62 -0
  300. package/src/store/use-follow-pascal.ts +53 -0
  301. package/src/store/use-point-ask.ts +247 -0
  302. package/src/store/use-stair-build-preview.ts +25 -5
  303. package/src/store/view-layout.test.ts +27 -1
  304. package/src/store/view-layout.ts +4 -1
  305. package/src/test-utils/render-current-markup.tsx +16 -0
  306. package/src/components/tools/stair/stair-click-guard.test.ts +0 -58
  307. package/src/components/tools/stair/stair-click-guard.ts +0 -72
  308. package/src/components/tools/stair/stair-defaults.ts +0 -15
  309. package/src/components/tools/stair/stair-tool.tsx +0 -682
@@ -0,0 +1,17 @@
1
+ import { getProjectFileSink, type ProjectFileSink } from '@pascal-app/core'
2
+
3
+ /** Persist before reporting a successful download; a failed save is shown by the export UI. */
4
+ export async function downloadProjectFile(
5
+ blob: Blob,
6
+ name: string,
7
+ sink: ProjectFileSink | null = getProjectFileSink(),
8
+ ) {
9
+ await sink?.({ blob, name })
10
+ if (typeof document === 'undefined') return
11
+ const url = URL.createObjectURL(blob)
12
+ const anchor = document.createElement('a')
13
+ anchor.href = url
14
+ anchor.download = name
15
+ anchor.click()
16
+ setTimeout(() => URL.revokeObjectURL(url), 0)
17
+ }
@@ -1,6 +1,7 @@
1
1
  import { useViewer } from '@pascal-app/viewer'
2
2
  import { useEffect } from 'react'
3
- import type { CaptureMode } from '../store/use-editor'
3
+ import useEditor, { type CaptureMode } from '../store/use-editor'
4
+ import { useBelowGrade, useGroundWindowBox } from './below-grade'
4
5
 
5
6
  /**
6
7
  * Whether the editor shows every level. Preview and the walkthrough show the
@@ -27,9 +28,15 @@ export function showsWholeBuilding(state: {
27
28
  *
28
29
  * The walkthrough shows the whole building, so the upper levels come back
29
30
  * while it is on; Preview hands the display to the viewer's own floor rule
30
- * (`useViewerFloorDisplay`).
31
+ * (`useViewerFloorDisplay`). A level below grade is marked as shown, and its
32
+ * ground veiled, except on the site, where the ground is what is being worked on.
31
33
  */
32
34
  export function useEditorLevelDisplay(showWholeBuilding: boolean) {
35
+ const sitePhase = useEditor((state) => state.phase === 'site')
36
+ useBelowGrade(!(showWholeBuilding || sitePhase))
37
+ // The ground window (a hillside in the eye's way) follows the shown level, and the whole
38
+ // building while it stands whole — except on the site, where the ground is the work.
39
+ useGroundWindowBox(!sitePhase)
33
40
  useEffect(() => {
34
41
  if (showWholeBuilding) return
35
42
  const keepStacked = (mode: string) => {
@@ -1,6 +1,6 @@
1
1
  import type { IconRef, LazyComponent } from '@pascal-app/core'
2
2
  import useEditor from '../store/use-editor'
3
- import { VIEW_2D, VIEW_3D } from '../store/view-layout'
3
+ import { VIEW_2D, VIEW_3D, withoutView } from '../store/view-layout'
4
4
 
5
5
  /**
6
6
  * A view fills a pane of the stage, beside or instead of the 3D scene and the
@@ -19,6 +19,10 @@ export type EditorHostView = {
19
19
  description?: string
20
20
  /** Shown in the view bar until the user unpins it. Default false: the "More views" menu only. */
21
21
  defaultPinned?: boolean
22
+ /** Always in the view bar: the user cannot unpin it. */
23
+ alwaysPinned?: boolean
24
+ /** Session views such as open documents can be dismissed from the tab bar. */
25
+ onClose?: () => void
22
26
  }
23
27
 
24
28
  /** A view as the stage needs it. The scene views have no component: the stage owns their canvases. */
@@ -93,6 +97,31 @@ class EditorHostViewRegistryImpl {
93
97
  this.emit()
94
98
  }
95
99
 
100
+ updateView(id: string, patch: Pick<EditorHostView, 'label'>): void {
101
+ const view = this.views.get(id)
102
+ if (!view) return
103
+ this.views.set(id, { ...view, ...patch })
104
+ this.emit()
105
+ }
106
+
107
+ unregisterView(id: string): void {
108
+ if (!this.views.delete(id)) return
109
+ useEditor.getState().closeView(id)
110
+ useEditor.setState((state) => {
111
+ const { [id]: _, ...pinnedViews } = state.pinnedViews
112
+ return {
113
+ pinnedViews,
114
+ viewLayouts: Object.fromEntries(
115
+ Object.entries(state.viewLayouts).map(([workspace, layout]) => [
116
+ workspace,
117
+ withoutView(layout, id),
118
+ ]),
119
+ ),
120
+ }
121
+ })
122
+ this.emit()
123
+ }
124
+
96
125
  private emit(): void {
97
126
  this.cached = Array.from(this.views.values())
98
127
  for (const listener of this.listeners) listener()
@@ -105,6 +134,10 @@ export function registerEditorHostView(view: EditorHostView): void {
105
134
  editorHostViewRegistry.registerView(view)
106
135
  }
107
136
 
137
+ export function unregisterEditorHostView(id: string): void {
138
+ editorHostViewRegistry.unregisterView(id)
139
+ }
140
+
108
141
  /**
109
142
  * Show a view: focus it if it is on screen, else put it in the focused pane —
110
143
  * or, with `beside`, split and open it next to what is there.
@@ -140,5 +173,6 @@ export function isViewPinned(
140
173
  view: EditorViewDescriptor,
141
174
  pinnedViews: Readonly<Record<string, boolean>>,
142
175
  ): boolean {
176
+ if (view.alwaysPinned) return true
143
177
  return pinnedViews[view.id] ?? view.defaultPinned ?? false
144
178
  }
@@ -56,7 +56,6 @@ export function createExportTextureUtils(
56
56
  ): ExportTextureUtils {
57
57
  let pending: Promise<WebGPURenderer> | null = null
58
58
  let disposed = false
59
- const quad = new QuadMesh()
60
59
 
61
60
  return {
62
61
  async decompress(texture, maxTextureSize = Number.POSITIVE_INFINITY) {
@@ -67,7 +66,7 @@ export function createExportTextureUtils(
67
66
  const renderer = await pending
68
67
 
69
68
  // Everything below the await is synchronous, which is what lets
70
- // overlapping calls share `quad` and the renderer's colour space.
69
+ // overlapping calls share the renderer's size and colour space.
71
70
  const image = texture.image as { width: number; height: number }
72
71
  const width = Math.min(image.width, maxTextureSize)
73
72
  const height = Math.min(image.height, maxTextureSize)
@@ -77,9 +76,8 @@ export function createExportTextureUtils(
77
76
  const previousColorSpace = renderer.outputColorSpace
78
77
  renderer.setSize(width, height)
79
78
  renderer.outputColorSpace = texture.colorSpace
80
- quad.material = material
81
79
  try {
82
- quad.render(renderer)
80
+ new QuadMesh(material).render(renderer)
83
81
  } finally {
84
82
  renderer.outputColorSpace = previousColorSpace
85
83
  material.dispose()
@@ -70,6 +70,7 @@ beforeEach(() => {
70
70
  readOnly: false,
71
71
  })
72
72
  useScene.temporal.getState().resume()
73
+ useScene.temporal.getState().clear()
73
74
  stop = initSpaceDetectionSync(useScene, { getState: () => ({ spaces: {}, setSpaces: () => {} }) })
74
75
  room(GROUND, rect(0, 0, 8, 5), 'Living')
75
76
  room(UPPER, rect(0, 0, 8, 5), 'Bedroom')
@@ -1,7 +1,13 @@
1
+ /// <reference path="./pdfkit-standard-fonts.d.ts" />
2
+ import { getProjectFileSink } from '@pascal-app/core'
1
3
  import type PdfKitDocument from 'pdfkit'
4
+ import { downloadProjectFile } from '../download-project-file'
2
5
 
3
6
  type PdfKitDocumentInstance = InstanceType<typeof PdfKitDocument>
4
7
 
8
+ /** pdfkit 0.20's browser-build export, which @types/pdfkit does not describe. */
9
+ type PdfKitStandardFontRegistry = { registerStdFonts?: (...fonts: unknown[]) => void }
10
+
5
11
  type PdfTextOptions = {
6
12
  align?: 'left' | 'center' | 'right'
7
13
  maxWidth?: number
@@ -461,16 +467,30 @@ export async function createFloorplanPdfDocument(
461
467
  defaultPageSize: readonly [number, number],
462
468
  options: { title?: string } = {},
463
469
  ) {
464
- const [{ default: PDFDocument }, { default: blobStream }, fonts] = await Promise.all([
465
- import('pdfkit/js/pdfkit.standalone'),
466
- import('blob-stream'),
467
- // an export in the standard fonts beats no export when the font files are unreachable
468
- loadFloorplanPdfFonts().catch((error: unknown) => {
469
- console.warn('[floorplan-export] Falling back to the standard PDF fonts', error)
470
- return null
471
- }),
472
- ])
473
- const raw = new PDFDocument({
470
+ const fileSink = getProjectFileSink()
471
+ const [pdfkit, helvetica, helveticaBold, courier, courierBold, { default: blobStream }, fonts] =
472
+ await Promise.all([
473
+ import('pdfkit'),
474
+ import('pdfkit/standard-fonts/Helvetica'),
475
+ import('pdfkit/standard-fonts/HelveticaBold'),
476
+ import('pdfkit/standard-fonts/Courier'),
477
+ import('pdfkit/standard-fonts/CourierBold'),
478
+ import('blob-stream'),
479
+ // an export in the standard fonts beats no export when the font files are unreachable
480
+ loadFloorplanPdfFonts().catch((error: unknown) => {
481
+ console.warn('[floorplan-export] Falling back to the standard PDF fonts', error)
482
+ return null
483
+ }),
484
+ ])
485
+ // The browser build has no standard-font metrics until they are registered, and
486
+ // the constructor already sets Helvetica; the Node build loads its own and has no registry.
487
+ ;(pdfkit as PdfKitStandardFontRegistry).registerStdFonts?.(
488
+ helvetica.default,
489
+ helveticaBold.default,
490
+ courier.default,
491
+ courierBold.default,
492
+ )
493
+ const raw = new pdfkit.default({
474
494
  autoFirstPage: false,
475
495
  compress: true,
476
496
  margin: 0,
@@ -487,12 +507,7 @@ export async function createFloorplanPdfDocument(
487
507
  raw.on('error', reject)
488
508
  raw.end()
489
509
  })
490
- const url = URL.createObjectURL(blob)
491
- const anchor = document.createElement('a')
492
- anchor.href = url
493
- anchor.download = filename
494
- anchor.click()
495
- setTimeout(() => URL.revokeObjectURL(url), 0)
510
+ await downloadProjectFile(blob, filename, fileSink)
496
511
  },
497
512
  }
498
513
  }
@@ -98,6 +98,45 @@ describe('renderFloorplanGeometryToPdfKit', () => {
98
98
  expect(pdf).toMatch(/f\*/)
99
99
  })
100
100
 
101
+ test('ends an unpainted shape so the next fill does not paint it', async () => {
102
+ // Zones emit fill/stroke 'none' outlines; walls fill right after them.
103
+ const geometry = {
104
+ kind: 'group',
105
+ children: [
106
+ {
107
+ kind: 'polygon',
108
+ points: [
109
+ [0, 0],
110
+ [4, 0],
111
+ [4, 4],
112
+ [0, 4],
113
+ ],
114
+ fill: 'none',
115
+ stroke: 'none',
116
+ },
117
+ {
118
+ kind: 'polygon',
119
+ points: [
120
+ [0, 0],
121
+ [1, 0],
122
+ [1, 0.2],
123
+ [0, 0.2],
124
+ ],
125
+ fill: '#374151',
126
+ },
127
+ ],
128
+ } as FloorplanGeometry
129
+
130
+ const pdf = await renderTestPdf(geometry)
131
+ const ops = pdf.split('\n')
132
+ const firstClose = ops.indexOf('h')
133
+
134
+ // the unpainted outline is closed and discarded ('n') before the filled one starts
135
+ expect(firstClose).toBeGreaterThan(-1)
136
+ expect(ops[firstClose + 1]).toBe('n')
137
+ expect(pdf).toMatch(/\bf\n/)
138
+ })
139
+
101
140
  test('writes a data-url PNG image without resolving it as an asset', async () => {
102
141
  const geometry = {
103
142
  kind: 'image',
@@ -217,6 +217,10 @@ function paintStyledGeometry(
217
217
  if (fill && stroke) raw.fillAndStroke(fill, stroke, fillRule)
218
218
  else if (fill) raw.fill(fill, fillRule)
219
219
  else if (stroke) raw.stroke(stroke)
220
+ // Unpainted shape (fill and stroke both none, e.g. zone hit areas): end the
221
+ // path without painting ('n'). Otherwise the open path is carried into the
222
+ // next fill and printed in that shape's colour.
223
+ else raw.addContent('n')
220
224
  }
221
225
 
222
226
  function isPaint(value: unknown): boolean {
@@ -0,0 +1,5 @@
1
+ // pdfkit 0.20 ships these entry points without types, and @types/pdfkit predates them.
2
+ declare module 'pdfkit/standard-fonts/*' {
3
+ const font: unknown
4
+ export default font
5
+ }
@@ -1,4 +1,5 @@
1
1
  import { afterEach, describe, expect, test } from 'bun:test'
2
+ import { setProjectFileSink } from '@pascal-app/core'
2
3
  import { exportSheetsToPdf, type SheetPdfPage } from './sheet-export'
3
4
 
4
5
  const sheet = (number: string, title: string): SheetPdfPage => ({
@@ -74,6 +75,29 @@ function titles(pdf: string): string[] {
74
75
  }
75
76
 
76
77
  describe('exportSheetsToPdf', () => {
78
+ test('retains the PDF in project files before downloading and reports a failed save', async () => {
79
+ let kept: { name: string; blob: Blob } | null = null
80
+ const clear = setProjectFileSink(async (file) => {
81
+ kept = file
82
+ })
83
+ try {
84
+ const { filename } = await exportToBytes([sheet('A0.0', 'Cover')], 'Project files')
85
+ expect(kept!.name).toBe(filename)
86
+ expect(await kept!.blob.text()).toStartWith('%PDF-')
87
+ } finally {
88
+ clear()
89
+ }
90
+ const clearFailed = setProjectFileSink(async () => {
91
+ throw new Error('File could not be saved')
92
+ })
93
+ try {
94
+ await expect(exportToBytes([sheet('A0.0', 'Cover')], 'Project files')).rejects.toThrow(
95
+ 'File could not be saved',
96
+ )
97
+ } finally {
98
+ clearFailed()
99
+ }
100
+ })
77
101
  test('bookmarks every sheet by its number and title and titles the document', async () => {
78
102
  const { pdf, filename } = await exportToBytes(
79
103
  [
@@ -2,14 +2,28 @@ import {
2
2
  type AnyNode,
3
3
  type AnyNodeId,
4
4
  type BuildingNode,
5
+ castYardDimensionsOriented,
6
+ classifyEdges,
7
+ edgeHeadingDeg,
8
+ edgeLength,
5
9
  envelopeFrontEdge,
6
10
  type FloorplanGeometry,
11
+ footprintOutline,
12
+ isRoofLevel,
7
13
  type LevelNode,
14
+ levelFootprintLoops,
15
+ outwardNormal,
8
16
  type SceneSnapshot,
9
17
  type SiteNode,
18
+ setbackForRole,
10
19
  sightTriangle,
20
+ siteFrame,
21
+ siteFrontEdge,
22
+ siteSetbackEnvelope,
11
23
  streetCorners,
12
24
  terrainFieldOf,
25
+ type YardDimension,
26
+ yawOf,
13
27
  } from '@pascal-app/core'
14
28
  import { sitePlanContributions } from './contributors'
15
29
  import {
@@ -20,21 +34,11 @@ import {
20
34
  } from './coverage'
21
35
  import {
22
36
  type Bounds,
23
- boundsInsidePolygon,
24
- castYardDimensionsOriented,
25
- classifyEdges,
26
37
  compassLabel,
27
- edgeHeadingDeg,
28
- edgeLength,
29
38
  formatFeetInches,
30
39
  METRES_PER_FOOT,
31
- outwardNormal,
32
40
  type Pt,
33
41
  polygonBounds,
34
- resolveFrontEdge,
35
- setbackEnvelope,
36
- setbackForRole,
37
- type YardDimension,
38
42
  } from './geometry'
39
43
  import {
40
44
  contourPrimitives,
@@ -47,17 +51,14 @@ import {
47
51
  streetEdgeNames,
48
52
  } from './site-annotations'
49
53
  import {
54
+ aboveGradeLevels,
50
55
  findBuilding,
51
56
  findLowestLevel,
52
57
  findSite,
53
- footprintOutline,
54
- levelFootprintLoops,
55
58
  roofOutlineRings,
56
- siteFrame,
57
- yawOf,
58
59
  } from './site-parts'
59
60
 
60
- export { footprintOutline, levelFootprintLoops, roofOutlineRings }
61
+ export { roofOutlineRings }
61
62
 
62
63
  /** Contract shared by every drawing producer (see docs/construction-documents.md). */
63
64
  export interface SitePlanDrawing {
@@ -71,8 +72,11 @@ export interface SitePlanDrawing {
71
72
  frontEdge: number
72
73
  lot: Pt[]
73
74
  envelope: Pt[]
74
- /** The envelope edge behind the lot's front line (the envelope has its own vertex count now). */
75
- envelopeFrontEdge: number
75
+ /**
76
+ * The envelope edge behind the lot's front line (the envelope has its own vertex count now);
77
+ * null with no envelope, or when no envelope edge faces the street.
78
+ */
79
+ envelopeFrontEdge: number | null
76
80
  /** The corner sight triangles (corner, leg end A, leg end B), site metres. */
77
81
  sightTriangles: Pt[][]
78
82
  /** Per-wall footprint bands of the lowest level, in SITE metres. */
@@ -316,10 +320,10 @@ export function buildSitePlanDrawing(scene: SceneSnapshot): SitePlanDrawing {
316
320
  const lot = (site?.polygon?.points ?? []) as Pt[]
317
321
  const building = findBuilding(scene, site)
318
322
  const level = findLowestLevel(scene, building)
319
- const footprintLoops = levelFootprintLoops(scene, level, building)
323
+ const footprintLoops = levelFootprintLoops(scene.nodes, level, building)
320
324
  const footprintBounds = unionBounds(footprintLoops)
321
325
  const northRotation = site?.northRotation ?? 0
322
- const frontEdge = resolveFrontEdge(lot, site?.frontEdge, northRotation)
326
+ const frontEdge = site ? siteFrontEdge(site) : 0
323
327
 
324
328
  const empty: SitePlanDrawing = {
325
329
  primitives: [],
@@ -330,7 +334,7 @@ export function buildSitePlanDrawing(scene: SceneSnapshot): SitePlanDrawing {
330
334
  frontEdge,
331
335
  lot,
332
336
  envelope: [],
333
- envelopeFrontEdge: 0,
337
+ envelopeFrontEdge: null,
334
338
  sightTriangles: [],
335
339
  footprintLoops,
336
340
  footprintBounds,
@@ -393,9 +397,7 @@ export function buildSitePlanDrawing(scene: SceneSnapshot): SitePlanDrawing {
393
397
  typeof site?.sightTriangleFt === 'number' && site.sightTriangleFt > 0
394
398
  ? site.sightTriangleFt * METRES_PER_FOOT
395
399
  : 0
396
- const envelope = site?.setbacks
397
- ? setbackEnvelope(lot, site.setbacks, frontEdge, { streetEdges, sightTriangleM })
398
- : []
400
+ const envelope = site ? siteSetbackEnvelope(site) : []
399
401
  // the corner sight triangles (clear-vision at a street intersection):
400
402
  // dashed, labelled with the leg — the ordinance's figure, verify locally
401
403
  const sightTriangles: Pt[][] = []
@@ -566,7 +568,11 @@ export function buildSitePlanDrawing(scene: SceneSnapshot): SitePlanDrawing {
566
568
  }
567
569
 
568
570
  // ── The roof line, dashed over the footprint ─────────────────────────
569
- for (const ring of roofOutlineRings(scene, level, building)) {
571
+ // Every storey's roofs, and the roof level's above them all.
572
+ const roofRings = aboveGradeLevels(scene, building).flatMap((roofed) =>
573
+ roofOutlineRings(scene, roofed, building),
574
+ )
575
+ for (const ring of roofRings) {
570
576
  primitives.push({
571
577
  kind: 'polygon',
572
578
  points: ring,
@@ -650,7 +656,9 @@ export function buildSitePlanDrawing(scene: SceneSnapshot): SitePlanDrawing {
650
656
  let storeys = 0
651
657
  for (const childId of building?.children ?? []) {
652
658
  const child = scene.nodes[childId as AnyNodeId] as AnyNode | undefined
653
- if (child?.type === 'level' && (child as LevelNode).level >= 0) storeys++
659
+ // The roof level is not a storey: a bungalow under it is one storey.
660
+ if (child?.type === 'level' && (child as LevelNode).level >= 0 && !isRoofLevel(child))
661
+ storeys++
654
662
  }
655
663
  const cx = (footprintBounds.minX + footprintBounds.maxX) / 2
656
664
  const cy = (footprintBounds.minY + footprintBounds.maxY) / 2
@@ -748,7 +756,7 @@ export function buildSitePlanDrawing(scene: SceneSnapshot): SitePlanDrawing {
748
756
  frontEdge,
749
757
  lot,
750
758
  envelope,
751
- envelopeFrontEdge: envelope.length >= 3 ? envelopeFrontEdge(lot, frontEdge, envelope) : 0,
759
+ envelopeFrontEdge: envelope.length >= 3 ? envelopeFrontEdge(lot, frontEdge, envelope) : null,
752
760
  sightTriangles,
753
761
  footprintLoops,
754
762
  footprintBounds,
@@ -758,22 +766,3 @@ export function buildSitePlanDrawing(scene: SceneSnapshot): SitePlanDrawing {
758
766
  },
759
767
  }
760
768
  }
761
-
762
- /**
763
- * Translation (site metres, `[dx, dz]`) that centres the building's footprint
764
- * on the lot centroid. `null` when there is nothing to move or the footprint
765
- * already sits inside the ring.
766
- */
767
- export function buildingRecentreOffset(
768
- lot: readonly Pt[],
769
- footprintBounds: Bounds | null,
770
- ): [number, number] | null {
771
- if (lot.length < 3 || !footprintBounds) return null
772
- if (boundsInsidePolygon(lot, footprintBounds)) return null
773
- const lotB = polygonBounds(lot)
774
- const lotCx = (lotB.minX + lotB.maxX) / 2
775
- const lotCy = (lotB.minY + lotB.maxY) / 2
776
- const fpCx = (footprintBounds.minX + footprintBounds.maxX) / 2
777
- const fpCy = (footprintBounds.minY + footprintBounds.maxY) / 2
778
- return [lotCx - fpCx, lotCy - fpCy]
779
- }
@@ -26,15 +26,13 @@
26
26
  * drop-in resolved one (`site.parcel.lotAreaSqFt`), else the drawn lot
27
27
  * polygon. Pure: reads a scene snapshot, touches no store.
28
28
  */
29
- import type { SceneSnapshot } from '@pascal-app/core'
29
+ import { footprintOutline, levelFootprintLoops, type SceneSnapshot } from '@pascal-app/core'
30
30
  import { METRES_PER_FOOT, type Pt, pointInPolygon, polygonArea, polygonBounds } from './geometry'
31
31
  import {
32
32
  aboveGradeLevels,
33
33
  findBuilding,
34
34
  findLowestLevel,
35
35
  findSite,
36
- footprintOutline,
37
- levelFootprintLoops,
38
36
  type OutdoorPart,
39
37
  outdoorParts,
40
38
  roofOutlineRings,
@@ -133,7 +131,7 @@ export function computeSiteCoverage(scene: Pick<SceneSnapshot, 'nodes'>): SiteCo
133
131
  const levels = storeys.length > 0 ? storeys : ground ? [ground] : []
134
132
 
135
133
  // the footprint: every storey's wall bands, unioned, outer rings only
136
- const loops = levels.flatMap((level) => levelFootprintLoops(snapshot, level, building))
134
+ const loops = levels.flatMap((level) => levelFootprintLoops(snapshot.nodes, level, building))
137
135
  const outline = loops.length > 0 ? footprintOutline(loops) : []
138
136
  const buildingM2 = outline.reduce((s, ring) => s + polygonArea(ring), 0)
139
137
 
@@ -22,7 +22,8 @@
22
22
  * Coordinates are the site plan frame (metres, x east, y south), the frame
23
23
  * the lot polygon and the OSM roads share.
24
24
  */
25
- import { edgeLength, outwardNormal, type Pt } from './geometry'
25
+ import { edgeLength, outwardNormal } from '@pascal-app/core'
26
+ import type { Pt } from './geometry'
26
27
  import { parseStreetName, sameStreet } from './street-name'
27
28
 
28
29
  export interface RoadCenterline {
@@ -1,20 +1,22 @@
1
1
  import { describe, expect, it } from 'bun:test'
2
- import { sightTriangle, streetCorners } from '@pascal-app/core'
3
2
  import {
4
- boundsInsidePolygon,
5
- castYardDimensions,
6
3
  classifyEdges,
7
4
  edgeHeadingDeg,
8
- formatFeetInches,
9
- METRES_PER_FOOT,
10
5
  mostNorthFacingEdge,
11
6
  outwardNormal,
12
- type Pt,
13
- polygonArea,
14
7
  rayToPolygon,
15
8
  resolveFrontEdge,
16
9
  setbackEnvelope,
17
10
  setbackForRole,
11
+ sightTriangle,
12
+ streetCorners,
13
+ } from '@pascal-app/core'
14
+ import {
15
+ castYardDimensions,
16
+ formatFeetInches,
17
+ METRES_PER_FOOT,
18
+ type Pt,
19
+ polygonArea,
18
20
  } from './geometry'
19
21
 
20
22
  /**
@@ -258,14 +260,6 @@ describe('rayToPolygon + castYardDimensions', () => {
258
260
  })
259
261
  })
260
262
 
261
- describe('boundsInsidePolygon', () => {
262
- it('is true only when every bbox corner is inside the lot', () => {
263
- expect(boundsInsidePolygon(LOT, { minX: 5, minY: 5, maxX: 25, maxY: 15 })).toBe(true)
264
- expect(boundsInsidePolygon(LOT, { minX: -5, minY: 5, maxX: 25, maxY: 15 })).toBe(false)
265
- expect(boundsInsidePolygon(LOT, { minX: 100, minY: 100, maxX: 110, maxY: 110 })).toBe(false)
266
- })
267
- })
268
-
269
263
  describe('formatFeetInches', () => {
270
264
  it('renders metres as feet-and-inches', () => {
271
265
  expect(formatFeetInches(7.62)).toBe(`25'-0"`)