@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
@@ -1,15 +1,13 @@
1
1
  import {
2
+ type AnyNode,
2
3
  type AnyNodeId,
3
- generateId,
4
4
  type StairNode,
5
- StairNode as StairNodeSchema,
6
5
  type StairSegmentNode,
7
- StairSegmentNode as StairSegmentNodeSchema,
8
- sceneRegistry,
9
6
  useScene,
10
7
  } from '@pascal-app/core'
11
8
  import { useViewer } from '@pascal-app/viewer'
12
9
  import useEditor from '../store/use-editor'
10
+ import { commitFreshPlacementSubtree, createFreshPlacementSubtree } from './fresh-planar-placement'
13
11
 
14
12
  type DuplicateStairOptions = {
15
13
  mode?: 'select' | 'move'
@@ -22,49 +20,21 @@ type DuplicateStairResult = {
22
20
  segmentIds: StairSegmentNode['id'][]
23
21
  }
24
22
 
25
- const MOVE_REGISTRY_RETRY_LIMIT = 12
26
-
27
- function stripDuplicateFlags(metadata: unknown) {
28
- if (typeof metadata !== 'object' || metadata === null || Array.isArray(metadata)) {
29
- return metadata
30
- }
31
-
32
- const nextMeta = { ...(metadata as Record<string, unknown>) }
33
- delete nextMeta.isNew
34
- delete nextMeta.isTransient
35
- return nextMeta
36
- }
37
-
38
- function moveStairWhenRegistered(stairId: StairNode['id'], attempt = 0) {
39
- const latestStair = useScene.getState().nodes[stairId as AnyNodeId]
40
- if (!latestStair || latestStair.type !== 'stair') {
41
- return
42
- }
43
-
44
- if (sceneRegistry.nodes.has(stairId)) {
45
- useEditor.getState().setMovingNode(latestStair)
46
- useViewer.getState().setSelection({ selectedIds: [] })
47
- return
48
- }
49
-
50
- if (attempt >= MOVE_REGISTRY_RETRY_LIMIT) {
51
- console.warn(`Duplicated stair "${stairId}" did not register before move mode started`)
52
- return
53
- }
54
-
55
- requestAnimationFrame(() => moveStairWhenRegistered(stairId, attempt + 1))
56
- }
57
-
23
+ /**
24
+ * Duplicates a stair through the shared fresh-subtree placement path.
25
+ *
26
+ * The draft passed to the move tool is the exact node stored in the scene,
27
+ * so its geometry, descendants, selection identity, and eventual commit all
28
+ * use one fresh ID graph.
29
+ */
58
30
  export function duplicateStairSubtree(
59
31
  sourceStairId: AnyNodeId,
60
32
  options: DuplicateStairOptions = {},
61
33
  ): DuplicateStairResult {
62
34
  const { mode = 'move', offset = [1, 0, 1], parentId: explicitParentId } = options
35
+ const sourceStair = useScene.getState().nodes[sourceStairId]
63
36
 
64
- const scene = useScene.getState()
65
- const sourceStair = scene.nodes[sourceStairId]
66
-
67
- if (!sourceStair || sourceStair.type !== 'stair') {
37
+ if (sourceStair?.type !== 'stair') {
68
38
  throw new Error(`Node "${sourceStairId}" is not a stair`)
69
39
  }
70
40
 
@@ -73,54 +43,56 @@ export function duplicateStairSubtree(
73
43
  throw new Error(`Stair "${sourceStairId}" is missing a parent level`)
74
44
  }
75
45
 
76
- const stairClone = StairNodeSchema.parse({
77
- ...structuredClone(sourceStair),
78
- id: generateId('stair'),
79
- parentId,
80
- children: [],
81
- position: [
82
- sourceStair.position[0] + offset[0],
83
- sourceStair.position[1] + offset[1],
84
- sourceStair.position[2] + offset[2],
85
- ] as StairNode['position'],
86
- metadata: stripDuplicateFlags(sourceStair.metadata),
87
- })
88
-
89
- const segmentClones: StairSegmentNode[] = []
90
- for (const childId of sourceStair.children ?? []) {
91
- const childNode = scene.nodes[childId as AnyNodeId]
92
- if (!childNode || childNode.type !== 'stair-segment') {
93
- continue
46
+ const temporal = useScene.temporal.getState()
47
+ const wasTracking = (temporal as { isTracking?: boolean }).isTracking !== false
48
+ if (wasTracking) temporal.pause()
49
+
50
+ let draftId: AnyNodeId | null = null
51
+ try {
52
+ draftId = createFreshPlacementSubtree(sourceStairId, {
53
+ parentId,
54
+ position: [
55
+ sourceStair.position[0] + offset[0],
56
+ sourceStair.position[1] + offset[1],
57
+ sourceStair.position[2] + offset[2],
58
+ ],
59
+ } as Partial<AnyNode>)
60
+ const draft = draftId ? useScene.getState().nodes[draftId] : null
61
+ if (draft?.type !== 'stair') {
62
+ throw new Error(`Duplicated stair "${sourceStairId}" was not created`)
94
63
  }
95
64
 
96
- const childClone = StairSegmentNodeSchema.parse({
97
- ...structuredClone(childNode),
98
- id: generateId('sseg'),
99
- parentId: stairClone.id,
100
- metadata: stripDuplicateFlags(childNode.metadata),
101
- })
102
- segmentClones.push(childClone)
103
- }
104
-
105
- scene.createNodes([
106
- { node: stairClone, parentId },
107
- ...segmentClones.map((segment) => ({ node: segment, parentId: stairClone.id as AnyNodeId })),
108
- ])
109
-
110
- const createdStair = useScene.getState().nodes[stairClone.id as AnyNodeId]
111
- if (!createdStair || createdStair.type !== 'stair') {
112
- throw new Error(`Duplicated stair "${stairClone.id}" was not created`)
113
- }
65
+ if (mode === 'select') {
66
+ const committedId = commitFreshPlacementSubtree(draft.id as AnyNodeId, {})
67
+ const committed = committedId ? useScene.getState().nodes[committedId] : null
68
+ if (committed?.type !== 'stair') {
69
+ throw new Error(`Duplicated stair "${sourceStairId}" could not be committed`)
70
+ }
71
+ if (wasTracking) temporal.resume()
72
+ useViewer.getState().setSelection({ selectedIds: [committed.id] })
73
+ return {
74
+ stair: committed,
75
+ segmentIds: stairSegmentIds(committed),
76
+ }
77
+ }
114
78
 
115
- if (mode === 'select') {
116
- useViewer.getState().setSelection({ selectedIds: [createdStair.id] })
117
- } else {
118
- useViewer.getState().setSelection({ selectedIds: [createdStair.id] })
119
- requestAnimationFrame(() => moveStairWhenRegistered(createdStair.id))
79
+ useViewer.getState().setSelection({ selectedIds: [] })
80
+ useEditor.getState().setMovingNode(draft)
81
+ return {
82
+ stair: draft,
83
+ segmentIds: stairSegmentIds(draft),
84
+ }
85
+ } catch (error) {
86
+ if (draftId && useScene.getState().nodes[draftId]) {
87
+ useScene.getState().deleteNode(draftId)
88
+ }
89
+ if (wasTracking) temporal.resume()
90
+ throw error
120
91
  }
92
+ }
121
93
 
122
- return {
123
- stair: createdStair,
124
- segmentIds: segmentClones.map((segment) => segment.id),
125
- }
94
+ function stairSegmentIds(stair: StairNode): StairSegmentNode['id'][] {
95
+ return (stair.children ?? []).filter((childId): childId is StairSegmentNode['id'] => {
96
+ return useScene.getState().nodes[childId as AnyNodeId]?.type === 'stair-segment'
97
+ })
126
98
  }
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  type AnyNode,
3
3
  type AnyNodeId,
4
+ DEFAULT_LEVEL_HEIGHT,
4
5
  LevelNode,
5
6
  type LevelNode as LevelNodeType,
6
7
  resolveBuildingForLevel,
@@ -154,6 +155,7 @@ export function resolveStairDestinationLevel({
154
155
  if (createMissing && buildingId) {
155
156
  const createdLevel = LevelNode.parse({
156
157
  children: [],
158
+ height: DEFAULT_LEVEL_HEIGHT,
157
159
  level: fromLevel.level + 1,
158
160
  parentId: buildingId,
159
161
  })
@@ -0,0 +1,71 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import type { AnyNode, WallNode } from '@pascal-app/core'
3
+ import { resolveSurfacePlanPointSnap } from './surface-plan-snap'
4
+
5
+ const wall = (id: string, start: [number, number], end: [number, number]): WallNode =>
6
+ ({
7
+ id,
8
+ type: 'wall',
9
+ start,
10
+ end,
11
+ visible: true,
12
+ }) as WallNode
13
+
14
+ const nodesOf = (...walls: WallNode[]): Record<string, AnyNode> =>
15
+ Object.fromEntries(walls.map((node) => [node.id, node as AnyNode]))
16
+
17
+ // The measurement tool passes `magnetic` explicitly so its snapping never
18
+ // depends on the construction snapping-mode chip (whose default 'grid' mode
19
+ // turns `isMagneticSnapActive()` off). These tests pin that seam.
20
+ describe('resolveSurfacePlanPointSnap magnetic override', () => {
21
+ const walls = [wall('wall-a', [0, 0], [4, 0]), wall('wall-b', [0, 0], [0, 4])]
22
+ const nodes = nodesOf(...walls)
23
+
24
+ test('magnetic: true acquires a shared wall corner from the full endpoint radius', () => {
25
+ const result = resolveSurfacePlanPointSnap({
26
+ rawPoint: [0.3, 0.2],
27
+ nodes,
28
+ magnetic: true,
29
+ align: false,
30
+ })
31
+ expect(result.point).toEqual([0, 0])
32
+ expect(result.wallSnap).toBe('endpoint')
33
+ expect(result.wallIds.sort()).toEqual(['wall-a', 'wall-b'])
34
+ })
35
+
36
+ test('magnetic: true acquires a T-junction crossing on a wall body', () => {
37
+ const crossing = [wall('wall-a', [0, 0], [4, 0]), wall('wall-c', [3, -1], [3, 3])]
38
+ const result = resolveSurfacePlanPointSnap({
39
+ rawPoint: [3.05, 0.1],
40
+ nodes: nodesOf(...crossing),
41
+ magnetic: true,
42
+ align: false,
43
+ })
44
+ expect(result.point).toEqual([3, 0])
45
+ expect(result.wallSnap).toBe('intersection')
46
+ })
47
+
48
+ test('magnetic: false keeps the fallback point outside the tight connect radius', () => {
49
+ const result = resolveSurfacePlanPointSnap({
50
+ rawPoint: [0.3, 0.2],
51
+ fallbackPoint: [0.3, 0.2],
52
+ nodes,
53
+ magnetic: false,
54
+ align: false,
55
+ })
56
+ expect(result.point).toEqual([0.3, 0.2])
57
+ expect(result.wallSnap).toBeNull()
58
+ })
59
+
60
+ test('magnetic: false still sticks within the connect radius so ends can meet', () => {
61
+ const result = resolveSurfacePlanPointSnap({
62
+ rawPoint: [0.03, 0.02],
63
+ fallbackPoint: [0.03, 0.02],
64
+ nodes,
65
+ magnetic: false,
66
+ align: false,
67
+ })
68
+ expect(result.point).toEqual([0, 0])
69
+ expect(result.wallSnap).toBe('endpoint')
70
+ })
71
+ })
@@ -0,0 +1,256 @@
1
+ import {
2
+ type AlignmentAnchor,
3
+ type AlignmentGuide,
4
+ type AnyNode,
5
+ collectAlignmentAnchors,
6
+ getWallCurveFrameAt,
7
+ getWallCurveLength,
8
+ isCurvedWall,
9
+ resolveLevelId,
10
+ useScene,
11
+ type WallNode,
12
+ } from '@pascal-app/core'
13
+ import {
14
+ getSegmentGridStep,
15
+ snapWallDraftPointDetailed,
16
+ WALL_CONNECT_SNAP_RADIUS,
17
+ type WallDraftSnapKind,
18
+ type WallPlanPoint,
19
+ type WallSnapRadii,
20
+ } from '../components/tools/wall/wall-drafting'
21
+ import useAlignmentGuides from '../store/use-alignment-guides'
22
+ import { isMagneticSnapActive } from '../store/use-editor'
23
+ import useWallSnapIndicator from '../store/use-wall-snap-indicator'
24
+ import { resolveAlignmentForFloorplanView } from './world-grid-snap'
25
+
26
+ const SURFACE_SNAP_MOVING_ID = '__surface_snap__'
27
+ export const SURFACE_ALIGNMENT_THRESHOLD_M = 0.08
28
+ const SURFACE_WALL_SNAP_RADII = {
29
+ endpoint: 0.38,
30
+ midpoint: 0.28,
31
+ intersection: 0.28,
32
+ wall: 0.18,
33
+ } satisfies WallSnapRadii
34
+ const WALL_SOURCE_MATCH_EPSILON = 0.035
35
+
36
+ export type SurfacePlanSnapInput = {
37
+ rawPoint: WallPlanPoint
38
+ fallbackPoint?: WallPlanPoint
39
+ levelId?: string | null
40
+ excludeId?: string | null
41
+ movingId?: string
42
+ nodes?: Readonly<Record<string, AnyNode>>
43
+ walls?: readonly WallNode[]
44
+ candidates?: readonly AlignmentAnchor[]
45
+ threshold?: number
46
+ magnetic?: boolean
47
+ align?: boolean
48
+ highlightWalls?: boolean
49
+ step?: number
50
+ snapRadii?: WallSnapRadii
51
+ }
52
+
53
+ export type SurfacePlanSnapResult = {
54
+ point: WallPlanPoint
55
+ wallSnap: WallDraftSnapKind | null
56
+ guides: AlignmentGuide[]
57
+ wallIds: string[]
58
+ }
59
+
60
+ export function getLevelWalls(
61
+ nodes: Readonly<Record<string, AnyNode>>,
62
+ levelId: string | null | undefined,
63
+ walls?: readonly WallNode[],
64
+ ): WallNode[] {
65
+ const source =
66
+ walls ?? Object.values(nodes).filter((node): node is WallNode => node.type === 'wall')
67
+ if (!levelId) return source.filter((wall) => wall.visible !== false)
68
+
69
+ return source.filter(
70
+ (wall) =>
71
+ wall.visible !== false && resolveLevelId(wall, nodes as Record<string, AnyNode>) === levelId,
72
+ )
73
+ }
74
+
75
+ function distanceSquared(a: WallPlanPoint, b: WallPlanPoint) {
76
+ const dx = a[0] - b[0]
77
+ const dz = a[1] - b[1]
78
+ return dx * dx + dz * dz
79
+ }
80
+
81
+ function wallMidpoint(wall: WallNode): WallPlanPoint {
82
+ if (isCurvedWall(wall)) {
83
+ const frame = getWallCurveFrameAt(wall, 0.5)
84
+ return [frame.point.x, frame.point.y]
85
+ }
86
+ return [(wall.start[0] + wall.end[0]) / 2, (wall.start[1] + wall.end[1]) / 2]
87
+ }
88
+
89
+ function distanceToSegmentSquared(point: WallPlanPoint, start: WallPlanPoint, end: WallPlanPoint) {
90
+ const dx = end[0] - start[0]
91
+ const dz = end[1] - start[1]
92
+ const lengthSquared = dx * dx + dz * dz
93
+ if (lengthSquared < 1e-9) return distanceSquared(point, start)
94
+
95
+ const t = Math.max(
96
+ 0,
97
+ Math.min(1, ((point[0] - start[0]) * dx + (point[1] - start[1]) * dz) / lengthSquared),
98
+ )
99
+ const projected: WallPlanPoint = [start[0] + dx * t, start[1] + dz * t]
100
+ return distanceSquared(point, projected)
101
+ }
102
+
103
+ function distanceToWallSquared(point: WallPlanPoint, wall: WallNode) {
104
+ if (!isCurvedWall(wall)) {
105
+ return distanceToSegmentSquared(point, wall.start, wall.end)
106
+ }
107
+
108
+ const sampleCount = Math.max(8, Math.ceil(getWallCurveLength(wall) / 0.3))
109
+ let bestDistanceSquared = Number.POSITIVE_INFINITY
110
+ let previous = getWallCurveFrameAt(wall, 0).point
111
+ for (let index = 1; index <= sampleCount; index += 1) {
112
+ const current = getWallCurveFrameAt(wall, index / sampleCount).point
113
+ const distance = distanceToSegmentSquared(
114
+ point,
115
+ [previous.x, previous.y],
116
+ [current.x, current.y],
117
+ )
118
+ bestDistanceSquared = Math.min(bestDistanceSquared, distance)
119
+ previous = current
120
+ }
121
+ return bestDistanceSquared
122
+ }
123
+
124
+ function closestWallIds(point: WallPlanPoint, walls: readonly WallNode[], count: number) {
125
+ return walls
126
+ .map((wall) => ({ id: wall.id, distance: distanceToWallSquared(point, wall) }))
127
+ .sort((a, b) => a.distance - b.distance)
128
+ .slice(0, count)
129
+ .map(({ id }) => id)
130
+ }
131
+
132
+ function findSnapSourceWallIds(
133
+ point: WallPlanPoint,
134
+ kind: WallDraftSnapKind,
135
+ walls: readonly WallNode[],
136
+ ): string[] {
137
+ const epsilonSquared = WALL_SOURCE_MATCH_EPSILON ** 2
138
+
139
+ if (kind === 'endpoint') {
140
+ const endpointMatches = walls.filter(
141
+ (wall) =>
142
+ distanceSquared(point, wall.start) <= epsilonSquared ||
143
+ distanceSquared(point, wall.end) <= epsilonSquared,
144
+ )
145
+ if (endpointMatches.length > 0) return endpointMatches.map((wall) => wall.id)
146
+ return closestWallIds(point, walls, 1)
147
+ }
148
+
149
+ if (kind === 'midpoint') {
150
+ const midpointMatches = walls.filter(
151
+ (wall) => distanceSquared(point, wallMidpoint(wall)) <= epsilonSquared,
152
+ )
153
+ if (midpointMatches.length > 0) return midpointMatches.map((wall) => wall.id)
154
+ return closestWallIds(point, walls, 1)
155
+ }
156
+
157
+ if (kind === 'intersection') {
158
+ const crossingMatches = walls.filter(
159
+ (wall) => distanceToWallSquared(point, wall) <= epsilonSquared,
160
+ )
161
+ if (crossingMatches.length > 0) return crossingMatches.map((wall) => wall.id).slice(0, 2)
162
+ return closestWallIds(point, walls, 2)
163
+ }
164
+
165
+ return closestWallIds(point, walls, 1)
166
+ }
167
+
168
+ export function clearSurfacePlanSnapFeedback() {
169
+ useAlignmentGuides.getState().clear()
170
+ useWallSnapIndicator.getState().clear()
171
+ }
172
+
173
+ export function resolveSurfacePlanPointSnap(input: SurfacePlanSnapInput): SurfacePlanSnapResult {
174
+ const nodes = input.nodes ?? useScene.getState().nodes
175
+ const walls = getLevelWalls(nodes, input.levelId, input.walls)
176
+ const fallbackPoint = input.fallbackPoint
177
+ const magnetic = input.magnetic ?? isMagneticSnapActive()
178
+
179
+ const wallSnap = snapWallDraftPointDetailed({
180
+ point: input.rawPoint,
181
+ walls,
182
+ step: input.step ?? getSegmentGridStep(),
183
+ magnetic,
184
+ snapRadii: input.snapRadii ?? SURFACE_WALL_SNAP_RADII,
185
+ gridSnap: fallbackPoint ? () => fallbackPoint : undefined,
186
+ })
187
+
188
+ if (wallSnap.snap) {
189
+ const wallIds =
190
+ input.highlightWalls === false
191
+ ? []
192
+ : findSnapSourceWallIds(wallSnap.point, wallSnap.snap, walls)
193
+ useWallSnapIndicator.getState().set({
194
+ x: wallSnap.point[0],
195
+ z: wallSnap.point[1],
196
+ kind: wallSnap.snap,
197
+ ...(wallIds.length > 0 ? { wallIds } : {}),
198
+ })
199
+ useAlignmentGuides.getState().clear()
200
+ return { point: wallSnap.point, wallSnap: wallSnap.snap, guides: [], wallIds }
201
+ }
202
+
203
+ useWallSnapIndicator.getState().clear()
204
+
205
+ // Alignment "lines" are DISPLAYED in every mode (grid / lines / angles /
206
+ // off); the magnetic pull onto an axis is applied only when magnetic
207
+ // ('lines'). `align: false` fully bypasses (no guides).
208
+ const basePoint = fallbackPoint ?? wallSnap.point
209
+ if (input.align === false) {
210
+ useAlignmentGuides.getState().clear()
211
+ return { point: basePoint, wallSnap: null, guides: [], wallIds: [] }
212
+ }
213
+
214
+ const movingId = input.movingId ?? SURFACE_SNAP_MOVING_ID
215
+ const allCandidates =
216
+ input.candidates ??
217
+ collectAlignmentAnchors(nodes, input.excludeId ?? movingId, input.levelId ?? null)
218
+
219
+ // In non-magnetic modes nothing pulls the point onto a guide, so restrict
220
+ // alignment to anchors already within connect distance — a corner then reads
221
+ // no differently from any other nearby point, and far corners stop lighting up
222
+ // from across the plan. Magnetic ('lines') keeps the full-range guides since
223
+ // its snap closes the gap. Mirrors the wall draft/endpoint tools. Filtering the
224
+ // candidates (both local-frame, like `basePoint`) rather than the resolved
225
+ // guides avoids the floor-plan view rotation baked into the guide coords.
226
+ const candidates = magnetic
227
+ ? allCandidates
228
+ : allCandidates.filter(
229
+ (candidate) =>
230
+ distanceSquared(basePoint, [candidate.x, candidate.z]) <= WALL_CONNECT_SNAP_RADIUS ** 2,
231
+ )
232
+
233
+ if (candidates.length === 0) {
234
+ useAlignmentGuides.getState().clear()
235
+ return { point: basePoint, wallSnap: null, guides: [], wallIds: [] }
236
+ }
237
+
238
+ const alignment = resolveAlignmentForFloorplanView({
239
+ moving: [{ nodeId: movingId, kind: 'corner', x: basePoint[0], z: basePoint[1] }],
240
+ candidates,
241
+ threshold: input.threshold ?? SURFACE_ALIGNMENT_THRESHOLD_M,
242
+ })
243
+
244
+ useAlignmentGuides.getState().set(alignment.guides)
245
+
246
+ if (!alignment.snap || !magnetic) {
247
+ return { point: basePoint, wallSnap: null, guides: alignment.guides, wallIds: [] }
248
+ }
249
+
250
+ return {
251
+ point: [basePoint[0] + alignment.snap.dx, basePoint[1] + alignment.snap.dz],
252
+ wallSnap: null,
253
+ guides: alignment.guides,
254
+ wallIds: [],
255
+ }
256
+ }