@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,331 @@
1
+ // Pure geometry for "magnetic" wall-draft snapping — no store / viewer / React
2
+ // deps, so it's unit-testable in isolation. Coordinates are XZ plan points
3
+ // (building-local meters). `wall-drafting.ts` layers grid/angle snapping and
4
+ // scene access on top of these primitives.
5
+
6
+ import {
7
+ getWallCurveFrameAt,
8
+ getWallCurveLength,
9
+ isCurvedWall,
10
+ type WallNode,
11
+ } from '@pascal-app/core'
12
+
13
+ export type WallPlanPoint = [number, number]
14
+
15
+ /** Which kind of existing-geometry snap produced a drafted point. */
16
+ export type WallDraftSnapKind = 'endpoint' | 'midpoint' | 'intersection' | 'wall'
17
+
18
+ export type WallSnapRadii = Partial<Record<WallDraftSnapKind, number>>
19
+
20
+ export type WallDraftSnapResult = {
21
+ point: WallPlanPoint
22
+ /**
23
+ * Set when `point` locked onto existing wall geometry (a corner, midpoint,
24
+ * crossing, or wall body) rather than a plain grid/angle position. This is
25
+ * the "magnetic" snap the beacon visualises; `null` for grid/angle-only.
26
+ */
27
+ snap: WallDraftSnapKind | null
28
+ }
29
+
30
+ export const WALL_JOIN_SNAP_RADIUS = 0.35
31
+ // Tight capture radius for the connectivity snap that runs in NON-magnetic modes
32
+ // (grid / off / angles). Joining an existing wall is treated as connectivity —
33
+ // separate from the 'lines' magnetic alignment — so a room can still close in
34
+ // those modes: within this distance of a wall the endpoint sticks onto it (and
35
+ // the beacon shows); beyond it, positioning is left to the mode. Kept small so
36
+ // only the last few cm near a wall stick, well above the room-detection junction
37
+ // tolerance so a captured endpoint always registers as connected.
38
+ export const WALL_CONNECT_SNAP_RADIUS = 0.05
39
+ // Generous radius for snapping to an *existing* wall's endpoint while
40
+ // drafting. Larger than `WALL_JOIN_SNAP_RADIUS` because endpoint snap
41
+ // is the strongest user intent (closing a polygon, attaching to a
42
+ // corner) and the cursor never lands pixel-perfect on a corner.
43
+ export const WALL_ENDPOINT_SNAP_RADIUS = 0.7
44
+ // Discrete "special point" snaps taken from the raw cursor (like the
45
+ // endpoint snap) but slightly tighter — a corner is the strongest intent,
46
+ // a midpoint / crossing is the next tier down.
47
+ export const WALL_MIDPOINT_SNAP_RADIUS = 0.5
48
+ export const WALL_INTERSECTION_SNAP_RADIUS = 0.5
49
+
50
+ export function distanceSquared(a: WallPlanPoint, b: WallPlanPoint): number {
51
+ const dx = a[0] - b[0]
52
+ const dz = a[1] - b[1]
53
+ return dx * dx + dz * dz
54
+ }
55
+
56
+ export function projectPointOntoWall(point: WallPlanPoint, wall: WallNode): WallPlanPoint | null {
57
+ const [x1, z1] = wall.start
58
+ const [x2, z2] = wall.end
59
+ const dx = x2 - x1
60
+ const dz = z2 - z1
61
+ const lengthSquared = dx * dx + dz * dz
62
+ if (lengthSquared < 1e-9) {
63
+ return null
64
+ }
65
+
66
+ const t = ((point[0] - x1) * dx + (point[1] - z1) * dz) / lengthSquared
67
+ if (t <= 0 || t >= 1) {
68
+ return null
69
+ }
70
+
71
+ return [x1 + dx * t, z1 + dz * t]
72
+ }
73
+
74
+ export function findWallSnapTarget(
75
+ point: WallPlanPoint,
76
+ walls: WallNode[],
77
+ options?: { ignoreWallIds?: string[]; radius?: number },
78
+ ): WallPlanPoint | null {
79
+ const ignoreWallIds = new Set(options?.ignoreWallIds ?? [])
80
+ const radiusSquared = (options?.radius ?? WALL_JOIN_SNAP_RADIUS) ** 2
81
+ let bestTarget: WallPlanPoint | null = null
82
+ let bestDistanceSquared = Number.POSITIVE_INFINITY
83
+
84
+ for (const wall of walls) {
85
+ if (ignoreWallIds.has(wall.id)) {
86
+ continue
87
+ }
88
+
89
+ const candidates: Array<WallPlanPoint | null> = [wall.start, wall.end]
90
+
91
+ if (isCurvedWall(wall)) {
92
+ const sampleCount = Math.max(8, Math.ceil(getWallCurveLength(wall) / 0.3))
93
+ for (let index = 0; index <= sampleCount; index += 1) {
94
+ const frame = getWallCurveFrameAt(wall, index / sampleCount)
95
+ candidates.push([frame.point.x, frame.point.y])
96
+ }
97
+ } else {
98
+ candidates.push(projectPointOntoWall(point, wall))
99
+ }
100
+ for (const candidate of candidates) {
101
+ if (!candidate) {
102
+ continue
103
+ }
104
+
105
+ const candidateDistanceSquared = distanceSquared(point, candidate)
106
+ if (
107
+ candidateDistanceSquared > radiusSquared ||
108
+ candidateDistanceSquared >= bestDistanceSquared
109
+ ) {
110
+ continue
111
+ }
112
+
113
+ bestTarget = candidate
114
+ bestDistanceSquared = candidateDistanceSquared
115
+ }
116
+ }
117
+
118
+ return bestTarget
119
+ }
120
+
121
+ /**
122
+ * Endpoint-only snap from the *raw* cursor (no grid pre-snap), with a
123
+ * generous radius. Use this before `findWallSnapTarget` so the strong
124
+ * "attach to an existing wall corner" intent isn't accidentally pushed
125
+ * out of range by an interim grid snap that moved the cursor away from
126
+ * the endpoint.
127
+ */
128
+ export function findWallEndpointFromRaw(
129
+ point: WallPlanPoint,
130
+ walls: WallNode[],
131
+ ignoreWallIds?: string[],
132
+ radius = WALL_ENDPOINT_SNAP_RADIUS,
133
+ ): WallPlanPoint | null {
134
+ const ignored = new Set(ignoreWallIds ?? [])
135
+ const radiusSquared = radius ** 2
136
+ let best: WallPlanPoint | null = null
137
+ let bestDistSq = Number.POSITIVE_INFINITY
138
+
139
+ for (const wall of walls) {
140
+ if (ignored.has(wall.id)) continue
141
+ for (const corner of [wall.start, wall.end] as WallPlanPoint[]) {
142
+ const d = distanceSquared(point, corner)
143
+ if (d <= radiusSquared && d < bestDistSq) {
144
+ best = corner
145
+ bestDistSq = d
146
+ }
147
+ }
148
+ }
149
+ return best
150
+ }
151
+
152
+ /** Midpoint of a wall — curve midpoint for curved walls, segment midpoint otherwise. */
153
+ function wallMidpoint(wall: WallNode): WallPlanPoint {
154
+ if (isCurvedWall(wall)) {
155
+ const frame = getWallCurveFrameAt(wall, 0.5)
156
+ return [frame.point.x, frame.point.y]
157
+ }
158
+ return [(wall.start[0] + wall.end[0]) / 2, (wall.start[1] + wall.end[1]) / 2]
159
+ }
160
+
161
+ /** Nearest wall midpoint to the raw cursor, within `WALL_MIDPOINT_SNAP_RADIUS`. */
162
+ export function findWallMidpointFromRaw(
163
+ point: WallPlanPoint,
164
+ walls: WallNode[],
165
+ ignoreWallIds?: string[],
166
+ radius = WALL_MIDPOINT_SNAP_RADIUS,
167
+ ): WallPlanPoint | null {
168
+ const ignored = new Set(ignoreWallIds ?? [])
169
+ const radiusSquared = radius ** 2
170
+ let best: WallPlanPoint | null = null
171
+ let bestDistSq = Number.POSITIVE_INFINITY
172
+
173
+ for (const wall of walls) {
174
+ if (ignored.has(wall.id)) continue
175
+ const mid = wallMidpoint(wall)
176
+ const d = distanceSquared(point, mid)
177
+ if (d <= radiusSquared && d < bestDistSq) {
178
+ best = mid
179
+ bestDistSq = d
180
+ }
181
+ }
182
+ return best
183
+ }
184
+
185
+ /** Crossing point of two straight segments, or null if they don't intersect within both. */
186
+ function segmentIntersection(
187
+ a1: WallPlanPoint,
188
+ a2: WallPlanPoint,
189
+ b1: WallPlanPoint,
190
+ b2: WallPlanPoint,
191
+ ): WallPlanPoint | null {
192
+ const rx = a2[0] - a1[0]
193
+ const rz = a2[1] - a1[1]
194
+ const sx = b2[0] - b1[0]
195
+ const sz = b2[1] - b1[1]
196
+ const denom = rx * sz - rz * sx
197
+ if (Math.abs(denom) < 1e-9) return null // parallel / collinear
198
+
199
+ const qpx = b1[0] - a1[0]
200
+ const qpz = b1[1] - a1[1]
201
+ const t = (qpx * sz - qpz * sx) / denom
202
+ const u = (qpx * rz - qpz * rx) / denom
203
+ if (t < 0 || t > 1 || u < 0 || u > 1) return null
204
+
205
+ return [a1[0] + t * rx, a1[1] + t * rz]
206
+ }
207
+
208
+ /**
209
+ * Nearest point where two existing straight walls cross, within
210
+ * `WALL_INTERSECTION_SNAP_RADIUS`. Curved walls are skipped. O(n²) over the
211
+ * level's walls — fine at editor scale.
212
+ */
213
+ export function findWallIntersectionFromRaw(
214
+ point: WallPlanPoint,
215
+ walls: WallNode[],
216
+ ignoreWallIds?: string[],
217
+ radius = WALL_INTERSECTION_SNAP_RADIUS,
218
+ ): WallPlanPoint | null {
219
+ const ignored = new Set(ignoreWallIds ?? [])
220
+ const straight = walls.filter((wall) => !ignored.has(wall.id) && !isCurvedWall(wall))
221
+ const radiusSquared = radius ** 2
222
+ let best: WallPlanPoint | null = null
223
+ let bestDistSq = Number.POSITIVE_INFINITY
224
+
225
+ for (let i = 0; i < straight.length; i += 1) {
226
+ for (let j = i + 1; j < straight.length; j += 1) {
227
+ const crossing = segmentIntersection(
228
+ straight[i]!.start,
229
+ straight[i]!.end,
230
+ straight[j]!.start,
231
+ straight[j]!.end,
232
+ )
233
+ if (!crossing) continue
234
+ const d = distanceSquared(point, crossing)
235
+ if (d <= radiusSquared && d < bestDistSq) {
236
+ best = crossing
237
+ bestDistSq = d
238
+ }
239
+ }
240
+ }
241
+ return best
242
+ }
243
+
244
+ /** Pick the candidate nearest to `point`, ignoring nulls. */
245
+ function nearestCandidate(
246
+ point: WallPlanPoint,
247
+ candidates: Array<WallDraftSnapResult | null | false>,
248
+ ): WallDraftSnapResult | null {
249
+ let best: WallDraftSnapResult | null = null
250
+ let bestDistSq = Number.POSITIVE_INFINITY
251
+ for (const candidate of candidates) {
252
+ if (!candidate) continue
253
+ const d = distanceSquared(point, candidate.point)
254
+ if (d < bestDistSq) {
255
+ best = candidate
256
+ bestDistSq = d
257
+ }
258
+ }
259
+ return best
260
+ }
261
+
262
+ // Tolerance for "the committed endpoint actually lies on existing wall
263
+ // geometry". Commit-time resolution (corner join, connect snap, split) puts
264
+ // the endpoint exactly on the geometry, so this only needs to absorb float
265
+ // drift — it is NOT a snap radius.
266
+ export const WALL_CHAIN_JOIN_TOLERANCE = 1e-3
267
+
268
+ /**
269
+ * True when a committed chain segment's resolved `end` lies on wall geometry
270
+ * (an endpoint, or a straight wall's interior) of a wall outside the current
271
+ * draft chain. The wall tools stop chaining there: a segment that tees into
272
+ * the existing network is a termination — continuing would draft the next
273
+ * segment on top of existing walls. `chainWallIds` excludes the chain's own
274
+ * segments (including the just-committed one) so edge/midpoint snaps onto a
275
+ * previous own segment don't read as a join. Curved wall interiors are
276
+ * skipped (their endpoints still count) — resolving an end onto a curve body
277
+ * is rare and continuing there matches the previous behaviour.
278
+ */
279
+ export function chainEndJoinsExistingWall(
280
+ end: WallPlanPoint,
281
+ walls: WallNode[],
282
+ chainWallIds: string[],
283
+ tolerance = WALL_CHAIN_JOIN_TOLERANCE,
284
+ ): boolean {
285
+ const ignored = new Set(chainWallIds)
286
+ const toleranceSquared = tolerance * tolerance
287
+
288
+ for (const wall of walls) {
289
+ if (ignored.has(wall.id)) continue
290
+
291
+ if (
292
+ distanceSquared(end, wall.start) <= toleranceSquared ||
293
+ distanceSquared(end, wall.end) <= toleranceSquared
294
+ ) {
295
+ return true
296
+ }
297
+
298
+ if (isCurvedWall(wall)) continue
299
+
300
+ const projected = projectPointOntoWall(end, wall)
301
+ if (projected && distanceSquared(end, projected) <= toleranceSquared) {
302
+ return true
303
+ }
304
+ }
305
+
306
+ return false
307
+ }
308
+
309
+ /**
310
+ * Discrete "special point" snap from the raw cursor, in priority order:
311
+ * 1. corners (endpoints) — strongest intent, largest radius
312
+ * 2. midpoints / crossings — next tier; the nearer of the two wins
313
+ * A corner within range always wins over a midpoint/crossing. Returns null
314
+ * when no special point is in range (caller falls back to grid/edge snap).
315
+ */
316
+ export function findWallSpecialPointSnap(
317
+ point: WallPlanPoint,
318
+ walls: WallNode[],
319
+ ignoreWallIds?: string[],
320
+ radii?: WallSnapRadii,
321
+ ): WallDraftSnapResult | null {
322
+ const endpoint = findWallEndpointFromRaw(point, walls, ignoreWallIds, radii?.endpoint)
323
+ if (endpoint) return { point: endpoint, snap: 'endpoint' }
324
+
325
+ const midpoint = findWallMidpointFromRaw(point, walls, ignoreWallIds, radii?.midpoint)
326
+ const intersection = findWallIntersectionFromRaw(point, walls, ignoreWallIds, radii?.intersection)
327
+ return nearestCandidate(point, [
328
+ midpoint && { point: midpoint, snap: 'midpoint' },
329
+ intersection && { point: intersection, snap: 'intersection' },
330
+ ])
331
+ }
@@ -18,7 +18,11 @@ export const ZoneBoundaryEditor: React.FC<ZoneBoundaryEditorProps> = ({ zoneId }
18
18
 
19
19
  const handlePolygonChange = useCallback(
20
20
  (newPolygon: Array<[number, number]>) => {
21
- updateNode(zoneId, { polygon: newPolygon })
21
+ updateNode(zoneId, {
22
+ polygon: newPolygon,
23
+ autoFromWalls: false,
24
+ boundaryWallIds: [],
25
+ })
22
26
  },
23
27
  [zoneId, updateNode],
24
28
  )
@@ -1,50 +1,27 @@
1
- import { emitter, type GridEvent, type LevelNode, useScene, ZoneNode } from '@pascal-app/core'
1
+ import {
2
+ DEFAULT_ANGLE_STEP,
3
+ emitter,
4
+ type GridEvent,
5
+ type LevelNode,
6
+ snapPointAlongAngleRay,
7
+ useScene,
8
+ ZoneNode,
9
+ } from '@pascal-app/core'
2
10
  import { useViewer } from '@pascal-app/viewer'
3
11
  import { useEffect, useMemo, useRef, useState } from 'react'
4
12
  import { BufferGeometry, DoubleSide, type Group, type Line, Shape, Vector3 } from 'three'
5
13
  import { EDITOR_LAYER } from './../../../lib/constants'
6
14
  import { sfxEmitter } from './../../../lib/sfx-bus'
7
- import useEditor from './../../../store/use-editor'
15
+ import {
16
+ clearSurfacePlanSnapFeedback,
17
+ resolveSurfacePlanPointSnap,
18
+ } from './../../../lib/surface-plan-snap'
19
+ import { snapWorldXZForActiveBuilding } from './../../../lib/world-grid-snap'
20
+ import useEditor, { isAngleSnapActive, isGridSnapActive } from './../../../store/use-editor'
8
21
  import { CursorSphere } from '../shared/cursor-sphere'
9
22
 
10
23
  const Y_OFFSET = 0.02
11
24
 
12
- /**
13
- * Snaps a point to the nearest axis-aligned or 45-degree diagonal from the last point
14
- */
15
- const calculateSnapPoint = (
16
- lastPoint: [number, number],
17
- currentPoint: [number, number],
18
- ): [number, number] => {
19
- const [x1, y1] = lastPoint
20
- const [x, y] = currentPoint
21
-
22
- const dx = x - x1
23
- const dy = y - y1
24
- const absDx = Math.abs(dx)
25
- const absDy = Math.abs(dy)
26
-
27
- // Calculate distances to horizontal, vertical, and diagonal lines
28
- const horizontalDist = absDy
29
- const verticalDist = absDx
30
- const diagonalDist = Math.abs(absDx - absDy)
31
-
32
- // Find the minimum distance to determine which axis to snap to
33
- const minDist = Math.min(horizontalDist, verticalDist, diagonalDist)
34
-
35
- if (minDist === diagonalDist) {
36
- // Snap to 45° diagonal
37
- const diagonalLength = Math.min(absDx, absDy)
38
- return [x1 + Math.sign(dx) * diagonalLength, y1 + Math.sign(dy) * diagonalLength]
39
- }
40
- if (minDist === horizontalDist) {
41
- // Snap to horizontal
42
- return [x, y1]
43
- }
44
- // Snap to vertical
45
- return [x1, y]
46
- }
47
-
48
25
  /**
49
26
  * Creates a zone with the given polygon points
50
27
  */
@@ -106,11 +83,35 @@ export const ZoneTool: React.FC = () => {
106
83
  if (!currentLevelId) return
107
84
 
108
85
  let cursorPosition: [number, number] = [0, 0]
86
+ let snappedCursorPosition: [number, number] | null = null
109
87
 
110
88
  // Initialize line geometries
111
89
  mainLineRef.current.geometry = new BufferGeometry()
112
90
  closingLineRef.current.geometry = new BufferGeometry()
113
91
 
92
+ // Snapping follows the active mode: `angles` locks the ray to 15° from the
93
+ // last vertex (grid quantizes the distance along it), otherwise `grid`
94
+ // quantizes to the world-aligned grid; the shared surface snap then layers
95
+ // wall-corner/midpoint/crossing magnetics and alignment guides on top —
96
+ // the same pipeline slab/ceiling drafting uses. No held-Shift bypass —
97
+ // Shift cycles the mode (see interaction-scope.md).
98
+ const snapDraftPoint = (
99
+ lastPoint: [number, number] | undefined,
100
+ gridPoint: [number, number],
101
+ rawPoint: [number, number],
102
+ ): [number, number] => {
103
+ const gridStep = isGridSnapActive() ? useEditor.getState().gridSnapStep : 0
104
+ const orthoPoint: [number, number] =
105
+ isAngleSnapActive() && lastPoint
106
+ ? [...snapPointAlongAngleRay(lastPoint, rawPoint, DEFAULT_ANGLE_STEP, gridStep)]
107
+ : gridPoint
108
+ return resolveSurfacePlanPointSnap({
109
+ rawPoint,
110
+ fallbackPoint: orthoPoint,
111
+ levelId: currentLevelId,
112
+ }).point
113
+ }
114
+
114
115
  const updateLines = () => {
115
116
  const points = pointsRef.current
116
117
  const y = levelYRef.current + Y_OFFSET
@@ -126,11 +127,9 @@ export const ZoneTool: React.FC = () => {
126
127
 
127
128
  // Add cursor point
128
129
  const lastPoint = points[points.length - 1]
129
- if (lastPoint) {
130
- const snapped = calculateSnapPoint(lastPoint, cursorPosition)
131
- if (isValidPoint(snapped)) {
132
- linePoints.push(new Vector3(snapped[0], y, snapped[1]))
133
- }
130
+ const snapped = snappedCursorPosition ?? cursorPosition
131
+ if (lastPoint && isValidPoint(snapped)) {
132
+ linePoints.push(new Vector3(snapped[0], y, snapped[1]))
134
133
  }
135
134
 
136
135
  // Update main line geometry
@@ -144,17 +143,14 @@ export const ZoneTool: React.FC = () => {
144
143
 
145
144
  // Update closing line (from cursor back to first point)
146
145
  const firstPoint = points[0]
147
- if (points.length >= 2 && lastPoint && isValidPoint(firstPoint)) {
148
- const snapped = calculateSnapPoint(lastPoint, cursorPosition)
149
- if (isValidPoint(snapped)) {
150
- const closingPoints = [
151
- new Vector3(snapped[0], y, snapped[1]),
152
- new Vector3(firstPoint[0], y, firstPoint[1]),
153
- ]
154
- closingLineRef.current.geometry.dispose()
155
- closingLineRef.current.geometry = new BufferGeometry().setFromPoints(closingPoints)
156
- closingLineRef.current.visible = true
157
- }
146
+ if (points.length >= 2 && lastPoint && isValidPoint(firstPoint) && isValidPoint(snapped)) {
147
+ const closingPoints = [
148
+ new Vector3(snapped[0], y, snapped[1]),
149
+ new Vector3(firstPoint[0], y, firstPoint[1]),
150
+ ]
151
+ closingLineRef.current.geometry.dispose()
152
+ closingLineRef.current.geometry = new BufferGeometry().setFromPoints(closingPoints)
153
+ closingLineRef.current.visible = true
158
154
  } else {
159
155
  closingLineRef.current.visible = false
160
156
  }
@@ -162,36 +158,45 @@ export const ZoneTool: React.FC = () => {
162
158
 
163
159
  const updatePreview = () => {
164
160
  const points = pointsRef.current
165
- const lastPoint = points[points.length - 1]
161
+ const cursorPt = snappedCursorPosition ?? cursorPosition
166
162
 
167
- let cursorPt: [number, number] | null = null
168
- if (lastPoint) {
169
- cursorPt = calculateSnapPoint(lastPoint, cursorPosition)
170
- } else if (points.length === 0) {
171
- cursorPt = cursorPosition
172
- }
173
-
174
- setPreview({ points: [...points], cursorPoint: cursorPt, levelY: levelYRef.current })
163
+ setPreview({
164
+ points: [...points],
165
+ cursorPoint: isValidPoint(cursorPt) ? cursorPt : null,
166
+ levelY: levelYRef.current,
167
+ })
175
168
  updateLines()
176
169
  }
177
170
 
171
+ // World-grid snap projected into building-local; rotated buildings
172
+ // used to pull the snap off the visible grid lines. Grid quantize only
173
+ // in grid mode; off / lines / angles leave the raw cursor.
174
+ const gridPointOf = (event: GridEvent): [number, number] =>
175
+ isGridSnapActive()
176
+ ? snapWorldXZForActiveBuilding(
177
+ event.position[0],
178
+ event.position[2],
179
+ useEditor.getState().gridSnapStep,
180
+ ).local
181
+ : [event.localPosition[0], event.localPosition[2]]
182
+
178
183
  const onGridMove = (event: GridEvent) => {
179
184
  if (!cursorRef.current) return
180
185
 
181
- // Snap to 0.5 grid
182
- const gridX = Math.round(event.localPosition[0] * 2) / 2
183
- const gridZ = Math.round(event.localPosition[2] * 2) / 2
184
- cursorPosition = [gridX, gridZ]
186
+ cursorPosition = gridPointOf(event)
185
187
  levelYRef.current = event.localPosition[1]
186
188
 
187
- // If we have points, snap to axis from last point
188
189
  const lastPoint = pointsRef.current[pointsRef.current.length - 1]
189
- const displayPoint = lastPoint
190
- ? calculateSnapPoint(lastPoint, cursorPosition)
191
- : cursorPosition
192
-
193
- // Play snap sound when the snapped position changes during drawing
190
+ const displayPoint = snapDraftPoint(lastPoint, cursorPosition, [
191
+ event.localPosition[0],
192
+ event.localPosition[2],
193
+ ])
194
+ snappedCursorPosition = displayPoint
195
+
196
+ // Play snap sound when the snapped position changes during drawing — only
197
+ // when a quantizing mode is active (off / lines move continuously).
194
198
  if (
199
+ (isGridSnapActive() || isAngleSnapActive()) &&
195
200
  pointsRef.current.length > 0 &&
196
201
  previousSnappedPointRef.current &&
197
202
  (displayPoint[0] !== previousSnappedPointRef.current[0] ||
@@ -209,15 +214,11 @@ export const ZoneTool: React.FC = () => {
209
214
  const onGridClick = (event: GridEvent) => {
210
215
  if (!currentLevelId) return
211
216
 
212
- const gridX = Math.round(event.localPosition[0] * 2) / 2
213
- const gridZ = Math.round(event.localPosition[2] * 2) / 2
214
- let clickPoint: [number, number] = [gridX, gridZ]
215
-
216
- // Snap to axis from last point
217
217
  const lastPoint = pointsRef.current[pointsRef.current.length - 1]
218
- if (lastPoint) {
219
- clickPoint = calculateSnapPoint(lastPoint, clickPoint)
220
- }
218
+ const clickPoint = snapDraftPoint(lastPoint, gridPointOf(event), [
219
+ event.localPosition[0],
220
+ event.localPosition[2],
221
+ ])
221
222
 
222
223
  // Check if clicking on the first point to close the shape
223
224
  const firstPoint = pointsRef.current[0]
@@ -236,7 +237,9 @@ export const ZoneTool: React.FC = () => {
236
237
  mainLineRef.current.visible = false
237
238
  closingLineRef.current.visible = false
238
239
  } else {
239
- // Add point to polygon
240
+ // Add point to polygon. Every non-closing vertex is a "start" tick;
241
+ // closing the polygon above fires the structure-build (end) cue.
242
+ sfxEmitter.emit('sfx:structure-build-start')
240
243
  pointsRef.current = [...pointsRef.current, clickPoint]
241
244
  updatePreview()
242
245
  }
@@ -269,6 +272,7 @@ export const ZoneTool: React.FC = () => {
269
272
 
270
273
  // Reset state on unmount
271
274
  pointsRef.current = []
275
+ clearSurfacePlanSnapFeedback()
272
276
  }
273
277
  }, [currentLevelId])
274
278
 
@@ -5,6 +5,7 @@ import {
5
5
  TooltipContent,
6
6
  TooltipTrigger,
7
7
  } from './../../../components/ui/primitives/tooltip'
8
+ import { triggerSFX } from './../../../lib/sfx-bus'
8
9
  import { cn } from './../../../lib/utils'
9
10
 
10
11
  interface ActionButtonProps extends React.ComponentProps<typeof Button> {
@@ -17,7 +18,18 @@ interface ActionButtonProps extends React.ComponentProps<typeof Button> {
17
18
 
18
19
  export const ActionButton = React.forwardRef<HTMLButtonElement, ActionButtonProps>(
19
20
  (
20
- { className, children, label, shortcut, isActive, tooltipContent, tooltipSide, ...props },
21
+ {
22
+ className,
23
+ children,
24
+ label,
25
+ shortcut,
26
+ isActive,
27
+ tooltipContent,
28
+ tooltipSide,
29
+ onClick,
30
+ onMouseEnter,
31
+ ...props
32
+ },
21
33
  ref,
22
34
  ) => {
23
35
  return (
@@ -25,6 +37,14 @@ export const ActionButton = React.forwardRef<HTMLButtonElement, ActionButtonProp
25
37
  <TooltipTrigger asChild>
26
38
  <Button
27
39
  className={cn('relative h-11 w-11 transition-all', className)}
40
+ onClick={(event) => {
41
+ triggerSFX('sfx:menu-click')
42
+ onClick?.(event)
43
+ }}
44
+ onMouseEnter={(event) => {
45
+ triggerSFX('sfx:menu-hover')
46
+ onMouseEnter?.(event)
47
+ }}
28
48
  ref={ref}
29
49
  {...props}
30
50
  >