@pascal-app/editor 0.9.1 → 1.0.0-beta.1

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 (366) hide show
  1. package/package.json +14 -9
  2. package/src/components/editor/alignment-3d-guide-layer.tsx +10 -2
  3. package/src/components/editor/bake-exporter.tsx +49 -0
  4. package/src/components/editor/bake-thumbnail.tsx +95 -0
  5. package/src/components/editor/camera-dragging-lifecycle.test.ts +49 -0
  6. package/src/components/editor/camera-dragging-lifecycle.ts +41 -0
  7. package/src/components/editor/custom-camera-controls.tsx +332 -199
  8. package/src/components/editor/delete-confirmation-dialog.tsx +54 -0
  9. package/src/components/editor/editor-layout-mobile.tsx +5 -0
  10. package/src/components/editor/editor-layout-v2.tsx +18 -0
  11. package/src/components/editor/elevation-3d-guide-layer.tsx +120 -0
  12. package/src/components/editor/export-manager.tsx +71 -63
  13. package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
  14. package/src/components/editor/first-person/build-collider-world.test.ts +102 -5
  15. package/src/components/editor/first-person/build-collider-world.ts +52 -5
  16. package/src/components/editor/first-person/terrain-collider.test.ts +132 -0
  17. package/src/components/editor/first-person/terrain-collider.ts +212 -0
  18. package/src/components/editor/first-person-controls.tsx +410 -136
  19. package/src/components/editor/floating-action-menu.tsx +435 -64
  20. package/src/components/editor/floating-building-action-menu.tsx +1 -1
  21. package/src/components/editor/floorplan-background-selection.test.ts +72 -0
  22. package/src/components/editor/floorplan-background-selection.ts +7 -20
  23. package/src/components/editor/floorplan-camera-sync.test.ts +249 -0
  24. package/src/components/editor/floorplan-camera-sync.ts +213 -0
  25. package/src/components/editor/floorplan-mode-coordinator.tsx +82 -0
  26. package/src/components/editor/floorplan-navigation-presentation.test.ts +142 -0
  27. package/src/components/editor/floorplan-navigation-presentation.ts +156 -0
  28. package/src/components/editor/floorplan-panel.tsx +2517 -1492
  29. package/src/components/editor/grid.tsx +166 -39
  30. package/src/components/editor/group-actions.ts +597 -0
  31. package/src/components/editor/group-floating-action-menu.tsx +130 -0
  32. package/src/components/editor/group-move-3d.ts +397 -0
  33. package/src/components/editor/group-rotate-handle.tsx +102 -64
  34. package/src/components/editor/group-selection-box-3d.tsx +173 -0
  35. package/src/components/editor/group-transform-shared.test.ts +192 -1
  36. package/src/components/editor/group-transform-shared.ts +240 -7
  37. package/src/components/editor/handles/handle-arrow.tsx +81 -23
  38. package/src/components/editor/handles/handle-drag-history.test.ts +96 -0
  39. package/src/components/editor/handles/handle-drag-history.ts +14 -0
  40. package/src/components/editor/handles/preview-overrides.test.ts +26 -0
  41. package/src/components/editor/handles/preview-overrides.ts +13 -0
  42. package/src/components/editor/handles/resize-snap.test.ts +52 -0
  43. package/src/components/editor/handles/resize-snap.ts +23 -0
  44. package/src/components/editor/handles/use-handle-drag.ts +49 -6
  45. package/src/components/editor/index.tsx +289 -35
  46. package/src/components/editor/measurement-pill.tsx +68 -23
  47. package/src/components/editor/node-action-menu.tsx +14 -1
  48. package/src/components/editor/node-arrow-handles.tsx +409 -180
  49. package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
  50. package/src/components/editor/quick-measurement-card.tsx +76 -0
  51. package/src/components/editor/quick-measurement-hud.tsx +34 -0
  52. package/src/components/editor/riser-diagram-panel.tsx +137 -0
  53. package/src/components/editor/selection-manager.tsx +814 -466
  54. package/src/components/editor/site-edge-labels.tsx +28 -6
  55. package/src/components/editor/slab-hole-highlights.tsx +13 -5
  56. package/src/components/editor/snapshot-capture-overlay.tsx +258 -115
  57. package/src/components/editor/three-context-bridge.ts +22 -0
  58. package/src/components/editor/thumbnail-generator.tsx +118 -253
  59. package/src/components/editor/use-floorplan-background-placement.ts +113 -45
  60. package/src/components/editor/use-floorplan-hit-testing.test.ts +82 -0
  61. package/src/components/editor/use-floorplan-hit-testing.ts +144 -161
  62. package/src/components/editor/use-floorplan-scene-data.ts +5 -5
  63. package/src/components/editor/use-mesh-settle-epoch.ts +27 -0
  64. package/src/components/editor/wall-measurement-label.tsx +10 -28
  65. package/src/components/editor/wall-move-side-handles.tsx +229 -39
  66. package/src/components/editor/wall-opening-highlights.tsx +63 -32
  67. package/src/components/editor/wall-snap-beacon-layer.tsx +162 -6
  68. package/src/components/editor-2d/floorplan-action-menu-layer.tsx +9 -5
  69. package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +2 -1
  70. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +40 -9
  71. package/src/components/editor-2d/floorplan-cursor-indicator-position.test.ts +31 -0
  72. package/src/components/editor-2d/floorplan-cursor-indicator-position.ts +39 -0
  73. package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
  74. package/src/components/editor-2d/floorplan-group-move.tsx +744 -0
  75. package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
  76. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1672 -0
  77. package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
  78. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +72 -0
  79. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +219 -20
  80. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +327 -59
  81. package/src/components/editor-2d/floorplan-render-context.test.ts +27 -0
  82. package/src/components/editor-2d/floorplan-render-context.tsx +113 -8
  83. package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +5 -3
  84. package/src/components/editor-2d/renderers/floorplan-annotation-layout.test.ts +285 -0
  85. package/src/components/editor-2d/renderers/floorplan-annotation-layout.ts +516 -0
  86. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.test.tsx +236 -0
  87. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +618 -0
  88. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +369 -0
  89. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +431 -32
  90. package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +89 -0
  91. package/src/components/editor-2d/renderers/floorplan-label-angle.ts +106 -0
  92. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +94 -26
  93. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +587 -0
  94. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +2280 -578
  95. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +20 -10
  96. package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
  97. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +425 -61
  98. package/src/components/systems/ceiling/ceiling-system.tsx +97 -4
  99. package/src/components/systems/roof/roof-edit-system.tsx +1768 -13
  100. package/src/components/systems/selection-affordance-manager.tsx +38 -0
  101. package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
  102. package/src/components/systems/zone/zone-system.tsx +42 -13
  103. package/src/components/tools/elevator/elevator-tool.tsx +34 -8
  104. package/src/components/tools/elevator/move-elevator-tool.tsx +13 -2
  105. package/src/components/tools/fence/fence-drafting.ts +107 -8
  106. package/src/components/tools/item/move-tool.tsx +17 -13
  107. package/src/components/tools/item/placement-math.test.ts +13 -1
  108. package/src/components/tools/item/placement-math.ts +28 -2
  109. package/src/components/tools/item/placement-strategies.ts +246 -5
  110. package/src/components/tools/item/placement-types.ts +13 -1
  111. package/src/components/tools/item/use-draft-node.ts +146 -57
  112. package/src/components/tools/item/use-placement-coordinator.tsx +813 -125
  113. package/src/components/tools/registry/move-registry-node-tool.tsx +636 -103
  114. package/src/components/tools/roof/roof-tool.tsx +336 -94
  115. package/src/components/tools/select/box-select-state.ts +18 -7
  116. package/src/components/tools/select/box-select-tool.tsx +176 -40
  117. package/src/components/tools/select/marquee-geometry.test.ts +161 -0
  118. package/src/components/tools/select/marquee-geometry.ts +155 -0
  119. package/src/components/tools/select/plane-box-select-tool.tsx +1 -8
  120. package/src/components/tools/select/select-candidates.ts +1 -1
  121. package/src/components/tools/shared/cursor-sphere.tsx +62 -26
  122. package/src/components/tools/shared/drag-bounding-box.tsx +28 -4
  123. package/src/components/tools/shared/facing-indicator.tsx +87 -0
  124. package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
  125. package/src/components/tools/shared/floor-stack-preview.ts +4 -0
  126. package/src/components/tools/shared/horizontal-construction-plane.ts +123 -0
  127. package/src/components/tools/shared/placement-box.tsx +183 -1
  128. package/src/components/tools/shared/pointer-support-cap.test.ts +98 -0
  129. package/src/components/tools/shared/pointer-support-cap.ts +273 -0
  130. package/src/components/tools/shared/polygon-editor.tsx +276 -35
  131. package/src/components/tools/site/site-boundary-editor.tsx +101 -42
  132. package/src/components/tools/site/stroke-pointer-ownership.test.ts +91 -0
  133. package/src/components/tools/site/stroke-pointer-ownership.ts +57 -0
  134. package/src/components/tools/site/terrain-brush-cursor.tsx +247 -0
  135. package/src/components/tools/site/terrain-sculpt-grid.tsx +192 -0
  136. package/src/components/tools/site/terrain-sculpt-tool.tsx +356 -0
  137. package/src/components/tools/stair/stair-click-guard.test.ts +58 -0
  138. package/src/components/tools/stair/stair-click-guard.ts +72 -0
  139. package/src/components/tools/stair/stair-defaults.ts +1 -0
  140. package/src/components/tools/stair/stair-tool.tsx +164 -42
  141. package/src/components/tools/tool-manager.tsx +149 -30
  142. package/src/components/tools/wall/wall-drafting.test.ts +447 -0
  143. package/src/components/tools/wall/wall-drafting.ts +304 -108
  144. package/src/components/tools/wall/wall-snap-geometry.test.ts +52 -0
  145. package/src/components/tools/wall/wall-snap-geometry.ts +137 -9
  146. package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
  147. package/src/components/tools/zone/zone-tool.tsx +98 -88
  148. package/src/components/ui/action-menu/camera-actions.tsx +24 -17
  149. package/src/components/ui/action-menu/control-modes.tsx +44 -94
  150. package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
  151. package/src/components/ui/action-menu/index.tsx +2 -4
  152. package/src/components/ui/action-menu/measurement-control.tsx +267 -0
  153. package/src/components/ui/action-menu/structure-tools.tsx +22 -13
  154. package/src/components/ui/action-menu/view-toggles.tsx +42 -90
  155. package/src/components/ui/command-palette/editor-commands.tsx +16 -3
  156. package/src/components/ui/command-palette/index.tsx +4 -3
  157. package/src/components/ui/controls/material-paint-panel.tsx +93 -18
  158. package/src/components/ui/controls/material-picker.tsx +162 -152
  159. package/src/components/ui/controls/material-properties-editor.tsx +108 -0
  160. package/src/components/ui/controls/metric-control.tsx +118 -44
  161. package/src/components/ui/controls/scene-material-list.tsx +247 -0
  162. package/src/components/ui/controls/slider-control.tsx +88 -38
  163. package/src/components/ui/controls/terrain-sculpt-panel.tsx +193 -0
  164. package/src/components/ui/floating-level-selector.tsx +74 -20
  165. package/src/components/ui/helpers/building-helper.tsx +10 -23
  166. package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
  167. package/src/components/ui/helpers/helper-manager.tsx +273 -14
  168. package/src/components/ui/helpers/item-helper.tsx +28 -32
  169. package/src/components/ui/helpers/registered-tool-helper.tsx +45 -11
  170. package/src/components/ui/helpers/roof-helper.tsx +10 -12
  171. package/src/components/ui/icon-ref.tsx +47 -0
  172. package/src/components/ui/item-catalog/catalog-items.tsx +39 -1
  173. package/src/components/ui/item-catalog/item-catalog.tsx +13 -36
  174. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  175. package/src/components/ui/panels/multi-selection-panel.tsx +57 -0
  176. package/src/components/ui/panels/node-display.ts +17 -17
  177. package/src/components/ui/panels/panel-manager.tsx +36 -14
  178. package/src/components/ui/panels/panel-wrapper.tsx +24 -3
  179. package/src/components/ui/panels/parametric-inspector.tsx +19 -4
  180. package/src/components/ui/panels/reference-panel.tsx +54 -19
  181. package/src/components/ui/panels/selection-breakdown.test.ts +24 -0
  182. package/src/components/ui/panels/selection-breakdown.ts +20 -0
  183. package/src/components/ui/primitives/shortcut-token.tsx +39 -3
  184. package/src/components/ui/primitives/sidebar.tsx +1 -0
  185. package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
  186. package/src/components/ui/sidebar/icon-rail.tsx +50 -31
  187. package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
  188. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +82 -41
  189. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +76 -6
  190. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +12 -4
  191. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +2 -1
  192. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -3
  193. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +10 -7
  194. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
  195. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
  196. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +10 -7
  197. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -1
  198. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +1 -1
  199. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +10 -7
  200. package/src/components/ui/sidebar/panels/site-panel/index.tsx +42 -17
  201. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
  202. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
  203. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +72 -25
  204. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +7 -3
  205. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +12 -8
  206. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +4 -3
  207. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +10 -7
  208. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
  209. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
  210. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +44 -3
  211. package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
  212. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
  213. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
  214. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
  215. package/src/components/ui/sidebar/tab-bar.tsx +42 -28
  216. package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
  217. package/src/components/ui/snap-target-badge.test.tsx +40 -0
  218. package/src/components/ui/snap-target-badge.tsx +88 -0
  219. package/src/components/viewer/viewer-controls-bar.tsx +466 -0
  220. package/src/components/viewer/viewer-scene-header.tsx +235 -0
  221. package/src/components/viewer-overlay.tsx +36 -596
  222. package/src/components/viewer-zone-system.tsx +6 -1
  223. package/src/components/walkthrough-hud.tsx +111 -0
  224. package/src/hooks/use-auto-save.test.ts +14 -0
  225. package/src/hooks/use-auto-save.ts +54 -7
  226. package/src/hooks/use-ceiling-events.ts +3 -2
  227. package/src/hooks/use-drag-action.ts +4 -1
  228. package/src/hooks/use-grid-events.ts +24 -0
  229. package/src/hooks/use-keyboard.ts +438 -70
  230. package/src/index.tsx +355 -9
  231. package/src/lib/active-placement-surface.test.ts +24 -0
  232. package/src/lib/active-placement-surface.ts +42 -0
  233. package/src/lib/camera-pose.test.ts +237 -0
  234. package/src/lib/camera-pose.ts +172 -0
  235. package/src/lib/ceiling-plan-snap.ts +24 -0
  236. package/src/lib/contextual-help.test.ts +112 -0
  237. package/src/lib/contextual-help.ts +144 -0
  238. package/src/lib/continuation.ts +64 -0
  239. package/src/lib/direct-manipulation.test.ts +275 -0
  240. package/src/lib/direct-manipulation.ts +118 -0
  241. package/src/lib/door-interaction.ts +1 -1
  242. package/src/lib/editor-api.ts +23 -35
  243. package/src/lib/elevation-guides.test.ts +107 -0
  244. package/src/lib/elevation-guides.ts +236 -0
  245. package/src/lib/floorplan/annotation-visibility.test.ts +327 -0
  246. package/src/lib/floorplan/annotation-visibility.ts +133 -0
  247. package/src/lib/floorplan/apply-alignment.test.ts +25 -0
  248. package/src/lib/floorplan/apply-alignment.ts +18 -12
  249. package/src/lib/floorplan/drawing-coordination.test.ts +46 -0
  250. package/src/lib/floorplan/drawing-coordination.ts +13 -0
  251. package/src/lib/floorplan/floorplan-export.test.ts +319 -0
  252. package/src/lib/floorplan/floorplan-export.tsx +978 -0
  253. package/src/lib/floorplan/floorplan-extension.test.ts +43 -0
  254. package/src/lib/floorplan/floorplan-extension.ts +181 -0
  255. package/src/lib/floorplan/floorplan-mode.test.ts +120 -0
  256. package/src/lib/floorplan/floorplan-mode.ts +73 -0
  257. package/src/lib/floorplan/floorplan-pdfkit-document.ts +194 -0
  258. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +107 -0
  259. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +680 -0
  260. package/src/lib/floorplan/geometry.ts +52 -0
  261. package/src/lib/floorplan/index.ts +3 -0
  262. package/src/lib/floorplan/items.ts +5 -2
  263. package/src/lib/floorplan/plan-coords.ts +21 -0
  264. package/src/lib/fresh-planar-placement.test.ts +283 -3
  265. package/src/lib/fresh-planar-placement.ts +85 -1
  266. package/src/lib/glb-export.test.ts +556 -0
  267. package/src/lib/glb-export.ts +1071 -0
  268. package/src/lib/ground-surface.ts +95 -0
  269. package/src/lib/history.test.ts +96 -0
  270. package/src/lib/history.ts +31 -0
  271. package/src/lib/interaction/hot-set.test.ts +133 -0
  272. package/src/lib/interaction/hot-set.ts +67 -0
  273. package/src/lib/interaction/overlay-policy.test.ts +52 -0
  274. package/src/lib/interaction/overlay-policy.ts +59 -0
  275. package/src/lib/interaction/scope.ts +243 -0
  276. package/src/lib/level-duplication.test.ts +2 -1
  277. package/src/lib/level-selection.ts +2 -2
  278. package/src/lib/material-paint.ts +36 -49
  279. package/src/lib/measurement-kind.test.ts +30 -0
  280. package/src/lib/measurement-kind.ts +19 -0
  281. package/src/lib/measurement-label.test.ts +47 -0
  282. package/src/lib/measurement-label.ts +68 -0
  283. package/src/lib/measurement-parser.ts +116 -0
  284. package/src/lib/measurements.test.ts +188 -0
  285. package/src/lib/measurements.ts +210 -0
  286. package/src/lib/paint-scope.test.ts +454 -0
  287. package/src/lib/paint-scope.ts +461 -0
  288. package/src/lib/placement-drag-release.ts +23 -0
  289. package/src/lib/planar-cursor-placement.test.ts +57 -0
  290. package/src/lib/plugin-panels.test.ts +19 -0
  291. package/src/lib/plugin-panels.ts +91 -0
  292. package/src/lib/quick-action-feedback.ts +33 -0
  293. package/src/lib/quick-action-nodes.test.ts +94 -0
  294. package/src/lib/quick-action-nodes.ts +47 -0
  295. package/src/lib/quick-measurement.test.ts +77 -0
  296. package/src/lib/quick-measurement.ts +109 -0
  297. package/src/lib/roof-duplication.ts +6 -6
  298. package/src/lib/roof-hover-outline-proxy.ts +22 -0
  299. package/src/lib/roof-wall-hit.ts +164 -0
  300. package/src/lib/scene-clipboard.test.ts +325 -0
  301. package/src/lib/scene-clipboard.ts +310 -21
  302. package/src/lib/scene.ts +28 -5
  303. package/src/lib/selection-routing.test.ts +313 -0
  304. package/src/lib/selection-routing.ts +180 -0
  305. package/src/lib/sfx/index.ts +1 -0
  306. package/src/lib/sfx/movement-tick.test.ts +65 -0
  307. package/src/lib/sfx/movement-tick.ts +18 -0
  308. package/src/lib/sfx-bus.ts +82 -14
  309. package/src/lib/sfx-player.test.ts +266 -0
  310. package/src/lib/sfx-player.ts +262 -69
  311. package/src/lib/site-boundary.test.ts +53 -0
  312. package/src/lib/site-boundary.ts +27 -0
  313. package/src/lib/slab-plan-snap.test.ts +93 -0
  314. package/src/lib/slab-plan-snap.ts +108 -0
  315. package/src/lib/snapping-mode.test.ts +165 -0
  316. package/src/lib/snapping-mode.ts +191 -0
  317. package/src/lib/stair-duplication.test.ts +103 -0
  318. package/src/lib/stair-duplication.ts +58 -86
  319. package/src/lib/stair-levels.ts +2 -0
  320. package/src/lib/surface-plan-snap.test.ts +71 -0
  321. package/src/lib/surface-plan-snap.ts +256 -0
  322. package/src/lib/terrain-sculpt.test.ts +429 -0
  323. package/src/lib/terrain-sculpt.ts +301 -0
  324. package/src/lib/terrain-verb-color.test.ts +51 -0
  325. package/src/lib/terrain-verb-color.ts +58 -0
  326. package/src/lib/touch-gesture-priority.test.ts +45 -0
  327. package/src/lib/touch-gesture-priority.ts +30 -0
  328. package/src/lib/use-linear-display.ts +40 -0
  329. package/src/lib/window-interaction.ts +1 -1
  330. package/src/lib/world-grid-snap.ts +37 -5
  331. package/src/store/camera-pose-store.test.ts +29 -0
  332. package/src/store/camera-pose-store.ts +27 -0
  333. package/src/store/live-draft-preview-stores.test.ts +136 -0
  334. package/src/store/navigation-sync-pose-store.test.ts +34 -0
  335. package/src/store/navigation-sync-pose-store.ts +27 -0
  336. package/src/store/terrain-sculpt-mode.test.ts +223 -0
  337. package/src/store/use-delete-confirmation.ts +27 -0
  338. package/src/store/use-direct-manipulation-feedback.ts +20 -0
  339. package/src/store/use-drawing-view.test.ts +23 -0
  340. package/src/store/use-drawing-view.ts +84 -0
  341. package/src/store/use-editor.tsx +665 -126
  342. package/src/store/use-elevation-guides.ts +40 -0
  343. package/src/store/use-facing-pose.ts +44 -0
  344. package/src/store/use-fence-curve-draft.ts +51 -0
  345. package/src/store/use-first-person-hud.ts +40 -0
  346. package/src/store/use-floorplan-annotation-visibility.ts +60 -0
  347. package/src/store/use-floorplan-draft-preview.ts +151 -0
  348. package/src/store/use-floorplan-marquee.ts +50 -0
  349. package/src/store/use-floorplan-mode.ts +110 -0
  350. package/src/store/use-interaction-scope.test.ts +206 -0
  351. package/src/store/use-interaction-scope.ts +140 -0
  352. package/src/store/use-measurement-draft.test.ts +559 -0
  353. package/src/store/use-measurement-draft.ts +570 -0
  354. package/src/store/use-opening-guides.ts +41 -0
  355. package/src/store/use-path-draft-preview.ts +90 -0
  356. package/src/store/use-placement-preview.ts +11 -3
  357. package/src/store/use-quick-measurement-hud.test.ts +53 -0
  358. package/src/store/use-quick-measurement-hud.ts +77 -0
  359. package/src/store/use-segment-draft-chain.ts +28 -0
  360. package/src/store/use-stair-build-preview.ts +53 -0
  361. package/src/store/use-wall-snap-indicator.ts +2 -0
  362. package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
  363. package/src/components/editor/group-move-handle.tsx +0 -316
  364. package/src/components/ui/panels/paint-panel.tsx +0 -163
  365. package/src/lib/floorplan/selection-tool.ts +0 -271
  366. package/src/lib/level-name.ts +0 -11
