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