@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,236 @@
1
+ import {
2
+ type AnyNode,
3
+ type AnyNodeId,
4
+ getWallBaseElevationForNodes,
5
+ getWallEffectiveHeightForNodes,
6
+ resolveCeilingHeight,
7
+ } from '@pascal-app/core'
8
+ import useElevationGuides from '../store/use-elevation-guides'
9
+
10
+ export const ELEVATION_ALIGNMENT_THRESHOLD_M = 0.08
11
+ const GUIDE_MATCH_EPSILON_M = 1e-4
12
+
13
+ export type ElevationGuideSource = {
14
+ nodeId: string
15
+ levelId: string | null | undefined
16
+ anchor: readonly [number, number]
17
+ }
18
+
19
+ export type ElevationSnapTarget = {
20
+ id: string
21
+ elevation: number
22
+ anchor: readonly [number, number]
23
+ label: string
24
+ }
25
+
26
+ export type ElevationSnapMatch = {
27
+ target: ElevationSnapTarget
28
+ elevation: number
29
+ }
30
+
31
+ function polygonCenter(polygon: ReadonlyArray<readonly [number, number]>): [number, number] {
32
+ if (polygon.length === 0) return [0, 0]
33
+ let x = 0
34
+ let z = 0
35
+ for (const point of polygon) {
36
+ x += point[0]
37
+ z += point[1]
38
+ }
39
+ return [x / polygon.length, z / polygon.length]
40
+ }
41
+
42
+ function segmentCenter(
43
+ start: readonly [number, number],
44
+ end: readonly [number, number],
45
+ ): [number, number] {
46
+ return [(start[0] + end[0]) / 2, (start[1] + end[1]) / 2]
47
+ }
48
+
49
+ function fenceBaseElevation(node: AnyNode, nodes: Record<string, AnyNode>): number {
50
+ if (node.type !== 'fence') return 0
51
+ const host = node.supportSlabId ? nodes[node.supportSlabId as AnyNodeId] : undefined
52
+ const hostElevation =
53
+ host?.type === 'slab' && (host.parentId ?? null) === (node.parentId ?? null)
54
+ ? (host.elevation ?? 0)
55
+ : 0
56
+ return hostElevation + (node.supportOffset ?? 0)
57
+ }
58
+
59
+ /**
60
+ * Structural Y datums on the source node's level. This is editor-runtime
61
+ * collection over authoritative vertical resolvers; matching remains a pure
62
+ * scalar operation in {@link resolveElevationSnapMatch}.
63
+ */
64
+ export function collectElevationSnapTargets(
65
+ source: ElevationGuideSource,
66
+ nodes: Record<string, AnyNode>,
67
+ ): ElevationSnapTarget[] {
68
+ if (!source.levelId) return []
69
+
70
+ const targets: ElevationSnapTarget[] = [
71
+ {
72
+ id: `${source.levelId}:level`,
73
+ elevation: 0,
74
+ anchor: source.anchor,
75
+ label: 'Level',
76
+ },
77
+ ]
78
+ const level = nodes[source.levelId as AnyNodeId]
79
+ if (level?.type !== 'level') return targets
80
+
81
+ for (const childId of level.children) {
82
+ if (childId === source.nodeId) continue
83
+ const node = nodes[childId as AnyNodeId]
84
+ if (!node) continue
85
+
86
+ if (node.type === 'slab') {
87
+ const center = polygonCenter(node.polygon)
88
+ const top = node.elevation ?? 0.05
89
+ targets.push({
90
+ id: `${node.id}:top`,
91
+ elevation: top,
92
+ anchor: center,
93
+ label: 'Slab top',
94
+ })
95
+ if (!node.recessed) {
96
+ targets.push({
97
+ id: `${node.id}:base`,
98
+ elevation: top - (node.thickness ?? 0.05),
99
+ anchor: center,
100
+ label: 'Slab underside',
101
+ })
102
+ }
103
+ continue
104
+ }
105
+
106
+ if (node.type === 'ceiling') {
107
+ targets.push({
108
+ id: `${node.id}:ceiling`,
109
+ elevation: resolveCeilingHeight(node, nodes as Record<AnyNodeId, AnyNode>),
110
+ anchor: polygonCenter(node.polygon),
111
+ label: 'Ceiling',
112
+ })
113
+ continue
114
+ }
115
+
116
+ if (node.type === 'wall') {
117
+ const base = getWallBaseElevationForNodes(node, nodes)
118
+ const center = segmentCenter(node.start, node.end)
119
+ targets.push({
120
+ id: `${node.id}:base`,
121
+ elevation: base,
122
+ anchor: center,
123
+ label: 'Wall base',
124
+ })
125
+ targets.push({
126
+ id: `${node.id}:top`,
127
+ elevation: base + getWallEffectiveHeightForNodes(node, nodes),
128
+ anchor: center,
129
+ label: 'Wall top',
130
+ })
131
+ continue
132
+ }
133
+
134
+ if (node.type === 'fence') {
135
+ const base = fenceBaseElevation(node, nodes)
136
+ const center = segmentCenter(node.start, node.end)
137
+ targets.push({
138
+ id: `${node.id}:base`,
139
+ elevation: base,
140
+ anchor: center,
141
+ label: 'Fence base',
142
+ })
143
+ targets.push({
144
+ id: `${node.id}:top`,
145
+ elevation: base + (node.height ?? 1.8),
146
+ anchor: center,
147
+ label: 'Fence top',
148
+ })
149
+ }
150
+ }
151
+
152
+ return targets
153
+ }
154
+
155
+ export function resolveElevationSnapMatch(
156
+ proposedElevation: number,
157
+ sourceAnchor: readonly [number, number],
158
+ targets: readonly ElevationSnapTarget[],
159
+ threshold = ELEVATION_ALIGNMENT_THRESHOLD_M,
160
+ ): ElevationSnapMatch | null {
161
+ let best: ElevationSnapTarget | null = null
162
+ let bestDelta = Number.POSITIVE_INFINITY
163
+ let bestPlanDistance = Number.POSITIVE_INFINITY
164
+
165
+ for (const target of targets) {
166
+ const delta = Math.abs(target.elevation - proposedElevation)
167
+ if (delta > threshold) continue
168
+ const dx = target.anchor[0] - sourceAnchor[0]
169
+ const dz = target.anchor[1] - sourceAnchor[1]
170
+ const planDistance = dx * dx + dz * dz
171
+ if (
172
+ delta < bestDelta - 1e-9 ||
173
+ (Math.abs(delta - bestDelta) <= 1e-9 && planDistance < bestPlanDistance)
174
+ ) {
175
+ best = target
176
+ bestDelta = delta
177
+ bestPlanDistance = planDistance
178
+ }
179
+ }
180
+
181
+ return best ? { target: best, elevation: best.elevation } : null
182
+ }
183
+
184
+ export function resolveStructuralElevationSnap(
185
+ source: ElevationGuideSource,
186
+ proposedElevation: number,
187
+ nodes: Record<string, AnyNode>,
188
+ ): number {
189
+ return (
190
+ resolveElevationSnapMatch(
191
+ proposedElevation,
192
+ source.anchor,
193
+ collectElevationSnapTargets(source, nodes),
194
+ )?.elevation ?? proposedElevation
195
+ )
196
+ }
197
+
198
+ export function publishStructuralElevationGuide(
199
+ source: ElevationGuideSource,
200
+ elevation: number,
201
+ nodes: Record<string, AnyNode>,
202
+ ): void {
203
+ if (!source.levelId) {
204
+ clearStructuralElevationGuide(source.nodeId)
205
+ return
206
+ }
207
+
208
+ const match = resolveElevationSnapMatch(
209
+ elevation,
210
+ source.anchor,
211
+ collectElevationSnapTargets(source, nodes),
212
+ GUIDE_MATCH_EPSILON_M,
213
+ )
214
+ if (!match) {
215
+ clearStructuralElevationGuide(source.nodeId)
216
+ return
217
+ }
218
+
219
+ const dx = match.target.anchor[0] - source.anchor[0]
220
+ const dz = match.target.anchor[1] - source.anchor[1]
221
+ const length = Math.hypot(dx, dz)
222
+ const direction: [number, number] = length > 1e-6 ? [dx / length, dz / length] : [1, 0]
223
+
224
+ useElevationGuides.getState().publish({
225
+ ownerId: source.nodeId,
226
+ levelId: source.levelId,
227
+ center: source.anchor,
228
+ direction,
229
+ elevation: match.elevation,
230
+ label: match.target.label,
231
+ })
232
+ }
233
+
234
+ export function clearStructuralElevationGuide(ownerId: string): void {
235
+ useElevationGuides.getState().clear(ownerId)
236
+ }
@@ -0,0 +1,327 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import type { FloorplanGeometry } from '@pascal-app/core'
3
+ import {
4
+ DEFAULT_FLOORPLAN_ANNOTATION_VISIBILITY,
5
+ filterFloorplanAnnotationGeometry,
6
+ normalizeFloorplanAnnotationVisibility,
7
+ } from './annotation-visibility'
8
+ import { floorplanGeometryMetadata, withFloorplanGeometryMetadata } from './floorplan-extension'
9
+
10
+ describe('floor-plan annotation visibility', () => {
11
+ test('fills missing persisted categories with visible defaults', () => {
12
+ expect(normalizeFloorplanAnnotationVisibility({ measurements: false })).toEqual({
13
+ ...DEFAULT_FLOORPLAN_ANNOTATION_VISIBILITY,
14
+ measurements: false,
15
+ })
16
+ })
17
+
18
+ test('removes automatic dimension primitives without removing plan geometry', () => {
19
+ const line = { kind: 'line', x1: 0, y1: 0, x2: 2, y2: 0 } satisfies FloorplanGeometry
20
+ const geometry = {
21
+ kind: 'group',
22
+ children: [
23
+ line,
24
+ {
25
+ kind: 'dimension-string',
26
+ segments: [{ start: [0, 0], end: [2, 0], text: '2.00m' }],
27
+ offsetNormal: [0, 1],
28
+ offsetDistance: 0.3,
29
+ extensionOvershoot: 0.1,
30
+ },
31
+ ],
32
+ } satisfies FloorplanGeometry
33
+
34
+ expect(
35
+ filterFloorplanAnnotationGeometry(geometry, {
36
+ ...DEFAULT_FLOORPLAN_ANNOTATION_VISIBILITY,
37
+ automaticDimensions: false,
38
+ }),
39
+ ).toEqual({ kind: 'group', children: [line] })
40
+ })
41
+
42
+ test('removes a complete curved automatic dimension group', () => {
43
+ const curvedDimension = {
44
+ kind: 'group',
45
+ metadata: floorplanGeometryMetadata({ annotationRole: 'automatic-dimension' }),
46
+ children: [
47
+ { kind: 'line', x1: 0, y1: 0, x2: 2, y2: 2 },
48
+ { kind: 'dimension-label', cx: 1, cy: 1, text: 'R 2m', angle: 0 },
49
+ ],
50
+ } satisfies FloorplanGeometry
51
+
52
+ expect(
53
+ filterFloorplanAnnotationGeometry(curvedDimension, {
54
+ ...DEFAULT_FLOORPLAN_ANNOTATION_VISIBILITY,
55
+ automaticDimensions: false,
56
+ }),
57
+ ).toBeNull()
58
+ })
59
+
60
+ test('removes only the opening mark from door geometry', () => {
61
+ const body = {
62
+ kind: 'polygon',
63
+ points: [
64
+ [0, 0],
65
+ [1, 0],
66
+ [1, 0.1],
67
+ ],
68
+ } satisfies FloorplanGeometry
69
+ const mark = {
70
+ kind: 'group',
71
+ metadata: floorplanGeometryMetadata({ annotationRole: 'opening-mark' }),
72
+ children: [
73
+ { kind: 'line', x1: 0.5, y1: 0, x2: 0.5, y2: 0.5 },
74
+ { kind: 'rect', x: 0.3, y: 0.5, width: 0.4, height: 0.3 },
75
+ { kind: 'text', x: 0.5, y: 0.65, text: '101', fontSize: 0.15, upright: true },
76
+ ],
77
+ } satisfies FloorplanGeometry
78
+ const geometry = { kind: 'group', children: [body, mark] } satisfies FloorplanGeometry
79
+
80
+ expect(
81
+ filterFloorplanAnnotationGeometry(geometry, {
82
+ ...DEFAULT_FLOORPLAN_ANNOTATION_VISIBILITY,
83
+ openingMarks: false,
84
+ }),
85
+ ).toEqual({ kind: 'group', children: [body] })
86
+ })
87
+
88
+ test('hides manual dimensions and measurements independently', () => {
89
+ const manualDimension = {
90
+ kind: 'text',
91
+ x: 0,
92
+ y: 0,
93
+ text: 'Annotation',
94
+ fontSize: 0.15,
95
+ metadata: floorplanGeometryMetadata({ annotationRole: 'manual-dimension' }),
96
+ } satisfies FloorplanGeometry
97
+ const measurement = {
98
+ ...manualDimension,
99
+ metadata: floorplanGeometryMetadata({ annotationRole: 'measurement' }),
100
+ } satisfies FloorplanGeometry
101
+
102
+ expect(
103
+ filterFloorplanAnnotationGeometry(manualDimension, {
104
+ ...DEFAULT_FLOORPLAN_ANNOTATION_VISIBILITY,
105
+ manualDimensions: false,
106
+ }),
107
+ ).toBeNull()
108
+ expect(
109
+ filterFloorplanAnnotationGeometry(
110
+ {
111
+ kind: 'group',
112
+ metadata: floorplanGeometryMetadata({ annotationRole: 'manual-dimension' }),
113
+ children: [
114
+ {
115
+ kind: 'dimension',
116
+ start: [0, 0],
117
+ end: [1, 0],
118
+ offsetNormal: [0, 1],
119
+ offsetDistance: 0.5,
120
+ extensionOvershoot: 0.1,
121
+ text: '1m',
122
+ },
123
+ ],
124
+ },
125
+ {
126
+ ...DEFAULT_FLOORPLAN_ANNOTATION_VISIBILITY,
127
+ automaticDimensions: false,
128
+ },
129
+ ),
130
+ ).not.toBeNull()
131
+ expect(
132
+ filterFloorplanAnnotationGeometry(measurement, {
133
+ ...DEFAULT_FLOORPLAN_ANNOTATION_VISIBILITY,
134
+ measurements: false,
135
+ }),
136
+ ).toBeNull()
137
+ })
138
+
139
+ test('keeps a contextual dimension when automatic dimensions are hidden', () => {
140
+ const contextualDimension = {
141
+ kind: 'group',
142
+ metadata: floorplanGeometryMetadata({ annotationRole: 'contextual-dimension' }),
143
+ children: [
144
+ {
145
+ kind: 'dimension',
146
+ start: [0, 0],
147
+ end: [2, 0],
148
+ offsetNormal: [0, 1],
149
+ offsetDistance: 0.3,
150
+ extensionOvershoot: 0.08,
151
+ text: '2m',
152
+ },
153
+ ],
154
+ } satisfies FloorplanGeometry
155
+
156
+ expect(
157
+ filterFloorplanAnnotationGeometry(contextualDimension, {
158
+ ...DEFAULT_FLOORPLAN_ANNOTATION_VISIBILITY,
159
+ automaticDimensions: false,
160
+ contextualDimensions: true,
161
+ }),
162
+ ).toEqual(contextualDimension)
163
+ })
164
+
165
+ test('removes a nested automatic wall string beside a contextual dimension', () => {
166
+ const wallBody = {
167
+ kind: 'polygon',
168
+ points: [
169
+ [0, 0],
170
+ [0.2, 0],
171
+ [0.2, 6],
172
+ [0, 6],
173
+ ],
174
+ } satisfies FloorplanGeometry
175
+ const automaticDimension = {
176
+ kind: 'dimension-string',
177
+ segments: [{ start: [0, 0], end: [0, 18], text: '18m' }],
178
+ offsetNormal: [1, 0],
179
+ offsetDistance: 0.55,
180
+ extensionOvershoot: 0.12,
181
+ } satisfies FloorplanGeometry
182
+ const contextualDimension = withFloorplanGeometryMetadata(
183
+ {
184
+ kind: 'dimension',
185
+ start: [0, 0],
186
+ end: [0, 6],
187
+ offsetNormal: [1, 0],
188
+ offsetDistance: 0.34,
189
+ extensionOvershoot: 0.08,
190
+ text: '6m',
191
+ } satisfies FloorplanGeometry,
192
+ { annotationRole: 'contextual-dimension' },
193
+ )
194
+ const geometry = {
195
+ kind: 'group',
196
+ children: [
197
+ {
198
+ kind: 'group',
199
+ children: [wallBody, automaticDimension],
200
+ },
201
+ contextualDimension,
202
+ ],
203
+ } satisfies FloorplanGeometry
204
+
205
+ expect(
206
+ filterFloorplanAnnotationGeometry(geometry, {
207
+ ...DEFAULT_FLOORPLAN_ANNOTATION_VISIBILITY,
208
+ automaticDimensions: false,
209
+ contextualDimensions: true,
210
+ }),
211
+ ).toEqual({
212
+ kind: 'group',
213
+ children: [{ kind: 'group', children: [wallBody] }, contextualDimension],
214
+ })
215
+ })
216
+
217
+ test('hides structural grids and only the center marks within column geometry', () => {
218
+ const centerMark = {
219
+ kind: 'line',
220
+ x1: -0.1,
221
+ y1: -0.1,
222
+ x2: 0.1,
223
+ y2: 0.1,
224
+ metadata: floorplanGeometryMetadata({ annotationRole: 'column-center' }),
225
+ } satisfies FloorplanGeometry
226
+ const gridReference = {
227
+ kind: 'text',
228
+ x: 0,
229
+ y: 0.3,
230
+ text: 'B-2',
231
+ fontSize: 0.13,
232
+ metadata: floorplanGeometryMetadata({ annotationRole: 'column-center' }),
233
+ } satisfies FloorplanGeometry
234
+ const footprint = {
235
+ kind: 'rect',
236
+ x: -0.2,
237
+ y: -0.2,
238
+ width: 0.4,
239
+ height: 0.4,
240
+ } satisfies FloorplanGeometry
241
+ const visibility = {
242
+ ...DEFAULT_FLOORPLAN_ANNOTATION_VISIBILITY,
243
+ structuralGrids: false,
244
+ }
245
+
246
+ expect(
247
+ filterFloorplanAnnotationGeometry(
248
+ {
249
+ kind: 'group',
250
+ children: [footprint, centerMark, gridReference],
251
+ },
252
+ visibility,
253
+ ),
254
+ ).toEqual({ kind: 'group', children: [footprint] })
255
+ expect(
256
+ filterFloorplanAnnotationGeometry(
257
+ {
258
+ kind: 'group',
259
+ metadata: floorplanGeometryMetadata({ annotationRole: 'structural-grid' }),
260
+ children: [footprint],
261
+ },
262
+ visibility,
263
+ ),
264
+ ).toBeNull()
265
+ })
266
+
267
+ test('hides room labels without removing the room footprint', () => {
268
+ const footprint = {
269
+ kind: 'polygon',
270
+ points: [
271
+ [0, 0],
272
+ [4, 0],
273
+ [4, 3],
274
+ ],
275
+ } satisfies FloorplanGeometry
276
+ const roomName = {
277
+ kind: 'text',
278
+ x: 2,
279
+ y: 1.5,
280
+ text: 'Office',
281
+ fontSize: 0.2,
282
+ metadata: floorplanGeometryMetadata({ annotationRole: 'room-label' }),
283
+ } satisfies FloorplanGeometry
284
+
285
+ expect(
286
+ filterFloorplanAnnotationGeometry(
287
+ { kind: 'group', children: [footprint, roomName] },
288
+ { ...DEFAULT_FLOORPLAN_ANNOTATION_VISIBILITY, roomLabels: false },
289
+ ),
290
+ ).toEqual({ kind: 'group', children: [footprint] })
291
+ })
292
+
293
+ test('hides stair notes and break lines without removing stair geometry', () => {
294
+ const footprint = {
295
+ kind: 'polygon',
296
+ points: [
297
+ [0, 0],
298
+ [1, 0],
299
+ [1, 3],
300
+ [0, 3],
301
+ ],
302
+ } satisfies FloorplanGeometry
303
+ const direction = {
304
+ kind: 'text',
305
+ x: 0.5,
306
+ y: 0.5,
307
+ text: 'UP',
308
+ fontSize: 0.16,
309
+ metadata: floorplanGeometryMetadata({ annotationRole: 'stair-annotation' }),
310
+ } satisfies FloorplanGeometry
311
+ const breakLine = {
312
+ kind: 'polyline',
313
+ points: [
314
+ [0, 2],
315
+ [1, 2],
316
+ ],
317
+ metadata: floorplanGeometryMetadata({ annotationRole: 'stair-annotation' }),
318
+ } satisfies FloorplanGeometry
319
+
320
+ expect(
321
+ filterFloorplanAnnotationGeometry(
322
+ { kind: 'group', children: [footprint, direction, breakLine] },
323
+ { ...DEFAULT_FLOORPLAN_ANNOTATION_VISIBILITY, stairAnnotations: false },
324
+ ),
325
+ ).toEqual({ kind: 'group', children: [footprint] })
326
+ })
327
+ })
@@ -0,0 +1,133 @@
1
+ import type { FloorplanGeometry } from '@pascal-app/core'
2
+ import { type FloorplanAnnotationRole, readFloorplanGeometryMetadata } from './floorplan-extension'
3
+
4
+ export type FloorplanAnnotationCategory =
5
+ | 'automaticDimensions'
6
+ | 'contextualDimensions'
7
+ | 'manualDimensions'
8
+ | 'measurements'
9
+ | 'openingMarks'
10
+ | 'structuralGrids'
11
+ | 'roomLabels'
12
+ | 'stairAnnotations'
13
+
14
+ export type FloorplanAnnotationVisibility = Record<FloorplanAnnotationCategory, boolean>
15
+
16
+ export const DEFAULT_FLOORPLAN_ANNOTATION_VISIBILITY: FloorplanAnnotationVisibility = {
17
+ automaticDimensions: true,
18
+ contextualDimensions: false,
19
+ manualDimensions: true,
20
+ measurements: true,
21
+ openingMarks: true,
22
+ structuralGrids: true,
23
+ roomLabels: true,
24
+ stairAnnotations: true,
25
+ }
26
+
27
+ export function revealFloorplanAnnotationRole(
28
+ visibility: FloorplanAnnotationVisibility,
29
+ role: FloorplanAnnotationRole,
30
+ ): FloorplanAnnotationVisibility {
31
+ const category = annotationCategoryForRole(role)
32
+ return visibility[category] ? visibility : { ...visibility, [category]: true }
33
+ }
34
+
35
+ export function normalizeFloorplanAnnotationVisibility(
36
+ value: unknown,
37
+ ): FloorplanAnnotationVisibility {
38
+ if (!value || typeof value !== 'object') return { ...DEFAULT_FLOORPLAN_ANNOTATION_VISIBILITY }
39
+ const persisted = value as Partial<Record<FloorplanAnnotationCategory, unknown>>
40
+ return {
41
+ automaticDimensions:
42
+ typeof persisted.automaticDimensions === 'boolean'
43
+ ? persisted.automaticDimensions
44
+ : DEFAULT_FLOORPLAN_ANNOTATION_VISIBILITY.automaticDimensions,
45
+ contextualDimensions:
46
+ typeof persisted.contextualDimensions === 'boolean'
47
+ ? persisted.contextualDimensions
48
+ : DEFAULT_FLOORPLAN_ANNOTATION_VISIBILITY.contextualDimensions,
49
+ manualDimensions:
50
+ typeof persisted.manualDimensions === 'boolean'
51
+ ? persisted.manualDimensions
52
+ : DEFAULT_FLOORPLAN_ANNOTATION_VISIBILITY.manualDimensions,
53
+ measurements:
54
+ typeof persisted.measurements === 'boolean'
55
+ ? persisted.measurements
56
+ : DEFAULT_FLOORPLAN_ANNOTATION_VISIBILITY.measurements,
57
+ openingMarks:
58
+ typeof persisted.openingMarks === 'boolean'
59
+ ? persisted.openingMarks
60
+ : DEFAULT_FLOORPLAN_ANNOTATION_VISIBILITY.openingMarks,
61
+ structuralGrids:
62
+ typeof persisted.structuralGrids === 'boolean'
63
+ ? persisted.structuralGrids
64
+ : DEFAULT_FLOORPLAN_ANNOTATION_VISIBILITY.structuralGrids,
65
+ roomLabels:
66
+ typeof persisted.roomLabels === 'boolean'
67
+ ? persisted.roomLabels
68
+ : DEFAULT_FLOORPLAN_ANNOTATION_VISIBILITY.roomLabels,
69
+ stairAnnotations:
70
+ typeof persisted.stairAnnotations === 'boolean'
71
+ ? persisted.stairAnnotations
72
+ : DEFAULT_FLOORPLAN_ANNOTATION_VISIBILITY.stairAnnotations,
73
+ }
74
+ }
75
+
76
+ export function filterFloorplanAnnotationGeometry(
77
+ geometry: FloorplanGeometry,
78
+ visibility: FloorplanAnnotationVisibility,
79
+ inheritedRole?: FloorplanAnnotationRole,
80
+ ): FloorplanGeometry | null {
81
+ const role = readFloorplanGeometryMetadata(geometry).annotationRole ?? inheritedRole
82
+ if (role && !isAnnotationRoleVisible(role, visibility)) return null
83
+ if (
84
+ !visibility.automaticDimensions &&
85
+ (role === undefined || role === 'automatic-dimension') &&
86
+ (geometry.kind === 'dimension' ||
87
+ geometry.kind === 'dimension-string' ||
88
+ geometry.kind === 'dimension-label' ||
89
+ geometry.kind === 'equal-spacing-badge')
90
+ ) {
91
+ return null
92
+ }
93
+ if (geometry.kind !== 'group') return geometry
94
+
95
+ const children = geometry.children
96
+ .map((child) => filterFloorplanAnnotationGeometry(child, visibility, role))
97
+ .filter((child): child is FloorplanGeometry => child !== null)
98
+ if (children.length === 0) return null
99
+ const childrenUnchanged =
100
+ children.length === geometry.children.length &&
101
+ children.every((child, index) => child === geometry.children[index])
102
+ if (childrenUnchanged) return geometry
103
+ return { ...geometry, children }
104
+ }
105
+
106
+ function isAnnotationRoleVisible(
107
+ role: FloorplanAnnotationRole,
108
+ visibility: FloorplanAnnotationVisibility,
109
+ ): boolean {
110
+ return visibility[annotationCategoryForRole(role)]
111
+ }
112
+
113
+ function annotationCategoryForRole(role: FloorplanAnnotationRole): FloorplanAnnotationCategory {
114
+ switch (role) {
115
+ case 'automatic-dimension':
116
+ return 'automaticDimensions'
117
+ case 'contextual-dimension':
118
+ return 'contextualDimensions'
119
+ case 'manual-dimension':
120
+ return 'manualDimensions'
121
+ case 'measurement':
122
+ return 'measurements'
123
+ case 'opening-mark':
124
+ return 'openingMarks'
125
+ case 'structural-grid':
126
+ case 'column-center':
127
+ return 'structuralGrids'
128
+ case 'room-label':
129
+ return 'roomLabels'
130
+ case 'stair-annotation':
131
+ return 'stairAnnotations'
132
+ }
133
+ }