@pascal-app/editor 0.8.0 → 0.9.2

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 (350) hide show
  1. package/package.json +15 -13
  2. package/src/components/editor/alignment-3d-guide-layer.tsx +168 -0
  3. package/src/components/editor/bake-exporter.tsx +47 -0
  4. package/src/components/editor/custom-camera-controls.tsx +795 -28
  5. package/src/components/editor/editor-layout-mobile.tsx +8 -3
  6. package/src/components/editor/editor-layout-v2.tsx +76 -51
  7. package/src/components/editor/export-manager.tsx +69 -32
  8. package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
  9. package/src/components/editor/first-person/build-collider-world.test.ts +182 -0
  10. package/src/components/editor/first-person/build-collider-world.ts +228 -83
  11. package/src/components/editor/first-person-controls.tsx +943 -27
  12. package/src/components/editor/floating-action-menu.tsx +676 -208
  13. package/src/components/editor/floating-building-action-menu.tsx +2 -2
  14. package/src/components/editor/floorplan-background-selection.test.ts +58 -0
  15. package/src/components/editor/floorplan-background-selection.ts +3 -2
  16. package/src/components/editor/floorplan-panel.tsx +6916 -12137
  17. package/src/components/editor/grid.tsx +197 -36
  18. package/src/components/editor/group-actions.ts +457 -0
  19. package/src/components/editor/group-floating-action-menu.tsx +130 -0
  20. package/src/components/editor/group-move-3d.ts +397 -0
  21. package/src/components/editor/group-rotate-handle.tsx +420 -0
  22. package/src/components/editor/group-selection-box-3d.tsx +173 -0
  23. package/src/components/editor/group-transform-shared.test.ts +396 -0
  24. package/src/components/editor/group-transform-shared.ts +489 -0
  25. package/src/components/editor/handles/handle-arrow.tsx +581 -0
  26. package/src/components/editor/handles/use-handle-drag.ts +246 -0
  27. package/src/components/editor/index.tsx +412 -162
  28. package/src/components/editor/measurement-pill.tsx +115 -0
  29. package/src/components/editor/node-action-menu.tsx +14 -1
  30. package/src/components/editor/node-arrow-handles.tsx +1646 -0
  31. package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
  32. package/src/components/editor/quick-measurement-card.tsx +70 -0
  33. package/src/components/editor/quick-measurement-hud.tsx +28 -0
  34. package/src/components/editor/riser-diagram-panel.tsx +137 -0
  35. package/src/components/editor/selection-manager.tsx +1106 -483
  36. package/src/components/editor/site-edge-labels.tsx +45 -21
  37. package/src/components/editor/slab-hole-highlights.tsx +474 -0
  38. package/src/components/editor/snapshot-capture-overlay.tsx +369 -155
  39. package/src/components/editor/three-context-bridge.ts +22 -0
  40. package/src/components/editor/thumbnail-generator.tsx +140 -41
  41. package/src/components/editor/use-floorplan-background-placement.ts +190 -35
  42. package/src/components/editor/use-floorplan-hit-testing.ts +24 -0
  43. package/src/components/editor/use-floorplan-scene-data.ts +19 -7
  44. package/src/components/editor/use-mesh-settle-epoch.ts +26 -0
  45. package/src/components/editor/wall-measurement-label.tsx +14 -19
  46. package/src/components/editor/wall-move-side-handles.tsx +870 -0
  47. package/src/components/editor/wall-opening-highlights.tsx +181 -0
  48. package/src/components/editor/wall-snap-beacon-layer.tsx +304 -0
  49. package/src/components/editor-2d/floorplan-action-menu-layer.tsx +15 -6
  50. package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +142 -0
  51. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +5 -3
  52. package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
  53. package/src/components/editor-2d/floorplan-group-move.tsx +701 -0
  54. package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
  55. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1656 -0
  56. package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
  57. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +392 -0
  58. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +857 -0
  59. package/src/components/editor-2d/floorplan-render-context.tsx +62 -0
  60. package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +77 -0
  61. package/src/components/editor-2d/floorplan-wall-move-ghost-layer.tsx +47 -0
  62. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +266 -0
  63. package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +15 -0
  64. package/src/components/editor-2d/renderers/floorplan-label-angle.ts +14 -0
  65. package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -66
  66. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +62 -0
  67. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +308 -0
  68. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +3325 -0
  69. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +24 -9
  70. package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
  71. package/src/components/editor-2d/svg-paths.ts +10 -2
  72. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +439 -57
  73. package/src/components/systems/ceiling/ceiling-system.tsx +109 -12
  74. package/src/components/systems/roof/roof-edit-system.tsx +1851 -22
  75. package/src/components/systems/selection-affordance-manager.tsx +38 -0
  76. package/src/components/systems/stair/stair-edit-system.tsx +1 -1
  77. package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
  78. package/src/components/systems/zone/zone-system.tsx +42 -13
  79. package/src/components/tools/elevator/elevator-defaults.ts +8 -0
  80. package/src/components/tools/elevator/elevator-tool.tsx +375 -0
  81. package/src/components/tools/elevator/move-elevator-tool.tsx +263 -0
  82. package/src/components/tools/fence/fence-drafting.ts +141 -27
  83. package/src/components/tools/item/move-tool.tsx +45 -105
  84. package/src/components/tools/item/placement-math.test.ts +22 -0
  85. package/src/components/tools/item/placement-math.ts +59 -7
  86. package/src/components/tools/item/placement-strategies.ts +432 -16
  87. package/src/components/tools/item/placement-types.ts +22 -1
  88. package/src/components/tools/item/use-draft-node.ts +51 -2
  89. package/src/components/tools/item/use-placement-coordinator.tsx +1399 -256
  90. package/src/components/tools/registry/move-registry-node-tool.tsx +1064 -0
  91. package/src/components/tools/roof/roof-tool.tsx +350 -28
  92. package/src/components/tools/select/box-select-state.ts +88 -0
  93. package/src/components/tools/select/box-select-tool.tsx +442 -520
  94. package/src/components/tools/select/marquee-geometry.test.ts +161 -0
  95. package/src/components/tools/select/marquee-geometry.ts +155 -0
  96. package/src/components/tools/select/plane-box-select-tool.tsx +570 -0
  97. package/src/components/tools/select/screen-rectangle-selection.ts +84 -0
  98. package/src/components/tools/select/select-candidates.test.ts +109 -0
  99. package/src/components/tools/select/select-candidates.ts +85 -0
  100. package/src/components/tools/shared/affordance-dispatch.ts +30 -0
  101. package/src/components/tools/shared/cursor-sphere.tsx +62 -26
  102. package/src/components/tools/shared/drag-bounding-box.tsx +195 -0
  103. package/src/components/tools/shared/facing-indicator.tsx +87 -0
  104. package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
  105. package/src/components/tools/shared/floor-stack-preview.ts +25 -0
  106. package/src/components/tools/shared/fresh-placement-visibility.ts +61 -0
  107. package/src/components/tools/shared/placement-box-geometry.ts +123 -0
  108. package/src/components/tools/shared/placement-box.tsx +305 -0
  109. package/src/components/tools/shared/polygon-editor.tsx +866 -175
  110. package/src/components/tools/shared/segment-angle.ts +56 -0
  111. package/src/components/tools/site/site-boundary-editor.tsx +520 -16
  112. package/src/components/tools/site/site-flag-model.ts +18 -0
  113. package/src/components/tools/stair/stair-defaults.ts +2 -1
  114. package/src/components/tools/stair/stair-tool.tsx +342 -37
  115. package/src/components/tools/tool-manager.tsx +239 -62
  116. package/src/components/tools/wall/wall-drafting.test.ts +330 -0
  117. package/src/components/tools/wall/wall-drafting.ts +248 -171
  118. package/src/components/tools/wall/wall-snap-geometry.test.ts +123 -0
  119. package/src/components/tools/wall/wall-snap-geometry.ts +331 -0
  120. package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
  121. package/src/components/tools/zone/zone-tool.tsx +86 -82
  122. package/src/components/ui/action-menu/action-button.tsx +21 -1
  123. package/src/components/ui/action-menu/camera-actions.tsx +24 -17
  124. package/src/components/ui/action-menu/control-modes.tsx +40 -170
  125. package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
  126. package/src/components/ui/action-menu/index.tsx +3 -117
  127. package/src/components/ui/action-menu/measurement-control.tsx +188 -0
  128. package/src/components/ui/action-menu/structure-tools.tsx +27 -93
  129. package/src/components/ui/action-menu/view-toggles.tsx +279 -90
  130. package/src/components/ui/command-palette/editor-commands.tsx +20 -15
  131. package/src/components/ui/command-palette/index.tsx +5 -3
  132. package/src/components/ui/controls/material-paint-panel.tsx +152 -0
  133. package/src/components/ui/controls/material-picker.tsx +87 -177
  134. package/src/components/ui/controls/material-properties-editor.tsx +108 -0
  135. package/src/components/ui/controls/metric-control.tsx +142 -46
  136. package/src/components/ui/controls/scene-material-list.tsx +247 -0
  137. package/src/components/ui/controls/slider-control.tsx +88 -38
  138. package/src/components/ui/floating-level-selector.tsx +47 -10
  139. package/src/components/ui/helpers/building-helper.tsx +10 -23
  140. package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
  141. package/src/components/ui/helpers/helper-manager.tsx +235 -23
  142. package/src/components/ui/helpers/item-helper.tsx +28 -32
  143. package/src/components/ui/helpers/registered-tool-helper.tsx +59 -0
  144. package/src/components/ui/helpers/roof-helper.tsx +10 -12
  145. package/src/components/ui/icon-ref.tsx +47 -0
  146. package/src/components/ui/item-catalog/catalog-items.tsx +40 -1
  147. package/src/components/ui/item-catalog/item-catalog.tsx +16 -36
  148. package/src/components/ui/level-duplicate-dialog.tsx +2 -1
  149. package/src/components/ui/panels/node-display.ts +17 -16
  150. package/src/components/ui/panels/panel-manager.tsx +55 -58
  151. package/src/components/ui/panels/panel-wrapper.tsx +238 -12
  152. package/src/components/ui/panels/parametric-inspector.tsx +482 -0
  153. package/src/components/ui/panels/reference-panel.tsx +54 -19
  154. package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
  155. package/src/components/ui/primitives/shortcut-token.tsx +39 -3
  156. package/src/components/ui/primitives/sidebar.tsx +1 -0
  157. package/src/components/ui/scene-loader.tsx +1 -3
  158. package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
  159. package/src/components/ui/sidebar/icon-rail.tsx +50 -31
  160. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +263 -0
  161. package/src/components/ui/sidebar/panels/items-panel/index.tsx +82 -8
  162. package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
  163. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +122 -41
  164. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +66 -7
  165. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +267 -0
  166. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -1
  167. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +9 -5
  168. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +86 -0
  169. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
  170. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
  171. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +92 -0
  172. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +75 -0
  173. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +3 -3
  174. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +86 -0
  175. package/src/components/ui/sidebar/panels/site-panel/index.tsx +42 -21
  176. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
  177. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -1
  178. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +140 -0
  179. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +48 -8
  180. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +132 -0
  181. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +9 -5
  182. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +86 -0
  183. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +7 -1
  184. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
  185. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +112 -39
  186. package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
  187. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
  188. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
  189. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
  190. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +51 -2
  191. package/src/components/ui/sidebar/tab-bar.tsx +75 -1
  192. package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
  193. package/src/components/ui/snap-target-badge.test.tsx +40 -0
  194. package/src/components/ui/snap-target-badge.tsx +88 -0
  195. package/src/components/viewer-overlay.tsx +253 -61
  196. package/src/components/viewer-zone-system.tsx +7 -2
  197. package/src/hooks/use-auto-save.test.ts +14 -0
  198. package/src/hooks/use-auto-save.ts +64 -10
  199. package/src/hooks/use-ceiling-events.ts +176 -0
  200. package/src/hooks/use-drag-action.ts +127 -0
  201. package/src/hooks/use-keyboard.ts +493 -95
  202. package/src/hooks/use-selection.ts +64 -0
  203. package/src/index.tsx +451 -6
  204. package/src/lib/active-placement-surface.ts +35 -0
  205. package/src/lib/ceiling-plan-snap.ts +24 -0
  206. package/src/lib/constants.ts +6 -2
  207. package/src/lib/contextual-help.test.ts +112 -0
  208. package/src/lib/contextual-help.ts +144 -0
  209. package/src/lib/continuation.ts +64 -0
  210. package/src/lib/direct-manipulation.test.ts +192 -0
  211. package/src/lib/direct-manipulation.ts +109 -0
  212. package/src/lib/editor-api.ts +50 -0
  213. package/src/lib/elevator-support.ts +96 -0
  214. package/src/lib/floorplan/apply-alignment.test.ts +25 -0
  215. package/src/lib/floorplan/apply-alignment.ts +117 -0
  216. package/src/lib/floorplan/floorplan-export.tsx +364 -0
  217. package/src/lib/floorplan/geometry.ts +53 -0
  218. package/src/lib/floorplan/index.ts +10 -0
  219. package/src/lib/floorplan/items.ts +5 -2
  220. package/src/lib/floorplan/plan-coords.ts +21 -0
  221. package/src/lib/floorplan/selection-tool.ts +40 -0
  222. package/src/lib/floorplan/stairs.ts +12 -1
  223. package/src/lib/fresh-planar-placement.test.ts +339 -0
  224. package/src/lib/fresh-planar-placement.ts +130 -0
  225. package/src/lib/glb-export.test.ts +441 -0
  226. package/src/lib/glb-export.ts +874 -0
  227. package/src/lib/history.ts +11 -1
  228. package/src/lib/interaction/hot-set.test.ts +133 -0
  229. package/src/lib/interaction/hot-set.ts +67 -0
  230. package/src/lib/interaction/overlay-policy.test.ts +51 -0
  231. package/src/lib/interaction/overlay-policy.ts +59 -0
  232. package/src/lib/interaction/scope.ts +211 -0
  233. package/src/lib/level-duplication.ts +54 -42
  234. package/src/lib/level-selection.ts +2 -2
  235. package/src/lib/material-paint.ts +227 -48
  236. package/src/lib/measurement-kind.test.ts +30 -0
  237. package/src/lib/measurement-kind.ts +19 -0
  238. package/src/lib/measurement-label.test.ts +47 -0
  239. package/src/lib/measurement-label.ts +68 -0
  240. package/src/lib/measurement-parser.ts +116 -0
  241. package/src/lib/measurements.test.ts +183 -0
  242. package/src/lib/measurements.ts +199 -0
  243. package/src/lib/paint-scope.test.ts +454 -0
  244. package/src/lib/paint-scope.ts +461 -0
  245. package/src/lib/placement-drag-release.ts +23 -0
  246. package/src/lib/placement-metadata.ts +29 -0
  247. package/src/lib/planar-cursor-placement.test.ts +100 -0
  248. package/src/lib/planar-cursor-placement.ts +42 -0
  249. package/src/lib/plugin-panels.test.ts +19 -0
  250. package/src/lib/plugin-panels.ts +91 -0
  251. package/src/lib/quick-measurement.test.ts +77 -0
  252. package/src/lib/quick-measurement.ts +109 -0
  253. package/src/lib/roof-duplication.ts +7 -7
  254. package/src/lib/roof-hover-outline-proxy.ts +22 -0
  255. package/src/lib/roof-wall-hit.ts +164 -0
  256. package/src/lib/scene-clipboard.test.ts +196 -0
  257. package/src/lib/scene-clipboard.ts +332 -0
  258. package/src/lib/scene.ts +40 -8
  259. package/src/lib/selection-routing.test.ts +298 -0
  260. package/src/lib/selection-routing.ts +175 -0
  261. package/src/lib/sfx/index.ts +1 -0
  262. package/src/lib/sfx/movement-tick.test.ts +65 -0
  263. package/src/lib/sfx/movement-tick.ts +18 -0
  264. package/src/lib/sfx-bus.ts +69 -10
  265. package/src/lib/sfx-player.test.ts +124 -0
  266. package/src/lib/sfx-player.ts +148 -55
  267. package/src/lib/site-boundary.ts +1 -0
  268. package/src/lib/slab-plan-snap.test.ts +93 -0
  269. package/src/lib/slab-plan-snap.ts +108 -0
  270. package/src/lib/snapping-mode.test.ts +138 -0
  271. package/src/lib/snapping-mode.ts +179 -0
  272. package/src/lib/stair-duplication.ts +4 -4
  273. package/src/lib/stair-levels.test.ts +146 -0
  274. package/src/lib/stair-levels.ts +177 -0
  275. package/src/lib/surface-plan-snap.test.ts +71 -0
  276. package/src/lib/surface-plan-snap.ts +256 -0
  277. package/src/lib/use-linear-display.ts +40 -0
  278. package/src/lib/world-grid-snap.ts +231 -0
  279. package/src/lib/zone-content.ts +130 -0
  280. package/src/store/use-alignment-guides.ts +21 -0
  281. package/src/store/use-direct-manipulation-feedback.ts +20 -0
  282. package/src/store/use-editor.tsx +767 -114
  283. package/src/store/use-facing-pose.ts +44 -0
  284. package/src/store/use-fence-curve-draft.ts +21 -0
  285. package/src/store/use-floorplan-draft-preview.ts +101 -0
  286. package/src/store/use-floorplan-marquee.ts +50 -0
  287. package/src/store/use-interaction-scope.test.ts +186 -0
  288. package/src/store/use-interaction-scope.ts +132 -0
  289. package/src/store/use-measurement-draft.test.ts +530 -0
  290. package/src/store/use-measurement-draft.ts +543 -0
  291. package/src/store/use-opening-guides.ts +41 -0
  292. package/src/store/use-placement-preview.ts +39 -0
  293. package/src/store/use-quick-measurement-hud.test.ts +53 -0
  294. package/src/store/use-quick-measurement-hud.ts +77 -0
  295. package/src/store/use-segment-draft-chain.ts +28 -0
  296. package/src/store/use-stair-build-preview.ts +43 -0
  297. package/src/store/use-wall-move-ghosts.ts +36 -0
  298. package/src/store/use-wall-snap-indicator.ts +34 -0
  299. package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
  300. package/src/components/editor/preset-thumbnail-generator.tsx +0 -125
  301. package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +0 -113
  302. package/src/components/tools/building/move-building-tool.tsx +0 -157
  303. package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +0 -43
  304. package/src/components/tools/ceiling/ceiling-hole-editor.tsx +0 -49
  305. package/src/components/tools/ceiling/ceiling-tool.tsx +0 -465
  306. package/src/components/tools/ceiling/move-ceiling-tool.tsx +0 -264
  307. package/src/components/tools/column/column-tool.tsx +0 -97
  308. package/src/components/tools/column/move-column-tool.tsx +0 -105
  309. package/src/components/tools/door/door-math.ts +0 -110
  310. package/src/components/tools/door/door-tool.tsx +0 -324
  311. package/src/components/tools/door/move-door-tool.tsx +0 -412
  312. package/src/components/tools/fence/curve-fence-tool.tsx +0 -178
  313. package/src/components/tools/fence/fence-tool.tsx +0 -346
  314. package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -441
  315. package/src/components/tools/fence/move-fence-tool.tsx +0 -302
  316. package/src/components/tools/item/item-tool.tsx +0 -26
  317. package/src/components/tools/roof/move-roof-tool.tsx +0 -364
  318. package/src/components/tools/slab/move-slab-tool.tsx +0 -182
  319. package/src/components/tools/slab/slab-boundary-editor.tsx +0 -43
  320. package/src/components/tools/slab/slab-hole-editor.tsx +0 -49
  321. package/src/components/tools/slab/slab-tool.tsx +0 -322
  322. package/src/components/tools/spawn/move-spawn-tool.tsx +0 -101
  323. package/src/components/tools/spawn/spawn-tool.tsx +0 -130
  324. package/src/components/tools/wall/curve-wall-tool.tsx +0 -178
  325. package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -426
  326. package/src/components/tools/wall/move-wall-tool.tsx +0 -358
  327. package/src/components/tools/wall/wall-tool.tsx +0 -332
  328. package/src/components/tools/window/move-window-tool.tsx +0 -447
  329. package/src/components/tools/window/window-math.ts +0 -117
  330. package/src/components/tools/window/window-tool.tsx +0 -332
  331. package/src/components/ui/helpers/ceiling-helper.tsx +0 -20
  332. package/src/components/ui/helpers/slab-helper.tsx +0 -20
  333. package/src/components/ui/helpers/wall-helper.tsx +0 -20
  334. package/src/components/ui/panels/ceiling-panel.tsx +0 -249
  335. package/src/components/ui/panels/column-panel.tsx +0 -759
  336. package/src/components/ui/panels/door-panel.tsx +0 -1299
  337. package/src/components/ui/panels/fence-panel.tsx +0 -222
  338. package/src/components/ui/panels/item-panel.tsx +0 -306
  339. package/src/components/ui/panels/paint-panel.tsx +0 -163
  340. package/src/components/ui/panels/presets/presets-popover.tsx +0 -511
  341. package/src/components/ui/panels/roof-panel.tsx +0 -280
  342. package/src/components/ui/panels/roof-segment-panel.tsx +0 -311
  343. package/src/components/ui/panels/slab-panel.tsx +0 -250
  344. package/src/components/ui/panels/spawn-panel.tsx +0 -161
  345. package/src/components/ui/panels/stair-panel.tsx +0 -577
  346. package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
  347. package/src/components/ui/panels/wall-panel.tsx +0 -273
  348. package/src/components/ui/panels/window-panel.tsx +0 -968
  349. package/src/contexts/presets-context.tsx +0 -121
  350. package/src/hooks/use-contextual-tools.ts +0 -61
