@pascal-app/editor 1.0.1 → 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 (498) hide show
  1. package/package.json +16 -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 +221 -128
  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 +37 -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 +536 -487
  27. package/src/components/editor/footprint-height-handle.tsx +204 -0
  28. package/src/components/editor/grid.tsx +31 -7
  29. package/src/components/editor/group-actions.ts +59 -11
  30. package/src/components/editor/group-floating-action-menu.tsx +19 -9
  31. package/src/components/editor/group-move-3d.ts +4 -6
  32. package/src/components/editor/group-rotate-handle.tsx +81 -35
  33. package/src/components/editor/group-selection-box-3d.tsx +25 -17
  34. package/src/components/editor/group-transform-shared.test.ts +126 -1
  35. package/src/components/editor/group-transform-shared.ts +79 -73
  36. package/src/components/editor/handles/handle-arrow.tsx +72 -2
  37. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  38. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  39. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  40. package/src/components/editor/handles/use-handle-drag.ts +272 -131
  41. package/src/components/editor/index.tsx +316 -176
  42. package/src/components/editor/join-walls-pill.tsx +44 -0
  43. package/src/components/editor/node-action-menu.tsx +70 -70
  44. package/src/components/editor/node-arrow-handles.tsx +167 -14
  45. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  46. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  47. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  48. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  49. package/src/components/editor/registry-action-contributions.tsx +132 -0
  50. package/src/components/editor/room-controls.tsx +847 -0
  51. package/src/components/editor/room-floor-highlight.tsx +128 -0
  52. package/src/components/editor/room-handles-2d.tsx +354 -0
  53. package/src/components/editor/room-handles.tsx +675 -0
  54. package/src/components/editor/room-highlight.tsx +225 -0
  55. package/src/components/editor/room-labels.test.ts +357 -0
  56. package/src/components/editor/room-labels.tsx +990 -0
  57. package/src/components/editor/room-transform-ghost.tsx +256 -0
  58. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  59. package/src/components/editor/selection-manager.tsx +451 -317
  60. package/src/components/editor/slab-hole-highlights.tsx +3 -0
  61. package/src/components/editor/snapshot-capture-overlay.tsx +4 -0
  62. package/src/components/editor/snapshot-capture.test.ts +52 -4
  63. package/src/components/editor/snapshot-capture.ts +30 -5
  64. package/src/components/editor/thumbnail-generator.test.ts +308 -0
  65. package/src/components/editor/thumbnail-generator.tsx +1035 -229
  66. package/src/components/editor/use-floorplan-background-placement.ts +31 -11
  67. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  68. package/src/components/editor/views/host-view.tsx +61 -0
  69. package/src/components/editor/views/plugins-view.tsx +670 -0
  70. package/src/components/editor/views/scene-region.tsx +59 -0
  71. package/src/components/editor/views/use-editor-views.ts +57 -0
  72. package/src/components/editor/views/view-bar.tsx +220 -0
  73. package/src/components/editor/wall-justification.test.ts +140 -0
  74. package/src/components/editor/wall-move-side-handles.tsx +114 -163
  75. package/src/components/editor/wall-opening-highlights.tsx +2 -3
  76. package/src/components/editor/wall-push-arrow.tsx +92 -0
  77. package/src/components/editor/wall-region-handles.tsx +509 -0
  78. package/src/components/editor/wall-snap-beacon-layer.tsx +3 -0
  79. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  80. package/src/components/editor-2d/floorplan-group-move.tsx +17 -24
  81. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  82. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  83. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +33 -12
  84. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  85. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +242 -126
  86. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  87. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  88. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  89. package/src/components/editor-2d/renderers/floorplan-draft-measurement.tsx +166 -0
  90. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  91. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +87 -8
  92. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +355 -245
  93. package/src/components/editor-2d/svg-paths.ts +12 -15
  94. package/src/components/procedural-items/inspector.tsx +40 -15
  95. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  96. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  97. package/src/components/systems/zone/zone-system.test.ts +47 -0
  98. package/src/components/systems/zone/zone-system.tsx +31 -66
  99. package/src/components/tools/__fixtures__/hosting.ts +10 -0
  100. package/src/components/tools/item/item-surface-placement.test.ts +3 -9
  101. package/src/components/tools/item/item-surface-strategy-resolver.test.ts +3 -9
  102. package/src/components/tools/item/placement-history.test.tsx +474 -0
  103. package/src/components/tools/item/placement-strategies.ts +63 -47
  104. package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +3 -3
  105. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  106. package/src/components/tools/item/use-draft-node.ts +207 -97
  107. package/src/components/tools/item/use-placement-coordinator.tsx +251 -111
  108. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  109. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  110. package/src/components/tools/registry/item-surface-move.test.ts +10 -2
  111. package/src/components/tools/registry/item-surface-move.ts +46 -24
  112. package/src/components/tools/registry/item-surface-resolver.test.ts +2 -2
  113. package/src/components/tools/registry/move-registry-node-tool.tsx +26 -10
  114. package/src/components/tools/select/box-select-tool.tsx +2 -12
  115. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  116. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  117. package/src/components/tools/select/select-candidates.test.ts +37 -4
  118. package/src/components/tools/select/select-candidates.ts +15 -11
  119. package/src/components/tools/shared/affordance-dispatch.test.ts +24 -0
  120. package/src/components/tools/shared/affordance-dispatch.ts +13 -0
  121. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  122. package/src/components/tools/shared/draft-measurement-label.tsx +38 -0
  123. package/src/components/tools/shared/pointer-support-cap.test.ts +27 -0
  124. package/src/components/tools/shared/pointer-support-cap.ts +17 -5
  125. package/src/components/tools/shared/surface-rejection.test.tsx +5 -4
  126. package/src/components/tools/shared/surface-rejection.tsx +14 -0
  127. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  128. package/src/components/tools/stair/stair-defaults.ts +0 -3
  129. package/src/components/tools/stair/stair-tool.tsx +90 -42
  130. package/src/components/tools/tool-manager.tsx +82 -23
  131. package/src/components/tools/wall/wall-drafting.test.ts +19 -4
  132. package/src/components/tools/zone/zone-tool.tsx +4 -12
  133. package/src/components/ui/action-menu/action-button.tsx +3 -0
  134. package/src/components/ui/action-menu/camera-actions.tsx +2 -1
  135. package/src/components/ui/action-menu/control-modes.tsx +2 -35
  136. package/src/components/ui/action-menu/measurement-control.tsx +3 -10
  137. package/src/components/ui/build-panel/build-panel.tsx +235 -0
  138. package/src/components/ui/command-palette/editor-commands.tsx +4 -30
  139. package/src/components/ui/command-palette/index.tsx +5 -24
  140. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  141. package/src/components/ui/controls/material-paint-panel.tsx +101 -91
  142. package/src/components/ui/controls/material-picker.tsx +8 -67
  143. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  144. package/src/components/ui/controls/select-control.tsx +65 -0
  145. package/src/components/ui/controls/slider-control.tsx +76 -31
  146. package/src/components/ui/floating-level-selector.tsx +9 -14
  147. package/src/components/ui/helpers/building-helper.tsx +4 -1
  148. package/src/components/ui/helpers/contextual-helper-panel.tsx +154 -70
  149. package/src/components/ui/helpers/helper-manager.test.tsx +69 -0
  150. package/src/components/ui/helpers/helper-manager.tsx +348 -19
  151. package/src/components/ui/helpers/item-helper.tsx +8 -0
  152. package/src/components/ui/helpers/registered-tool-helper.tsx +10 -33
  153. package/src/components/ui/icon-ref.tsx +45 -1
  154. package/src/components/ui/item-catalog/item-catalog.tsx +5 -30
  155. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  156. package/src/components/ui/node-icon-image.tsx +16 -0
  157. package/src/components/ui/panels/collections/collections-popover.tsx +20 -14
  158. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  159. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  160. package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
  161. package/src/components/ui/panels/mobile-selection-bar.tsx +2 -2
  162. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  163. package/src/components/ui/panels/multi-field-value.ts +6 -3
  164. package/src/components/ui/panels/multi-height-mode.tsx +27 -21
  165. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  166. package/src/components/ui/panels/multi-selection-panel.tsx +43 -4
  167. package/src/components/ui/panels/node-display.ts +8 -3
  168. package/src/components/ui/panels/panel-manager.tsx +103 -68
  169. package/src/components/ui/panels/panel-wrapper.tsx +78 -36
  170. package/src/components/ui/panels/parametric-field-control.tsx +1 -1
  171. package/src/components/ui/panels/parametric-inspector.tsx +7 -19
  172. package/src/components/ui/panels/reference-panel.tsx +2 -3
  173. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  174. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  175. package/src/components/ui/panels/room-name-options.ts +47 -0
  176. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  177. package/src/components/ui/panels/room-panel.tsx +725 -0
  178. package/src/components/ui/panels/shape-choice.tsx +58 -0
  179. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  180. package/src/components/ui/primitives/color-dot.tsx +36 -24
  181. package/src/components/ui/primitives/shortcut-token.tsx +4 -0
  182. package/src/components/ui/primitives/sidebar.tsx +2 -0
  183. package/src/components/ui/right-stack.test.ts +29 -0
  184. package/src/components/ui/right-stack.tsx +145 -0
  185. package/src/components/ui/sidebar/icon-rail.tsx +5 -0
  186. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +2 -2
  187. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +615 -372
  188. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +7 -7
  189. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +1 -1
  190. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  191. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  192. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  193. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  194. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  195. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  196. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  197. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  198. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  199. package/src/components/ui/sidebar/panels/site-panel/index.tsx +176 -211
  200. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  201. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  202. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  203. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  204. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  205. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  206. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  207. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  208. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  209. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  210. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +0 -27
  211. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  212. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  213. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  214. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  215. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +0 -2
  216. package/src/components/ui/sidebar/tab-bar.tsx +10 -0
  217. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  218. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  219. package/src/components/viewer/floorplan-preview-visibility.test.ts +35 -0
  220. package/src/components/viewer/floorplan-preview-visibility.ts +19 -0
  221. package/src/components/viewer/floorplan-preview.tsx +6 -18
  222. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  223. package/src/components/viewer/viewer-rooms.tsx +175 -0
  224. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  225. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  226. package/src/components/viewer/viewer-stage.tsx +2 -2
  227. package/src/components/viewer-zone-system.tsx +9 -9
  228. package/src/hooks/use-auto-save.integration.test.tsx +311 -0
  229. package/src/hooks/use-auto-save.ts +55 -14
  230. package/src/hooks/use-ceiling-events.test.ts +1 -0
  231. package/src/hooks/use-ceiling-events.ts +2 -0
  232. package/src/hooks/use-grid-events.test.ts +2 -0
  233. package/src/hooks/use-grid-events.ts +16 -11
  234. package/src/hooks/use-keyboard.test.ts +182 -0
  235. package/src/hooks/use-keyboard.ts +122 -91
  236. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  237. package/src/hooks/use-open-wall-ends.ts +238 -0
  238. package/src/hooks/use-placement-notice.test.ts +48 -0
  239. package/src/hooks/use-placement-notice.ts +42 -0
  240. package/src/hooks/use-selected-room.ts +143 -0
  241. package/src/index.tsx +306 -39
  242. package/src/lib/catalog-panel-model.ts +39 -0
  243. package/src/lib/catalog-selection.test.ts +27 -0
  244. package/src/lib/catalog-selection.ts +8 -0
  245. package/src/lib/ceiling-edit-session.test.ts +321 -0
  246. package/src/lib/ceiling-edit-session.ts +206 -0
  247. package/src/lib/ceiling-region-session.test.ts +179 -0
  248. package/src/lib/contextual-help.test.ts +27 -1
  249. package/src/lib/contextual-help.ts +16 -9
  250. package/src/lib/continuation.test.ts +19 -1
  251. package/src/lib/continuation.ts +25 -4
  252. package/src/lib/door-interaction.ts +14 -0
  253. package/src/lib/editor-level-display.test.ts +96 -0
  254. package/src/lib/editor-level-display.ts +48 -0
  255. package/src/lib/editor-views.ts +144 -0
  256. package/src/lib/export-geometry-wait.test.ts +90 -0
  257. package/src/lib/floor-edit-session.ts +153 -0
  258. package/src/lib/floor-footprints.test.ts +563 -0
  259. package/src/lib/floor-footprints.ts +395 -0
  260. package/src/lib/floor-foundation-support.test.ts +283 -0
  261. package/src/lib/floor-opening-draft.ts +218 -0
  262. package/src/lib/floor-openings.test.ts +216 -0
  263. package/src/lib/floor-region-geometry.ts +191 -0
  264. package/src/lib/floor-region-session.test.ts +485 -0
  265. package/src/lib/floor-region-session.ts +589 -0
  266. package/src/lib/floor-region-snap.ts +101 -0
  267. package/src/lib/floor-step-hover.test.ts +32 -0
  268. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  269. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  270. package/src/lib/floorplan/dimension-drive.ts +479 -0
  271. package/src/lib/floorplan/floorplan-export.test.ts +15 -9
  272. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  273. package/src/lib/floorplan/floorplan-extension.ts +48 -0
  274. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  275. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  276. package/src/lib/floorplan/floorplan-pdfkit-document.ts +339 -18
  277. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +138 -8
  278. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +56 -25
  279. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  280. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  281. package/src/lib/floorplan/geometry.ts +13 -6
  282. package/src/lib/floorplan/index.ts +0 -9
  283. package/src/lib/floorplan/items.ts +5 -2
  284. package/src/lib/floorplan/marks.test.ts +213 -0
  285. package/src/lib/floorplan/marks.ts +238 -0
  286. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  287. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  288. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  289. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  290. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  291. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  292. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  293. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  294. package/src/lib/floorplan/schedules.test.ts +189 -0
  295. package/src/lib/floorplan/schedules.ts +339 -0
  296. package/src/lib/floorplan/sheet-export.test.ts +100 -0
  297. package/src/lib/floorplan/sheet-export.ts +318 -0
  298. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +779 -0
  299. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  300. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  301. package/src/lib/floorplan/site-plan/coverage.ts +232 -0
  302. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  303. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  304. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  305. package/src/lib/floorplan/site-plan/front-edge.ts +156 -0
  306. package/src/lib/floorplan/site-plan/geometry.test.ts +275 -0
  307. package/src/lib/floorplan/site-plan/geometry.ts +557 -0
  308. package/src/lib/floorplan/site-plan/index.ts +71 -0
  309. package/src/lib/floorplan/site-plan/site-annotations.test.ts +366 -0
  310. package/src/lib/floorplan/site-plan/site-annotations.ts +1083 -0
  311. package/src/lib/floorplan/site-plan/site-parts.ts +379 -0
  312. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  313. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  314. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  315. package/src/lib/floorplan/site-plan/yards.test.ts +69 -0
  316. package/src/lib/floorplan/types.ts +1 -27
  317. package/src/lib/fresh-planar-placement.test.ts +36 -0
  318. package/src/lib/fresh-planar-placement.ts +191 -36
  319. package/src/lib/geometry-script/author.ts +152 -0
  320. package/src/lib/geometry-script/client.ts +152 -0
  321. package/src/lib/geometry-script/protocol.ts +12 -0
  322. package/src/lib/gesture-lifecycle.test.ts +595 -0
  323. package/src/lib/gesture-lifecycle.ts +290 -0
  324. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  325. package/src/lib/glb-export.test.ts +544 -16
  326. package/src/lib/glb-export.ts +256 -125
  327. package/src/lib/grid-frame.test.ts +19 -0
  328. package/src/lib/grid-frame.ts +18 -0
  329. package/src/lib/history.test.ts +6 -0
  330. package/src/lib/history.ts +7 -0
  331. package/src/lib/hud-title.ts +109 -0
  332. package/src/lib/hydration-gated-storage.ts +49 -0
  333. package/src/lib/ifc-export.test.ts +174 -0
  334. package/src/lib/ifc-export.ts +201 -0
  335. package/src/lib/ifc-parts.ts +15 -0
  336. package/src/lib/inspector-expanded.test.ts +44 -0
  337. package/src/lib/inspector-expanded.ts +36 -0
  338. package/src/lib/interaction/scope.ts +27 -1
  339. package/src/lib/keyboard-pan.test.ts +184 -0
  340. package/src/lib/keyboard-pan.ts +238 -0
  341. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  342. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  343. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  344. package/src/lib/lot/clean-ring.test.ts +186 -0
  345. package/src/lib/lot/clean-ring.ts +217 -0
  346. package/src/lib/lot/dossier.test.ts +202 -0
  347. package/src/lib/lot/dossier.ts +498 -0
  348. package/src/lib/lot/drop-in.test.ts +326 -0
  349. package/src/lib/lot/drop-in.ts +407 -0
  350. package/src/lib/lot/index.ts +43 -0
  351. package/src/lib/lot/lot-patch.test.ts +340 -0
  352. package/src/lib/lot/lot-patch.ts +394 -0
  353. package/src/lib/lot/parcel-provider.ts +43 -0
  354. package/src/lib/lot/terrain.test.ts +161 -0
  355. package/src/lib/lot/terrain.ts +266 -0
  356. package/src/lib/material-catalog-model.ts +74 -0
  357. package/src/lib/material-paint-panel-model.ts +80 -0
  358. package/src/lib/material-paint.ts +4 -0
  359. package/src/lib/mezzanine-actions.test.ts +342 -0
  360. package/src/lib/mezzanine-draft.test.ts +501 -0
  361. package/src/lib/mezzanine-draft.ts +278 -0
  362. package/src/lib/mezzanine-messages.ts +26 -0
  363. package/src/lib/mezzanine-selection.test.ts +207 -0
  364. package/src/lib/mezzanine-stairs.ts +46 -0
  365. package/src/lib/model-export.ts +3 -1
  366. package/src/lib/node-action-movement.ts +17 -6
  367. package/src/lib/node-display-label.ts +37 -0
  368. package/src/lib/orbit-floor-clearance.test.ts +165 -0
  369. package/src/lib/orbit-floor-clearance.ts +106 -0
  370. package/src/lib/overlay-transform.test.ts +28 -0
  371. package/src/lib/overlay-transform.ts +12 -0
  372. package/src/lib/paint-commit-route.test.ts +37 -0
  373. package/src/lib/paint-commit-route.ts +17 -0
  374. package/src/lib/paint-eyedropper.test.tsx +165 -0
  375. package/src/lib/paint-eyedropper.ts +99 -0
  376. package/src/lib/paint-pass-through.test.ts +111 -0
  377. package/src/lib/paint-pass-through.ts +76 -0
  378. package/src/lib/paint-region-hover.ts +25 -0
  379. package/src/lib/paint-region-mode.ts +154 -0
  380. package/src/lib/paint-regions.test.ts +166 -0
  381. package/src/lib/paint-regions.ts +192 -0
  382. package/src/lib/paint-scope.test.ts +317 -35
  383. package/src/lib/paint-scope.ts +266 -25
  384. package/src/lib/panel-rows.ts +96 -0
  385. package/src/lib/panel-tool-options.test.ts +34 -0
  386. package/src/lib/panel-tool-options.ts +46 -0
  387. package/src/lib/parametric-node-update.ts +37 -0
  388. package/src/lib/plate-paint-affected.test.ts +300 -0
  389. package/src/lib/plate-paint-affected.ts +177 -0
  390. package/src/lib/plugin-chat.ts +50 -0
  391. package/src/lib/plugin-panels.test.ts +25 -0
  392. package/src/lib/plugin-panels.ts +94 -0
  393. package/src/lib/print-shell-compiler.ts +1 -0
  394. package/src/lib/registered-draft-snap.ts +12 -0
  395. package/src/lib/roof-duplication.ts +10 -2
  396. package/src/lib/room-assembly-overlay.test.ts +275 -0
  397. package/src/lib/room-assembly-overlay.ts +266 -0
  398. package/src/lib/room-built-on.test.ts +379 -0
  399. package/src/lib/room-built-on.ts +138 -0
  400. package/src/lib/room-construction-commands.ts +712 -0
  401. package/src/lib/room-construction.test.ts +746 -0
  402. package/src/lib/room-construction.ts +385 -0
  403. package/src/lib/room-controls-phase6.test.ts +630 -0
  404. package/src/lib/room-divide-session.ts +523 -0
  405. package/src/lib/room-handle-drag.ts +1068 -0
  406. package/src/lib/room-name-catalog.test.ts +90 -0
  407. package/src/lib/room-name-catalog.ts +168 -0
  408. package/src/lib/room-push-dimensions.test.ts +311 -0
  409. package/src/lib/room-push-dimensions.ts +251 -0
  410. package/src/lib/room-selection-commands.ts +62 -0
  411. package/src/lib/room-selection.test.ts +973 -0
  412. package/src/lib/room-selection.ts +417 -0
  413. package/src/lib/room-structure-commands.ts +94 -0
  414. package/src/lib/room-structure.test.ts +921 -0
  415. package/src/lib/room-transform-session.ts +570 -0
  416. package/src/lib/room-zone-routing.test.ts +294 -0
  417. package/src/lib/room-zone-routing.ts +197 -0
  418. package/src/lib/scene-clipboard.test.ts +364 -0
  419. package/src/lib/scene-clipboard.ts +229 -48
  420. package/src/lib/scene-material-refs.test.ts +79 -0
  421. package/src/lib/scene-material-refs.ts +79 -0
  422. package/src/lib/scene.test.ts +87 -0
  423. package/src/lib/scene.ts +39 -45
  424. package/src/lib/selection-routing.test.ts +129 -45
  425. package/src/lib/selection-routing.ts +53 -74
  426. package/src/lib/session-writes.test.ts +120 -0
  427. package/src/lib/session-writes.ts +205 -0
  428. package/src/lib/sidebar-panel.ts +62 -0
  429. package/src/lib/site-boundary.test.ts +4 -6
  430. package/src/lib/site-boundary.ts +1 -1
  431. package/src/lib/snapping-mode.test.ts +17 -2
  432. package/src/lib/snapping-mode.ts +19 -7
  433. package/src/lib/spatial-drag-plane.test.ts +53 -0
  434. package/src/lib/spatial-drag-plane.ts +19 -0
  435. package/src/lib/spatial-pointer-input.test.ts +72 -0
  436. package/src/lib/spatial-pointer-input.ts +62 -0
  437. package/src/lib/spatial-snap.test.ts +47 -0
  438. package/src/lib/terrace-draft.test.ts +145 -0
  439. package/src/lib/terrace-draft.ts +213 -0
  440. package/src/lib/terrain-panel-model.ts +117 -0
  441. package/src/lib/tool-cursor-icon.test.ts +16 -0
  442. package/src/lib/tool-cursor-icon.ts +11 -0
  443. package/src/lib/typing-target.ts +9 -0
  444. package/src/lib/units.ts +77 -39
  445. package/src/lib/vector-edges.test.ts +58 -0
  446. package/src/lib/vector-edges.ts +400 -0
  447. package/src/lib/viewer-selection.test.ts +577 -0
  448. package/src/lib/viewer-selection.ts +569 -0
  449. package/src/lib/wall-draw-variant.test.ts +71 -0
  450. package/src/lib/wall-draw-variant.ts +91 -0
  451. package/src/lib/wall-paint-region-session.ts +246 -0
  452. package/src/lib/wall-paint-region.test.ts +373 -0
  453. package/src/lib/wall-polygon-draft.test.ts +262 -0
  454. package/src/lib/wall-polygon-draft.ts +286 -0
  455. package/src/lib/wall-push.test.ts +453 -0
  456. package/src/lib/wall-region-face.ts +352 -0
  457. package/src/lib/wall-region-handles.test.ts +304 -0
  458. package/src/lib/wall-region-handles.ts +368 -0
  459. package/src/lib/wall-region-list.test.ts +119 -0
  460. package/src/lib/wall-region-list.ts +136 -0
  461. package/src/lib/wall-region-snap.ts +110 -0
  462. package/src/lib/window-interaction.ts +14 -0
  463. package/src/lib/world-grid-snap.test.ts +48 -0
  464. package/src/lib/world-grid-snap.ts +9 -5
  465. package/src/lib/zone-content.ts +4 -1
  466. package/src/lib/zone-name.test.ts +17 -0
  467. package/src/lib/zone-name.ts +17 -0
  468. package/src/store/opens-in-select-mode.test.ts +223 -0
  469. package/src/store/paint-mode.test.ts +188 -0
  470. package/src/store/rotation-snapping.test.ts +53 -0
  471. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  472. package/src/store/tool-mode.test.ts +137 -6
  473. package/src/store/use-delete-confirmation.ts +28 -1
  474. package/src/store/use-drawing-view.test.ts +24 -3
  475. package/src/store/use-drawing-view.ts +29 -1
  476. package/src/store/use-editor.tsx +442 -323
  477. package/src/store/use-floorplan-draft-preview.ts +23 -1
  478. package/src/store/use-floorplan-marquee.ts +12 -2
  479. package/src/store/use-interaction-scope.ts +155 -6
  480. package/src/store/use-quick-measurement-hud.ts +3 -3
  481. package/src/store/use-stair-build-preview.ts +13 -5
  482. package/src/store/use-wall-move-ghosts.ts +4 -2
  483. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  484. package/src/store/use-wall-region-handles-selection.ts +139 -0
  485. package/src/store/view-layout.test.ts +175 -0
  486. package/src/store/view-layout.ts +196 -0
  487. package/src/test-utils/immediate-animation-frames.ts +16 -0
  488. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  489. package/src/test-utils/selection-harness.ts +82 -0
  490. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  491. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  492. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  493. package/src/components/tools/fence/fence-drafting.ts +0 -266
  494. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  495. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  496. package/src/lib/floorplan/stairs.ts +0 -489
  497. package/src/lib/level-duplication.test.ts +0 -144
  498. 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 = {
@@ -57,18 +62,36 @@ export type FloorplanToolContext = {
57
62
 
58
63
  export type FloorplanNodeExtension<N extends AnyNode = AnyNode> = {
59
64
  tool?: () => Promise<{ default: ComponentType<FloorplanToolContext> }>
65
+ directDrag?: boolean
60
66
  availableModes?: readonly FloorplanToolMode[]
61
67
  preferredView?: '2d' | '3d'
62
68
  referencedSelectionAnnotationRole?: FloorplanAnnotationRole
63
69
  contextualDimensions?: (node: N, ctx: GeometryContext) => FloorplanGeometry | null
64
70
  actionMenu?: {
65
71
  canCurve?: (args: { node: N; nodes: Readonly<Record<AnyNodeId, AnyNode>> }) => boolean
72
+ /** Buttons the kind adds to the selection's action menu (2D and 3D); each decides its own visibility. */
73
+ actions?: () => Promise<{ default: ComponentType }>
66
74
  }
75
+ /**
76
+ * Plan layers mounted while a node of this kind is in a `reshaping` scope of
77
+ * that name (the 2D sibling of `def.affordanceTools[reshape]`).
78
+ */
79
+ reshapeLayers?: Record<string, () => Promise<{ default: ComponentType<FloorplanToolContext> }>>
80
+ /**
81
+ * Nodes the plan treats as one with this node when toggling it out of a
82
+ * selection (a slab and its ceiling share an outline and a click).
83
+ */
84
+ selectionCounterparts?: (args: {
85
+ node: N
86
+ nodes: Readonly<Record<AnyNodeId, AnyNode>>
87
+ }) => AnyNodeId[]
67
88
  schedule?: (args: {
68
89
  siblings: ReadonlyArray<N>
69
90
  nodes: Readonly<Record<string, AnyNode>>
70
91
  levelId: AnyNodeId
71
92
  unit: 'metric' | 'imperial'
93
+ /** A drafted sheet's schedule: marks match the sheet's tags. */
94
+ drafting?: boolean
72
95
  }) => FloorplanSchedule | null
73
96
  linkedLevelIds?: (node: N) => readonly AnyNodeId[]
74
97
  resolveForDrawing?: (args: {
@@ -82,11 +105,30 @@ type FloorplanGeometryMetadata = {
82
105
  annotationRole?: FloorplanAnnotationRole
83
106
  annotationObstacle?: 'bounds' | 'outline'
84
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
85
114
  }
86
115
 
87
116
  type FloorplanContextExtension = {
88
117
  automaticDimensions: boolean
89
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
90
132
  metricNotation: FloorplanMetricNotation
91
133
  wallDimensionReference: FloorplanWallDimensionReference
92
134
  }
@@ -140,6 +182,8 @@ export function createFloorplanContextExtensions(
140
182
  [FLOORPLAN_CONTEXT_EXTENSION_KEY]: {
141
183
  automaticDimensions: values.automaticDimensions !== false,
142
184
  purpose: values.purpose === 'document' ? 'document' : 'edit',
185
+ drafting: values.drafting === true,
186
+ roomLabelOverlay: values.roomLabelOverlay === true,
143
187
  metricNotation: values.metricNotation === 'millimeters' ? 'millimeters' : 'meters',
144
188
  wallDimensionReference: normalizeFloorplanWallDimensionReference(
145
189
  values.wallDimensionReference,
@@ -155,6 +199,8 @@ export function readFloorplanContext(ctx: GeometryContext): FloorplanContextExte
155
199
  return {
156
200
  automaticDimensions: extension.automaticDimensions !== false,
157
201
  purpose: extension.purpose === 'document' ? 'document' : 'edit',
202
+ drafting: extension.drafting === true,
203
+ roomLabelOverlay: extension.roomLabelOverlay === true,
158
204
  metricNotation: extension.metricNotation === 'millimeters' ? 'millimeters' : 'meters',
159
205
  wallDimensionReference: normalizeFloorplanWallDimensionReference(
160
206
  extension.wallDimensionReference,
@@ -164,6 +210,8 @@ export function readFloorplanContext(ctx: GeometryContext): FloorplanContextExte
164
210
  return {
165
211
  automaticDimensions: true,
166
212
  purpose: 'edit',
213
+ drafting: false,
214
+ roomLabelOverlay: false,
167
215
  metricNotation: 'meters',
168
216
  wallDimensionReference: DEFAULT_FLOORPLAN_WALL_DIMENSION_REFERENCE,
169
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
+ }