@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,298 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { type AnyNode, nodeRegistry, registerNode } from '@pascal-app/core'
3
+ import { z } from 'zod'
4
+ import {
5
+ resolveCanvasSelectionNode,
6
+ resolveNodeSelectionTarget,
7
+ resolveSelectedIdsForNodeClick,
8
+ selectionModifiersFromEvent,
9
+ shouldPreserveSelectedRoofHostTarget,
10
+ } from './selection-routing'
11
+
12
+ function registerTestDefinition(kind: string, overrides: Record<string, unknown> = {}) {
13
+ if (nodeRegistry.has(kind)) return
14
+ registerNode({
15
+ kind,
16
+ schemaVersion: 1,
17
+ schema: z.object({ type: z.literal(kind) }) as never,
18
+ category: 'furnish',
19
+ defaults: () => ({ type: kind }) as never,
20
+ capabilities: {},
21
+ renderer: { kind: 'parametric', module: async () => ({ default: () => null }) },
22
+ ...overrides,
23
+ } as never)
24
+ }
25
+
26
+ describe('resolveSelectedIdsForNodeClick', () => {
27
+ test('preserves the pre-routing selection when a phase switch clears current ids', () => {
28
+ expect(
29
+ resolveSelectedIdsForNodeClick({
30
+ baseSelectedIds: ['wall_1'],
31
+ currentSelectedIds: [],
32
+ modifierKeys: { meta: true, ctrl: false, shift: false },
33
+ nodeId: 'item_1',
34
+ }),
35
+ ).toEqual(['wall_1', 'item_1'])
36
+ })
37
+
38
+ test('toggles from the pre-routing selection while a modifier is held', () => {
39
+ expect(
40
+ resolveSelectedIdsForNodeClick({
41
+ baseSelectedIds: ['wall_1', 'item_1'],
42
+ currentSelectedIds: [],
43
+ modifierKeys: { meta: false, ctrl: false, shift: true },
44
+ nodeId: 'item_1',
45
+ }),
46
+ ).toEqual(['wall_1'])
47
+ })
48
+ })
49
+
50
+ describe('selectionModifiersFromEvent', () => {
51
+ test('falls back to tracked modifier state when the click event omits keys', () => {
52
+ expect(selectionModifiersFromEvent({}, { meta: false, ctrl: true, shift: false })).toEqual({
53
+ meta: false,
54
+ ctrl: true,
55
+ shift: false,
56
+ })
57
+ })
58
+
59
+ test('prefers explicit event key state over stale tracked modifiers', () => {
60
+ expect(
61
+ selectionModifiersFromEvent(
62
+ { metaKey: false, ctrlKey: false, shiftKey: false },
63
+ { meta: true, ctrl: true, shift: true },
64
+ ),
65
+ ).toEqual({
66
+ meta: false,
67
+ ctrl: false,
68
+ shift: false,
69
+ })
70
+ })
71
+ })
72
+
73
+ describe('resolveNodeSelectionTarget', () => {
74
+ test('routes furniture items to furnish', () => {
75
+ const node = {
76
+ id: 'item_1',
77
+ type: 'item',
78
+ asset: { category: 'furniture' },
79
+ } as unknown as AnyNode
80
+
81
+ expect(resolveNodeSelectionTarget(node)).toEqual({ phase: 'furnish' })
82
+ })
83
+
84
+ test('routes door and window catalog items to structure', () => {
85
+ const node = {
86
+ id: 'item_1',
87
+ type: 'item',
88
+ asset: { category: 'door' },
89
+ } as unknown as AnyNode
90
+
91
+ expect(resolveNodeSelectionTarget(node)).toEqual({
92
+ phase: 'structure',
93
+ structureLayer: 'elements',
94
+ })
95
+ })
96
+ })
97
+
98
+ describe('resolveCanvasSelectionNode', () => {
99
+ // Mirrors the cabinet-module setup: modules proxy to their run for grouped
100
+ // move / rotate, but declare `selectionProxy.bypassDirectPick` so a direct
101
+ // body click selects the clicked module.
102
+ const groupKind = 'selection-routing-proxy-group-test'
103
+ const memberKind = 'selection-routing-proxy-member-test'
104
+
105
+ function registerBypassKinds() {
106
+ registerTestDefinition(groupKind)
107
+ registerTestDefinition(memberKind, {
108
+ selectionProxy: {
109
+ bypassDirectPick: (node: AnyNode, proxyTarget: AnyNode) =>
110
+ (node.type as string) === memberKind && (proxyTarget.type as string) === groupKind,
111
+ },
112
+ })
113
+ }
114
+
115
+ test('keeps proxied members individually selectable when the kind declares bypassDirectPick', () => {
116
+ registerBypassKinds()
117
+ const run = { id: 'group_run', type: groupKind, metadata: {} } as unknown as AnyNode
118
+ const module = {
119
+ id: 'group_member',
120
+ type: memberKind,
121
+ parentId: run.id,
122
+ metadata: { nodeSelectionProxyId: run.id },
123
+ } as unknown as AnyNode
124
+
125
+ expect(
126
+ resolveCanvasSelectionNode({
127
+ node: module,
128
+ nodes: {
129
+ [run.id]: run,
130
+ [module.id]: module,
131
+ },
132
+ selectedIds: [],
133
+ }),
134
+ ).toBe(module)
135
+ })
136
+
137
+ test('keeps nested proxied members leaf-selectable by default', () => {
138
+ registerBypassKinds()
139
+ const rootRun = { id: 'group_root_run', type: groupKind, metadata: {} } as unknown as AnyNode
140
+ const legRun = {
141
+ id: 'group_leg_run',
142
+ type: groupKind,
143
+ parentId: rootRun.id,
144
+ metadata: { nodeSelectionProxyId: rootRun.id },
145
+ } as unknown as AnyNode
146
+ const nestedCornerBase = {
147
+ id: 'group_nested_member',
148
+ type: memberKind,
149
+ parentId: legRun.id,
150
+ metadata: { nodeSelectionProxyId: legRun.id },
151
+ } as unknown as AnyNode
152
+
153
+ expect(
154
+ resolveCanvasSelectionNode({
155
+ node: nestedCornerBase,
156
+ nodes: {
157
+ [rootRun.id]: rootRun,
158
+ [legRun.id]: legRun,
159
+ [nestedCornerBase.id]: nestedCornerBase,
160
+ },
161
+ selectedIds: [],
162
+ }),
163
+ ).toBe(nestedCornerBase)
164
+ })
165
+
166
+ test('follows the proxy when the kind declares no bypass', () => {
167
+ const kind = 'selection-routing-proxy-no-bypass-test'
168
+ registerTestDefinition(kind)
169
+ const group = { id: 'plain_group', type: kind, metadata: {} } as unknown as AnyNode
170
+ const member = {
171
+ id: 'plain_member',
172
+ type: kind,
173
+ parentId: group.id,
174
+ metadata: { nodeSelectionProxyId: group.id },
175
+ } as unknown as AnyNode
176
+
177
+ expect(
178
+ resolveCanvasSelectionNode({
179
+ node: member,
180
+ nodes: {
181
+ [group.id]: group,
182
+ [member.id]: member,
183
+ },
184
+ selectedIds: [],
185
+ }),
186
+ ).toBe(group)
187
+ })
188
+
189
+ test('keeps parent-frame children routed to their parent when that parent is solely selected', () => {
190
+ const kind = 'selection-routing-parent-frame-test'
191
+ registerTestDefinition(kind, {
192
+ capabilities: {
193
+ movable: {
194
+ axes: ['x', 'z'],
195
+ gridSnap: true,
196
+ parentFrame: {
197
+ resolveParent: (node: AnyNode, nodes: Readonly<Record<string, AnyNode>>) =>
198
+ (node.parentId ? nodes[node.parentId] : null) ?? null,
199
+ },
200
+ },
201
+ },
202
+ })
203
+
204
+ const parent = { id: 'parent_1', type: groupKind, metadata: {} } as unknown as AnyNode
205
+ const child = {
206
+ id: 'child_1',
207
+ type: kind,
208
+ parentId: parent.id,
209
+ metadata: {},
210
+ } as unknown as AnyNode
211
+
212
+ expect(
213
+ resolveCanvasSelectionNode({
214
+ node: child,
215
+ nodes: {
216
+ [parent.id]: parent,
217
+ [child.id]: child,
218
+ },
219
+ selectedIds: [parent.id],
220
+ }),
221
+ ).toBe(parent)
222
+ })
223
+
224
+ test('prefers an explicit selection proxy before parent-frame routing', () => {
225
+ const kind = 'selection-routing-proxy-before-parent-frame-test'
226
+ registerTestDefinition(kind, {
227
+ capabilities: {
228
+ movable: {
229
+ axes: ['x', 'z'],
230
+ gridSnap: true,
231
+ parentFrame: {
232
+ resolveParent: (node: AnyNode, nodes: Readonly<Record<string, AnyNode>>) =>
233
+ (node.parentId ? nodes[node.parentId] : null) ?? null,
234
+ },
235
+ },
236
+ },
237
+ })
238
+
239
+ const root = { id: 'root_1', type: groupKind, metadata: {} } as unknown as AnyNode
240
+ const proxyGroup = { id: 'proxy_1', type: groupKind, metadata: {} } as unknown as AnyNode
241
+ const child = {
242
+ id: 'child_proxy_1',
243
+ type: kind,
244
+ parentId: root.id,
245
+ metadata: { nodeSelectionProxyId: proxyGroup.id },
246
+ } as unknown as AnyNode
247
+
248
+ expect(
249
+ resolveCanvasSelectionNode({
250
+ node: child,
251
+ nodes: {
252
+ [root.id]: root,
253
+ [proxyGroup.id]: proxyGroup,
254
+ [child.id]: child,
255
+ },
256
+ selectedIds: [root.id],
257
+ }),
258
+ ).toBe(proxyGroup)
259
+ })
260
+ })
261
+
262
+ describe('shouldPreserveSelectedRoofHostTarget', () => {
263
+ test('keeps the roof host target while that roof is the sole armed selection', () => {
264
+ const node = { id: 'roof_1', type: 'roof' } as unknown as AnyNode
265
+
266
+ expect(
267
+ shouldPreserveSelectedRoofHostTarget({
268
+ node,
269
+ selectedIds: ['roof_1'],
270
+ armedRoofId: 'roof_1',
271
+ }),
272
+ ).toBe(true)
273
+ })
274
+
275
+ test('falls back to segment targeting when the roof host is not armed', () => {
276
+ const node = { id: 'roof_1', type: 'roof' } as unknown as AnyNode
277
+
278
+ expect(
279
+ shouldPreserveSelectedRoofHostTarget({
280
+ node,
281
+ selectedIds: ['roof_1'],
282
+ armedRoofId: null,
283
+ }),
284
+ ).toBe(false)
285
+ })
286
+
287
+ test('falls back to segment targeting when the roof is no longer the sole selection', () => {
288
+ const node = { id: 'roof_1', type: 'roof' } as unknown as AnyNode
289
+
290
+ expect(
291
+ shouldPreserveSelectedRoofHostTarget({
292
+ node,
293
+ selectedIds: ['roof_1', 'wall_1'],
294
+ armedRoofId: 'roof_1',
295
+ }),
296
+ ).toBe(false)
297
+ })
298
+ })
@@ -0,0 +1,175 @@
1
+ import {
2
+ type AnyNode,
3
+ type ItemNode,
4
+ nodeRegistry,
5
+ resolveSelectionProxyId,
6
+ } from '@pascal-app/core'
7
+
8
+ export type SelectionModifierKeys = {
9
+ meta: boolean
10
+ ctrl: boolean
11
+ shift: boolean
12
+ }
13
+
14
+ export type NodeSelectionTarget = {
15
+ phase: 'site' | 'structure' | 'furnish'
16
+ structureLayer?: 'zones' | 'elements'
17
+ }
18
+
19
+ function shouldBypassSelectionProxy(node: AnyNode, target: AnyNode): boolean {
20
+ if (node.id === target.id) return false
21
+ // Kind-declared bypass (`def.selectionProxy.bypassDirectPick`): the kind
22
+ // keeps a proxy for grouped affordances but wants a direct body click to
23
+ // select the clicked node itself.
24
+ return nodeRegistry.get(node.type)?.selectionProxy?.bypassDirectPick?.(node, target) ?? false
25
+ }
26
+
27
+ export function resolveCanvasSelectionNode({
28
+ node,
29
+ nodes,
30
+ selectedIds,
31
+ }: {
32
+ node: AnyNode
33
+ nodes: Readonly<Record<string, AnyNode | undefined>>
34
+ selectedIds: readonly string[]
35
+ }): AnyNode {
36
+ const proxiedTarget = nodes[resolveSelectionProxyId(node, nodes)] ?? node
37
+ let target = shouldBypassSelectionProxy(node, proxiedTarget) ? node : proxiedTarget
38
+ const parentFrame = nodeRegistry.get(target.type)?.capabilities?.movable?.parentFrame
39
+ if (parentFrame) {
40
+ const parent = parentFrame.resolveParent(target, nodes as Readonly<Record<string, AnyNode>>)
41
+ if (parent && selectedIds.length === 1 && selectedIds[0] === parent.id) {
42
+ target = parent
43
+ }
44
+ }
45
+ return target
46
+ }
47
+
48
+ export function isSelectionModifierActive(keys: SelectionModifierKeys): boolean {
49
+ return keys.meta || keys.ctrl || keys.shift
50
+ }
51
+
52
+ export function selectionModifiersFromEvent(
53
+ event?: {
54
+ metaKey?: boolean
55
+ ctrlKey?: boolean
56
+ shiftKey?: boolean
57
+ nativeEvent?: {
58
+ metaKey?: boolean
59
+ ctrlKey?: boolean
60
+ shiftKey?: boolean
61
+ }
62
+ } | null,
63
+ fallback?: Partial<SelectionModifierKeys>,
64
+ ): SelectionModifierKeys {
65
+ const fromEvent = (
66
+ key: keyof SelectionModifierKeys,
67
+ eventKey: 'metaKey' | 'ctrlKey' | 'shiftKey',
68
+ ) => {
69
+ if (typeof event?.[eventKey] === 'boolean') return event[eventKey]
70
+ if (typeof event?.nativeEvent?.[eventKey] === 'boolean') return event.nativeEvent[eventKey]
71
+ return Boolean(fallback?.[key])
72
+ }
73
+
74
+ return {
75
+ meta: fromEvent('meta', 'metaKey'),
76
+ ctrl: fromEvent('ctrl', 'ctrlKey'),
77
+ shift: fromEvent('shift', 'shiftKey'),
78
+ }
79
+ }
80
+
81
+ export function resolveSelectedIdsForNodeClick({
82
+ baseSelectedIds,
83
+ currentSelectedIds,
84
+ modifierKeys,
85
+ nodeId,
86
+ }: {
87
+ baseSelectedIds?: readonly string[]
88
+ currentSelectedIds: readonly string[]
89
+ modifierKeys: SelectionModifierKeys
90
+ nodeId: string
91
+ }): string[] {
92
+ if (isSelectionModifierActive(modifierKeys)) {
93
+ const selectedIds = baseSelectedIds ?? currentSelectedIds
94
+ if (selectedIds.includes(nodeId)) {
95
+ return selectedIds.filter((id) => id !== nodeId)
96
+ }
97
+ return [...selectedIds, nodeId]
98
+ }
99
+
100
+ return [nodeId]
101
+ }
102
+
103
+ export function shouldPreserveSelectedRoofHostTarget({
104
+ node,
105
+ selectedIds,
106
+ armedRoofId,
107
+ }: {
108
+ node: AnyNode
109
+ selectedIds: readonly string[]
110
+ armedRoofId: string | null
111
+ }): boolean {
112
+ return (
113
+ node.type === 'roof' &&
114
+ armedRoofId === node.id &&
115
+ selectedIds.length === 1 &&
116
+ selectedIds[0] === node.id
117
+ )
118
+ }
119
+
120
+ export function resolveNodeSelectionTarget(node: AnyNode): NodeSelectionTarget | null {
121
+ if (node.type === 'building') {
122
+ return { phase: 'site' }
123
+ }
124
+
125
+ if (node.type === 'zone') {
126
+ return {
127
+ phase: 'structure',
128
+ structureLayer: 'zones',
129
+ }
130
+ }
131
+
132
+ if (node.type === 'item') {
133
+ const item = node as ItemNode
134
+ if (item.asset.category === 'door' || item.asset.category === 'window') {
135
+ return {
136
+ phase: 'structure',
137
+ structureLayer: 'elements',
138
+ }
139
+ }
140
+ return { phase: 'furnish' }
141
+ }
142
+
143
+ if (
144
+ node.type === 'wall' ||
145
+ node.type === 'fence' ||
146
+ node.type === 'column' ||
147
+ node.type === 'elevator' ||
148
+ node.type === 'slab' ||
149
+ node.type === 'ceiling' ||
150
+ node.type === 'roof' ||
151
+ node.type === 'roof-segment' ||
152
+ node.type === 'stair' ||
153
+ node.type === 'stair-segment' ||
154
+ node.type === 'spawn' ||
155
+ node.type === 'window' ||
156
+ node.type === 'door'
157
+ ) {
158
+ return {
159
+ phase: 'structure',
160
+ structureLayer: 'elements',
161
+ }
162
+ }
163
+
164
+ const def = nodeRegistry.get(node.type)
165
+ if (!def) return null
166
+
167
+ if (def.category === 'furnish') {
168
+ return { phase: 'furnish' }
169
+ }
170
+
171
+ return {
172
+ phase: 'structure',
173
+ structureLayer: 'elements',
174
+ }
175
+ }
@@ -1,2 +1,3 @@
1
1
  export { initSFXBus, sfxEmitter, triggerSFX } from '../sfx-bus'