@@ -0,0 +1,44 @@
1
+ // Ephemeral store for the forward-facing floor triangle shown while placing or
2
+ // moving a node. A single editor-side overlay (`<FacingPoseIndicator>`)
3
+ // subscribes and renders the triangle; every placement/move path publishes its
4
+ // ghost pose here instead of drawing its own triangle. This is deliberately the
5
+ // one renderer for the facing indicator: rendering it from inside a tool's own
6
+ // cursor ghost (especially tools living in `@pascal-app/nodes`) left it
7
+ // invisible, while the editor-side overlay renders reliably. Producers clear on
8
+ // commit, cancel, and unmount.
9
+ //
10
+ // Poses are in the same building-local frame the tools already work in (the
11
+ // overlay is mounted inside ToolManager's building-local group).
12
+
13
+ import { create } from 'zustand'
14
+
15
+ export type FacingPose = {
16
+ /** Ghost origin in building-local space. */
17
+ position: [number, number, number]
18
+ /** Ghost yaw (radians). The triangle inherits this so it points where the
19
+ * node faces. */
20
+ rotationY: number
21
+ /** Footprint depth along the ghost's local +Z; the triangle sits just past
22
+ * `center[1] + depth / 2`. */
23
+ depth: number
24
+ /** Footprint centre offset `[x, z]` in the ghost's local frame. Defaults to
25
+ * the origin. Kinds whose forward edge isn't centred on the origin (e.g. a
26
+ * stair, whose run starts at the entry) shift the triangle via this. */
27
+ center?: [number, number]
28
+ /** Point along local -Z (the front is the -Z side) instead of +Z. */
29
+ reversed?: boolean
30
+ }
31
+
32
+ type FacingPoseState = {
33
+ pose: FacingPose | null
34
+ set(pose: FacingPose): void
35
+ clear(): void
36
+ }
37
+
38
+ const useFacingPose = create<FacingPoseState>((set) => ({
39
+ pose: null,
40
+ set: (pose) => set({ pose }),
41
+ clear: () => set({ pose: null }),
42
+ }))
43
+
44
+ export default useFacingPose
@@ -0,0 +1,21 @@
1
+ // Ephemeral store: how many points the in-progress curved-fence draft has
2
+ // placed. Written by the 3D spline draft tool (`@pascal-app/nodes`
3
+ // fence/tool.tsx) and read by the contextual helper so the "finish curve" hint
4
+ // only surfaces once the user has actually started drawing. Reset on commit,
5
+ // cancel, and unmount — never persisted, never in undo history.
6
+
7
+ import { create } from 'zustand'
8
+
9
+ type FenceCurveDraftState = {
10
+ pointCount: number
11
+ setPointCount(count: number): void
12
+ reset(): void
13
+ }
14
+
15
+ const useFenceCurveDraft = create<FenceCurveDraftState>((set) => ({
16
+ pointCount: 0,
17
+ setPointCount: (count) => set({ pointCount: count }),
18
+ reset: () => set({ pointCount: 0 }),
19
+ }))
20
+
21
+ export default useFenceCurveDraft
@@ -0,0 +1,101 @@
1
+ // Ephemeral store for the 2D floor-plan's in-flight DRAFT preview state — the
2
+ // hot, per-pointer-move values every build/edit tool republishes on `grid:move`
3
+ // (the snapped cursor point today; wall/fence/roof draft endpoints as later
4
+ // slices land here). It exists so those per-move updates DON'T live in
5
+ // `FloorplanPanel`'s own `useState`: the panel is a ~10k-line component whose
6
+ // render costs ~120-220ms, so a `setState` per move made every 2D draft tool
7
+ // feel laggy. Producers write via `getState().setX(...)` (no panel re-render);
8
+ // the small overlay leaves subscribe and re-render alone. Same pattern that
9
+ // keeps stair / column / elevator placement smooth (`useStairBuildPreview`,
10
+ // `usePlacementPreview`).
11
+ //
12
+ // Editor-only. Producers clear on tool-inactive, commit, and unmount.
13
+
14
+ import type { WallPlanPoint } from '@pascal-app/core'
15
+ import { create } from 'zustand'
16
+
17
+ /** Screen-space (SVG-local px) cursor point — drives the coordinate badge. */
18
+ type SvgPoint = { x: number; y: number }
19
+
20
+ type FloorplanDraftPreviewState = {
21
+ /** Snapped plan-XZ point under the cursor; drives the crosshair + the
22
+ * cursor-following polygon-draft preview. `null` when idle. */
23
+ cursorPoint: WallPlanPoint | null
24
+ /** Screen-space cursor point driving the coordinate-indicator badge. Set on
25
+ * every SVG `pointermove` while a build/select tool is active, so it's the
26
+ * single hottest 2D update — keeping it out of panel state is what stops the
27
+ * panel re-rendering per move. `null` when idle. */
28
+ cursorPosition: SvgPoint | null
29
+ /** Live END point of the open wall / fence / roof draft segment — the per-move
30
+ * endpoint that drives the 2D draft polygon + measurement. Each is `null`
31
+ * unless that tool's draft is open. The START points stay in panel state
32
+ * (set per click, low-frequency). */
33
+ wallDraftEnd: WallPlanPoint | null
34
+ fenceDraftEnd: WallPlanPoint | null
35
+ roofDraftEnd: WallPlanPoint | null
36
+ /** Set the snapped cursor point. No-ops (skips the store update, so
37
+ * subscribers don't re-render) when unchanged — `grid:move` fires far more
38
+ * often than the snapped cell actually changes. */
39
+ setCursorPoint(point: WallPlanPoint | null): void
40
+ /** Set the screen-space cursor point (deduped on x/y). */
41
+ setCursorPosition(point: SvgPoint | null): void
42
+ setWallDraftEnd(point: WallPlanPoint | null): void
43
+ setFenceDraftEnd(point: WallPlanPoint | null): void
44
+ setRoofDraftEnd(point: WallPlanPoint | null): void
45
+ reset(): void
46
+ }
47
+
48
+ function setPlanPointField(
49
+ field: 'wallDraftEnd' | 'fenceDraftEnd' | 'roofDraftEnd',
50
+ point: WallPlanPoint | null,
51
+ ) {
52
+ return (
53
+ state: FloorplanDraftPreviewState,
54
+ ): Partial<FloorplanDraftPreviewState> | typeof state => {
55
+ const prev = state[field]
56
+ if (!point && !prev) return state
57
+ if (point && prev && prev[0] === point[0] && prev[1] === point[1]) return state
58
+ return { [field]: point }
59
+ }
60
+ }
61
+
62
+ export const useFloorplanDraftPreview = create<FloorplanDraftPreviewState>((set) => ({
63
+ cursorPoint: null,
64
+ cursorPosition: null,
65
+ wallDraftEnd: null,
66
+ fenceDraftEnd: null,
67
+ roofDraftEnd: null,
68
+ setCursorPoint: (point) =>
69
+ set((state) => {
70
+ const prev = state.cursorPoint
71
+ if (!point && !prev) return state
72
+ if (point && prev && prev[0] === point[0] && prev[1] === point[1]) return state
73
+ return { cursorPoint: point }
74
+ }),
75
+ setCursorPosition: (point) =>
76
+ set((state) => {
77
+ const prev = state.cursorPosition
78
+ if (!point && !prev) return state
79
+ if (point && prev && prev.x === point.x && prev.y === point.y) return state
80
+ return { cursorPosition: point }
81
+ }),
82
+ setWallDraftEnd: (point) => set(setPlanPointField('wallDraftEnd', point)),
83
+ setFenceDraftEnd: (point) => set(setPlanPointField('fenceDraftEnd', point)),
84
+ setRoofDraftEnd: (point) => set(setPlanPointField('roofDraftEnd', point)),
85
+ reset: () =>
86
+ set((state) =>
87
+ state.cursorPoint === null &&
88
+ state.cursorPosition === null &&
89
+ state.wallDraftEnd === null &&
90
+ state.fenceDraftEnd === null &&
91
+ state.roofDraftEnd === null
92
+ ? state
93
+ : {
94
+ cursorPoint: null,
95
+ cursorPosition: null,
96
+ wallDraftEnd: null,
97
+ fenceDraftEnd: null,
98
+ roofDraftEnd: null,
99
+ },
100
+ ),
101
+ }))
@@ -0,0 +1,50 @@
1
+ // Ephemeral store for the 2D floor-plan marquee (box-select) drag — the hot,
2
+ // per-pointer-move rectangle the select tool republishes on every move. It
3
+ // lives here, not in `FloorplanPanel`'s `useState`, for the same reason as
4
+ // `useFloorplanDraftPreview`: the panel is a ~10k-line component whose render
5
+ // costs ~120-220ms, so a `setState` per move made dragging a selection box
6
+ // re-render the whole panel. Producers write via `getState()` (no panel
7
+ // re-render); the small marquee overlay leaf subscribes and re-renders alone.
8
+ //
9
+ // The whole drag struct lives here (not just the moving corner) so the
10
+ // pointer-move / -up handlers read it non-reactively via `getState()` and the
11
+ // panel never subscribes. Editor-only; reset on pointer-up / cancel /
12
+ // tool-inactive.
13
+
14
+ import type { WallPlanPoint } from '@pascal-app/core'
15
+ import { create } from 'zustand'
16
+
17
+ export type FloorplanMarqueeDrag = {
18
+ pointerId: number
19
+ startClientX: number
20
+ startClientY: number
21
+ startPlanPoint: WallPlanPoint
22
+ /** Moving corner under the cursor — the only field that changes per move. */
23
+ currentPlanPoint: WallPlanPoint
24
+ }
25
+
26
+ type FloorplanMarqueeState = {
27
+ drag: FloorplanMarqueeDrag | null
28
+ begin(drag: FloorplanMarqueeDrag): void
29
+ /** Advance the moving corner. No-ops (skips the store update, so the overlay
30
+ * doesn't re-render) when the snapped point is unchanged or no drag is open. */
31
+ setCurrent(point: WallPlanPoint): void
32
+ reset(): void
33
+ }
34
+
35
+ export const useFloorplanMarquee = create<FloorplanMarqueeState>((set) => ({
36
+ drag: null,
37
+ begin: (drag) => set({ drag }),
38
+ setCurrent: (point) =>
39
+ set((state) => {
40
+ const prev = state.drag
41
+ if (!prev) return state
42
+ if (prev.currentPlanPoint[0] === point[0] && prev.currentPlanPoint[1] === point[1]) {
43
+ return state
44
+ }
45
+ return { drag: { ...prev, currentPlanPoint: point } }
46
+ }),
47
+ reset: () => set((state) => (state.drag === null ? state : { drag: null })),
48
+ }))
49
+
50
+ export default useFloorplanMarquee
@@ -0,0 +1,186 @@
1
+ import { afterEach, describe, expect, test } from 'bun:test'
2
+ import type { AnyNode } from '@pascal-app/core'
3
+ import {
4
+ type ActiveInteractionScope,
5
+ editingHoleInfo,
6
+ handleDragInfo,
7
+ isActive,
8
+ isIdle,
9
+ scopeNodeId,
10
+ selectionEnabled,
11
+ } from '../lib/interaction/scope'
12
+ import useInteractionScope from './use-interaction-scope'
13
+
14
+ // A placing/moving scope carries the node inline. Tests only assert on id/type,
15
+ // so a structural stand-in is enough.
16
+ const mockNode = (id: string, type: string): AnyNode => ({ id, type }) as unknown as AnyNode
17
+
18
+ function reset() {
19
+ useInteractionScope.getState().end()
20
+ }
21
+ afterEach(reset)
22
+
23
+ describe('use-interaction-scope state machine', () => {
24
+ test('starts idle', () => {
25
+ expect(useInteractionScope.getState().scope.kind).toBe('idle')
26
+ expect(isIdle(useInteractionScope.getState().scope)).toBe(true)
27
+ })
28
+
29
+ test('begin enters an interaction; end returns to idle atomically', () => {
30
+ const s = useInteractionScope.getState()
31
+ s.begin({
32
+ kind: 'moving',
33
+ node: mockNode('item_1', 'item'),
34
+ nodeId: 'item_1',
35
+ nodeType: 'item',
36
+ view: '3d',
37
+ })
38
+ expect(useInteractionScope.getState().scope).toEqual({
39
+ kind: 'moving',
40
+ node: mockNode('item_1', 'item'),
41
+ nodeId: 'item_1',
42
+ nodeType: 'item',
43
+ view: '3d',
44
+ })
45
+ s.end()
46
+ // No interaction payload leaks past end — the scope is plain idle, so a
47
+ // stale nodeId/handle is unrepresentable.
48
+ expect(useInteractionScope.getState().scope).toEqual({ kind: 'idle' })
49
+ expect(scopeNodeId(useInteractionScope.getState().scope)).toBeNull()
50
+ })
51
+
52
+ test('begin is single-owner: a new interaction replaces the prior one', () => {
53
+ const s = useInteractionScope.getState()
54
+ s.begin({ kind: 'drafting', tool: 'wall' })
55
+ s.begin({ kind: 'handle-drag', nodeId: 'wall_1', handle: 'height' })
56
+ const scope = useInteractionScope.getState().scope
57
+ expect(scope.kind).toBe('handle-drag')
58
+ // The prior drafting payload is gone — illegal "drafting + handle-drag"
59
+ // combination is unrepresentable.
60
+ expect(scopeNodeId(scope)).toBe('wall_1')
61
+ })
62
+
63
+ test('update patches the live payload of the active scope', () => {
64
+ const s = useInteractionScope.getState()
65
+ s.begin({
66
+ kind: 'placing',
67
+ node: mockNode('i1', 'item'),
68
+ nodeId: 'i1',
69
+ nodeType: 'item',
70
+ view: '3d',
71
+ pressDrag: false,
72
+ })
73
+ s.update({ pressDrag: true })
74
+ const scope = useInteractionScope.getState().scope
75
+ expect(scope.kind === 'placing' && scope.pressDrag).toBe(true)
76
+ })
77
+
78
+ test('update is a no-op when idle', () => {
79
+ useInteractionScope.getState().update({
80
+ kind: 'moving',
81
+ node: mockNode('x', 'item'),
82
+ nodeId: 'x',
83
+ nodeType: 'item',
84
+ view: '3d',
85
+ })
86
+ expect(useInteractionScope.getState().scope.kind).toBe('idle')
87
+ })
88
+
89
+ test('update cannot change which interaction is running', () => {
90
+ const s = useInteractionScope.getState()
91
+ s.begin({
92
+ kind: 'moving',
93
+ node: mockNode('i1', 'item'),
94
+ nodeId: 'i1',
95
+ nodeType: 'item',
96
+ view: '3d',
97
+ })
98
+ s.update({
99
+ kind: 'placing',
100
+ node: mockNode('i1', 'item'),
101
+ nodeId: 'i1',
102
+ nodeType: 'item',
103
+ view: '3d',
104
+ pressDrag: true,
105
+ })
106
+ expect(useInteractionScope.getState().scope.kind).toBe('moving')
107
+ })
108
+
109
+ test('selectionEnabled only while idle', () => {
110
+ const s = useInteractionScope.getState()
111
+ expect(selectionEnabled(useInteractionScope.getState().scope)).toBe(true)
112
+ s.begin({ kind: 'box-select' })
113
+ expect(selectionEnabled(useInteractionScope.getState().scope)).toBe(false)
114
+ expect(isActive(useInteractionScope.getState().scope)).toBe(true)
115
+ })
116
+
117
+ test('end is idempotent', () => {
118
+ const s = useInteractionScope.getState()
119
+ s.end()
120
+ s.end()
121
+ expect(useInteractionScope.getState().scope.kind).toBe('idle')
122
+ })
123
+ })
124
+
125
+ describe('derived flag views are leak-free (no parallel flags)', () => {
126
+ const scope = () => useInteractionScope.getState().scope
127
+
128
+ test('handleDragInfo mirrors handle-drag and clears on end', () => {
129
+ const s = useInteractionScope.getState()
130
+ s.begin({ kind: 'handle-drag', nodeId: 'wall_1', handle: 'height' })
131
+ expect(handleDragInfo(scope())).toEqual({ nodeId: 'wall_1', label: 'height' })
132
+ s.end()
133
+ // After end the derived view is null — a stale activeHandleDrag is
134
+ // unrepresentable because it is a pure function of the single scope.
135
+ expect(handleDragInfo(scope())).toBeNull()
136
+ })
137
+
138
+ test('editingHoleInfo mirrors a hole reshape and clears on end', () => {
139
+ const s = useInteractionScope.getState()
140
+ s.begin({ kind: 'reshaping', nodeId: 'slab_1', reshape: 'hole', holeIndex: 2 })
141
+ expect(editingHoleInfo(scope())).toEqual({ nodeId: 'slab_1', holeIndex: 2 })
142
+ s.end()
143
+ expect(editingHoleInfo(scope())).toBeNull()
144
+ })
145
+
146
+ test('a non-hole reshape never reads as an editing hole', () => {
147
+ const s = useInteractionScope.getState()
148
+ s.begin({ kind: 'reshaping', nodeId: 'wall_1', reshape: 'curve' })
149
+ expect(editingHoleInfo(scope())).toBeNull()
150
+ })
151
+
152
+ test('switching interactions never leaks the prior derived view', () => {
153
+ const s = useInteractionScope.getState()
154
+ s.begin({ kind: 'handle-drag', nodeId: 'wall_1', handle: 'height' })
155
+ // Single-owner replacement: the handle-drag view must vanish the instant a
156
+ // different interaction begins — the two cannot be simultaneously active.
157
+ s.begin({ kind: 'reshaping', nodeId: 'slab_1', reshape: 'hole', holeIndex: 0 })
158
+ expect(handleDragInfo(scope())).toBeNull()
159
+ expect(editingHoleInfo(scope())).toEqual({ nodeId: 'slab_1', holeIndex: 0 })
160
+ })
161
+
162
+ test('every active scope kind leaves at most the views it owns', () => {
163
+ const s = useInteractionScope.getState()
164
+ const kinds: ActiveInteractionScope[] = [
165
+ {
166
+ kind: 'placing',
167
+ node: mockNode('i', 'item'),
168
+ nodeId: 'i',
169
+ nodeType: 'item',
170
+ view: '3d',
171
+ pressDrag: false,
172
+ },
173
+ { kind: 'moving', node: mockNode('i', 'item'), nodeId: 'i', nodeType: 'item', view: '3d' },
174
+ { kind: 'drafting', tool: 'wall' },
175
+ { kind: 'box-select' },
176
+ { kind: 'painting' },
177
+ ]
178
+ for (const k of kinds) {
179
+ s.begin(k)
180
+ // None of these own a handle-drag or hole view.
181
+ expect(handleDragInfo(scope())).toBeNull()
182
+ expect(editingHoleInfo(scope())).toBeNull()
183
+ }
184
+ s.end()
185
+ })
186
+ })
@@ -0,0 +1,132 @@
1
+ 'use client'
2
+
3
+ import { type AnyNode, type AnyNodeId, useScene } from '@pascal-app/core'
4
+ import { useRef } from 'react'
5
+ import { create } from 'zustand'
6
+ import { useShallow } from 'zustand/react/shallow'
7
+ import {
8
+ type ActiveInteractionScope,
9
+ controlPointReshapeInfo,
10
+ editingHoleInfo,
11
+ endpointReshapeInfo,
12
+ handleDragInfo,
13
+ IDLE_SCOPE,
14
+ type InteractionScope,
15
+ isCurveReshape,
16
+ movingNodeOf,
17
+ reshapingNodeId,
18
+ tangentReshapeInfo,
19
+ } from '../lib/interaction/scope'
20
+
21
+ // The authoritative interaction state machine. A single owner holds exactly one
22
+ // scope at a time. `begin` enters an interaction (atomically replacing any prior
23
+ // one — a single owner, no producer races), `update` narrows the live payload,
24
+ // and `end` returns to idle atomically so no interaction payload can leak past
25
+ // the end of its interaction. There is no setter that can leave the store in an
26
+ // illegal half-state: the only writable shape is `InteractionScope`.
27
+
28
+ export type InteractionScopeState = {
29
+ scope: InteractionScope
30
+ // Enter an interaction. If one is already active it is ended first, so the
31
+ // store is always single-owner.
32
+ begin: (scope: ActiveInteractionScope) => void
33
+ // Patch the current scope's payload. Ignored when idle, or when the patch's
34
+ // implied kind differs from the active kind — payload updates must not change
35
+ // which interaction is running (use `begin` for that).
36
+ update: (patch: Partial<ActiveInteractionScope>) => void
37
+ // Return to idle atomically. Both commit and cancel paths call this; the
38
+ // distinction (write vs revert) lives in the interaction body, not here.
39
+ end: () => void
40
+ // Return to idle only if the active scope matches `match`. Used when scope is
41
+ // driven from independent legacy flag clears, so clearing one flag (e.g. a
42
+ // fence curve) cannot stomp an unrelated active scope (e.g. a wall move).
43
+ endIf: (match: (scope: ActiveInteractionScope) => boolean) => void
44
+ }
45
+
46
+ const useInteractionScope = create<InteractionScopeState>((set, get) => ({
47
+ scope: IDLE_SCOPE,
48
+ begin: (scope) => set({ scope }),
49
+ update: (patch) =>
50
+ set((state) => {
51
+ if (state.scope.kind === 'idle') return state
52
+ if ('kind' in patch && patch.kind !== state.scope.kind) return state
53
+ return { scope: { ...state.scope, ...patch } as InteractionScope }
54
+ }),
55
+ end: () => {
56
+ if (get().scope.kind === 'idle') return
57
+ set({ scope: IDLE_SCOPE })
58
+ },
59
+ endIf: (match) => {
60
+ const scope = get().scope
61
+ if (scope.kind === 'idle') return
62
+ if (match(scope)) set({ scope: IDLE_SCOPE })
63
+ },
64
+ }))
65
+
66
+ // Derived, reference-stable views of the active scope, replacing the legacy
67
+ // `useEditor.activeHandleDrag` / `useEditor.editingHole` flags. `useShallow`
68
+ // keeps the result reference-stable across unrelated scope changes, so hot-path
69
+ // subscribers (camera controls, floating menu) don't re-render on every update.
70
+ export const useActiveHandleDrag = (): { nodeId: string; label: string } | null =>
71
+ useInteractionScope(useShallow((s) => handleDragInfo(s.scope)))
72
+
73
+ export const useEditingHole = (): { nodeId: string; holeIndex: number } | null =>
74
+ useInteractionScope(useShallow((s) => editingHoleInfo(s.scope)))
75
+
76
+ // Imperative (non-React) reads for event handlers / effects.
77
+ export const getEditingHole = (): { nodeId: string; holeIndex: number } | null =>
78
+ editingHoleInfo(useInteractionScope.getState().scope)
79
+
80
+ export const getIsCurveReshape = (): boolean => isCurveReshape(useInteractionScope.getState().scope)
81
+
82
+ // Replaces the legacy `curvingWall` / `curvingFence` existence flags. The
83
+ // wall-vs-fence distinction (both now map to one `reshaping/'curve'` scope) is
84
+ // recovered by reading the reshaped node's type from `useReshapingNode`.
85
+ export const useIsCurveReshape = (): boolean => useInteractionScope((s) => isCurveReshape(s.scope))
86
+
87
+ // Replaces the legacy `movingWallEndpoint` / `movingFenceEndpoint` payloads,
88
+ // minus the node (fetch it from `useReshapingNode`).
89
+ export const useEndpointReshape = (): { nodeId: string; endpoint: 'start' | 'end' } | null =>
90
+ useInteractionScope(useShallow((s) => endpointReshapeInfo(s.scope)))
91
+
92
+ export const useControlPointReshape = (): { nodeId: string; index: number } | null =>
93
+ useInteractionScope(useShallow((s) => controlPointReshapeInfo(s.scope)))
94
+
95
+ export const useTangentReshape = (): { nodeId: string; index: number; side: 'in' | 'out' } | null =>
96
+ useInteractionScope(useShallow((s) => tangentReshapeInfo(s.scope)))
97
+
98
+ // The node currently being reshaped (curve / endpoint / hole), looked up live
99
+ // from the scene by the scope's `nodeId`. During a reshape the scene node holds
100
+ // the same data the legacy `curvingWall` / `movingWallEndpoint.wall` carried, so
101
+ // consumers that need the full node (affordance-tool mounts, wall-vs-fence type
102
+ // checks) read it here instead of from a parallel flag.
103
+ export const useReshapingNode = (): AnyNode | null => {
104
+ const nodeId = useInteractionScope((s) => reshapingNodeId(s.scope))
105
+ // Snapshot the node ONCE when the reshape begins (keyed on nodeId), like the
106
+ // legacy `curvingWall` / `movingWallEndpoint.wall` flags did. The affordance
107
+ // tools write the node live during the drag; subscribing to the live scene
108
+ // node would feed those writes straight back into the tool — the curve resets
109
+ // on pointer-stop, the endpoint drag loops and freezes. nodeId is stable for
110
+ // the whole gesture, so a ref snapshot stays frozen until the next reshape.
111
+ const snapshot = useRef<{ id: string | null; node: AnyNode | null }>({ id: null, node: null })
112
+ if (snapshot.current.id !== nodeId) {
113
+ snapshot.current = {
114
+ id: nodeId,
115
+ node: nodeId ? (useScene.getState().nodes[nodeId as AnyNodeId] ?? null) : null,
116
+ }
117
+ }
118
+ return snapshot.current.node
119
+ }
120
+
121
+ // The node currently being placed or moved. Replaces the legacy
122
+ // `useEditor.movingNode` flag. Unlike `useReshapingNode`, no `useRef` snapshot is
123
+ // needed: the node is carried inline in the scope and set once at `begin`, so it
124
+ // is already a stable reference for the whole gesture (nothing calls `begin` mid
125
+ // drag). Returns null whenever no placing/moving interaction is active.
126
+ export const useMovingNode = (): AnyNode | null => useInteractionScope((s) => movingNodeOf(s.scope))
127
+
128
+ // Imperative (non-React) read for event handlers / effects.
129
+ export const getMovingNode = (): AnyNode | null =>
130
+ movingNodeOf(useInteractionScope.getState().scope)
131
+
132
+ export default useInteractionScope