@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
@@ -27,6 +27,11 @@ export type FloorplanAnnotationRole =
27
27
  | 'structural-grid'
28
28
  | 'column-center'
29
29
  | 'room-label'
30
+ /** A room's finish / ceiling-height lines — detail under the name, off in the clean plan. */
31
+ | 'room-detail'
32
+ /** The roof plan proper: outline, ridges, hips, slope arrows. Off over a floor plan unless asked. */
33
+ | 'roof-plan'
34
+ | 'roof-pitch'
30
35
  | 'stair-annotation'
31
36
 
32
37
  export type FloorplanSchedule = {
@@ -85,6 +90,8 @@ export type FloorplanNodeExtension<N extends AnyNode = AnyNode> = {
85
90
  nodes: Readonly<Record<string, AnyNode>>
86
91
  levelId: AnyNodeId
87
92
  unit: 'metric' | 'imperial'
93
+ /** A drafted sheet's schedule: marks match the sheet's tags. */
94
+ drafting?: boolean
88
95
  }) => FloorplanSchedule | null
89
96
  linkedLevelIds?: (node: N) => readonly AnyNodeId[]
90
97
  resolveForDrawing?: (args: {
@@ -98,11 +105,30 @@ type FloorplanGeometryMetadata = {
98
105
  annotationRole?: FloorplanAnnotationRole
99
106
  annotationObstacle?: 'bounds' | 'outline'
100
107
  renderPass?: 'overlay'
108
+ /**
109
+ * Annotation text only: the size to print at, in paper points, instead of
110
+ * the size its role implies. Lets a sheet set a room tag's name larger than
111
+ * its number without inventing a role per size.
112
+ */
113
+ textSizePt?: number
101
114
  }
102
115
 
103
116
  type FloorplanContextExtension = {
104
117
  automaticDimensions: boolean
105
118
  purpose: FloorplanRenderPurpose
119
+ /**
120
+ * Sheet drafting (the Sheets plan viewports): kinds draw the permit-set
121
+ * convention — fixtures as labelled linework, furniture as light outlines,
122
+ * no raster sprites. Only `collectSheetGeometry` sets it; the editor and the
123
+ * read-only viewer never do, so their plans keep today's look.
124
+ */
125
+ drafting: boolean
126
+ /**
127
+ * The editor canvas draws room labels as screen pills over the plan
128
+ * (`RoomLabels2D`), so zones skip their drawn tags and wash there. Exports,
129
+ * sheets and the read-only viewer keep the drawn tags.
130
+ */
131
+ roomLabelOverlay: boolean
106
132
  metricNotation: FloorplanMetricNotation
107
133
  wallDimensionReference: FloorplanWallDimensionReference
108
134
  }
@@ -156,6 +182,8 @@ export function createFloorplanContextExtensions(
156
182
  [FLOORPLAN_CONTEXT_EXTENSION_KEY]: {
157
183
  automaticDimensions: values.automaticDimensions !== false,
158
184
  purpose: values.purpose === 'document' ? 'document' : 'edit',
185
+ drafting: values.drafting === true,
186
+ roomLabelOverlay: values.roomLabelOverlay === true,
159
187
  metricNotation: values.metricNotation === 'millimeters' ? 'millimeters' : 'meters',
160
188
  wallDimensionReference: normalizeFloorplanWallDimensionReference(
161
189
  values.wallDimensionReference,
@@ -171,6 +199,8 @@ export function readFloorplanContext(ctx: GeometryContext): FloorplanContextExte
171
199
  return {
172
200
  automaticDimensions: extension.automaticDimensions !== false,
173
201
  purpose: extension.purpose === 'document' ? 'document' : 'edit',
202
+ drafting: extension.drafting === true,
203
+ roomLabelOverlay: extension.roomLabelOverlay === true,
174
204
  metricNotation: extension.metricNotation === 'millimeters' ? 'millimeters' : 'meters',
175
205
  wallDimensionReference: normalizeFloorplanWallDimensionReference(
176
206
  extension.wallDimensionReference,
@@ -180,6 +210,8 @@ export function readFloorplanContext(ctx: GeometryContext): FloorplanContextExte
180
210
  return {
181
211
  automaticDimensions: true,
182
212
  purpose: 'edit',
213
+ drafting: false,
214
+ roomLabelOverlay: false,
183
215
  metricNotation: 'meters',
184
216
  wallDimensionReference: DEFAULT_FLOORPLAN_WALL_DIMENSION_REFERENCE,
185
217
  }
@@ -62,6 +62,8 @@ describe('floor-plan mode', () => {
62
62
  openingMarks: false,
63
63
  structuralGrids: false,
64
64
  roomLabels: true,
65
+ roomDetails: false,
66
+ roofPlan: false,
65
67
  stairAnnotations: false,
66
68
  })
67
69
 
@@ -107,6 +109,8 @@ describe('floor-plan mode', () => {
107
109
  openingMarks: false,
108
110
  structuralGrids: false,
109
111
  roomLabels: true,
112
+ roomDetails: false,
113
+ roofPlan: false,
110
114
  stairAnnotations: false,
111
115
  })
112
116
  })
@@ -58,6 +58,8 @@ export function resolveFloorplanAnnotationVisibility(
58
58
  openingMarks: false,
59
59
  structuralGrids: false,
60
60
  roomLabels: true,
61
+ roomDetails: false,
62
+ roofPlan: false,
61
63
  stairAnnotations: false,
62
64
  }
63
65
  return interactive && context.referencedAnnotationRole
@@ -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
@@ -9,6 +15,124 @@ type PdfTextOptions = {
9
15
 
10
16
  type PdfShapeStyle = 'F' | 'S'
11
17
 
18
+ /**
19
+ * The faces every exported PDF embeds. pdfkit's standard 14 fonts are
20
+ * WinAnsi-only: the ≤ ≥ ≈ → △ Δ ⅛ ′ ″ the sheets print came out as byte
21
+ * pairs of the code point ("≤ 195 mph" printed as `"d 195 mph`), and the
22
+ * fonts were not embedded at all. Liberation Sans is metric-compatible with
23
+ * Helvetica, so every width the sheets were laid out against is unchanged;
24
+ * Geist Mono keeps Courier's 0.6 em advance. Both are SIL OFL 1.1 — the
25
+ * licences sit beside the files.
26
+ */
27
+ const PDF_FONT_FILES = {
28
+ sans: new URL('./pdf-fonts/LiberationSans-Regular.ttf', import.meta.url),
29
+ 'sans-bold': new URL('./pdf-fonts/LiberationSans-Bold.ttf', import.meta.url),
30
+ mono: new URL('./pdf-fonts/GeistMono-Regular.ttf', import.meta.url),
31
+ 'mono-bold': new URL('./pdf-fonts/GeistMono-Bold.ttf', import.meta.url),
32
+ } as const
33
+
34
+ export type FloorplanPdfFace = keyof typeof PDF_FONT_FILES
35
+ export type FloorplanPdfFonts = Record<FloorplanPdfFace, ArrayBuffer>
36
+
37
+ /** pdfkit's built-in faces, used when the embedded ones could not be fetched. */
38
+ const STANDARD_FACES: Record<FloorplanPdfFace, string> = {
39
+ sans: 'Helvetica',
40
+ 'sans-bold': 'Helvetica-Bold',
41
+ mono: 'Courier',
42
+ 'mono-bold': 'Courier-Bold',
43
+ }
44
+
45
+ /** A glyph the face lacks is taken from the other family at the same weight. */
46
+ const OTHER_FAMILY: Record<FloorplanPdfFace, FloorplanPdfFace> = {
47
+ sans: 'mono',
48
+ 'sans-bold': 'mono-bold',
49
+ mono: 'sans',
50
+ 'mono-bold': 'sans-bold',
51
+ }
52
+
53
+ /**
54
+ * Symbols the sheets print that neither face carries, drawn as linework in
55
+ * the text's colour, with their advance in em. The warning triangle keeps a
56
+ * real "!" inside it, so the text layer still reads as a warning.
57
+ */
58
+ const DRAWN_SYMBOLS = {
59
+ '⚠': { symbol: 'warning', advanceEm: 0.9 },
60
+ '▸': { symbol: 'pointer', advanceEm: 0.5 },
61
+ '∠': { symbol: 'angle', advanceEm: 0.8 },
62
+ } as const
63
+
64
+ type DrawnSymbol = (typeof DRAWN_SYMBOLS)[keyof typeof DRAWN_SYMBOLS]['symbol']
65
+
66
+ type TextRun =
67
+ | { kind: 'text'; face: FloorplanPdfFace; text: string }
68
+ | { kind: 'symbol'; symbol: DrawnSymbol; advanceEm: number }
69
+
70
+ /** The SVG `dominant-baseline` values the floor-plan geometry uses. */
71
+ export type FloorplanPdfBaseline = 'auto' | 'alphabetic' | 'middle' | 'central' | 'hanging'
72
+
73
+ export type FloorplanPdfTextStyle = {
74
+ fontFamily?: string
75
+ fontWeight?: number | string
76
+ fontSize: number
77
+ }
78
+
79
+ /**
80
+ * SVG text's white-space handling (`white-space: normal`; the geometry has no
81
+ * way to ask for `pre`): runs of spaces, tabs and line breaks print as one
82
+ * space and the ends are trimmed. `APN 06075` reads with one space on
83
+ * screen, so it does on paper — and is measured that way for anchoring.
84
+ */
85
+ function collapseSvgWhitespace(text: string): string {
86
+ return text.replace(/[ \t\r\n]+/g, ' ').trim()
87
+ }
88
+
89
+ /** pdfkit keeps the current face's metrics and its fontkit font on `_font`; neither is public API. */
90
+ type EmbeddedFace = {
91
+ ascender: number
92
+ descender: number
93
+ xHeight: number
94
+ font: { hasGlyphForCodePoint: (codePoint: number) => boolean }
95
+ }
96
+
97
+ let pdfFonts: Promise<FloorplanPdfFonts> | null = null
98
+
99
+ /** The embedded faces' bytes, fetched once and shared by every export. */
100
+ export function loadFloorplanPdfFonts(): Promise<FloorplanPdfFonts> {
101
+ if (pdfFonts) return pdfFonts
102
+ const loading = Promise.all(
103
+ Object.entries(PDF_FONT_FILES).map(async ([face, url]) => {
104
+ const response = await fetch(url)
105
+ if (!response.ok) {
106
+ throw new Error(
107
+ `[floorplan-export] Could not load the PDF font ${face} (HTTP ${response.status})`,
108
+ )
109
+ }
110
+ return [face, await response.arrayBuffer()] as const
111
+ }),
112
+ ).then((entries) => Object.fromEntries(entries) as FloorplanPdfFonts)
113
+ pdfFonts = loading
114
+ // a failed fetch is retried by the next export rather than cached
115
+ loading.catch(() => {
116
+ if (pdfFonts === loading) pdfFonts = null
117
+ })
118
+ return loading
119
+ }
120
+
121
+ /** SVG font-family / font-weight → the embedded face that sets it. */
122
+ export function resolveFloorplanPdfFace(
123
+ fontFamily: string | undefined,
124
+ fontWeight: number | string | undefined,
125
+ ): FloorplanPdfFace {
126
+ const family = fontFamily?.toLocaleLowerCase() ?? ''
127
+ const mono = family.includes('mono') || family.includes('courier')
128
+ const numericWeight = Number.parseInt(String(fontWeight ?? 400), 10)
129
+ const bold =
130
+ String(fontWeight).toLocaleLowerCase() === 'bold' ||
131
+ (Number.isFinite(numericWeight) && numericWeight >= 500)
132
+ if (mono) return bold ? 'mono-bold' : 'mono'
133
+ return bold ? 'sans-bold' : 'sans'
134
+ }
135
+
12
136
  export class FloorplanPdfDocument {
13
137
  readonly raw: PdfKitDocumentInstance
14
138
  readonly internal: {
@@ -19,11 +143,21 @@ export class FloorplanPdfDocument {
19
143
  }
20
144
 
21
145
  private currentFontSize = 12
146
+ private currentFace: FloorplanPdfFace = 'sans'
22
147
  private readonly defaultPageSize: readonly [number, number]
148
+ private readonly faces = new Map<FloorplanPdfFace, EmbeddedFace>()
149
+ private readonly embedded: boolean
23
150
 
24
- constructor(raw: PdfKitDocumentInstance, defaultPageSize: readonly [number, number]) {
151
+ /** `fonts` null sets every face in pdfkit's standard fonts (WinAnsi only). */
152
+ constructor(
153
+ raw: PdfKitDocumentInstance,
154
+ defaultPageSize: readonly [number, number],
155
+ fonts: FloorplanPdfFonts | null,
156
+ ) {
25
157
  this.raw = raw
26
158
  this.defaultPageSize = defaultPageSize
159
+ this.embedded = fonts !== null
160
+ for (const [face, bytes] of Object.entries(fonts ?? {})) raw.registerFont(face, bytes)
27
161
  this.internal = {
28
162
  pageSize: {
29
163
  getWidth: () => this.raw.page?.width ?? this.defaultPageSize[0],
@@ -61,22 +195,17 @@ export class FloorplanPdfDocument {
61
195
  }
62
196
 
63
197
  setFont(family: string, weight: string = 'normal'): this {
64
- const normalizedFamily = family.toLocaleLowerCase()
65
- const normalizedWeight = weight.toLocaleLowerCase()
66
- const bold = normalizedWeight === 'bold' || Number.parseInt(normalizedWeight, 10) >= 500
67
- const base = normalizedFamily.includes('courier') ? 'Courier' : 'Helvetica'
68
- this.raw.font(bold ? `${base}-Bold` : base)
198
+ this.currentFace = resolveFloorplanPdfFace(family, weight)
69
199
  return this
70
200
  }
71
201
 
72
202
  setFontSize(size: number): this {
73
203
  this.currentFontSize = size
74
- this.raw.fontSize(size)
75
204
  return this
76
205
  }
77
206
 
78
207
  getTextWidth(value: string): number {
79
- return this.raw.widthOfString(value, { lineBreak: false })
208
+ return this.measureRuns(this.textRuns(value, this.currentFace), this.currentFontSize)
80
209
  }
81
210
 
82
211
  splitTextToSize(value: string, maxWidth: number): string[] {
@@ -103,20 +232,58 @@ export class FloorplanPdfDocument {
103
232
  baselineY: number,
104
233
  options: PdfTextOptions = {},
105
234
  ) {
106
- const lines = typeof value === 'string' ? value.split('\n') : value
235
+ const given = typeof value === 'string' ? value.split('\n') : value
236
+ const lines =
237
+ options.maxWidth === undefined
238
+ ? given
239
+ : given.flatMap((line) => this.splitTextToSize(line, options.maxWidth ?? 0))
107
240
  const lineHeight = this.currentFontSize * 1.2
108
241
  lines.forEach((line, index) => {
109
- const width = this.getTextWidth(line)
242
+ const runs = this.textRuns(line, this.currentFace)
243
+ const width = this.measureRuns(runs, this.currentFontSize)
110
244
  const drawX =
111
245
  options.align === 'center' ? x - width / 2 : options.align === 'right' ? x - width : x
112
- this.raw.text(line, drawX, baselineY - this.currentFontSize * 0.78 + index * lineHeight, {
113
- lineBreak: false,
114
- width: options.maxWidth,
115
- })
246
+ this.drawRuns(runs, drawX, baselineY + index * lineHeight, this.currentFontSize)
116
247
  })
117
248
  return this
118
249
  }
119
250
 
251
+ /** The width `drawText` sets `text` at — the same runs, face by face. */
252
+ measureText(text: string, style: FloorplanPdfTextStyle): number {
253
+ const face = resolveFloorplanPdfFace(style.fontFamily, style.fontWeight)
254
+ return this.measureRuns(this.textRuns(collapseSvgWhitespace(text), face), style.fontSize)
255
+ }
256
+
257
+ /**
258
+ * Native (selectable, searchable) text in the current fill colour, anchored
259
+ * at (x, y) the way SVG anchors it: `anchor` is `text-anchor`, `baseline`
260
+ * is `dominant-baseline`, resolved against the embedded face's metrics.
261
+ */
262
+ drawText(
263
+ text: string,
264
+ x: number,
265
+ y: number,
266
+ style: FloorplanPdfTextStyle,
267
+ placement: { anchor: 'start' | 'middle' | 'end'; baseline: FloorplanPdfBaseline },
268
+ ): void {
269
+ const face = resolveFloorplanPdfFace(style.fontFamily, style.fontWeight)
270
+ const runs = this.textRuns(collapseSvgWhitespace(text), face)
271
+ const width = this.measureRuns(runs, style.fontSize)
272
+ const startX =
273
+ placement.anchor === 'middle' ? x - width / 2 : placement.anchor === 'end' ? x - width : x
274
+ const metrics = this.face(face)
275
+ const em = style.fontSize / 1000
276
+ const baselineShift =
277
+ placement.baseline === 'middle'
278
+ ? (metrics.xHeight / 2) * em
279
+ : placement.baseline === 'central'
280
+ ? ((metrics.ascender + metrics.descender) / 2) * em
281
+ : placement.baseline === 'hanging'
282
+ ? 0.8 * metrics.ascender * em
283
+ : 0
284
+ this.drawRuns(runs, startX, y + baselineShift, style.fontSize)
285
+ }
286
+
120
287
  line(x1: number, y1: number, x2: number, y2: number): this {
121
288
  this.raw.moveTo(x1, y1).lineTo(x2, y2).stroke()
122
289
  return this
@@ -165,17 +332,174 @@ export class FloorplanPdfDocument {
165
332
  else this.raw.stroke()
166
333
  return this
167
334
  }
335
+
336
+ private setFace(face: FloorplanPdfFace) {
337
+ return this.raw.font(this.embedded ? face : STANDARD_FACES[face])
338
+ }
339
+
340
+ private face(face: FloorplanPdfFace): EmbeddedFace {
341
+ let embedded = this.faces.get(face)
342
+ if (!embedded) {
343
+ this.setFace(face)
344
+ embedded = (this.raw as unknown as { _font: EmbeddedFace })._font
345
+ this.faces.set(face, embedded)
346
+ }
347
+ return embedded
348
+ }
349
+
350
+ /** Splits `text` into runs each face can set, plus the symbols drawn as linework. */
351
+ private textRuns(text: string, face: FloorplanPdfFace): TextRun[] {
352
+ // printable ASCII is in every face; the standard fonts have no glyph table to consult
353
+ if (!this.embedded || /^[\x20-\x7e]*$/.test(text)) return [{ kind: 'text', face, text }]
354
+ const runs: TextRun[] = []
355
+ for (const char of text) {
356
+ const codePoint = char.codePointAt(0) ?? 0
357
+ let runFace = face
358
+ if (!this.face(face).font.hasGlyphForCodePoint(codePoint)) {
359
+ const drawn = DRAWN_SYMBOLS[char as keyof typeof DRAWN_SYMBOLS]
360
+ if (this.face(OTHER_FAMILY[face]).font.hasGlyphForCodePoint(codePoint)) {
361
+ runFace = OTHER_FAMILY[face]
362
+ } else if (drawn) {
363
+ runs.push({ kind: 'symbol', symbol: drawn.symbol, advanceEm: drawn.advanceEm })
364
+ continue
365
+ }
366
+ // anything else prints as the face's .notdef box — visibly missing, never garbage
367
+ }
368
+ const last = runs[runs.length - 1]
369
+ if (last?.kind === 'text' && last.face === runFace) last.text += char
370
+ else runs.push({ kind: 'text', face: runFace, text: char })
371
+ }
372
+ return runs
373
+ }
374
+
375
+ private measureRuns(runs: readonly TextRun[], fontSize: number): number {
376
+ let width = 0
377
+ for (const run of runs) {
378
+ width +=
379
+ run.kind === 'text'
380
+ ? this.setFace(run.face).fontSize(fontSize).widthOfString(run.text)
381
+ : run.advanceEm * fontSize
382
+ }
383
+ return width
384
+ }
385
+
386
+ private drawRuns(runs: readonly TextRun[], x: number, baselineY: number, fontSize: number) {
387
+ let cursor = x
388
+ for (const run of runs) {
389
+ if (run.kind === 'text') {
390
+ this.setFace(run.face).fontSize(fontSize)
391
+ this.raw.text(run.text, cursor, baselineY, { baseline: 'alphabetic', lineBreak: false })
392
+ cursor += this.raw.widthOfString(run.text)
393
+ } else {
394
+ this.drawSymbol(run.symbol, cursor, baselineY, fontSize)
395
+ cursor += run.advanceEm * fontSize
396
+ }
397
+ }
398
+ }
399
+
400
+ private drawSymbol(symbol: DrawnSymbol, x: number, baselineY: number, fontSize: number) {
401
+ const raw = this.raw
402
+ if (symbol === 'angle') {
403
+ // two strokes from a vertex on the baseline: the base, and a leg rising
404
+ // to the cap height — filled bands, so the text's own colour paints them
405
+ const vertex: [number, number] = [x + 0.08 * fontSize, baselineY]
406
+ const tip: [number, number] = [x + 0.56 * fontSize, baselineY - 0.7 * fontSize]
407
+ const width = 0.07 * fontSize
408
+ raw
409
+ .save()
410
+ // the base starts a little behind the vertex so the corner is closed
411
+ .polygon(
412
+ ...strokeBand([x + 0.05 * fontSize, baselineY], [x + 0.72 * fontSize, baselineY], width),
413
+ )
414
+ .polygon(...strokeBand(vertex, tip, width))
415
+ .fill('non-zero')
416
+ .restore()
417
+ return
418
+ }
419
+ if (symbol === 'pointer') {
420
+ // a small solid triangle on the x-height, like a list pointer
421
+ const midY = baselineY - 0.26 * fontSize
422
+ const half = 0.17 * fontSize
423
+ raw
424
+ .save()
425
+ .polygon(
426
+ [x + 0.1 * fontSize, midY - half],
427
+ [x + 0.4 * fontSize, midY],
428
+ [x + 0.1 * fontSize, midY + half],
429
+ )
430
+ .fill('non-zero')
431
+ .restore()
432
+ return
433
+ }
434
+ // warning: an outlined triangle from just under the baseline to the cap
435
+ // height, its "!" standing on the text's own baseline (so text extraction
436
+ // keeps it on the line it warns about)
437
+ const left = x + 0.04 * fontSize
438
+ const width = 0.82 * fontSize
439
+ const base = baselineY + 0.1 * fontSize
440
+ const height = 0.82 * fontSize
441
+ const stroke = 0.07 * fontSize
442
+ const apex: [number, number] = [left + width / 2, base - height]
443
+ const outer: [number, number][] = [[left, base], [left + width, base], apex]
444
+ const leg = Math.hypot(width / 2, height)
445
+ const inradius = (width * height) / (width + 2 * leg)
446
+ const centre: [number, number] = [left + width / 2, base - inradius]
447
+ const k = (inradius - stroke) / inradius
448
+ const inner = outer.map(
449
+ ([px, py]) =>
450
+ [centre[0] + (px - centre[0]) * k, centre[1] + (py - centre[1]) * k] as [number, number],
451
+ )
452
+ raw
453
+ .save()
454
+ .polygon(...outer)
455
+ .polygon(...inner)
456
+ .fill('even-odd')
457
+ .restore()
458
+ this.setFace('sans-bold').fontSize(0.55 * fontSize)
459
+ raw.text('!', centre[0] - raw.widthOfString('!') / 2, baselineY, {
460
+ baseline: 'alphabetic',
461
+ lineBreak: false,
462
+ })
463
+ }
168
464
  }
169
465
 
170
- export async function createFloorplanPdfDocument(defaultPageSize: readonly [number, number]) {
171
- const [{ default: PDFDocument }, { default: blobStream }] = await Promise.all([
172
- import('pdfkit/js/pdfkit.standalone'),
173
- import('blob-stream'),
174
- ])
175
- const raw = new PDFDocument({ autoFirstPage: false, compress: true, margin: 0 })
466
+ export async function createFloorplanPdfDocument(
467
+ defaultPageSize: readonly [number, number],
468
+ options: { title?: string } = {},
469
+ ) {
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({
494
+ autoFirstPage: false,
495
+ compress: true,
496
+ margin: 0,
497
+ // the viewer's title bar shows the document title, not the file name
498
+ ...(options.title ? { info: { Title: options.title }, displayTitle: true } : {}),
499
+ })
176
500
  const stream = raw.pipe(blobStream())
177
501
  return {
178
- doc: new FloorplanPdfDocument(raw, defaultPageSize),
502
+ doc: new FloorplanPdfDocument(raw, defaultPageSize, fonts),
179
503
  save: async (filename: string) => {
180
504
  const blob = await new Promise<Blob>((resolve, reject) => {
181
505
  stream.on('finish', () => resolve(stream.toBlob('application/pdf')))
@@ -183,12 +507,24 @@ export async function createFloorplanPdfDocument(defaultPageSize: readonly [numb
183
507
  raw.on('error', reject)
184
508
  raw.end()
185
509
  })
186
- const url = URL.createObjectURL(blob)
187
- const anchor = document.createElement('a')
188
- anchor.href = url
189
- anchor.download = filename
190
- anchor.click()
191
- setTimeout(() => URL.revokeObjectURL(url), 0)
510
+ await downloadProjectFile(blob, filename, fileSink)
192
511
  },
193
512
  }
194
513
  }
514
+
515
+ /** The quad a straight stroke of `width` from `a` to `b` covers (same winding for any direction). */
516
+ function strokeBand(
517
+ a: readonly [number, number],
518
+ b: readonly [number, number],
519
+ width: number,
520
+ ): [number, number][] {
521
+ const length = Math.hypot(b[0] - a[0], b[1] - a[1])
522
+ const nx = (-(b[1] - a[1]) / length) * (width / 2)
523
+ const ny = ((b[0] - a[0]) / length) * (width / 2)
524
+ return [
525
+ [a[0] + nx, a[1] + ny],
526
+ [b[0] + nx, b[1] + ny],
527
+ [b[0] - nx, b[1] - ny],
528
+ [a[0] - nx, a[1] - ny],
529
+ ]
530
+ }