@@ -0,0 +1,680 @@
1
+ import { type FloorplanGeometry, type FloorplanPoint, loadAssetUrl } from '@pascal-app/core'
2
+ import {
3
+ type ArchitecturalDimensionLayout,
4
+ computeArchitecturalDimensionLayout,
5
+ } from '../../components/editor-2d/renderers/floorplan-dimension-renderer'
6
+ import {
7
+ documentCircleGeometryAttrs,
8
+ documentRectGeometryAttrs,
9
+ resolveDocumentAnnotationGroupChildren,
10
+ } from '../../components/editor-2d/renderers/floorplan-geometry-renderer'
11
+ import { resolveFloorplanLabelAngle } from '../../components/editor-2d/renderers/floorplan-label-angle'
12
+ import type { FloorplanExportBounds } from './floorplan-export'
13
+ import { readFloorplanGeometryMetadata } from './floorplan-extension'
14
+ import type { FloorplanPdfDocument } from './floorplan-pdfkit-document'
15
+
16
+ const DIMENSION_LINE_WIDTH_PT = 0.5
17
+ const DIMENSION_TICK_WIDTH_PT = 0.75
18
+ const DIMENSION_TEXT_FONT_FAMILY = 'Courier'
19
+ const DIMENSION_TEXT_FONT_SIZE_PT = 8
20
+ const DIMENSION_TEXT_FONT_WEIGHT = 400
21
+ const DIMENSION_BASELINE_OFFSET_PT = 5
22
+ const DEFAULT_ANNOTATION_FONT_SIZE_PT = 8
23
+ const ROOM_NUMBER_FONT_SIZE_PT = 7
24
+ const ROOM_DETAIL_FONT_SIZE_PT = 5.5
25
+ const MARK_FONT_SIZE_PT = 7
26
+
27
+ type DimensionGeometry = Extract<FloorplanGeometry, { kind: 'dimension' }>
28
+ type DimensionStringGeometry = Extract<FloorplanGeometry, { kind: 'dimension-string' }>
29
+ type StyledGeometry = Extract<
30
+ FloorplanGeometry,
31
+ { kind: 'path' | 'polygon' | 'polyline' | 'rect' | 'circle' | 'line' }
32
+ >
33
+
34
+ type RenderContext = {
35
+ annotationLabelShiftIndex: number
36
+ annotationLabelShifts: readonly FloorplanPoint[]
37
+ annotationLayer: boolean
38
+ sceneRotationDeg: number
39
+ unitsPerPoint: number
40
+ }
41
+
42
+ export type FloorplanPdfKitPlacement = {
43
+ x: number
44
+ y: number
45
+ width: number
46
+ height: number
47
+ }
48
+
49
+ export async function renderFloorplanGeometryToPdfKit(
50
+ doc: FloorplanPdfDocument,
51
+ geometry: FloorplanGeometry,
52
+ options: {
53
+ annotationLabelShifts?: readonly FloorplanPoint[]
54
+ annotationLayer: boolean
55
+ placement: FloorplanPdfKitPlacement
56
+ rotationDeg: number
57
+ viewport: FloorplanExportBounds
58
+ },
59
+ ): Promise<void> {
60
+ const pointsPerUnit = options.placement.width / options.viewport.width
61
+ if (!Number.isFinite(pointsPerUnit) || pointsPerUnit <= 0) return
62
+
63
+ const raw = doc.raw
64
+ raw.save()
65
+ raw.translate(options.placement.x, options.placement.y)
66
+ raw.scale(pointsPerUnit)
67
+ raw.translate(-options.viewport.x, -options.viewport.y)
68
+ raw.rotate(options.rotationDeg, { origin: [0, 0] })
69
+ await renderGeometry(doc, geometry, {
70
+ annotationLabelShiftIndex: 0,
71
+ annotationLabelShifts: options.annotationLabelShifts ?? [],
72
+ annotationLayer: options.annotationLayer,
73
+ sceneRotationDeg: options.rotationDeg,
74
+ unitsPerPoint: 1 / pointsPerUnit,
75
+ })
76
+ raw.restore()
77
+ }
78
+
79
+ async function renderGeometry(
80
+ doc: FloorplanPdfDocument,
81
+ geometry: FloorplanGeometry,
82
+ context: RenderContext,
83
+ ): Promise<void> {
84
+ const raw = doc.raw
85
+ switch (geometry.kind) {
86
+ case 'path':
87
+ raw.save().path(geometry.d)
88
+ paintStyledGeometry(raw, geometry, context)
89
+ raw.restore()
90
+ return
91
+ case 'polygon':
92
+ if (geometry.points.length < 2) return
93
+ raw.save().polygon(...geometry.points.map(([x, y]) => [x, y] as [number, number]))
94
+ paintStyledGeometry(raw, geometry, context)
95
+ raw.restore()
96
+ return
97
+ case 'polyline':
98
+ if (geometry.points.length < 2) return
99
+ raw.save().moveTo(geometry.points[0]![0], geometry.points[0]![1])
100
+ for (const [x, y] of geometry.points.slice(1)) raw.lineTo(x, y)
101
+ paintStyledGeometry(raw, geometry, context)
102
+ raw.restore()
103
+ return
104
+ case 'rect':
105
+ raw.save()
106
+ {
107
+ const attrs = documentRectGeometryAttrs(
108
+ geometry,
109
+ context.annotationLayer ? context.unitsPerPoint : undefined,
110
+ )
111
+ if ((attrs.rx ?? 0) > 0 || (attrs.ry ?? 0) > 0) {
112
+ raw.roundedRect(
113
+ attrs.x,
114
+ attrs.y,
115
+ attrs.width,
116
+ attrs.height,
117
+ Math.max(attrs.rx ?? 0, attrs.ry ?? 0),
118
+ )
119
+ } else {
120
+ raw.rect(attrs.x, attrs.y, attrs.width, attrs.height)
121
+ }
122
+ }
123
+ paintStyledGeometry(raw, geometry, context)
124
+ raw.restore()
125
+ return
126
+ case 'circle':
127
+ raw
128
+ .save()
129
+ .circle(
130
+ geometry.cx,
131
+ geometry.cy,
132
+ documentCircleGeometryAttrs(
133
+ geometry,
134
+ context.annotationLayer ? context.unitsPerPoint : undefined,
135
+ ).r,
136
+ )
137
+ paintStyledGeometry(raw, geometry, context)
138
+ raw.restore()
139
+ return
140
+ case 'line':
141
+ raw.save().moveTo(geometry.x1, geometry.y1).lineTo(geometry.x2, geometry.y2)
142
+ paintStyledGeometry(raw, geometry, context)
143
+ raw.restore()
144
+ return
145
+ case 'text':
146
+ drawGeometryText(doc, geometry, context)
147
+ return
148
+ case 'dimension':
149
+ drawDimension(doc, geometry, context)
150
+ return
151
+ case 'dimension-string':
152
+ drawDimensionString(doc, geometry, context)
153
+ return
154
+ case 'dimension-label':
155
+ drawDimensionLabel(doc, geometry, context)
156
+ return
157
+ case 'equal-spacing-badge':
158
+ drawEqualSpacingBadge(doc, geometry, context)
159
+ return
160
+ case 'image':
161
+ await drawImage(doc, geometry)
162
+ return
163
+ case 'group':
164
+ raw.save()
165
+ if (geometry.transform?.translate) {
166
+ raw.translate(geometry.transform.translate[0], geometry.transform.translate[1])
167
+ }
168
+ if (geometry.transform?.rotate !== undefined) {
169
+ raw.rotate((geometry.transform.rotate * 180) / Math.PI, { origin: [0, 0] })
170
+ }
171
+ for (const child of resolveDocumentAnnotationGroupChildren(
172
+ geometry.children,
173
+ context.annotationLayer ? context.unitsPerPoint : undefined,
174
+ )) {
175
+ await renderGeometry(doc, child, context)
176
+ }
177
+ raw.restore()
178
+ return
179
+ default:
180
+ return
181
+ }
182
+ }
183
+
184
+ function paintStyledGeometry(
185
+ raw: FloorplanPdfDocument['raw'],
186
+ geometry: StyledGeometry,
187
+ context: RenderContext,
188
+ ): void {
189
+ const fill = geometry.fill && geometry.fill !== 'none' ? geometry.fill : null
190
+ const stroke = geometry.stroke && geometry.stroke !== 'none' ? geometry.stroke : null
191
+ const opacity = geometry.opacity ?? 1
192
+ const fillOpacity = (geometry.fillOpacity ?? 1) * opacity
193
+ const strokeOpacity = (geometry.strokeOpacity ?? 1) * opacity
194
+
195
+ if (fill) raw.fillColor(fill).fillOpacity(fillOpacity)
196
+ if (stroke) {
197
+ raw.strokeColor(stroke).strokeOpacity(strokeOpacity)
198
+ raw.lineWidth(resolveStrokeWidth(geometry, context))
199
+ if (geometry.strokeLinecap) raw.lineCap(geometry.strokeLinecap)
200
+ if (geometry.strokeLinejoin) raw.lineJoin(geometry.strokeLinejoin)
201
+ applyDash(
202
+ raw,
203
+ geometry.strokeDasharray,
204
+ geometry.vectorEffect === 'non-scaling-stroke',
205
+ context,
206
+ )
207
+ }
208
+
209
+ if (fill && stroke) raw.fillAndStroke(fill, stroke)
210
+ else if (fill) raw.fill(fill)
211
+ else if (stroke) raw.stroke(stroke)
212
+ }
213
+
214
+ function resolveStrokeWidth(geometry: StyledGeometry, context: RenderContext): number {
215
+ if (context.annotationLayer) return DIMENSION_LINE_WIDTH_PT * context.unitsPerPoint
216
+ if (geometry.vectorEffect === 'non-scaling-stroke') {
217
+ return (geometry.strokeWidth ?? 1) * context.unitsPerPoint
218
+ }
219
+ return geometry.strokeWidth ?? DIMENSION_LINE_WIDTH_PT * context.unitsPerPoint
220
+ }
221
+
222
+ function applyDash(
223
+ raw: FloorplanPdfDocument['raw'],
224
+ dasharray: string | undefined,
225
+ nonScaling: boolean,
226
+ context: RenderContext,
227
+ ): void {
228
+ if (!dasharray) {
229
+ raw.undash()
230
+ return
231
+ }
232
+ const values = dasharray
233
+ .split(/[\s,]+/)
234
+ .map(Number)
235
+ .filter((value) => Number.isFinite(value) && value > 0)
236
+ if (values.length === 0) return
237
+ const scale = nonScaling ? context.unitsPerPoint : 1
238
+ raw.dash(values[0]! * scale, { space: (values[1] ?? values[0]!) * scale })
239
+ }
240
+
241
+ function drawGeometryText(
242
+ doc: FloorplanPdfDocument,
243
+ geometry: Extract<FloorplanGeometry, { kind: 'text' }>,
244
+ context: RenderContext,
245
+ ): void {
246
+ const dimensionValue =
247
+ readFloorplanGeometryMetadata(geometry).annotationRole === 'automatic-dimension'
248
+ const fontSize = context.annotationLayer
249
+ ? (dimensionValue ? DIMENSION_TEXT_FONT_SIZE_PT : annotationTextSizePt(geometry)) *
250
+ context.unitsPerPoint
251
+ : geometry.fontSize
252
+ const outlinedForScreen = geometry.paintOrder === 'stroke' && !!geometry.stroke
253
+ const fill =
254
+ outlinedForScreen && geometry.fill?.toLocaleLowerCase() === '#ffffff'
255
+ ? (geometry.stroke ?? '#111827')
256
+ : (geometry.fill ?? '#171717')
257
+ drawNativeText(doc, {
258
+ angleDeg: geometry.upright ? -context.sceneRotationDeg : 0,
259
+ anchor: geometry.textAnchor ?? 'start',
260
+ fill,
261
+ fontFamily: dimensionValue ? DIMENSION_TEXT_FONT_FAMILY : geometry.fontFamily,
262
+ fontSize,
263
+ fontWeight: dimensionValue ? DIMENSION_TEXT_FONT_WEIGHT : geometry.fontWeight,
264
+ opacity: geometry.opacity,
265
+ text: geometry.text,
266
+ x: geometry.x,
267
+ y: geometry.y,
268
+ })
269
+ }
270
+
271
+ function annotationTextSizePt(geometry: Extract<FloorplanGeometry, { kind: 'text' }>): number {
272
+ switch (readFloorplanGeometryMetadata(geometry).annotationRole) {
273
+ case 'room-label':
274
+ if (geometry.fontSize >= 0.18) return DEFAULT_ANNOTATION_FONT_SIZE_PT
275
+ if (geometry.fontSize >= 0.145) return ROOM_NUMBER_FONT_SIZE_PT
276
+ return ROOM_DETAIL_FONT_SIZE_PT
277
+ case 'column-center':
278
+ case 'stair-annotation':
279
+ return MARK_FONT_SIZE_PT
280
+ default:
281
+ return DEFAULT_ANNOTATION_FONT_SIZE_PT
282
+ }
283
+ }
284
+
285
+ function drawDimension(
286
+ doc: FloorplanPdfDocument,
287
+ geometry: DimensionGeometry,
288
+ context: RenderContext,
289
+ ): void {
290
+ const layout = computeArchitecturalDimensionLayout(
291
+ geometry,
292
+ context.sceneRotationDeg,
293
+ context.unitsPerPoint,
294
+ )
295
+ if (!layout) return
296
+ drawDimensionLayout(doc, geometry, layout, context)
297
+ }
298
+
299
+ function drawDimensionString(
300
+ doc: FloorplanPdfDocument,
301
+ geometry: DimensionStringGeometry,
302
+ context: RenderContext,
303
+ ): void {
304
+ const entries = geometry.segments.flatMap((segment) => {
305
+ const dimension: DimensionGeometry = {
306
+ kind: 'dimension',
307
+ start: segment.start,
308
+ end: segment.end,
309
+ dimensionStart: segment.dimensionStart,
310
+ dimensionEnd: segment.dimensionEnd,
311
+ offsetNormal: geometry.offsetNormal,
312
+ offsetDistance: geometry.offsetDistance,
313
+ extensionOvershoot: geometry.extensionOvershoot,
314
+ extensionStartGap: geometry.extensionStartGap,
315
+ terminator: geometry.terminator,
316
+ textPosition: geometry.textPosition,
317
+ text: segment.text,
318
+ stroke: geometry.stroke,
319
+ }
320
+ const layout = computeArchitecturalDimensionLayout(
321
+ dimension,
322
+ context.sceneRotationDeg,
323
+ context.unitsPerPoint,
324
+ )
325
+ return layout ? [{ dimension, layout }] : []
326
+ })
327
+ if (entries.length === 0) return
328
+
329
+ const extensionLines = new Map<string, readonly [FloorplanPoint, FloorplanPoint]>()
330
+ const terminators = new Map<
331
+ string,
332
+ { point: FloorplanPoint; toward: FloorplanPoint; layout: ArchitecturalDimensionLayout }
333
+ >()
334
+ for (const { layout } of entries) {
335
+ extensionLines.set(pointKey(layout.dimensionStart), [
336
+ layout.extensionStart,
337
+ layout.extensionStartTip,
338
+ ])
339
+ extensionLines.set(pointKey(layout.dimensionEnd), [layout.extensionEnd, layout.extensionEndTip])
340
+ terminators.set(pointKey(layout.dimensionStart), {
341
+ point: layout.dimensionStart,
342
+ toward: layout.dimensionEnd,
343
+ layout,
344
+ })
345
+ terminators.set(pointKey(layout.dimensionEnd), {
346
+ point: layout.dimensionEnd,
347
+ toward: layout.dimensionStart,
348
+ layout,
349
+ })
350
+ }
351
+
352
+ const stroke = geometry.stroke ?? '#334155'
353
+ for (const [start, end] of extensionLines.values())
354
+ drawDimensionLine(doc, start, end, stroke, context)
355
+ for (const { layout } of entries) {
356
+ drawDimensionLine(doc, layout.dimensionLineStart, layout.dimensionLineEnd, stroke, context)
357
+ }
358
+ for (const terminator of terminators.values()) {
359
+ drawDimensionTerminator(
360
+ doc,
361
+ geometry.terminator ?? 'architectural-tick',
362
+ terminator.point,
363
+ terminator.toward,
364
+ terminator.layout,
365
+ stroke,
366
+ context,
367
+ )
368
+ }
369
+ for (const { dimension, layout } of entries)
370
+ drawDimensionText(doc, dimension, layout, stroke, context)
371
+ }
372
+
373
+ function drawDimensionLayout(
374
+ doc: FloorplanPdfDocument,
375
+ geometry: DimensionGeometry,
376
+ layout: ArchitecturalDimensionLayout,
377
+ context: RenderContext,
378
+ ): void {
379
+ const stroke = geometry.stroke ?? '#334155'
380
+ drawDimensionLine(doc, layout.extensionStart, layout.extensionStartTip, stroke, context)
381
+ drawDimensionLine(doc, layout.extensionEnd, layout.extensionEndTip, stroke, context)
382
+ drawDimensionLine(doc, layout.dimensionLineStart, layout.dimensionLineEnd, stroke, context)
383
+ drawDimensionTerminator(
384
+ doc,
385
+ geometry.terminator ?? 'architectural-tick',
386
+ layout.dimensionStart,
387
+ layout.dimensionEnd,
388
+ layout,
389
+ stroke,
390
+ context,
391
+ )
392
+ drawDimensionTerminator(
393
+ doc,
394
+ geometry.terminator ?? 'architectural-tick',
395
+ layout.dimensionEnd,
396
+ layout.dimensionStart,
397
+ layout,
398
+ stroke,
399
+ context,
400
+ )
401
+ drawDimensionText(doc, geometry, layout, stroke, context)
402
+ }
403
+
404
+ function drawDimensionLine(
405
+ doc: FloorplanPdfDocument,
406
+ start: FloorplanPoint,
407
+ end: FloorplanPoint,
408
+ stroke: string,
409
+ context: RenderContext,
410
+ widthPt = DIMENSION_LINE_WIDTH_PT,
411
+ ): void {
412
+ doc.raw
413
+ .save()
414
+ .strokeColor(stroke)
415
+ .strokeOpacity(1)
416
+ .lineCap('butt')
417
+ .lineWidth(widthPt * context.unitsPerPoint)
418
+ .moveTo(start[0], start[1])
419
+ .lineTo(end[0], end[1])
420
+ .stroke()
421
+ .restore()
422
+ }
423
+
424
+ function drawDimensionTerminator(
425
+ doc: FloorplanPdfDocument,
426
+ terminator: NonNullable<DimensionGeometry['terminator']>,
427
+ point: FloorplanPoint,
428
+ toward: FloorplanPoint,
429
+ layout: ArchitecturalDimensionLayout,
430
+ stroke: string,
431
+ context: RenderContext,
432
+ ): void {
433
+ const direction = normalized(point, toward)
434
+ if (!direction) return
435
+ const tickHalfLength = Math.hypot(layout.tickHalfVector[0], layout.tickHalfVector[1])
436
+ if (terminator === 'dot') {
437
+ doc.raw
438
+ .save()
439
+ .fillColor(stroke)
440
+ .circle(point[0], point[1], tickHalfLength * 0.45)
441
+ .fill()
442
+ .restore()
443
+ return
444
+ }
445
+ if (terminator === 'filled-arrow' || terminator === 'open-arrow') {
446
+ const base = addScaled(point, direction, tickHalfLength * 1.7)
447
+ const normal: FloorplanPoint = [-direction[1], direction[0]]
448
+ const wing = tickHalfLength * 0.65
449
+ const left: FloorplanPoint = [base[0] + normal[0] * wing, base[1] + normal[1] * wing]
450
+ const right: FloorplanPoint = [base[0] - normal[0] * wing, base[1] - normal[1] * wing]
451
+ if (terminator === 'filled-arrow') {
452
+ doc.raw
453
+ .save()
454
+ .fillColor(stroke)
455
+ .polygon([point[0], point[1]], [left[0], left[1]], [right[0], right[1]])
456
+ .fill()
457
+ .restore()
458
+ return
459
+ }
460
+ drawDimensionLine(doc, point, left, stroke, context, DIMENSION_TICK_WIDTH_PT)
461
+ drawDimensionLine(doc, point, right, stroke, context, DIMENSION_TICK_WIDTH_PT)
462
+ return
463
+ }
464
+ const [tickX, tickY] = layout.tickHalfVector
465
+ drawDimensionLine(
466
+ doc,
467
+ [point[0] - tickX, point[1] - tickY],
468
+ [point[0] + tickX, point[1] + tickY],
469
+ stroke,
470
+ context,
471
+ DIMENSION_TICK_WIDTH_PT,
472
+ )
473
+ }
474
+
475
+ function drawDimensionText(
476
+ doc: FloorplanPdfDocument,
477
+ geometry: DimensionGeometry,
478
+ layout: ArchitecturalDimensionLayout,
479
+ stroke: string,
480
+ context: RenderContext,
481
+ ): void {
482
+ const fontSize = DIMENSION_TEXT_FONT_SIZE_PT * context.unitsPerPoint
483
+ const y =
484
+ geometry.textPosition === 'centered'
485
+ ? fontSize * 0.35
486
+ : -DIMENSION_BASELINE_OFFSET_PT * context.unitsPerPoint
487
+
488
+ const raw = doc.raw
489
+ const shift = nextAnnotationLabelShift(context)
490
+ raw
491
+ .save()
492
+ .translate(layout.labelPoint[0], layout.labelPoint[1])
493
+ .rotate(layout.labelAngleDeg)
494
+ .translate(shift[0], shift[1])
495
+ drawNativeText(doc, {
496
+ anchor: 'middle',
497
+ fill: stroke,
498
+ fontFamily: DIMENSION_TEXT_FONT_FAMILY,
499
+ fontSize,
500
+ fontWeight: DIMENSION_TEXT_FONT_WEIGHT,
501
+ text: geometry.text,
502
+ x: 0,
503
+ y,
504
+ })
505
+ raw.restore()
506
+ }
507
+
508
+ function drawDimensionLabel(
509
+ doc: FloorplanPdfDocument,
510
+ geometry: Extract<FloorplanGeometry, { kind: 'dimension-label' }>,
511
+ context: RenderContext,
512
+ ): void {
513
+ const unitsPerPoint = context.unitsPerPoint
514
+ const fontSize = DIMENSION_TEXT_FONT_SIZE_PT * unitsPerPoint
515
+ const padX = 6 * unitsPerPoint
516
+ const padY = 3 * unitsPerPoint
517
+ const textWidth = geometry.text.length * 6.2 * unitsPerPoint
518
+ const plateWidth = textWidth + padX * 2
519
+ const plateHeight = fontSize + padY * 2
520
+ const angle = resolveFloorplanLabelAngle(
521
+ geometry.angle,
522
+ context.sceneRotationDeg,
523
+ geometry.screenUpright,
524
+ )
525
+ const offset = -(geometry.offsetPx ?? 0) * unitsPerPoint
526
+ const shift = nextAnnotationLabelShift(context)
527
+ const raw = doc.raw
528
+ raw
529
+ .save()
530
+ .translate(geometry.cx, geometry.cy)
531
+ .rotate(angle)
532
+ .translate(shift[0], shift[1] + offset)
533
+ raw
534
+ .fillColor('#ffffff')
535
+ .fillOpacity(0.92)
536
+ .roundedRect(-plateWidth / 2, -plateHeight / 2, plateWidth, plateHeight, 3 * unitsPerPoint)
537
+ .fill()
538
+ drawNativeText(doc, {
539
+ anchor: 'middle',
540
+ fill: '#111827',
541
+ fontFamily: DIMENSION_TEXT_FONT_FAMILY,
542
+ fontSize,
543
+ fontWeight: DIMENSION_TEXT_FONT_WEIGHT,
544
+ text: geometry.text,
545
+ x: 0,
546
+ y: 0,
547
+ })
548
+ raw.restore()
549
+ }
550
+
551
+ function drawEqualSpacingBadge(
552
+ doc: FloorplanPdfDocument,
553
+ geometry: Extract<FloorplanGeometry, { kind: 'equal-spacing-badge' }>,
554
+ context: RenderContext,
555
+ ): void {
556
+ const fontSize = 7 * context.unitsPerPoint
557
+ const width = Math.max(
558
+ 14 * context.unitsPerPoint,
559
+ geometry.text.length * fontSize * 0.62 + 6 * context.unitsPerPoint,
560
+ )
561
+ const height = 12 * context.unitsPerPoint
562
+ const angle = resolveFloorplanLabelAngle(geometry.angle, context.sceneRotationDeg)
563
+ const raw = doc.raw
564
+ raw.save().translate(geometry.point[0], geometry.point[1]).rotate(angle)
565
+ raw
566
+ .fillColor('#ffffff')
567
+ .roundedRect(-width / 2, -height / 2, width, height, height / 2)
568
+ .fill()
569
+ drawNativeText(doc, {
570
+ anchor: 'middle',
571
+ fill: '#334155',
572
+ fontFamily: 'Courier',
573
+ fontSize,
574
+ fontWeight: 600,
575
+ text: geometry.text,
576
+ x: 0,
577
+ y: 0,
578
+ })
579
+ raw.restore()
580
+ }
581
+
582
+ function drawNativeText(
583
+ doc: FloorplanPdfDocument,
584
+ options: {
585
+ angleDeg?: number
586
+ anchor: 'start' | 'middle' | 'end'
587
+ fill: string
588
+ fontFamily?: string
589
+ fontSize: number
590
+ fontWeight?: number | string
591
+ opacity?: number
592
+ text: string
593
+ x: number
594
+ y: number
595
+ },
596
+ ): void {
597
+ const raw = doc.raw
598
+ const normalizedFamily = options.fontFamily?.toLocaleLowerCase() ?? ''
599
+ const family =
600
+ normalizedFamily.includes('mono') || normalizedFamily.includes('courier')
601
+ ? 'Courier'
602
+ : 'Helvetica'
603
+ const numericWeight = Number.parseInt(String(options.fontWeight ?? 400), 10)
604
+ const bold =
605
+ options.fontWeight === 'bold' || (Number.isFinite(numericWeight) && numericWeight >= 500)
606
+ raw.save().translate(options.x, options.y)
607
+ if (options.angleDeg) raw.rotate(options.angleDeg)
608
+ raw
609
+ .font(bold ? `${family}-Bold` : family)
610
+ .fontSize(options.fontSize)
611
+ .fillColor(options.fill)
612
+ raw.fillOpacity(options.opacity ?? 1)
613
+ const width = raw.widthOfString(options.text, { lineBreak: false })
614
+ const x = options.anchor === 'middle' ? -width / 2 : options.anchor === 'end' ? -width : 0
615
+ raw.text(options.text, x, -options.fontSize * 0.42, { lineBreak: false })
616
+ raw.restore()
617
+ }
618
+
619
+ async function drawImage(
620
+ doc: FloorplanPdfDocument,
621
+ geometry: Extract<FloorplanGeometry, { kind: 'image' }>,
622
+ ): Promise<void> {
623
+ try {
624
+ const url = await loadAssetUrl(geometry.url)
625
+ if (!url) return
626
+ const response = await fetch(url)
627
+ if (!response.ok) return
628
+ const dataUrl = await blobToDataUrl(await response.blob())
629
+ const raw = doc.raw
630
+ raw.save().translate(geometry.center[0], geometry.center[1])
631
+ if (geometry.rotation) raw.rotate((geometry.rotation * 180) / Math.PI)
632
+ raw.opacity(geometry.opacity ?? 1)
633
+ const options =
634
+ geometry.preserveAspectRatio === 'none'
635
+ ? { width: geometry.width, height: geometry.height }
636
+ : {
637
+ fit: [geometry.width, geometry.height] as [number, number],
638
+ align: 'center' as const,
639
+ valign: 'center' as const,
640
+ }
641
+ raw.image(dataUrl, -geometry.width / 2, -geometry.height / 2, options)
642
+ raw.restore()
643
+ } catch {
644
+ return
645
+ }
646
+ }
647
+
648
+ function blobToDataUrl(blob: Blob): Promise<string> {
649
+ return new Promise((resolve, reject) => {
650
+ const reader = new FileReader()
651
+ reader.onerror = () => reject(reader.error)
652
+ reader.onload = () => resolve(String(reader.result))
653
+ reader.readAsDataURL(blob)
654
+ })
655
+ }
656
+
657
+ function normalized(start: FloorplanPoint, end: FloorplanPoint): FloorplanPoint | null {
658
+ const dx = end[0] - start[0]
659
+ const dy = end[1] - start[1]
660
+ const magnitude = Math.hypot(dx, dy)
661
+ return magnitude <= 1e-6 ? null : [dx / magnitude, dy / magnitude]
662
+ }
663
+
664
+ function addScaled(
665
+ point: FloorplanPoint,
666
+ direction: FloorplanPoint,
667
+ distance: number,
668
+ ): FloorplanPoint {
669
+ return [point[0] + direction[0] * distance, point[1] + direction[1] * distance]
670
+ }
671
+
672
+ function pointKey(point: FloorplanPoint): string {
673
+ return `${point[0].toFixed(6)},${point[1].toFixed(6)}`
674
+ }
675
+
676
+ function nextAnnotationLabelShift(context: RenderContext): FloorplanPoint {
677
+ const shift = context.annotationLabelShifts[context.annotationLabelShiftIndex] ?? [0, 0]
678
+ context.annotationLabelShiftIndex += 1
679
+ return shift
680
+ }