@pascal-app/editor 1.0.3 → 1.0.4

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 (440) hide show
  1. package/package.json +11 -9
  2. package/src/components/editor/action-menu-button.tsx +83 -0
  3. package/src/components/editor/bake-thumbnail.tsx +315 -4
  4. package/src/components/editor/capture-level-picker.tsx +72 -0
  5. package/src/components/editor/custom-camera-controls.tsx +202 -57
  6. package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
  7. package/src/components/editor/duplicate-node.ts +244 -0
  8. package/src/components/editor/editor-layout-mobile.tsx +36 -25
  9. package/src/components/editor/editor-layout-v2.tsx +131 -58
  10. package/src/components/editor/export-manager.tsx +34 -8
  11. package/src/components/editor/first-person/build-collider-world.test.ts +51 -0
  12. package/src/components/editor/first-person/build-collider-world.ts +6 -2
  13. package/src/components/editor/first-person/mechanism-targets.test.ts +75 -0
  14. package/src/components/editor/first-person/mechanism-targets.ts +36 -0
  15. package/src/components/editor/first-person-controls.tsx +205 -2
  16. package/src/components/editor/floating-action-menu.tsx +63 -207
  17. package/src/components/editor/floor-aware-camera-controls.test.ts +442 -0
  18. package/src/components/editor/floor-aware-camera-controls.ts +324 -0
  19. package/src/components/editor/floor-openings-overlay.tsx +243 -0
  20. package/src/components/editor/floor-region-controls.tsx +846 -0
  21. package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
  22. package/src/components/editor/floor-surface.test.ts +165 -0
  23. package/src/components/editor/floor-surface.ts +182 -0
  24. package/src/components/editor/floorplan-background-selection.test.ts +11 -2
  25. package/src/components/editor/floorplan-background-selection.ts +1 -3
  26. package/src/components/editor/floorplan-panel.tsx +344 -342
  27. package/src/components/editor/footprint-height-handle.tsx +204 -0
  28. package/src/components/editor/group-actions.ts +54 -2
  29. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  30. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  31. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  32. package/src/components/editor/handles/use-handle-drag.ts +269 -197
  33. package/src/components/editor/index.tsx +294 -169
  34. package/src/components/editor/join-walls-pill.tsx +44 -0
  35. package/src/components/editor/node-action-menu.tsx +68 -70
  36. package/src/components/editor/node-arrow-handles.tsx +25 -6
  37. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  38. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  39. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  40. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  41. package/src/components/editor/registry-action-contributions.tsx +80 -1
  42. package/src/components/editor/room-controls.tsx +847 -0
  43. package/src/components/editor/room-floor-highlight.tsx +128 -0
  44. package/src/components/editor/room-handles-2d.tsx +354 -0
  45. package/src/components/editor/room-handles.tsx +675 -0
  46. package/src/components/editor/room-highlight.tsx +225 -0
  47. package/src/components/editor/room-labels.test.ts +357 -0
  48. package/src/components/editor/room-labels.tsx +990 -0
  49. package/src/components/editor/room-transform-ghost.tsx +256 -0
  50. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  51. package/src/components/editor/selection-manager.tsx +451 -317
  52. package/src/components/editor/slab-hole-highlights.tsx +3 -0
  53. package/src/components/editor/snapshot-capture-overlay.tsx +4 -0
  54. package/src/components/editor/snapshot-capture.test.ts +52 -4
  55. package/src/components/editor/snapshot-capture.ts +30 -5
  56. package/src/components/editor/thumbnail-generator.test.ts +308 -0
  57. package/src/components/editor/thumbnail-generator.tsx +1035 -229
  58. package/src/components/editor/use-floorplan-background-placement.ts +31 -11
  59. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  60. package/src/components/editor/views/host-view.tsx +61 -0
  61. package/src/components/editor/views/plugins-view.tsx +670 -0
  62. package/src/components/editor/views/scene-region.tsx +59 -0
  63. package/src/components/editor/views/use-editor-views.ts +57 -0
  64. package/src/components/editor/views/view-bar.tsx +220 -0
  65. package/src/components/editor/wall-justification.test.ts +140 -0
  66. package/src/components/editor/wall-move-side-handles.tsx +43 -142
  67. package/src/components/editor/wall-push-arrow.tsx +92 -0
  68. package/src/components/editor/wall-region-handles.tsx +509 -0
  69. package/src/components/editor/wall-snap-beacon-layer.tsx +3 -0
  70. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  71. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  72. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  73. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  74. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +113 -102
  75. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  76. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  77. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  78. package/src/components/editor-2d/renderers/{floorplan-draft-wall-measurement.tsx → floorplan-draft-measurement.tsx} +8 -8
  79. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  80. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +38 -8
  81. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +291 -227
  82. package/src/components/editor-2d/svg-paths.ts +12 -15
  83. package/src/components/procedural-items/inspector.tsx +40 -15
  84. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  85. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  86. package/src/components/systems/zone/zone-system.test.ts +47 -0
  87. package/src/components/systems/zone/zone-system.tsx +29 -68
  88. package/src/components/tools/item/placement-history.test.tsx +474 -0
  89. package/src/components/tools/item/placement-strategies.ts +8 -4
  90. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  91. package/src/components/tools/item/use-draft-node.ts +166 -95
  92. package/src/components/tools/item/use-placement-coordinator.tsx +93 -46
  93. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  94. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  95. package/src/components/tools/registry/move-registry-node-tool.tsx +3 -1
  96. package/src/components/tools/select/box-select-tool.tsx +2 -12
  97. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  98. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  99. package/src/components/tools/select/select-candidates.test.ts +37 -4
  100. package/src/components/tools/select/select-candidates.ts +15 -11
  101. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  102. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  103. package/src/components/tools/stair/stair-defaults.ts +0 -3
  104. package/src/components/tools/stair/stair-tool.tsx +90 -42
  105. package/src/components/tools/tool-manager.tsx +46 -20
  106. package/src/components/tools/wall/wall-drafting.test.ts +19 -4
  107. package/src/components/tools/zone/zone-tool.tsx +4 -12
  108. package/src/components/ui/action-menu/action-button.tsx +3 -0
  109. package/src/components/ui/action-menu/camera-actions.tsx +2 -1
  110. package/src/components/ui/action-menu/control-modes.tsx +2 -35
  111. package/src/components/ui/action-menu/measurement-control.tsx +3 -10
  112. package/src/components/ui/build-panel/build-panel.tsx +235 -0
  113. package/src/components/ui/command-palette/editor-commands.tsx +4 -30
  114. package/src/components/ui/command-palette/index.tsx +5 -24
  115. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  116. package/src/components/ui/controls/material-paint-panel.tsx +97 -26
  117. package/src/components/ui/controls/material-picker.tsx +4 -1
  118. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  119. package/src/components/ui/controls/select-control.tsx +65 -0
  120. package/src/components/ui/floating-level-selector.tsx +9 -14
  121. package/src/components/ui/helpers/building-helper.tsx +4 -1
  122. package/src/components/ui/helpers/contextual-helper-panel.tsx +154 -70
  123. package/src/components/ui/helpers/helper-manager.test.tsx +69 -0
  124. package/src/components/ui/helpers/helper-manager.tsx +325 -14
  125. package/src/components/ui/helpers/item-helper.tsx +8 -0
  126. package/src/components/ui/helpers/registered-tool-helper.tsx +8 -1
  127. package/src/components/ui/icon-ref.tsx +45 -1
  128. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  129. package/src/components/ui/node-icon-image.tsx +16 -0
  130. package/src/components/ui/panels/collections/collections-popover.tsx +20 -14
  131. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  132. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  133. package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
  134. package/src/components/ui/panels/mobile-selection-bar.tsx +2 -2
  135. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  136. package/src/components/ui/panels/multi-field-value.ts +6 -3
  137. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  138. package/src/components/ui/panels/multi-selection-panel.tsx +43 -4
  139. package/src/components/ui/panels/node-display.ts +8 -3
  140. package/src/components/ui/panels/panel-manager.tsx +103 -68
  141. package/src/components/ui/panels/panel-wrapper.tsx +78 -36
  142. package/src/components/ui/panels/parametric-field-control.tsx +1 -1
  143. package/src/components/ui/panels/parametric-inspector.tsx +4 -4
  144. package/src/components/ui/panels/reference-panel.tsx +2 -3
  145. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  146. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  147. package/src/components/ui/panels/room-name-options.ts +47 -0
  148. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  149. package/src/components/ui/panels/room-panel.tsx +725 -0
  150. package/src/components/ui/panels/shape-choice.tsx +58 -0
  151. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  152. package/src/components/ui/primitives/color-dot.tsx +36 -24
  153. package/src/components/ui/primitives/sidebar.tsx +2 -0
  154. package/src/components/ui/right-stack.test.ts +29 -0
  155. package/src/components/ui/right-stack.tsx +145 -0
  156. package/src/components/ui/sidebar/icon-rail.tsx +5 -0
  157. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +31 -4
  158. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +5 -5
  159. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  160. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  161. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  162. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  163. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  164. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  165. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  166. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  167. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  168. package/src/components/ui/sidebar/panels/site-panel/index.tsx +176 -211
  169. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  170. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  171. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  172. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  173. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  174. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  175. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  176. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  177. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  178. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  179. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +0 -27
  180. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  181. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  182. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  183. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  184. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +0 -2
  185. package/src/components/ui/sidebar/tab-bar.tsx +10 -0
  186. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  187. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  188. package/src/components/viewer/floorplan-preview.tsx +5 -6
  189. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  190. package/src/components/viewer/viewer-rooms.tsx +175 -0
  191. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  192. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  193. package/src/components/viewer/viewer-stage.tsx +2 -2
  194. package/src/components/viewer-zone-system.tsx +3 -8
  195. package/src/hooks/use-auto-save.integration.test.tsx +123 -9
  196. package/src/hooks/use-auto-save.ts +34 -8
  197. package/src/hooks/use-keyboard.test.ts +178 -3
  198. package/src/hooks/use-keyboard.ts +89 -71
  199. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  200. package/src/hooks/use-open-wall-ends.ts +238 -0
  201. package/src/hooks/use-placement-notice.test.ts +48 -0
  202. package/src/hooks/use-placement-notice.ts +42 -0
  203. package/src/hooks/use-selected-room.ts +143 -0
  204. package/src/index.tsx +246 -39
  205. package/src/lib/ceiling-edit-session.test.ts +321 -0
  206. package/src/lib/ceiling-edit-session.ts +206 -0
  207. package/src/lib/ceiling-region-session.test.ts +179 -0
  208. package/src/lib/continuation.test.ts +19 -1
  209. package/src/lib/continuation.ts +25 -4
  210. package/src/lib/door-interaction.ts +14 -0
  211. package/src/lib/editor-level-display.test.ts +96 -0
  212. package/src/lib/editor-level-display.ts +48 -0
  213. package/src/lib/editor-views.ts +144 -0
  214. package/src/lib/export-geometry-wait.test.ts +90 -0
  215. package/src/lib/floor-edit-session.ts +153 -0
  216. package/src/lib/floor-footprints.test.ts +563 -0
  217. package/src/lib/floor-footprints.ts +395 -0
  218. package/src/lib/floor-foundation-support.test.ts +283 -0
  219. package/src/lib/floor-opening-draft.ts +218 -0
  220. package/src/lib/floor-openings.test.ts +216 -0
  221. package/src/lib/floor-region-geometry.ts +191 -0
  222. package/src/lib/floor-region-session.test.ts +485 -0
  223. package/src/lib/floor-region-session.ts +589 -0
  224. package/src/lib/floor-region-snap.ts +101 -0
  225. package/src/lib/floor-step-hover.test.ts +32 -0
  226. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  227. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  228. package/src/lib/floorplan/dimension-drive.ts +479 -0
  229. package/src/lib/floorplan/floorplan-export.test.ts +12 -8
  230. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  231. package/src/lib/floorplan/floorplan-extension.ts +32 -0
  232. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  233. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  234. package/src/lib/floorplan/floorplan-pdfkit-document.ts +339 -18
  235. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +138 -8
  236. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +56 -25
  237. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  238. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  239. package/src/lib/floorplan/geometry.ts +13 -6
  240. package/src/lib/floorplan/index.ts +0 -9
  241. package/src/lib/floorplan/items.ts +5 -2
  242. package/src/lib/floorplan/marks.test.ts +213 -0
  243. package/src/lib/floorplan/marks.ts +238 -0
  244. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  245. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  246. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  247. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  248. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  249. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  250. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  251. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  252. package/src/lib/floorplan/schedules.test.ts +189 -0
  253. package/src/lib/floorplan/schedules.ts +339 -0
  254. package/src/lib/floorplan/sheet-export.test.ts +100 -0
  255. package/src/lib/floorplan/sheet-export.ts +318 -0
  256. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +779 -0
  257. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  258. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  259. package/src/lib/floorplan/site-plan/coverage.ts +232 -0
  260. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  261. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  262. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  263. package/src/lib/floorplan/site-plan/front-edge.ts +156 -0
  264. package/src/lib/floorplan/site-plan/geometry.test.ts +275 -0
  265. package/src/lib/floorplan/site-plan/geometry.ts +557 -0
  266. package/src/lib/floorplan/site-plan/index.ts +71 -0
  267. package/src/lib/floorplan/site-plan/site-annotations.test.ts +366 -0
  268. package/src/lib/floorplan/site-plan/site-annotations.ts +1083 -0
  269. package/src/lib/floorplan/site-plan/site-parts.ts +379 -0
  270. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  271. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  272. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  273. package/src/lib/floorplan/site-plan/yards.test.ts +69 -0
  274. package/src/lib/floorplan/types.ts +1 -27
  275. package/src/lib/fresh-planar-placement.test.ts +36 -0
  276. package/src/lib/fresh-planar-placement.ts +59 -36
  277. package/src/lib/geometry-script/author.ts +152 -0
  278. package/src/lib/geometry-script/client.ts +152 -0
  279. package/src/lib/geometry-script/protocol.ts +12 -0
  280. package/src/lib/gesture-lifecycle.test.ts +595 -0
  281. package/src/lib/gesture-lifecycle.ts +290 -0
  282. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  283. package/src/lib/glb-export.test.ts +485 -0
  284. package/src/lib/glb-export.ts +237 -119
  285. package/src/lib/history.test.ts +6 -0
  286. package/src/lib/history.ts +7 -0
  287. package/src/lib/hud-title.ts +109 -0
  288. package/src/lib/hydration-gated-storage.ts +49 -0
  289. package/src/lib/ifc-export.test.ts +174 -0
  290. package/src/lib/ifc-export.ts +201 -0
  291. package/src/lib/ifc-parts.ts +15 -0
  292. package/src/lib/inspector-expanded.test.ts +44 -0
  293. package/src/lib/inspector-expanded.ts +36 -0
  294. package/src/lib/interaction/scope.ts +18 -0
  295. package/src/lib/keyboard-pan.test.ts +129 -7
  296. package/src/lib/keyboard-pan.ts +161 -12
  297. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  298. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  299. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  300. package/src/lib/lot/clean-ring.test.ts +186 -0
  301. package/src/lib/lot/clean-ring.ts +217 -0
  302. package/src/lib/lot/dossier.test.ts +202 -0
  303. package/src/lib/lot/dossier.ts +498 -0
  304. package/src/lib/lot/drop-in.test.ts +326 -0
  305. package/src/lib/lot/drop-in.ts +407 -0
  306. package/src/lib/lot/index.ts +43 -0
  307. package/src/lib/lot/lot-patch.test.ts +340 -0
  308. package/src/lib/lot/lot-patch.ts +394 -0
  309. package/src/lib/lot/parcel-provider.ts +43 -0
  310. package/src/lib/lot/terrain.test.ts +161 -0
  311. package/src/lib/lot/terrain.ts +266 -0
  312. package/src/lib/material-catalog-model.ts +3 -1
  313. package/src/lib/material-paint-panel-model.ts +9 -5
  314. package/src/lib/material-paint.ts +4 -0
  315. package/src/lib/mezzanine-actions.test.ts +342 -0
  316. package/src/lib/mezzanine-draft.test.ts +501 -0
  317. package/src/lib/mezzanine-draft.ts +278 -0
  318. package/src/lib/mezzanine-messages.ts +26 -0
  319. package/src/lib/mezzanine-selection.test.ts +207 -0
  320. package/src/lib/mezzanine-stairs.ts +46 -0
  321. package/src/lib/model-export.ts +3 -1
  322. package/src/lib/node-action-movement.ts +17 -6
  323. package/src/lib/node-display-label.ts +37 -0
  324. package/src/lib/orbit-floor-clearance.test.ts +165 -0
  325. package/src/lib/orbit-floor-clearance.ts +106 -0
  326. package/src/lib/paint-commit-route.test.ts +37 -0
  327. package/src/lib/paint-commit-route.ts +17 -0
  328. package/src/lib/paint-eyedropper.test.tsx +165 -0
  329. package/src/lib/paint-eyedropper.ts +99 -0
  330. package/src/lib/paint-pass-through.test.ts +111 -0
  331. package/src/lib/paint-pass-through.ts +76 -0
  332. package/src/lib/paint-region-hover.ts +25 -0
  333. package/src/lib/paint-region-mode.ts +154 -0
  334. package/src/lib/paint-regions.test.ts +166 -0
  335. package/src/lib/paint-regions.ts +192 -0
  336. package/src/lib/paint-scope.test.ts +317 -35
  337. package/src/lib/paint-scope.ts +266 -25
  338. package/src/lib/panel-rows.ts +21 -0
  339. package/src/lib/parametric-node-update.ts +1 -1
  340. package/src/lib/plate-paint-affected.test.ts +300 -0
  341. package/src/lib/plate-paint-affected.ts +177 -0
  342. package/src/lib/plugin-chat.ts +50 -0
  343. package/src/lib/plugin-panels.test.ts +25 -0
  344. package/src/lib/plugin-panels.ts +94 -0
  345. package/src/lib/print-shell-compiler.ts +1 -0
  346. package/src/lib/registered-draft-snap.ts +12 -0
  347. package/src/lib/roof-duplication.ts +10 -2
  348. package/src/lib/room-assembly-overlay.test.ts +275 -0
  349. package/src/lib/room-assembly-overlay.ts +266 -0
  350. package/src/lib/room-built-on.test.ts +379 -0
  351. package/src/lib/room-built-on.ts +138 -0
  352. package/src/lib/room-construction-commands.ts +712 -0
  353. package/src/lib/room-construction.test.ts +746 -0
  354. package/src/lib/room-construction.ts +385 -0
  355. package/src/lib/room-controls-phase6.test.ts +630 -0
  356. package/src/lib/room-divide-session.ts +523 -0
  357. package/src/lib/room-handle-drag.ts +1068 -0
  358. package/src/lib/room-name-catalog.test.ts +90 -0
  359. package/src/lib/room-name-catalog.ts +168 -0
  360. package/src/lib/room-push-dimensions.test.ts +311 -0
  361. package/src/lib/room-push-dimensions.ts +251 -0
  362. package/src/lib/room-selection-commands.ts +62 -0
  363. package/src/lib/room-selection.test.ts +973 -0
  364. package/src/lib/room-selection.ts +417 -0
  365. package/src/lib/room-structure-commands.ts +94 -0
  366. package/src/lib/room-structure.test.ts +921 -0
  367. package/src/lib/room-transform-session.ts +570 -0
  368. package/src/lib/room-zone-routing.test.ts +294 -0
  369. package/src/lib/room-zone-routing.ts +197 -0
  370. package/src/lib/scene-clipboard.test.ts +364 -0
  371. package/src/lib/scene-clipboard.ts +229 -48
  372. package/src/lib/scene-material-refs.test.ts +79 -0
  373. package/src/lib/scene-material-refs.ts +79 -0
  374. package/src/lib/scene.test.ts +24 -36
  375. package/src/lib/scene.ts +33 -57
  376. package/src/lib/selection-routing.test.ts +129 -45
  377. package/src/lib/selection-routing.ts +53 -74
  378. package/src/lib/session-writes.test.ts +120 -0
  379. package/src/lib/session-writes.ts +205 -0
  380. package/src/lib/sidebar-panel.ts +62 -0
  381. package/src/lib/site-boundary.test.ts +4 -6
  382. package/src/lib/site-boundary.ts +1 -1
  383. package/src/lib/snapping-mode.test.ts +4 -0
  384. package/src/lib/snapping-mode.ts +8 -1
  385. package/src/lib/terrace-draft.test.ts +145 -0
  386. package/src/lib/terrace-draft.ts +213 -0
  387. package/src/lib/tool-cursor-icon.test.ts +16 -0
  388. package/src/lib/tool-cursor-icon.ts +11 -0
  389. package/src/lib/typing-target.ts +9 -0
  390. package/src/lib/units.ts +77 -39
  391. package/src/lib/vector-edges.test.ts +58 -0
  392. package/src/lib/vector-edges.ts +400 -0
  393. package/src/lib/viewer-selection.test.ts +577 -0
  394. package/src/lib/viewer-selection.ts +569 -0
  395. package/src/lib/wall-draw-variant.test.ts +71 -0
  396. package/src/lib/wall-draw-variant.ts +91 -0
  397. package/src/lib/wall-paint-region-session.ts +246 -0
  398. package/src/lib/wall-paint-region.test.ts +373 -0
  399. package/src/lib/wall-polygon-draft.test.ts +262 -0
  400. package/src/lib/wall-polygon-draft.ts +286 -0
  401. package/src/lib/wall-push.test.ts +453 -0
  402. package/src/lib/wall-region-face.ts +352 -0
  403. package/src/lib/wall-region-handles.test.ts +304 -0
  404. package/src/lib/wall-region-handles.ts +368 -0
  405. package/src/lib/wall-region-list.test.ts +119 -0
  406. package/src/lib/wall-region-list.ts +136 -0
  407. package/src/lib/wall-region-snap.ts +110 -0
  408. package/src/lib/window-interaction.ts +14 -0
  409. package/src/lib/zone-content.ts +4 -1
  410. package/src/lib/zone-name.test.ts +17 -0
  411. package/src/lib/zone-name.ts +17 -0
  412. package/src/store/opens-in-select-mode.test.ts +223 -0
  413. package/src/store/paint-mode.test.ts +188 -0
  414. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  415. package/src/store/tool-mode.test.ts +137 -6
  416. package/src/store/use-delete-confirmation.ts +28 -1
  417. package/src/store/use-drawing-view.test.ts +24 -3
  418. package/src/store/use-drawing-view.ts +29 -1
  419. package/src/store/use-editor.tsx +432 -321
  420. package/src/store/use-floorplan-draft-preview.ts +13 -0
  421. package/src/store/use-floorplan-marquee.ts +12 -2
  422. package/src/store/use-quick-measurement-hud.ts +3 -3
  423. package/src/store/use-stair-build-preview.ts +13 -5
  424. package/src/store/use-wall-move-ghosts.ts +4 -2
  425. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  426. package/src/store/use-wall-region-handles-selection.ts +139 -0
  427. package/src/store/view-layout.test.ts +175 -0
  428. package/src/store/view-layout.ts +196 -0
  429. package/src/test-utils/immediate-animation-frames.ts +16 -0
  430. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  431. package/src/test-utils/selection-harness.ts +82 -0
  432. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  433. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  434. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  435. package/src/components/tools/fence/fence-drafting.ts +0 -266
  436. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  437. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  438. package/src/lib/floorplan/stairs.ts +0 -489
  439. package/src/lib/level-duplication.test.ts +0 -144
  440. 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