2
2
  export { playSFX, SFX, type SFXName, updateSFXVolumes } from '../sfx-player'
3
+ export { movementSfxStepKey } from './movement-tick'
@@ -0,0 +1,65 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { movementSfxStepKey } from './movement-tick'
3
+
4
+ describe('movementSfxStepKey', () => {
5
+ test('keeps tiny free-move changes in the same bucket', () => {
6
+ expect(
7
+ movementSfxStepKey({
8
+ coords: [1.01, 2.04],
9
+ gridSnapActive: false,
10
+ gridStep: 0.5,
11
+ }),
12
+ ).toBe(
13
+ movementSfxStepKey({
14
+ coords: [1.04, 2.01],
15
+ gridSnapActive: false,
16
+ gridStep: 0.5,
17
+ }),
18
+ )
19
+ })
20
+
21
+ test('changes bucket after crossing the free-move cadence', () => {
22
+ expect(
23
+ movementSfxStepKey({
24
+ coords: [1.01, 2.04],
25
+ gridSnapActive: false,
26
+ gridStep: 0.5,
27
+ }),
28
+ ).not.toBe(
29
+ movementSfxStepKey({
30
+ coords: [1.16, 2.04],
31
+ gridSnapActive: false,
32
+ gridStep: 0.5,
33
+ }),
34
+ )
35
+ })
36
+
37
+ test('uses the live grid step when grid snapping is active', () => {
38
+ expect(
39
+ movementSfxStepKey({
40
+ coords: [0.24, 0],
41
+ gridSnapActive: true,
42
+ gridStep: 0.5,
43
+ }),
44
+ ).toBe(
45
+ movementSfxStepKey({
46
+ coords: [0.21, 0],
47
+ gridSnapActive: true,
48
+ gridStep: 0.5,
49
+ }),
50
+ )
51
+ expect(
52
+ movementSfxStepKey({
53
+ coords: [0.24, 0],
54
+ gridSnapActive: true,
55
+ gridStep: 0.5,
56
+ }),
57
+ ).not.toBe(
58
+ movementSfxStepKey({
59
+ coords: [0.76, 0],
60
+ gridSnapActive: true,
61
+ gridStep: 0.5,
62
+ }),
63
+ )
64
+ })
65
+ })
@@ -0,0 +1,18 @@
1
+ const DEFAULT_FREE_MOVEMENT_SFX_STEP_M = 0.1
2
+
3
+ type MovementSfxStepKeyArgs = {
4
+ coords: readonly number[]
5
+ gridSnapActive: boolean
6
+ gridStep: number
7
+ freeStep?: number
8
+ }
9
+
10
+ export function movementSfxStepKey({
11
+ coords,
12
+ gridSnapActive,
13
+ gridStep,
14
+ freeStep = DEFAULT_FREE_MOVEMENT_SFX_STEP_M,
15
+ }: MovementSfxStepKeyArgs): string {
16
+ const step = gridSnapActive && gridStep > 0 ? gridStep : freeStep
17
+ return coords.map((coord) => Math.round(coord / step)).join(',')
18
+ }
@@ -1,5 +1,5 @@
1
1
  import mitt from 'mitt'
