@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,570 @@
1
+ 'use client'
2
+
3
+ import {
4
+ areMeasurementPointsCoplanar,
5
+ MEASUREMENT_PLANAR_TOLERANCE,
6
+ type MeasurementAnchor,
7
+ type MeasurementFeatureAnchor,
8
+ MeasurementNode,
9
+ type MeasurementSnapKind,
10
+ measurementNormal,
11
+ useScene,
12
+ } from '@pascal-app/core'
13
+ import { useViewer } from '@pascal-app/viewer'
14
+ import { create } from 'zustand'
15
+
16
+ export type MeasurementKind = 'distance' | 'angle' | 'area' | 'perimeter' | 'volume'
17
+ export type MeasurementDraftOwner = '2d' | '3d'
18
+ export type MeasurementDraftStage = 'collecting' | 'extruding' | 'ready'
19
+ export type MeasurementAxis = 'x' | 'y' | 'z'
20
+ export type MeasurementPoint = [number, number, number]
21
+
22
+ export type MeasurementAxisGuide = {
23
+ axis: MeasurementAxis
24
+ from: MeasurementPoint
25
+ to: MeasurementPoint
26
+ snapped: boolean
27
+ proximity?: boolean
28
+ }
29
+
30
+ export type MeasurementSurfacePoint = {
31
+ point: MeasurementPoint
32
+ normal: MeasurementPoint
33
+ targetNodeId: string | null
34
+ anchor?: MeasurementFeatureAnchor
35
+ semantic?: {
36
+ label: string
37
+ length: number | null
38
+ snapKind: MeasurementSnapKind
39
+ }
40
+ }
41
+
42
+ export type MeasurementVertexDrag = {
43
+ owner: MeasurementDraftOwner
44
+ index: number
45
+ originalPoint: MeasurementPoint
46
+ originalAnchor: MeasurementFeatureAnchor | null
47
+ inserted: boolean
48
+ }
49
+
50
+ export type MeasurementDraftPayload =
51
+ | { kind: 'distance'; points: [MeasurementAnchor, MeasurementAnchor] }
52
+ | { kind: 'angle'; points: [MeasurementAnchor, MeasurementAnchor, MeasurementAnchor] }
53
+ | { kind: 'area'; base: MeasurementAnchor[] }
54
+ | { kind: 'perimeter'; base: MeasurementAnchor[] }
55
+ | { kind: 'volume'; base: MeasurementAnchor[]; extrusion: MeasurementPoint }
56
+
57
+ type MeasurementDraftState = {
58
+ kind: MeasurementKind
59
+ owner: MeasurementDraftOwner | null
60
+ levelId: string | null
61
+ stage: MeasurementDraftStage
62
+ points: MeasurementPoint[]
63
+ anchors: Array<MeasurementFeatureAnchor | null>
64
+ hover: MeasurementSurfacePoint | null
65
+ hoverOwner: MeasurementDraftOwner | null
66
+ axisGuide: MeasurementAxisGuide | null
67
+ vertexDrag: MeasurementVertexDrag | null
68
+ collectionPlane: { point: MeasurementPoint; normal: MeasurementPoint } | null
69
+ baseNormal: MeasurementPoint | null
70
+ extrusionHeight: number
71
+ error: string | null
72
+ setKind(kind: MeasurementKind): void
73
+ setHover(
74
+ owner: MeasurementDraftOwner,
75
+ hover: MeasurementSurfacePoint | null,
76
+ axisGuide?: MeasurementAxisGuide | null,
77
+ ): void
78
+ beginVertexDrag(owner: MeasurementDraftOwner, index: number): boolean
79
+ beginMidpointVertexDrag(owner: MeasurementDraftOwner, edgeIndex: number): boolean
80
+ updateDraggedVertex(
81
+ owner: MeasurementDraftOwner,
82
+ hover: MeasurementSurfacePoint,
83
+ axisGuide?: MeasurementAxisGuide | null,
84
+ ): boolean
85
+ finishVertexDrag(owner: MeasurementDraftOwner): boolean
86
+ cancelVertexDrag(owner: MeasurementDraftOwner): boolean
87
+ addPoint(
88
+ owner: MeasurementDraftOwner,
89
+ point: MeasurementPoint,
90
+ anchor?: MeasurementFeatureAnchor,
91
+ surfaceNormal?: MeasurementPoint,
92
+ ): boolean
93
+ closeBase(owner: MeasurementDraftOwner, preferredNormal?: MeasurementPoint): boolean
94
+ setExtrusionHeight(owner: MeasurementDraftOwner, height: number): boolean
95
+ finishExtrusion(owner: MeasurementDraftOwner): boolean
96
+ removeLast(owner: MeasurementDraftOwner): boolean
97
+ getCommitPayload(owner: MeasurementDraftOwner): MeasurementDraftPayload | null
98
+ reset(): void
99
+ }
100
+
101
+ const MIN_EXTRUSION = 0.001
102
+
103
+ const clonePoint = (point: MeasurementPoint): MeasurementPoint => [...point]
104
+
105
+ function normalizePoint(point: MeasurementPoint | undefined): MeasurementPoint | null {
106
+ if (!point?.every(Number.isFinite)) return null
107
+ const length = Math.hypot(...point)
108
+ return length > 1e-9 ? [point[0] / length, point[1] / length, point[2] / length] : null
109
+ }
110
+
111
+ function projectPointToPlane(
112
+ point: MeasurementPoint,
113
+ plane: { point: MeasurementPoint; normal: MeasurementPoint } | null,
114
+ ): MeasurementPoint {
115
+ if (!plane) return clonePoint(point)
116
+ const distance =
117
+ (point[0] - plane.point[0]) * plane.normal[0] +
118
+ (point[1] - plane.point[1]) * plane.normal[1] +
119
+ (point[2] - plane.point[2]) * plane.normal[2]
120
+ return [
121
+ point[0] - plane.normal[0] * distance,
122
+ point[1] - plane.normal[1] * distance,
123
+ point[2] - plane.normal[2] * distance,
124
+ ]
125
+ }
126
+
127
+ function anchorWithFallback(
128
+ anchor: MeasurementFeatureAnchor | undefined,
129
+ fallback: MeasurementPoint,
130
+ ): MeasurementFeatureAnchor | null {
131
+ return anchor ? { ...anchor, fallback: clonePoint(fallback) } : null
132
+ }
133
+
134
+ export function measurementPolygonMidpoints(
135
+ points: readonly MeasurementPoint[],
136
+ ): Array<{ edgeIndex: number; point: MeasurementPoint }> {
137
+ if (points.length < 3) return []
138
+ return points.map((start, edgeIndex) => {
139
+ const end = points[(edgeIndex + 1) % points.length]!
140
+ return {
141
+ edgeIndex,
142
+ point: [(start[0] + end[0]) / 2, (start[1] + end[1]) / 2, (start[2] + end[2]) / 2],
143
+ }
144
+ })
145
+ }
146
+
147
+ function idleState(kind: MeasurementKind) {
148
+ return {
149
+ kind,
150
+ owner: null,
151
+ levelId: null,
152
+ stage: 'collecting' as const,
153
+ points: [],
154
+ anchors: [],
155
+ hover: null,
156
+ hoverOwner: null,
157
+ axisGuide: null,
158
+ vertexDrag: null,
159
+ collectionPlane: null,
160
+ baseNormal: null,
161
+ extrusionHeight: 0,
162
+ error: null,
163
+ }
164
+ }
165
+
166
+ function payloadFor(state: MeasurementDraftState): MeasurementDraftPayload | null {
167
+ if (state.stage !== 'ready') return null
168
+
169
+ const anchorAt = (index: number): MeasurementAnchor =>
170
+ state.anchors[index] ?? clonePoint(state.points[index]!)
171
+
172
+ if (state.kind === 'distance') {
173
+ const [start, end] = state.points
174
+ if (!(start && end)) return null
175
+ return { kind: 'distance', points: [anchorAt(0), anchorAt(1)] }
176
+ }
177
+
178
+ if (state.kind === 'angle') {
179
+ if (state.points.length !== 3) return null
180
+ return { kind: 'angle', points: [anchorAt(0), anchorAt(1), anchorAt(2)] }
181
+ }
182
+
183
+ if (state.kind === 'area' || state.kind === 'perimeter') {
184
+ if (state.points.length < 3) return null
185
+ return { kind: state.kind, base: state.points.map((_, index) => anchorAt(index)) }
186
+ }
187
+
188
+ if (!(state.baseNormal && state.points.length >= 3)) return null
189
+ return {
190
+ kind: 'volume',
191
+ base: state.points.map((_, index) => anchorAt(index)),
192
+ extrusion: [
193
+ state.baseNormal[0] * state.extrusionHeight,
194
+ state.baseNormal[1] * state.extrusionHeight,
195
+ state.baseNormal[2] * state.extrusionHeight,
196
+ ],
197
+ }
198
+ }
199
+
200
+ export const useMeasurementDraft = create<MeasurementDraftState>((set, get) => ({
201
+ ...idleState('distance'),
202
+
203
+ setKind: (kind) => set((state) => (state.kind === kind ? state : { ...idleState(kind) })),
204
+
205
+ setHover: (owner, hover, axisGuide = null) =>
206
+ set((state) => {
207
+ if (state.owner && state.owner !== owner) return state
208
+ if (state.levelId && state.levelId !== useViewer.getState().selection.levelId) return state
209
+ if (state.vertexDrag) return state
210
+ if (!hover && !state.hover && !state.axisGuide) return state
211
+ return { hover, hoverOwner: hover ? owner : null, axisGuide }
212
+ }),
213
+
214
+ beginVertexDrag: (owner, index) => {
215
+ const state = get()
216
+ const point = state.points[index]
217
+ if (
218
+ state.owner !== owner ||
219
+ state.levelId !== useViewer.getState().selection.levelId ||
220
+ state.stage !== 'collecting' ||
221
+ state.vertexDrag ||
222
+ !Number.isInteger(index) ||
223
+ !point
224
+ ) {
225
+ return false
226
+ }
227
+
228
+ set({
229
+ vertexDrag: {
230
+ owner,
231
+ index,
232
+ originalPoint: clonePoint(point),
233
+ originalAnchor: state.anchors[index] ?? null,
234
+ inserted: false,
235
+ },
236
+ hover: null,
237
+ hoverOwner: null,
238
+ axisGuide: null,
239
+ error: null,
240
+ })
241
+ return true
242
+ },
243
+
244
+ beginMidpointVertexDrag: (owner, edgeIndex) => {
245
+ const state = get()
246
+ if (
247
+ state.owner !== owner ||
248
+ state.levelId !== useViewer.getState().selection.levelId ||
249
+ state.stage !== 'collecting' ||
250
+ state.vertexDrag ||
251
+ state.kind === 'distance' ||
252
+ state.kind === 'angle' ||
253
+ state.points.length < 3 ||
254
+ !Number.isInteger(edgeIndex) ||
255
+ edgeIndex < 0 ||
256
+ edgeIndex >= state.points.length
257
+ ) {
258
+ return false
259
+ }
260
+
261
+ const start = state.points[edgeIndex]!
262
+ const end = state.points[(edgeIndex + 1) % state.points.length]!
263
+ const midpoint: MeasurementPoint = [
264
+ (start[0] + end[0]) / 2,
265
+ (start[1] + end[1]) / 2,
266
+ (start[2] + end[2]) / 2,
267
+ ]
268
+ const index = edgeIndex + 1
269
+ const points = [...state.points.slice(0, index), midpoint, ...state.points.slice(index)]
270
+ const anchors = [...state.anchors.slice(0, index), null, ...state.anchors.slice(index)]
271
+
272
+ set({
273
+ points,
274
+ anchors,
275
+ vertexDrag: {
276
+ owner,
277
+ index,
278
+ originalPoint: clonePoint(midpoint),
279
+ originalAnchor: null,
280
+ inserted: true,
281
+ },
282
+ hover: null,
283
+ hoverOwner: null,
284
+ axisGuide: null,
285
+ error: null,
286
+ })
287
+ return true
288
+ },
289
+
290
+ updateDraggedVertex: (owner, hover, axisGuide = null) => {
291
+ const state = get()
292
+ const drag = state.vertexDrag
293
+ if (
294
+ !drag ||
295
+ drag.owner !== owner ||
296
+ state.levelId !== useViewer.getState().selection.levelId ||
297
+ state.stage !== 'collecting' ||
298
+ ![...hover.point, ...hover.normal].every(Number.isFinite)
299
+ ) {
300
+ return false
301
+ }
302
+
303
+ const projectedPoint = projectPointToPlane(hover.point, state.collectionPlane)
304
+ const points = state.points.map((point, index) =>
305
+ index === drag.index ? projectedPoint : point,
306
+ )
307
+ const anchors = state.anchors.map((anchor, index) =>
308
+ index === drag.index ? anchorWithFallback(hover.anchor, projectedPoint) : anchor,
309
+ )
310
+ set({
311
+ points,
312
+ anchors,
313
+ hover: {
314
+ point: clonePoint(projectedPoint),
315
+ normal: clonePoint(hover.normal),
316
+ targetNodeId: hover.targetNodeId,
317
+ anchor: hover.anchor,
318
+ semantic: hover.semantic,
319
+ },
320
+ hoverOwner: owner,
321
+ axisGuide: axisGuide
322
+ ? {
323
+ axis: axisGuide.axis,
324
+ from: clonePoint(axisGuide.from),
325
+ to: clonePoint(axisGuide.to),
326
+ snapped: axisGuide.snapped,
327
+ proximity: axisGuide.proximity,
328
+ }
329
+ : null,
330
+ error: null,
331
+ })
332
+ return true
333
+ },
334
+
335
+ finishVertexDrag: (owner) => {
336
+ const drag = get().vertexDrag
337
+ if (!drag || drag.owner !== owner) return false
338
+ set({ vertexDrag: null, hover: null, hoverOwner: null, axisGuide: null, error: null })
339
+ return true
340
+ },
341
+
342
+ cancelVertexDrag: (owner) => {
343
+ const state = get()
344
+ const drag = state.vertexDrag
345
+ if (!drag || drag.owner !== owner) return false
346
+ const points = drag.inserted
347
+ ? state.points.filter((_, index) => index !== drag.index)
348
+ : state.points.map((point, index) =>
349
+ index === drag.index ? clonePoint(drag.originalPoint) : point,
350
+ )
351
+ const anchors = drag.inserted
352
+ ? state.anchors.filter((_, index) => index !== drag.index)
353
+ : state.anchors.map((anchor, index) => (index === drag.index ? drag.originalAnchor : anchor))
354
+ set({
355
+ points,
356
+ anchors,
357
+ vertexDrag: null,
358
+ hover: null,
359
+ hoverOwner: null,
360
+ axisGuide: null,
361
+ error: null,
362
+ })
363
+ return true
364
+ },
365
+
366
+ addPoint: (owner, point, anchor, surfaceNormal) => {
367
+ const state = get()
368
+ const activeLevelId = useViewer.getState().selection.levelId
369
+ if (!activeLevelId) return false
370
+ if (state.stage !== 'collecting' || state.vertexDrag || (state.owner && state.owner !== owner))
371
+ return false
372
+ if (state.levelId && state.levelId !== activeLevelId) {
373
+ set({ error: 'The active level changed. Start a new measurement.' })
374
+ return false
375
+ }
376
+ if (state.kind === 'distance' && state.points.length >= 2) return false
377
+ if (state.kind === 'angle' && state.points.length >= 3) return false
378
+
379
+ const polygon = state.kind === 'area' || state.kind === 'perimeter' || state.kind === 'volume'
380
+ const projectedPoint = polygon
381
+ ? projectPointToPlane(point, state.collectionPlane)
382
+ : clonePoint(point)
383
+ const points = [...state.points, projectedPoint]
384
+ const anchors = [...state.anchors, anchorWithFallback(anchor, projectedPoint)]
385
+ const planeNormal = polygon && state.points.length === 0 ? normalizePoint(surfaceNormal) : null
386
+ const ready =
387
+ (state.kind === 'distance' && points.length === 2) ||
388
+ (state.kind === 'angle' && points.length === 3)
389
+ set({
390
+ owner,
391
+ levelId: state.levelId ?? activeLevelId,
392
+ points,
393
+ anchors,
394
+ collectionPlane: planeNormal
395
+ ? { point: clonePoint(projectedPoint), normal: planeNormal }
396
+ : state.collectionPlane,
397
+ stage: ready ? 'ready' : 'collecting',
398
+ hover: null,
399
+ hoverOwner: null,
400
+ axisGuide: null,
401
+ error: null,
402
+ })
403
+ return true
404
+ },
405
+
406
+ closeBase: (owner, preferredNormal) => {
407
+ const state = get()
408
+ if (
409
+ state.owner !== owner ||
410
+ state.levelId !== useViewer.getState().selection.levelId ||
411
+ state.stage !== 'collecting' ||
412
+ state.vertexDrag ||
413
+ state.kind === 'distance' ||
414
+ state.kind === 'angle' ||
415
+ state.points.length < 3
416
+ ) {
417
+ return false
418
+ }
419
+
420
+ if (!areMeasurementPointsCoplanar(state.points, MEASUREMENT_PLANAR_TOLERANCE)) {
421
+ set({ error: 'Measurement points must be on one plane.' })
422
+ return false
423
+ }
424
+
425
+ let normal = measurementNormal(state.points)
426
+ if (!normal) {
427
+ set({ error: 'Measurement points must enclose an area.' })
428
+ return false
429
+ }
430
+ if (
431
+ preferredNormal &&
432
+ normal[0] * preferredNormal[0] +
433
+ normal[1] * preferredNormal[1] +
434
+ normal[2] * preferredNormal[2] <
435
+ 0
436
+ ) {
437
+ normal = [
438
+ normal[0] === 0 ? 0 : -normal[0],
439
+ normal[1] === 0 ? 0 : -normal[1],
440
+ normal[2] === 0 ? 0 : -normal[2],
441
+ ]
442
+ }
443
+
444
+ set({
445
+ baseNormal: clonePoint(normal),
446
+ stage: state.kind === 'volume' ? 'extruding' : 'ready',
447
+ hover: null,
448
+ hoverOwner: null,
449
+ axisGuide: null,
450
+ error: null,
451
+ })
452
+ return true
453
+ },
454
+
455
+ setExtrusionHeight: (owner, height) => {
456
+ const state = get()
457
+ if (
458
+ state.owner !== owner ||
459
+ state.levelId !== useViewer.getState().selection.levelId ||
460
+ state.stage !== 'extruding' ||
461
+ !Number.isFinite(height)
462
+ ) {
463
+ return false
464
+ }
465
+ set({ extrusionHeight: height, error: null })
466
+ return true
467
+ },
468
+
469
+ finishExtrusion: (owner) => {
470
+ const state = get()
471
+ if (
472
+ state.owner !== owner ||
473
+ state.levelId !== useViewer.getState().selection.levelId ||
474
+ state.stage !== 'extruding' ||
475
+ Math.abs(state.extrusionHeight) < MIN_EXTRUSION
476
+ ) {
477
+ return false
478
+ }
479
+ set({ stage: 'ready', hover: null, hoverOwner: null, axisGuide: null, error: null })
480
+ return true
481
+ },
482
+
483
+ removeLast: (owner) => {
484
+ const state = get()
485
+ if (
486
+ state.owner !== owner ||
487
+ state.levelId !== useViewer.getState().selection.levelId ||
488
+ state.vertexDrag ||
489
+ state.points.length === 0
490
+ ) {
491
+ return false
492
+ }
493
+
494
+ const points = state.points.slice(0, -1)
495
+ const anchors = state.anchors.slice(0, -1)
496
+ set({
497
+ owner: points.length > 0 ? owner : null,
498
+ levelId: points.length > 0 ? state.levelId : null,
499
+ stage: 'collecting',
500
+ points,
501
+ anchors,
502
+ hover: null,
503
+ hoverOwner: null,
504
+ axisGuide: null,
505
+ collectionPlane: points.length > 0 ? state.collectionPlane : null,
506
+ baseNormal: null,
507
+ extrusionHeight: 0,
508
+ error: null,
509
+ })
510
+ return true
511
+ },
512
+
513
+ getCommitPayload: (owner) => {
514
+ const state = get()
515
+ return state.owner === owner && state.levelId === useViewer.getState().selection.levelId
516
+ ? payloadFor(state)
517
+ : null
518
+ },
519
+
520
+ reset: () => set((state) => ({ ...idleState(state.kind) })),
521
+ }))
522
+
523
+ export function commitMeasurementDraft(owner: MeasurementDraftOwner): MeasurementNode['id'] | null {
524
+ const draft = useMeasurementDraft.getState()
525
+ const levelId = draft.levelId
526
+ if (!levelId || useViewer.getState().selection.levelId !== levelId) {
527
+ draft.reset()
528
+ return null
529
+ }
530
+ const measurement = draft.getCommitPayload(owner)
531
+ if (!measurement) return null
532
+
533
+ const { createNode, nodes } = useScene.getState()
534
+ const count = Object.values(nodes).filter((node) => node.type === 'measurement').length
535
+ const node = MeasurementNode.parse({ name: `Measurement ${count + 1}`, measurement })
536
+ createNode(node, levelId)
537
+ draft.reset()
538
+ return node.id
539
+ }
540
+
541
+ export function finishMeasurementDraft(
542
+ owner: MeasurementDraftOwner,
543
+ preferredNormal?: MeasurementPoint,
544
+ ): boolean {
545
+ const draft = useMeasurementDraft.getState()
546
+ if (draft.stage === 'collecting') {
547
+ if (draft.kind === 'distance' || draft.kind === 'angle') return false
548
+ if (!draft.closeBase(owner, preferredNormal)) return false
549
+ if (draft.kind === 'area' || draft.kind === 'perimeter') {
550
+ return commitMeasurementDraft(owner) !== null
551
+ }
552
+ return true
553
+ }
554
+ if (draft.stage === 'extruding') {
555
+ if (!draft.finishExtrusion(owner)) return false
556
+ }
557
+ return commitMeasurementDraft(owner) !== null
558
+ }
559
+
560
+ export function handleMeasurementDraftEscape(
561
+ owner: MeasurementDraftOwner,
562
+ preferredNormal?: MeasurementPoint,
563
+ ): boolean {
564
+ const draft = useMeasurementDraft.getState()
565
+ if (draft.owner !== owner) return false
566
+
567
+ const preserveArea = draft.kind === 'area' && draft.points.length >= 3
568
+ if (!finishMeasurementDraft(owner, preferredNormal) && !preserveArea) draft.reset()
569
+ return true
570
+ }
@@ -0,0 +1,41 @@
1
+ // Ephemeral store for the 3D opening proximity/alignment guides published by the
2
+ // door/window move + placement tools during a drag — the wall-plane counterpart
3
+ // of `useAlignmentGuides` (which only carries floor-plane XZ guides). Guides are
4
+ // already transformed into the move tool's render frame — the same building-local
5
+ // frame as the drag cursor (ToolManager's group) — so the renderer stays dumb.
6
+ // Producers clear on commit, cancel, leave, and unmount.
7
+
8
+ import { create } from 'zustand'
9
+
10
+ export type OpeningGuideVec3 = [number, number, number]
11
+
12
+ // A stable identity per guide slot (`sill`, `head`, `gap:left`, `vertical`,
13
+ // `spacing:0`, …) so the renderer can key by semantic role: as the guide set
14
+ // churns each drag tick, a slot that persists keeps its React element — and its
15
+ // drei `<Html>` portal — mounted instead of remounting when the list shape
16
+ // shifts under index keys.
17
+ export type OpeningGuide3D =
18
+ // A measured line + distance pill: sill (floor → bottom edge), head (top edge
19
+ // → wall top), or along-wall edge-to-edge proximity.
20
+ | { kind: 'dimension'; id: string; from: OpeningGuideVec3; to: OpeningGuideVec3; value: number }
21
+ // A dashed line connecting two openings that share a sill / centre / top.
22
+ | { kind: 'align-line'; id: string; from: OpeningGuideVec3; to: OpeningGuideVec3 }
23
+ // A Figma-style "=" badge marking one gap in an equal-spacing run.
24
+ | { kind: 'badge'; id: string; at: OpeningGuideVec3; value: number }
25
+
26
+ type OpeningGuidesState = {
27
+ guides: OpeningGuide3D[]
28
+ set(guides: OpeningGuide3D[]): void
29
+ clear(): void
30
+ }
31
+
32
+ const useOpeningGuides = create<OpeningGuidesState>((set) => ({
33
+ guides: [],
34
+ set: (guides) => set({ guides }),
35
+ // No-op when already empty so the common no-guide hover frame (fallback
36
+ // cursor, invalid target, roof hover) doesn't push a fresh `[]` and notify
37
+ // subscribers — the layer would re-render to the same nothing every tick.
38
+ clear: () => set((s) => (s.guides.length > 0 ? { guides: [] } : s)),
39
+ }))
40
+
41
+ export default useOpeningGuides
@@ -0,0 +1,90 @@
1
+ import type { AnyNode } from '@pascal-app/core'
2
+ import { create } from 'zustand'
3
+
4
+ export type PathDraftKind = 'duct-segment' | 'lineset' | 'liquid-line' | 'pipe-segment'
5
+ export type PathDraftPoint = [number, number, number]
6
+ export type PathDraftParameter = boolean | number | string
7
+ export type PathDraftParameters = Record<string, PathDraftParameter>
8
+
9
+ type PathDraftPreviewState = {
10
+ kind: PathDraftKind | null
11
+ points: PathDraftPoint[]
12
+ cursor: PathDraftPoint | null
13
+ parameters: PathDraftParameters
14
+ relatedNodes: AnyNode[]
15
+ setDraft(
16
+ kind: PathDraftKind,
17
+ points: readonly PathDraftPoint[],
18
+ cursor: PathDraftPoint | null,
19
+ parameters?: Readonly<PathDraftParameters>,
20
+ relatedNodes?: readonly AnyNode[],
21
+ ): void
22
+ clear(kind: PathDraftKind): void
23
+ reset(): void
24
+ }
25
+
26
+ function pointEquals(a: PathDraftPoint | null, b: PathDraftPoint | null) {
27
+ return (!a && !b) || Boolean(a && b && a[0] === b[0] && a[1] === b[1] && a[2] === b[2])
28
+ }
29
+
30
+ function pointsEqual(a: readonly PathDraftPoint[], b: readonly PathDraftPoint[]) {
31
+ return a.length === b.length && a.every((point, index) => pointEquals(point, b[index] ?? null))
32
+ }
33
+
34
+ function parametersEqual(a: PathDraftParameters, b: Readonly<PathDraftParameters>) {
35
+ const keys = Object.keys(a)
36
+ const nextKeys = Object.keys(b)
37
+ return keys.length === nextKeys.length && keys.every((key) => a[key] === b[key])
38
+ }
39
+
40
+ function relatedNodesEqual(a: readonly AnyNode[], b: readonly AnyNode[]) {
41
+ return (
42
+ a.length === b.length &&
43
+ a.every((node, index) => JSON.stringify(node) === JSON.stringify(b[index]))
44
+ )
45
+ }
46
+
47
+ const EMPTY_DRAFT: Pick<
48
+ PathDraftPreviewState,
49
+ 'cursor' | 'kind' | 'parameters' | 'points' | 'relatedNodes'
50
+ > = {
51
+ cursor: null,
52
+ kind: null,
53
+ parameters: {},
54
+ points: [],
55
+ relatedNodes: [],
56
+ }
57
+
58
+ export const usePathDraftPreview = create<PathDraftPreviewState>((set) => ({
59
+ ...EMPTY_DRAFT,
60
+ setDraft: (kind, points, cursor, parameters = {}, relatedNodes = []) =>
61
+ set((state) => {
62
+ if (
63
+ state.kind === kind &&
64
+ pointEquals(state.cursor, cursor) &&
65
+ pointsEqual(state.points, points) &&
66
+ parametersEqual(state.parameters, parameters) &&
67
+ relatedNodesEqual(state.relatedNodes, relatedNodes)
68
+ ) {
69
+ return state
70
+ }
71
+ return {
72
+ cursor: cursor ? [...cursor] : null,
73
+ kind,
74
+ parameters: { ...parameters },
75
+ points: points.map((point) => [...point]),
76
+ relatedNodes: relatedNodes.map((node) => structuredClone(node)),
77
+ }
78
+ }),
79
+ clear: (kind) => set((state) => (state.kind === kind ? EMPTY_DRAFT : state)),
80
+ reset: () =>
81
+ set((state) =>
82
+ state.kind === null &&
83
+ state.points.length === 0 &&
84
+ state.cursor === null &&
85
+ Object.keys(state.parameters).length === 0 &&
86
+ state.relatedNodes.length === 0
87
+ ? state
88
+ : EMPTY_DRAFT,
89
+ ),
90
+ }))