@@ -9,6 +9,124 @@ type PdfTextOptions = {
9
9
 
10
10
  type PdfShapeStyle = 'F' | 'S'
11
11
 
12
+ /**
13
+ * The faces every exported PDF embeds. pdfkit's standard 14 fonts are
14
+ * WinAnsi-only: the ≤ ≥ ≈ → △ Δ ⅛ ′ ″ the sheets print came out as byte
15
+ * pairs of the code point ("≤ 195 mph" printed as `"d 195 mph`), and the
16
+ * fonts were not embedded at all. Liberation Sans is metric-compatible with
17
+ * Helvetica, so every width the sheets were laid out against is unchanged;
18
+ * Geist Mono keeps Courier's 0.6 em advance. Both are SIL OFL 1.1 — the
19
+ * licences sit beside the files.
20
+ */
21
+ const PDF_FONT_FILES = {
22
+ sans: new URL('./pdf-fonts/LiberationSans-Regular.ttf', import.meta.url),
23
+ 'sans-bold': new URL('./pdf-fonts/LiberationSans-Bold.ttf', import.meta.url),
24
+ mono: new URL('./pdf-fonts/GeistMono-Regular.ttf', import.meta.url),
25
+ 'mono-bold': new URL('./pdf-fonts/GeistMono-Bold.ttf', import.meta.url),
26
+ } as const
27
+
28
+ export type FloorplanPdfFace = keyof typeof PDF_FONT_FILES
29
+ export type FloorplanPdfFonts = Record<FloorplanPdfFace, ArrayBuffer>
30
+
31
+ /** pdfkit's built-in faces, used when the embedded ones could not be fetched. */
32
+ const STANDARD_FACES: Record<FloorplanPdfFace, string> = {
33
+ sans: 'Helvetica',
34
+ 'sans-bold': 'Helvetica-Bold',
35
+ mono: 'Courier',
36
+ 'mono-bold': 'Courier-Bold',
37
+ }
38
+
39
+ /** A glyph the face lacks is taken from the other family at the same weight. */
40
+ const OTHER_FAMILY: Record<FloorplanPdfFace, FloorplanPdfFace> = {
41
+ sans: 'mono',
42
+ 'sans-bold': 'mono-bold',
43
+ mono: 'sans',
44
+ 'mono-bold': 'sans-bold',
45
+ }
46
+
47
+ /**
48
+ * Symbols the sheets print that neither face carries, drawn as linework in
49
+ * the text's colour, with their advance in em. The warning triangle keeps a
50
+ * real "!" inside it, so the text layer still reads as a warning.
51
+ */
52
+ const DRAWN_SYMBOLS = {
53
+ '⚠': { symbol: 'warning', advanceEm: 0.9 },
54
+ '▸': { symbol: 'pointer', advanceEm: 0.5 },
55
+ '∠': { symbol: 'angle', advanceEm: 0.8 },
56
+ } as const
57
+
58
+ type DrawnSymbol = (typeof DRAWN_SYMBOLS)[keyof typeof DRAWN_SYMBOLS]['symbol']
59
+
60
+ type TextRun =
61
+ | { kind: 'text'; face: FloorplanPdfFace; text: string }
62
+ | { kind: 'symbol'; symbol: DrawnSymbol; advanceEm: number }
63
+
64
+ /** The SVG `dominant-baseline` values the floor-plan geometry uses. */
65
+ export type FloorplanPdfBaseline = 'auto' | 'alphabetic' | 'middle' | 'central' | 'hanging'
66
+
67
+ export type FloorplanPdfTextStyle = {
68
+ fontFamily?: string
69
+ fontWeight?: number | string
70
+ fontSize: number
71
+ }
72
+
73
+ /**
74
+ * SVG text's white-space handling (`white-space: normal`; the geometry has no
75
+ * way to ask for `pre`): runs of spaces, tabs and line breaks print as one
76
+ * space and the ends are trimmed. `APN 06075` reads with one space on
77
+ * screen, so it does on paper — and is measured that way for anchoring.
78
+ */
79
+ function collapseSvgWhitespace(text: string): string {
80
+ return text.replace(/[ \t\r\n]+/g, ' ').trim()
81
+ }
82
+
83
+ /** pdfkit keeps the current face's metrics and its fontkit font on `_font`; neither is public API. */
84
+ type EmbeddedFace = {
85
+ ascender: number
86
+ descender: number
87
+ xHeight: number
88
+ font: { hasGlyphForCodePoint: (codePoint: number) => boolean }
89
+ }
90
+
91
+ let pdfFonts: Promise<FloorplanPdfFonts> | null = null
92
+
93
+ /** The embedded faces' bytes, fetched once and shared by every export. */
94
+ export function loadFloorplanPdfFonts(): Promise<FloorplanPdfFonts> {
95
+ if (pdfFonts) return pdfFonts
96
+ const loading = Promise.all(
97
+ Object.entries(PDF_FONT_FILES).map(async ([face, url]) => {
98
+ const response = await fetch(url)
99
+ if (!response.ok) {
100
+ throw new Error(
101
+ `[floorplan-export] Could not load the PDF font ${face} (HTTP ${response.status})`,
102
+ )
103
+ }
104
+ return [face, await response.arrayBuffer()] as const
105
+ }),
106
+ ).then((entries) => Object.fromEntries(entries) as FloorplanPdfFonts)
107
+ pdfFonts = loading
108
+ // a failed fetch is retried by the next export rather than cached
109
+ loading.catch(() => {
110
+ if (pdfFonts === loading) pdfFonts = null
111
+ })
112
+ return loading
113
+ }
114
+
115
+ /** SVG font-family / font-weight → the embedded face that sets it. */
116
+ export function resolveFloorplanPdfFace(
117
+ fontFamily: string | undefined,
118
+ fontWeight: number | string | undefined,
119
+ ): FloorplanPdfFace {
120
+ const family = fontFamily?.toLocaleLowerCase() ?? ''
121
+ const mono = family.includes('mono') || family.includes('courier')
122
+ const numericWeight = Number.parseInt(String(fontWeight ?? 400), 10)
123
+ const bold =
124
+ String(fontWeight).toLocaleLowerCase() === 'bold' ||
125
+ (Number.isFinite(numericWeight) && numericWeight >= 500)
126
+ if (mono) return bold ? 'mono-bold' : 'mono'
127
+ return bold ? 'sans-bold' : 'sans'
128
+ }
129
+
12
130
  export class FloorplanPdfDocument {
13
131
  readonly raw: PdfKitDocumentInstance
14
132
  readonly internal: {
@@ -19,11 +137,21 @@ export class FloorplanPdfDocument {
19
137
  }
20
138
 
21
139
  private currentFontSize = 12
140
+ private currentFace: FloorplanPdfFace = 'sans'
22
141
  private readonly defaultPageSize: readonly [number, number]
142
+ private readonly faces = new Map<FloorplanPdfFace, EmbeddedFace>()
143
+ private readonly embedded: boolean
23
144
 
24
- constructor(raw: PdfKitDocumentInstance, defaultPageSize: readonly [number, number]) {
145
+ /** `fonts` null sets every face in pdfkit's standard fonts (WinAnsi only). */
146
+ constructor(
147
+ raw: PdfKitDocumentInstance,
148
+ defaultPageSize: readonly [number, number],
149
+ fonts: FloorplanPdfFonts | null,
150
+ ) {
25
151
  this.raw = raw
26
152
  this.defaultPageSize = defaultPageSize
153
+ this.embedded = fonts !== null
154
+ for (const [face, bytes] of Object.entries(fonts ?? {})) raw.registerFont(face, bytes)
27
155
  this.internal = {
28
156
  pageSize: {
29
157
  getWidth: () => this.raw.page?.width ?? this.defaultPageSize[0],
@@ -61,22 +189,17 @@ export class FloorplanPdfDocument {
61
189
  }
62
190
 
63
191
  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)
192
+ this.currentFace = resolveFloorplanPdfFace(family, weight)
69
193
  return this
70
194
  }
71
195
 
72
196
  setFontSize(size: number): this {
73
197
  this.currentFontSize = size
74
- this.raw.fontSize(size)
75
198
  return this
76
199
  }
77
200
 
78
201
  getTextWidth(value: string): number {
79
- return this.raw.widthOfString(value, { lineBreak: false })
202
+ return this.measureRuns(this.textRuns(value, this.currentFace), this.currentFontSize)
80
203
  }
81
204
 
82
205
  splitTextToSize(value: string, maxWidth: number): string[] {
@@ -103,20 +226,58 @@ export class FloorplanPdfDocument {
103
226
  baselineY: number,
104
227
  options: PdfTextOptions = {},
105
228
  ) {
106
- const lines = typeof value === 'string' ? value.split('\n') : value
229
+ const given = typeof value === 'string' ? value.split('\n') : value
230
+ const lines =
231
+ options.maxWidth === undefined
232
+ ? given
233
+ : given.flatMap((line) => this.splitTextToSize(line, options.maxWidth ?? 0))
107
234
  const lineHeight = this.currentFontSize * 1.2
108
235
  lines.forEach((line, index) => {
109
- const width = this.getTextWidth(line)
236
+ const runs = this.textRuns(line, this.currentFace)
237
+ const width = this.measureRuns(runs, this.currentFontSize)
110
238
  const drawX =
111
239
  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
- })
240
+ this.drawRuns(runs, drawX, baselineY + index * lineHeight, this.currentFontSize)
116
241
  })
117
242
  return this
118
243
  }
119
244
 
245
+ /** The width `drawText` sets `text` at — the same runs, face by face. */
246
+ measureText(text: string, style: FloorplanPdfTextStyle): number {
247
+ const face = resolveFloorplanPdfFace(style.fontFamily, style.fontWeight)
248
+ return this.measureRuns(this.textRuns(collapseSvgWhitespace(text), face), style.fontSize)
249
+ }
250
+
251
+ /**
252
+ * Native (selectable, searchable) text in the current fill colour, anchored
253
+ * at (x, y) the way SVG anchors it: `anchor` is `text-anchor`, `baseline`
254
+ * is `dominant-baseline`, resolved against the embedded face's metrics.
255
+ */
256
+ drawText(
257
+ text: string,
258
+ x: number,
259
+ y: number,
260
+ style: FloorplanPdfTextStyle,
261
+ placement: { anchor: 'start' | 'middle' | 'end'; baseline: FloorplanPdfBaseline },
262
+ ): void {
263
+ const face = resolveFloorplanPdfFace(style.fontFamily, style.fontWeight)
264
+ const runs = this.textRuns(collapseSvgWhitespace(text), face)
265
+ const width = this.measureRuns(runs, style.fontSize)
266
+ const startX =
267
+ placement.anchor === 'middle' ? x - width / 2 : placement.anchor === 'end' ? x - width : x
268
+ const metrics = this.face(face)
269
+ const em = style.fontSize / 1000
270
+ const baselineShift =
271
+ placement.baseline === 'middle'
272
+ ? (metrics.xHeight / 2) * em
273
+ : placement.baseline === 'central'
274
+ ? ((metrics.ascender + metrics.descender) / 2) * em
275
+ : placement.baseline === 'hanging'
276
+ ? 0.8 * metrics.ascender * em
277
+ : 0
278
+ this.drawRuns(runs, startX, y + baselineShift, style.fontSize)
279
+ }
280
+
120
281
  line(x1: number, y1: number, x2: number, y2: number): this {
121
282
  this.raw.moveTo(x1, y1).lineTo(x2, y2).stroke()
122
283
  return this
@@ -165,17 +326,160 @@ export class FloorplanPdfDocument {
165
326
  else this.raw.stroke()
166
327
  return this
167
328
  }
329
+
330
+ private setFace(face: FloorplanPdfFace) {
331
+ return this.raw.font(this.embedded ? face : STANDARD_FACES[face])
332
+ }
333
+
334
+ private face(face: FloorplanPdfFace): EmbeddedFace {
335
+ let embedded = this.faces.get(face)
336
+ if (!embedded) {
337
+ this.setFace(face)
338
+ embedded = (this.raw as unknown as { _font: EmbeddedFace })._font
339
+ this.faces.set(face, embedded)
340
+ }
341
+ return embedded
342
+ }
343
+
344
+ /** Splits `text` into runs each face can set, plus the symbols drawn as linework. */
345
+ private textRuns(text: string, face: FloorplanPdfFace): TextRun[] {
346
+ // printable ASCII is in every face; the standard fonts have no glyph table to consult
347
+ if (!this.embedded || /^[\x20-\x7e]*$/.test(text)) return [{ kind: 'text', face, text }]
348
+ const runs: TextRun[] = []
349
+ for (const char of text) {
350
+ const codePoint = char.codePointAt(0) ?? 0
351
+ let runFace = face
352
+ if (!this.face(face).font.hasGlyphForCodePoint(codePoint)) {
353
+ const drawn = DRAWN_SYMBOLS[char as keyof typeof DRAWN_SYMBOLS]
354
+ if (this.face(OTHER_FAMILY[face]).font.hasGlyphForCodePoint(codePoint)) {
355
+ runFace = OTHER_FAMILY[face]
356
+ } else if (drawn) {
357
+ runs.push({ kind: 'symbol', symbol: drawn.symbol, advanceEm: drawn.advanceEm })
358
+ continue
359
+ }
360
+ // anything else prints as the face's .notdef box — visibly missing, never garbage
361
+ }
362
+ const last = runs[runs.length - 1]
363
+ if (last?.kind === 'text' && last.face === runFace) last.text += char
364
+ else runs.push({ kind: 'text', face: runFace, text: char })
365
+ }
366
+ return runs
367
+ }
368
+
369
+ private measureRuns(runs: readonly TextRun[], fontSize: number): number {
370
+ let width = 0
371
+ for (const run of runs) {
372
+ width +=
373
+ run.kind === 'text'
374
+ ? this.setFace(run.face).fontSize(fontSize).widthOfString(run.text)
375
+ : run.advanceEm * fontSize
376
+ }
377
+ return width
378
+ }
379
+
380
+ private drawRuns(runs: readonly TextRun[], x: number, baselineY: number, fontSize: number) {
381
+ let cursor = x
382
+ for (const run of runs) {
383
+ if (run.kind === 'text') {
384
+ this.setFace(run.face).fontSize(fontSize)
385
+ this.raw.text(run.text, cursor, baselineY, { baseline: 'alphabetic', lineBreak: false })
386
+ cursor += this.raw.widthOfString(run.text)
387
+ } else {
388
+ this.drawSymbol(run.symbol, cursor, baselineY, fontSize)
389
+ cursor += run.advanceEm * fontSize
390
+ }
391
+ }
392
+ }
393
+
394
+ private drawSymbol(symbol: DrawnSymbol, x: number, baselineY: number, fontSize: number) {
395
+ const raw = this.raw
396
+ if (symbol === 'angle') {
397
+ // two strokes from a vertex on the baseline: the base, and a leg rising
398
+ // to the cap height — filled bands, so the text's own colour paints them
399
+ const vertex: [number, number] = [x + 0.08 * fontSize, baselineY]
400
+ const tip: [number, number] = [x + 0.56 * fontSize, baselineY - 0.7 * fontSize]
401
+ const width = 0.07 * fontSize
402
+ raw
403
+ .save()
404
+ // the base starts a little behind the vertex so the corner is closed
405
+ .polygon(
406
+ ...strokeBand([x + 0.05 * fontSize, baselineY], [x + 0.72 * fontSize, baselineY], width),
407
+ )
408
+ .polygon(...strokeBand(vertex, tip, width))
409
+ .fill('non-zero')
410
+ .restore()
411
+ return
412
+ }
413
+ if (symbol === 'pointer') {
414
+ // a small solid triangle on the x-height, like a list pointer
415
+ const midY = baselineY - 0.26 * fontSize
416
+ const half = 0.17 * fontSize
417
+ raw
418
+ .save()
419
+ .polygon(
420
+ [x + 0.1 * fontSize, midY - half],
421
+ [x + 0.4 * fontSize, midY],
422
+ [x + 0.1 * fontSize, midY + half],
423
+ )
424
+ .fill('non-zero')
425
+ .restore()
426
+ return
427
+ }
428
+ // warning: an outlined triangle from just under the baseline to the cap
429
+ // height, its "!" standing on the text's own baseline (so text extraction
430
+ // keeps it on the line it warns about)
431
+ const left = x + 0.04 * fontSize
432
+ const width = 0.82 * fontSize
433
+ const base = baselineY + 0.1 * fontSize
434
+ const height = 0.82 * fontSize
435
+ const stroke = 0.07 * fontSize
436
+ const apex: [number, number] = [left + width / 2, base - height]
437
+ const outer: [number, number][] = [[left, base], [left + width, base], apex]
438
+ const leg = Math.hypot(width / 2, height)
439
+ const inradius = (width * height) / (width + 2 * leg)
440
+ const centre: [number, number] = [left + width / 2, base - inradius]
441
+ const k = (inradius - stroke) / inradius
442
+ const inner = outer.map(
443
+ ([px, py]) =>
444
+ [centre[0] + (px - centre[0]) * k, centre[1] + (py - centre[1]) * k] as [number, number],
445
+ )
446
+ raw
447
+ .save()
448
+ .polygon(...outer)
449
+ .polygon(...inner)
450
+ .fill('even-odd')
451
+ .restore()
452
+ this.setFace('sans-bold').fontSize(0.55 * fontSize)
453
+ raw.text('!', centre[0] - raw.widthOfString('!') / 2, baselineY, {
454
+ baseline: 'alphabetic',
455
+ lineBreak: false,
456
+ })
457
+ }
168
458
  }
169
459
 
170
- export async function createFloorplanPdfDocument(defaultPageSize: readonly [number, number]) {
171
- const [{ default: PDFDocument }, { default: blobStream }] = await Promise.all([
460
+ export async function createFloorplanPdfDocument(
461
+ defaultPageSize: readonly [number, number],
462
+ options: { title?: string } = {},
463
+ ) {
464
+ const [{ default: PDFDocument }, { default: blobStream }, fonts] = await Promise.all([
172
465
  import('pdfkit/js/pdfkit.standalone'),
173
466
  import('blob-stream'),
467
+ // an export in the standard fonts beats no export when the font files are unreachable
468
+ loadFloorplanPdfFonts().catch((error: unknown) => {
469
+ console.warn('[floorplan-export] Falling back to the standard PDF fonts', error)
470
+ return null
471
+ }),
174
472
  ])
175
- const raw = new PDFDocument({ autoFirstPage: false, compress: true, margin: 0 })
473
+ const raw = new PDFDocument({
474
+ autoFirstPage: false,
475
+ compress: true,
476
+ margin: 0,
477
+ // the viewer's title bar shows the document title, not the file name
478
+ ...(options.title ? { info: { Title: options.title }, displayTitle: true } : {}),
479
+ })
176
480
  const stream = raw.pipe(blobStream())
177
481
  return {
178
- doc: new FloorplanPdfDocument(raw, defaultPageSize),
482
+ doc: new FloorplanPdfDocument(raw, defaultPageSize, fonts),
179
483
  save: async (filename: string) => {
180
484
  const blob = await new Promise<Blob>((resolve, reject) => {
181
485
  stream.on('finish', () => resolve(stream.toBlob('application/pdf')))
@@ -192,3 +496,20 @@ export async function createFloorplanPdfDocument(defaultPageSize: readonly [numb
192
496
  },
193
497
  }
194
498
  }
499
+
500
+ /** The quad a straight stroke of `width` from `a` to `b` covers (same winding for any direction). */
501
+ function strokeBand(
502
+ a: readonly [number, number],
503
+ b: readonly [number, number],
504
+ width: number,
505
+ ): [number, number][] {
506
+ const length = Math.hypot(b[0] - a[0], b[1] - a[1])
507
+ const nx = (-(b[1] - a[1]) / length) * (width / 2)
508
+ const ny = ((b[0] - a[0]) / length) * (width / 2)
509
+ return [
510
+ [a[0] + nx, a[1] + ny],
511
+ [b[0] + nx, b[1] + ny],
512
+ [b[0] - nx, b[1] - ny],
513
+ [a[0] - nx, a[1] - ny],
514
+ ]
515
+ }