2
- import { playSFX } from './sfx-player'
2
+ import { disposeSFX, playSFX } from './sfx-player'
3
3
 
4
4
  /**
5
5
  * SFX-specific events that tools can trigger
@@ -10,9 +10,14 @@ type SFXEvents = {
10
10
  'sfx:item-pick': undefined
11
11
  'sfx:item-place': undefined
12
12
  'sfx:item-rotate': undefined
13
+ 'sfx:resize': undefined
14
+ 'sfx:structure-build-start': undefined
13
15
  'sfx:structure-build': undefined
14
16
  'sfx:structure-delete': undefined
15
17
  'sfx:snapshot-capture': undefined
18
+ 'sfx:menu-hover': undefined
19
+ 'sfx:menu-click': undefined
20
+ 'sfx:paint-apply': undefined
16
21
  }
17
22
 
18
23
  /**
@@ -23,6 +28,20 @@ export const sfxEmitter = mitt<SFXEvents>()
23
28
 
24
29
  let sfxBusInitialized = false
25
30
 
31
+ const handleGridSnap = () => playSFX('gridSnap')
32
+ const handleItemDelete = () => playSFX('itemDelete')
33
+ const handleItemPick = () => playSFX('itemPick')
34
+ const handleItemPlace = () => playSFX('itemPlace')
35
+ const handleItemRotate = () => playSFX('itemRotate')
36
+ const handleResize = () => playSFX('resize')
37
+ const handleStructureBuildStart = () => playSFX('structureBuildStart')
38
+ const handleStructureBuild = () => playSFX('structureBuildEnd')
39
+ const handleStructureDelete = () => playSFX('structureDelete')
40
+ const handleSnapshotCapture = () => playSFX('snapshotCapture')
41
+ const handleMenuHover = () => playSFX('menuHover')
42
+ const handleMenuClick = () => playSFX('menuClick')
43
+ const handlePaintApply = () => playSFX('paintApply')
44
+
26
45
  /**
27
46
  * Initialize SFX Bus - connects SFX events to actual sound playback.
28
47
  * Safe to call multiple times; re-registration is a no-op once initialized.
@@ -30,15 +49,39 @@ let sfxBusInitialized = false
30
49
  export function initSFXBus() {
31
50
  if (sfxBusInitialized) return
32
51
  sfxBusInitialized = true
33
- // Map SFX events to sound playback
34
- sfxEmitter.on('sfx:grid-snap', () => playSFX('gridSnap'))
35
- sfxEmitter.on('sfx:item-delete', () => playSFX('itemDelete'))
36
- sfxEmitter.on('sfx:item-pick', () => playSFX('itemPick'))
37
- sfxEmitter.on('sfx:item-place', () => playSFX('itemPlace'))
38
- sfxEmitter.on('sfx:item-rotate', () => playSFX('itemRotate'))
39
- sfxEmitter.on('sfx:structure-build', () => playSFX('structureBuild'))
40
- sfxEmitter.on('sfx:structure-delete', () => playSFX('structureDelete'))
41
- sfxEmitter.on('sfx:snapshot-capture', () => playSFX('snapshotCapture'))
52
+ sfxEmitter.on('sfx:grid-snap', handleGridSnap)
53
+ sfxEmitter.on('sfx:item-delete', handleItemDelete)
54
+ sfxEmitter.on('sfx:item-pick', handleItemPick)
55
+ sfxEmitter.on('sfx:item-place', handleItemPlace)
56
+ sfxEmitter.on('sfx:item-rotate', handleItemRotate)
57
+ sfxEmitter.on('sfx:resize', handleResize)
58
+ sfxEmitter.on('sfx:structure-build-start', handleStructureBuildStart)
59
+ sfxEmitter.on('sfx:structure-build', handleStructureBuild)
60
+ sfxEmitter.on('sfx:structure-delete', handleStructureDelete)
61
+ sfxEmitter.on('sfx:snapshot-capture', handleSnapshotCapture)
62
+ sfxEmitter.on('sfx:menu-hover', handleMenuHover)
63
+ sfxEmitter.on('sfx:menu-click', handleMenuClick)
64
+ sfxEmitter.on('sfx:paint-apply', handlePaintApply)
65
+ }
66
+
67
+ export function disposeSFXBus() {
68
+ if (sfxBusInitialized) {
69
+ sfxEmitter.off('sfx:grid-snap', handleGridSnap)
70
+ sfxEmitter.off('sfx:item-delete', handleItemDelete)
71
+ sfxEmitter.off('sfx:item-pick', handleItemPick)
72
+ sfxEmitter.off('sfx:item-place', handleItemPlace)
73
+ sfxEmitter.off('sfx:item-rotate', handleItemRotate)
74
+ sfxEmitter.off('sfx:resize', handleResize)
75
+ sfxEmitter.off('sfx:structure-build-start', handleStructureBuildStart)
76
+ sfxEmitter.off('sfx:structure-build', handleStructureBuild)
77
+ sfxEmitter.off('sfx:structure-delete', handleStructureDelete)
78
+ sfxEmitter.off('sfx:snapshot-capture', handleSnapshotCapture)
79
+ sfxEmitter.off('sfx:menu-hover', handleMenuHover)
80
+ sfxEmitter.off('sfx:menu-click', handleMenuClick)
81
+ sfxEmitter.off('sfx:paint-apply', handlePaintApply)
82
+ sfxBusInitialized = false
83
+ }
84
+ disposeSFX()
42
85
  }
43
86
 
44
87
  /**
@@ -49,3 +92,19 @@ export function initSFXBus() {
49
92
  export function triggerSFX(event: keyof SFXEvents) {
50
93
  sfxEmitter.emit(event)
51
94
  }
95
+
96
+ /**
97
+ * Node types whose deletion should use the lighter item-delete cue rather
98
+ * than the heavier structure-delete one. Shelves are furniture-like placeable
99
+ * objects, so they sound like items being removed, not structures demolished.
100
+ */
101
+ const ITEM_DELETE_NODE_TYPES = new Set(['item', 'shelf'])
102
+
103
+ /**
104
+ * Emit the delete SFX appropriate for a deleted node's type.
105
+ */
106
+ export function emitDeleteSFX(nodeType: string | undefined) {
107
+ sfxEmitter.emit(
108
+ nodeType && ITEM_DELETE_NODE_TYPES.has(nodeType) ? 'sfx:item-delete' : 'sfx:structure-delete',
109
+ )
110
+ }