@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
@@ -2,7 +2,7 @@ import { describe, expect, test } from 'bun:test'
2
2
  import type { FloorplanGeometry } from '@pascal-app/core'
3
3
  import PDFDocument from 'pdfkit'
4
4
  import { floorplanGeometryMetadata } from './floorplan-extension'
5
- import { FloorplanPdfDocument } from './floorplan-pdfkit-document'
5
+ import { FloorplanPdfDocument, loadFloorplanPdfFonts } from './floorplan-pdfkit-document'
6
6
  import { renderFloorplanGeometryToPdfKit } from './floorplan-pdfkit-renderer'
7
7
 
8
8
  describe('renderFloorplanGeometryToPdfKit', () => {
@@ -20,7 +20,7 @@ describe('renderFloorplanGeometryToPdfKit', () => {
20
20
  const pdf = await renderTestPdf(geometry)
21
21
 
22
22
  expect(pdf).toContain('BT')
23
- expect(pdf).toContain(`[<${Buffer.from('13m').toString('hex')}> 0] TJ`)
23
+ expect(extractPdfText(pdf)).toBe('13m')
24
24
  expect(pdf).toMatch(/0\.1 w/)
25
25
  expect(pdf).toMatch(/0\.15 w/)
26
26
  expect(pdf).not.toMatch(/ c\n/)
@@ -40,7 +40,7 @@ describe('renderFloorplanGeometryToPdfKit', () => {
40
40
 
41
41
  const pdf = await renderTestPdf(geometry, 45)
42
42
 
43
- expect(pdf).toContain(`<${Buffer.from('OOM 101').toString('hex')}>`)
43
+ expect(extractPdfText(pdf)).toBe('ROOM 101')
44
44
  expect(pdf).toContain('BT')
45
45
  })
46
46
 
@@ -76,13 +76,13 @@ describe('renderFloorplanGeometryToPdfKit', () => {
76
76
 
77
77
  const pdfs = await Promise.all(geometries.map((geometry) => renderTestPdf(geometry)))
78
78
  const baseFonts = pdfs.flatMap((pdf) =>
79
- [...pdf.matchAll(/\/BaseFont \/([^\n]+)/g)].map((match) => match[1]),
79
+ [...pdf.matchAll(/\/BaseFont \/(?:[A-Z]{6}\+)?([^\n]+)/g)].map((match) => match[1]),
80
80
  )
81
81
  const fontSizes = pdfs.flatMap((pdf) =>
82
82
  [...pdf.matchAll(/\/F\d+ ([\d.]+) Tf/g)].map((match) => match[1]),
83
83
  )
84
84
 
85
- expect([...new Set(baseFonts)]).toEqual(['Courier'])
85
+ expect([...new Set(baseFonts)]).toEqual(['GeistMono-Regular'])
86
86
  expect([...new Set(fontSizes)]).toEqual(['1.6'])
87
87
  })
88
88
  test('uses even-odd fill for compound plugin paths', async () => {
@@ -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',
@@ -113,17 +152,101 @@ describe('renderFloorplanGeometryToPdfKit', () => {
113
152
  })
114
153
  })
115
154
 
116
- async function renderTestPdf(geometry: FloorplanGeometry, rotationDeg = 0): Promise<string> {
155
+ describe('embedded PDF fonts', () => {
156
+ // what the permit set prints: code limits, dimension strings, schedule
157
+ // callouts, revision deltas — none of them in pdfkit's WinAnsi standard fonts
158
+ const PROBLEM_TEXT =
159
+ 'Vult ≤ 195 mph, runs ≥ 24" · VERIFY ≈ 3\'-2" × 6\'-10 ½" — 29.69°N ¼ ¾ ⅛ 5′ 6″ △ ΔT → √0.6'
160
+
161
+ const textAt = (text: string, fontFamily?: string, fontWeight?: number) =>
162
+ ({
163
+ kind: 'text',
164
+ x: 1,
165
+ y: 1,
166
+ text,
167
+ fontSize: 0.5,
168
+ fontFamily,
169
+ fontWeight,
170
+ dominantBaseline: 'alphabetic',
171
+ }) satisfies FloorplanGeometry
172
+
173
+ test('the problem glyphs round-trip through the text layer in the sans and mono families', async () => {
174
+ for (const [family, weight] of [
175
+ ['"IBM Plex Sans", Helvetica, Arial, sans-serif', 400],
176
+ ['"IBM Plex Sans", Helvetica, Arial, sans-serif', 700],
177
+ ['"IBM Plex Mono", Menlo, monospace', 400],
178
+ ['"IBM Plex Mono", Menlo, monospace', 700],
179
+ ] as const) {
180
+ const pdf = await renderTestPdf(textAt(PROBLEM_TEXT, family, weight), 0, false)
181
+ expect(extractPdfText(pdf)).toBe(PROBLEM_TEXT)
182
+ }
183
+ })
184
+
185
+ test('embeds subset TrueType faces, never the unembedded standard 14', async () => {
186
+ const pdf = await renderTestPdf(textAt(PROBLEM_TEXT), 0, false)
187
+ const baseFonts = [...pdf.matchAll(/\/BaseFont \/([^\n]+)/g)].map((match) => match[1])
188
+ expect(baseFonts.length).toBeGreaterThan(0)
189
+ for (const name of baseFonts) expect(name).toMatch(/^[A-Z]{6}\+(LiberationSans|GeistMono)/)
190
+ expect(pdf).toContain('/FontFile2')
191
+ expect(pdf).not.toMatch(/\/BaseFont \/(Helvetica|Courier)/)
192
+ })
193
+
194
+ test('draws the symbols neither face carries — warning, pointer, angle — as linework', async () => {
195
+ const pdf = await renderTestPdf(textAt('∠ 45° ⚠ open porch ▸ note'), 0, false)
196
+ // the warning keeps a real "!" in the text layer; nothing falls to the .notdef glyph
197
+ expect(extractPdfText(pdf)).toBe(' 45° ! open porch note')
198
+ expect(extractPdfText(pdf)).not.toContain('\u0000')
199
+ // the triangle's ring is an even-odd fill; the pointer and the angle's strokes are plain fills
200
+ expect(pdf).toMatch(/f\*/)
201
+ expect(pdf.match(/\nf\n/g)?.length).toBeGreaterThanOrEqual(2)
202
+ })
203
+
204
+ test('collapses runs of white space the way SVG text does', async () => {
205
+ const pdf = await renderTestPdf(
206
+ { ...textAt(' APN 06075 \t 030\n062 '), textAnchor: 'end' },
207
+ 0,
208
+ false,
209
+ )
210
+ expect(extractPdfText(pdf)).toBe('APN 06075 030 062')
211
+
212
+ const raw = new PDFDocument({ autoFirstPage: false, compress: false })
213
+ const doc = new FloorplanPdfDocument(raw, [200, 200], await loadFloorplanPdfFonts())
214
+ const style = { fontFamily: 'sans-serif', fontSize: 10 }
215
+ expect(doc.measureText(`RIDGE 20'-1"`, style)).toBe(doc.measureText(`RIDGE 20'-1"`, style))
216
+ // a no-break space is content, not white space
217
+ expect(doc.measureText('A\u00a0\u00a0B', style)).toBeGreaterThan(doc.measureText('A B', style))
218
+ raw.end()
219
+ })
220
+
221
+ test('measures a mixed-face string as the sum of the runs it draws', async () => {
222
+ const raw = new PDFDocument({ autoFirstPage: false, compress: false })
223
+ const doc = new FloorplanPdfDocument(raw, [200, 200], await loadFloorplanPdfFonts())
224
+ const style = { fontFamily: 'sans-serif', fontSize: 10 }
225
+ const whole = doc.measureText('≤3△', style)
226
+ const parts =
227
+ doc.measureText('≤3', style) + doc.measureText('△', { ...style, fontFamily: 'monospace' })
228
+ expect(whole).toBeCloseTo(parts, 6)
229
+ // Liberation Sans keeps Helvetica's advance widths (the sheets were laid out against them)
230
+ expect(doc.measureText('0', style)).toBeCloseTo(5.56, 2)
231
+ raw.end()
232
+ })
233
+ })
234
+
235
+ async function renderTestPdf(
236
+ geometry: FloorplanGeometry,
237
+ rotationDeg = 0,
238
+ annotationLayer = true,
239
+ ): Promise<string> {
117
240
  const raw = new PDFDocument({ autoFirstPage: false, compress: false })
118
241
  const chunks: Buffer[] = []
119
242
  raw.on('data', (chunk: Buffer) => chunks.push(chunk))
120
243
  const completed = new Promise<string>((resolve) => {
121
244
  raw.on('end', () => resolve(Buffer.concat(chunks).toString('latin1')))
122
245
  })
123
- const doc = new FloorplanPdfDocument(raw, [200, 200])
246
+ const doc = new FloorplanPdfDocument(raw, [200, 200], await loadFloorplanPdfFonts())
124
247
  doc.addPage()
125
248
  await renderFloorplanGeometryToPdfKit(doc, geometry, {
126
- annotationLayer: true,
249
+ annotationLayer,
127
250
  placement: { x: 20, y: 20, width: 100, height: 100 },
128
251
  rotationDeg,
129
252
  viewport: { x: 0, y: -2, width: 20, height: 20 },
@@ -131,3 +254,49 @@ async function renderTestPdf(geometry: FloorplanGeometry, rotationDeg = 0): Prom
131
254
  raw.end()
132
255
  return completed
133
256
  }
257
+
258
+ /**
259
+ * The text an uncompressed pdfkit file carries, in drawing order: every TJ's
260
+ * glyph ids decoded through its font's ToUnicode CMap — what a PDF reader's
261
+ * text layer (search, copy, pdftotext) reads back.
262
+ */
263
+ function extractPdfText(pdf: string): string {
264
+ const objects = new Map<string, string>()
265
+ for (const match of pdf.matchAll(/(\d+) 0 obj\n([\s\S]*?)\nendobj/g)) {
266
+ objects.set(match[1]!, match[2]!)
267
+ }
268
+ const fontObjects = new Map<string, string>()
269
+ for (const match of pdf.matchAll(/\/(F\d+) (\d+) 0 R/g)) fontObjects.set(match[1]!, match[2]!)
270
+ const cmaps = new Map<string, Map<number, string>>()
271
+ const cmapFor = (resource: string) => {
272
+ let cmap = cmaps.get(resource)
273
+ if (cmap) return cmap
274
+ cmap = new Map()
275
+ const font = objects.get(fontObjects.get(resource) ?? '') ?? ''
276
+ const stream = objects.get(font.match(/\/ToUnicode (\d+) 0 R/)?.[1] ?? '') ?? ''
277
+ for (const range of stream.matchAll(/<([0-9a-f]{4})> <[0-9a-f]{4}> \[([^\]]*)\]/g)) {
278
+ let gid = Number.parseInt(range[1]!, 16)
279
+ for (const entry of range[2]!.matchAll(/<([0-9a-f ]+)>/g)) {
280
+ const units = entry[1]!.split(' ').map((hex) => Number.parseInt(hex, 16))
281
+ cmap.set(gid++, String.fromCharCode(...units))
282
+ }
283
+ }
284
+ cmaps.set(resource, cmap)
285
+ return cmap
286
+ }
287
+ let text = ''
288
+ let font = ''
289
+ for (const op of pdf.matchAll(/\/(F\d+) [\d.]+ Tf|\[([^\]]*)\] TJ/g)) {
290
+ if (op[1]) {
291
+ font = op[1]
292
+ continue
293
+ }
294
+ const cmap = cmapFor(font)
295
+ for (const hex of op[2]!.matchAll(/<([0-9a-f]+)>/g)) {
296
+ for (let i = 0; i < hex[1]!.length; i += 4) {
297
+ text += cmap.get(Number.parseInt(hex[1]!.slice(i, i + 4), 16)) ?? '\uFFFD'
298
+ }
299
+ }
300
+ }
301
+ return text
302
+ }
@@ -11,11 +11,11 @@ import {
11
11
  import { resolveFloorplanLabelAngle } from '../../components/editor-2d/renderers/floorplan-label-angle'
12
12
  import type { FloorplanExportBounds } from './floorplan-export'
13
13
  import { readFloorplanGeometryMetadata } from './floorplan-extension'
14
- import type { FloorplanPdfDocument } from './floorplan-pdfkit-document'
14
+ import type { FloorplanPdfBaseline, FloorplanPdfDocument } from './floorplan-pdfkit-document'
15
15
 
16
16
  const DIMENSION_LINE_WIDTH_PT = 0.5
17
17
  const DIMENSION_TICK_WIDTH_PT = 0.75
18
- const DIMENSION_TEXT_FONT_FAMILY = 'Courier'
18
+ const DIMENSION_TEXT_FONT_FAMILY = 'monospace'
19
19
  const DIMENSION_TEXT_FONT_SIZE_PT = 8
20
20
  const DIMENSION_TEXT_FONT_WEIGHT = 400
21
21
  const DIMENSION_BASELINE_OFFSET_PT = 5
@@ -35,6 +35,7 @@ type RenderContext = {
35
35
  annotationLabelShiftIndex: number
36
36
  annotationLabelShifts: readonly FloorplanPoint[]
37
37
  annotationLayer: boolean
38
+ dimensionTextSizePt: number
38
39
  sceneRotationDeg: number
39
40
  unitsPerPoint: number
40
41
  }
@@ -52,6 +53,8 @@ export async function renderFloorplanGeometryToPdfKit(
52
53
  options: {
53
54
  annotationLabelShifts?: readonly FloorplanPoint[]
54
55
  annotationLayer: boolean
56
+ /** Paper size of dimension strings; a drafted sheet prints them larger. */
57
+ dimensionTextSizePt?: number
55
58
  placement: FloorplanPdfKitPlacement
56
59
  rotationDeg: number
57
60
  viewport: FloorplanExportBounds
@@ -70,6 +73,7 @@ export async function renderFloorplanGeometryToPdfKit(
70
73
  annotationLabelShiftIndex: 0,
71
74
  annotationLabelShifts: options.annotationLabelShifts ?? [],
72
75
  annotationLayer: options.annotationLayer,
76
+ dimensionTextSizePt: options.dimensionTextSizePt ?? DIMENSION_TEXT_FONT_SIZE_PT,
73
77
  sceneRotationDeg: options.rotationDeg,
74
78
  unitsPerPoint: 1 / pointsPerUnit,
75
79
  })
@@ -186,8 +190,11 @@ function paintStyledGeometry(
186
190
  geometry: StyledGeometry,
187
191
  context: RenderContext,
188
192
  ): void {
189
- const fill = geometry.fill && geometry.fill !== 'none' ? geometry.fill : null
190
- const stroke = geometry.stroke && geometry.stroke !== 'none' ? geometry.stroke : null
193
+ // SVG's `transparent` / `none` mean "no paint" — pdfkit would read either
194
+ // as a colour name it does not know and paint black (the furniture sprites'
195
+ // hit-target polygons printed as black boxes).
196
+ const fill = isPaint(geometry.fill) ? (geometry.fill as string) : null
197
+ const stroke = isPaint(geometry.stroke) ? (geometry.stroke as string) : null
191
198
  const opacity = geometry.opacity ?? 1
192
199
  const fillOpacity = (geometry.fillOpacity ?? 1) * opacity
193
200
  const strokeOpacity = (geometry.strokeOpacity ?? 1) * opacity
@@ -210,6 +217,22 @@ function paintStyledGeometry(
210
217
  if (fill && stroke) raw.fillAndStroke(fill, stroke, fillRule)
211
218
  else if (fill) raw.fill(fill, fillRule)
212
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')
224
+ }
225
+
226
+ function isPaint(value: unknown): boolean {
227
+ if (typeof value !== 'string') return false
228
+ const v = value.trim().toLowerCase()
229
+ return (
230
+ v !== '' &&
231
+ v !== 'none' &&
232
+ v !== 'transparent' &&
233
+ !v.startsWith('url(') &&
234
+ !v.startsWith('var(')
235
+ )
213
236
  }
214
237
 
215
238
  function resolveStrokeWidth(geometry: StyledGeometry, context: RenderContext): number {
@@ -247,7 +270,7 @@ function drawGeometryText(
247
270
  const dimensionValue =
248
271
  readFloorplanGeometryMetadata(geometry).annotationRole === 'automatic-dimension'
249
272
  const fontSize = context.annotationLayer
250
- ? (dimensionValue ? DIMENSION_TEXT_FONT_SIZE_PT : annotationTextSizePt(geometry)) *
273
+ ? (dimensionValue ? context.dimensionTextSizePt : annotationTextSizePt(geometry)) *
251
274
  context.unitsPerPoint
252
275
  : geometry.fontSize
253
276
  const outlinedForScreen = geometry.paintOrder === 'stroke' && !!geometry.stroke
@@ -258,6 +281,7 @@ function drawGeometryText(
258
281
  drawNativeText(doc, {
259
282
  angleDeg: geometry.upright ? -context.sceneRotationDeg : 0,
260
283
  anchor: geometry.textAnchor ?? 'start',
284
+ baseline: geometry.dominantBaseline ?? 'middle',
261
285
  fill,
262
286
  fontFamily: dimensionValue ? DIMENSION_TEXT_FONT_FAMILY : geometry.fontFamily,
263
287
  fontSize,
@@ -270,7 +294,10 @@ function drawGeometryText(
270
294
  }
271
295
 
272
296
  function annotationTextSizePt(geometry: Extract<FloorplanGeometry, { kind: 'text' }>): number {
273
- switch (readFloorplanGeometryMetadata(geometry).annotationRole) {
297
+ const metadata = readFloorplanGeometryMetadata(geometry)
298
+ // an explicit paper size wins over the role's (a sheet's room tag sets its own)
299
+ if (typeof metadata.textSizePt === 'number' && metadata.textSizePt > 0) return metadata.textSizePt
300
+ switch (metadata.annotationRole) {
274
301
  case 'room-label':
275
302
  if (geometry.fontSize >= 0.18) return DEFAULT_ANNOTATION_FONT_SIZE_PT
276
303
  if (geometry.fontSize >= 0.145) return ROOM_NUMBER_FONT_SIZE_PT
@@ -480,7 +507,7 @@ function drawDimensionText(
480
507
  stroke: string,
481
508
  context: RenderContext,
482
509
  ): void {
483
- const fontSize = DIMENSION_TEXT_FONT_SIZE_PT * context.unitsPerPoint
510
+ const fontSize = context.dimensionTextSizePt * context.unitsPerPoint
484
511
  const y =
485
512
  geometry.textPosition === 'centered'
486
513
  ? fontSize * 0.35
@@ -495,6 +522,7 @@ function drawDimensionText(
495
522
  .translate(shift[0], shift[1])
496
523
  drawNativeText(doc, {
497
524
  anchor: 'middle',
525
+ baseline: 'alphabetic',
498
526
  fill: stroke,
499
527
  fontFamily: DIMENSION_TEXT_FONT_FAMILY,
500
528
  fontSize,
@@ -512,7 +540,7 @@ function drawDimensionLabel(
512
540
  context: RenderContext,
513
541
  ): void {
514
542
  const unitsPerPoint = context.unitsPerPoint
515
- const fontSize = DIMENSION_TEXT_FONT_SIZE_PT * unitsPerPoint
543
+ const fontSize = context.dimensionTextSizePt * unitsPerPoint
516
544
  const padX = 6 * unitsPerPoint
517
545
  const padY = 3 * unitsPerPoint
518
546
  const textWidth = geometry.text.length * 6.2 * unitsPerPoint
@@ -538,6 +566,7 @@ function drawDimensionLabel(
538
566
  .fill()
539
567
  drawNativeText(doc, {
540
568
  anchor: 'middle',
569
+ baseline: 'middle',
541
570
  fill: '#111827',
542
571
  fontFamily: DIMENSION_TEXT_FONT_FAMILY,
543
572
  fontSize,
@@ -569,8 +598,9 @@ function drawEqualSpacingBadge(
569
598
  .fill()
570
599
  drawNativeText(doc, {
571
600
  anchor: 'middle',
601
+ baseline: 'middle',
572
602
  fill: '#334155',
573
- fontFamily: 'Courier',
603
+ fontFamily: DIMENSION_TEXT_FONT_FAMILY,
574
604
  fontSize,
575
605
  fontWeight: 600,
576
606
  text: geometry.text,
@@ -585,6 +615,7 @@ function drawNativeText(
585
615
  options: {
586
616
  angleDeg?: number
587
617
  anchor: 'start' | 'middle' | 'end'
618
+ baseline: FloorplanPdfBaseline
588
619
  fill: string
589
620
  fontFamily?: string
590
621
  fontSize: number
@@ -596,24 +627,17 @@ function drawNativeText(
596
627
  },
597
628
  ): void {
598
629
  const raw = doc.raw
599
- const normalizedFamily = options.fontFamily?.toLocaleLowerCase() ?? ''
600
- const family =
601
- normalizedFamily.includes('mono') || normalizedFamily.includes('courier')
602
- ? 'Courier'
603
- : 'Helvetica'
604
- const numericWeight = Number.parseInt(String(options.fontWeight ?? 400), 10)
605
- const bold =
606
- options.fontWeight === 'bold' || (Number.isFinite(numericWeight) && numericWeight >= 500)
607
630
  raw.save().translate(options.x, options.y)
608
631
  if (options.angleDeg) raw.rotate(options.angleDeg)
609
- raw
610
- .font(bold ? `${family}-Bold` : family)
611
- .fontSize(options.fontSize)
612
- .fillColor(options.fill)
632
+ raw.fillColor(options.fill)
613
633
  raw.fillOpacity(options.opacity ?? 1)
614
- const width = raw.widthOfString(options.text, { lineBreak: false })
615
- const x = options.anchor === 'middle' ? -width / 2 : options.anchor === 'end' ? -width : 0
616
- raw.text(options.text, x, -options.fontSize * 0.42, { lineBreak: false })
634
+ doc.drawText(
635
+ options.text,
636
+ 0,
637
+ 0,
638
+ { fontFamily: options.fontFamily, fontSize: options.fontSize, fontWeight: options.fontWeight },
639
+ { anchor: options.anchor, baseline: options.baseline },
640
+ )
617
641
  raw.restore()
618
642
  }
619
643
 
@@ -653,7 +677,18 @@ async function drawImage(
653
677
  raw.restore()
654
678
  }
655
679
 
656
- function blobToDataUrl(blob: Blob): Promise<string> {
680
+ async function blobToDataUrl(blob: Blob): Promise<string> {
681
+ // FileReader exists only in browsers; the headless export (scripts/demo/
682
+ // print-set.ts) runs the same renderer under Bun, where the bytes are
683
+ // base64-encoded directly instead.
684
+ if (typeof FileReader === 'undefined') {
685
+ const bytes = new Uint8Array(await blob.arrayBuffer())
686
+ let binary = ''
687
+ for (let i = 0; i < bytes.length; i += 0x8000) {
688
+ binary += String.fromCharCode(...bytes.subarray(i, i + 0x8000))
689
+ }
690
+ return `data:${blob.type || 'image/png'};base64,${btoa(binary)}`
691
+ }
657
692
  return new Promise((resolve, reject) => {
658
693
  const reader = new FileReader()
659
694
  reader.onerror = () => reject(reader.error)
@@ -0,0 +1,138 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
2
+ import {
3
+ type AnyNode,
4
+ type AnyNodeDefinition,
5
+ LevelNode,
6
+ loadPlugin,
7
+ nodeRegistry,
8
+ } from '@pascal-app/core'
9
+ import { renderToStaticMarkup } from 'react-dom/server'
10
+ import { z } from 'zod'
11
+ import { FloorplanPreview } from '../../components/viewer/floorplan-preview'
12
+ import { DEFAULT_FLOORPLAN_ANNOTATION_VISIBILITY } from './annotation-visibility'
13
+ import { collectFloorplanGeometry, floorplanExportInstalledPlugins } from './floorplan-export'
14
+ import { FLOORPLAN_NODE_EXTENSION_KEY } from './floorplan-extension'
15
+
16
+ // Plugin API v1, no-install case: a plugin kind's floor-plan output appears in
17
+ // the read-only preview and the PDF export only while the project has the
18
+ // plugin installed.
19
+
20
+ const PLUGIN_ID = 'fixture:floorplan'
21
+ const KIND = 'fixture:bed'
22
+ const LINKED_KIND = 'fixture:shaft'
23
+ const MARKER_FILL = '#a09a09'
24
+
25
+ const level = LevelNode.parse({ id: 'level_plugin_plan', children: ['fxbed_plan'] })
26
+ const bed = {
27
+ object: 'node',
28
+ id: 'fxbed_plan',
29
+ type: KIND,
30
+ parentId: level.id,
31
+ visible: true,
32
+ metadata: {},
33
+ } as unknown as AnyNode
34
+ // Lives on the upper level but draws on this one through `linkedLevelIds`.
35
+ const upper = LevelNode.parse({ id: 'level_plugin_upper', level: 1, children: ['fxshaft_plan'] })
36
+ const shaft = {
37
+ object: 'node',
38
+ id: 'fxshaft_plan',
39
+ type: LINKED_KIND,
40
+ parentId: upper.id,
41
+ visible: true,
42
+ metadata: {},
43
+ } as unknown as AnyNode
44
+ const nodes = { [level.id]: level, [bed.id]: bed, [upper.id]: upper, [shaft.id]: shaft } as Record<
45
+ string,
46
+ AnyNode
47
+ >
48
+
49
+ let restoreRegistry: () => void
50
+
51
+ beforeEach(async () => {
52
+ restoreRegistry = nodeRegistry._snapshot()
53
+ await loadPlugin({
54
+ id: PLUGIN_ID,
55
+ apiVersion: 1,
56
+ nodes: [
57
+ {
58
+ kind: KIND,
59
+ schemaVersion: 1,
60
+ schema: z.looseObject({ id: z.string(), type: z.literal(KIND) }),
61
+ category: 'structure',
62
+ defaults: () => ({}),
63
+ capabilities: {},
64
+ floorplan: () => ({ kind: 'rect', x: 0, y: 0, width: 2, height: 1, fill: MARKER_FILL }),
65
+ } as unknown as AnyNodeDefinition,
66
+ {
67
+ kind: LINKED_KIND,
68
+ schemaVersion: 1,
69
+ schema: z.looseObject({ id: z.string(), type: z.literal(LINKED_KIND) }),
70
+ category: 'structure',
71
+ defaults: () => ({}),
72
+ capabilities: {},
73
+ extensions: { [FLOORPLAN_NODE_EXTENSION_KEY]: { linkedLevelIds: () => [level.id] } },
74
+ floorplan: () => ({ kind: 'rect', x: 3, y: 0, width: 1, height: 1 }),
75
+ } as unknown as AnyNodeDefinition,
76
+ ],
77
+ })
78
+ })
79
+
80
+ afterEach(() => {
81
+ restoreRegistry()
82
+ })
83
+
84
+ function exportedIds(installedPlugins: readonly string[] | undefined) {
85
+ return collectFloorplanGeometry(
86
+ nodes,
87
+ level.id,
88
+ 'full',
89
+ 'metric',
90
+ 'meters',
91
+ DEFAULT_FLOORPLAN_ANNOTATION_VISIBILITY,
92
+ 'floor-plan',
93
+ 'finished-faces',
94
+ installedPlugins,
95
+ ).map(({ id }) => id)
96
+ }
97
+
98
+ function previewMarkup(installedPlugins: readonly string[]) {
99
+ return renderToStaticMarkup(
100
+ <FloorplanPreview
101
+ levelId={level.id}
102
+ scene={{ nodes, installedPlugins }}
103
+ showCompass={false}
104
+ showLevelSelector={false}
105
+ />,
106
+ )
107
+ }
108
+
109
+ describe('floor-plan output of a plugin kind', () => {
110
+ test('the read-only preview draws it only while the plugin is installed', () => {
111
+ expect(previewMarkup([PLUGIN_ID])).toContain(MARKER_FILL)
112
+ expect(previewMarkup([])).not.toContain(MARKER_FILL)
113
+ })
114
+
115
+ test('the PDF export draws level and linked-level kinds while the plugin is installed', () => {
116
+ expect(exportedIds([PLUGIN_ID]).sort()).toEqual([bed.id, shaft.id].sort())
117
+ })
118
+
119
+ test('the PDF export omits level and linked-level kinds while the plugin is not installed', () => {
120
+ expect(exportedIds([])).toEqual([])
121
+ })
122
+
123
+ test('a legacy scene without explicit install state draws every loaded plugin kind', () => {
124
+ const legacy = floorplanExportInstalledPlugins({
125
+ installedPlugins: ['host:default-panel'],
126
+ hasExplicitPluginInstallState: false,
127
+ })
128
+
129
+ expect(legacy).toBeUndefined()
130
+ expect(exportedIds(legacy).sort()).toEqual([bed.id, shaft.id].sort())
131
+ expect(
132
+ floorplanExportInstalledPlugins({
133
+ installedPlugins: [],
134
+ hasExplicitPluginInstallState: true,
135
+ }),
136
+ ).toEqual([])
137
+ })
138
+ })
@@ -10,6 +10,10 @@ export type FloorplanViewState = {
10
10
  unit: 'metric' | 'imperial'
11
11
  metricNotation?: 'meters' | 'millimeters'
12
12
  purpose?: 'edit' | 'document'
13
+ /** Sheet drafting conventions (see `FloorplanContextExtension.drafting`). */
14
+ drafting?: boolean
15
+ /** Room labels drawn as screen pills (see `FloorplanContextExtension.roomLabelOverlay`). */
16
+ roomLabelOverlay?: boolean
13
17
  wallDimensionReference?: FloorplanWallDimensionReference
14
18
  highlighted: boolean
15
19
  hovered: boolean
@@ -59,6 +63,8 @@ export function buildFloorplanContext(
59
63
  automaticDimensions: viewState.automaticDimensions,
60
64
  metricNotation: viewState.metricNotation ?? 'meters',
61
65
  purpose: viewState.purpose ?? 'edit',
66
+ drafting: viewState.drafting === true,
67
+ roomLabelOverlay: viewState.roomLabelOverlay === true,
62
68
  wallDimensionReference: viewState.wallDimensionReference,
63
69
  }),
64
70
  viewState: viewState.palette
@@ -1,4 +1,5 @@
1
1
  import type { Point2D } from '@pascal-app/core'
2
+ import { getWallFaceOffsets, type WallJustification } from '@pascal-app/core'
2
3
  import type { FloorplanLineSegment, FloorplanSelectionBounds } from './types'
3
4
 
4
5
  // Baseline rotation (deg) that orients the plan-local scene "north up" on
@@ -107,7 +108,11 @@ export function movePlanPointTowards(start: Point2D, end: Point2D, distance: num
107
108
  return interpolatePlanPoint(start, end, Math.min(1, distance / totalDistance))
108
109
  }
109
110
 
110
- export function getThickPlanLinePolygon(line: FloorplanLineSegment, thickness: number): Point2D[] {
111
+ export function getThickPlanLinePolygon(
112
+ line: FloorplanLineSegment,
113
+ thickness: number,
114
+ justification?: WallJustification,
115
+ ): Point2D[] {
111
116
  const dx = line.end.x - line.start.x
112
117
  const dy = line.end.y - line.start.y
113
118
  const length = Math.hypot(dx, dy)
@@ -116,15 +121,17 @@ export function getThickPlanLinePolygon(line: FloorplanLineSegment, thickness: n
116
121
  return [line.start, line.end, line.end, line.start]
117
122
  }
118
123
 
119
- const halfThickness = thickness / 2
120
- const normalX = (-dy / length) * halfThickness
121
- const normalY = (dx / length) * halfThickness
124
+ const { a, b } = getWallFaceOffsets({ thickness, justification })
125
+ const normalX = (-dy / length) * a
126
+ const normalY = (dx / length) * a
122
127
 
128
+ const backX = (-dy / length) * -b
129
+ const backY = (dx / length) * -b
123
130
  return [
124
131
  { x: line.start.x + normalX, y: line.start.y + normalY },
125
132
  { x: line.end.x + normalX, y: line.end.y + normalY },
126
- { x: line.end.x - normalX, y: line.end.y - normalY },
127
- { x: line.start.x - normalX, y: line.start.y - normalY },
133
+ { x: line.end.x - backX, y: line.end.y - backY },
134
+ { x: line.start.x - backX, y: line.start.y - backY },
128
135
  ]
129
136
  }
130
137
 
@@ -29,20 +29,11 @@ export {
29
29
  collectLevelDescendants,
30
30
  getItemFloorplanTransform,
31
31
  } from './items'
32
- export {
33
- buildFloorplanStairEntry,
34
- computeFloorplanStairSegmentTransforms,
35
- getFloorplanStairSegmentPolygon,
36
- } from './stairs'
37
32
  export type {
38
33
  FloorplanItemEntry,
39
34
  FloorplanLineSegment,
40
35
  FloorplanNodeTransform,
41
36
  FloorplanSelectionBounds,
42
- FloorplanStairArrowEntry,
43
- FloorplanStairEntry,
44
- FloorplanStairSegmentEntry,
45
37
  LevelDescendantMap,
46
- StairSegmentTransform,
47
38
  } from './types'
48
39
  export { getFloorplanWall, getFloorplanWallThickness } from './walls'