@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
@@ -1,28 +1,102 @@
1
1
  import {
2
+ type AlignmentAnchor,
2
3
  type AnyNode,
3
4
  type AnyNodeId,
5
+ collectAlignmentAnchors,
4
6
  emitter,
5
7
  type GridEvent,
6
8
  type LevelNode,
7
9
  RoofNode,
8
10
  RoofSegmentNode,
11
+ resolveBuildingForLevel,
9
12
  sceneRegistry,
10
13
  useScene,
14
+ type WallNode,
15
+ wallSegmentAnchors,
11
16
  } from '@pascal-app/core'
17
+ import { clearSurfacePlanSnapFeedback, resolveSurfacePlanPointSnap } from '@pascal-app/editor'
12
18
  import { useViewer } from '@pascal-app/viewer'
13
19
  import { useEffect, useMemo, useRef, useState } from 'react'
14
20
  import * as THREE from 'three'
15
- import { BufferGeometry, DoubleSide, type Group, type Line, Vector3 } from 'three'
21
+ import {
22
+ BufferGeometry,
23
+ DoubleSide,
24
+ Float32BufferAttribute,
25
+ type Group,
26
+ type Line,
27
+ Vector3,
28
+ } from 'three'
16
29
  import { markToolCancelConsumed } from '../../../hooks/use-keyboard'
17
30
  import { EDITOR_LAYER } from '../../../lib/constants'
18
31
  import { sfxEmitter } from '../../../lib/sfx-bus'
19
- import useEditor from '../../../store/use-editor'
32
+ import { snapWorldXZForActiveBuilding } from '../../../lib/world-grid-snap'
33
+ import useEditor, { isGridSnapActive, isMagneticSnapActive } from '../../../store/use-editor'
20
34
  import { CursorSphere } from '../shared/cursor-sphere'
21
35
 
22
36
  const DEFAULT_WALL_HEIGHT = 0.5
23
- const DEFAULT_ROOF_HEIGHT = 2.5
37
+ const DEFAULT_PITCH_DEG = 40
24
38
  const GRID_OFFSET = 0.02
25
39
 
40
+ // Walls that are direct children of a level.
41
+ function getLevelWalls(
42
+ levelId: string | null,
43
+ nodes: Readonly<Record<string, AnyNode>>,
44
+ ): WallNode[] {
45
+ if (!levelId) return []
46
+ const levelNode = nodes[levelId]
47
+ if (levelNode?.type !== 'level') return []
48
+ return (levelNode as LevelNode).children
49
+ .map((childId) => nodes[childId])
50
+ .filter((node): node is WallNode => node?.type === 'wall')
51
+ }
52
+
53
+ // Walls on the level directly beneath the active one. Levels share the same
54
+ // local XZ origin (they only differ in world Y), so these walls live in the
55
+ // identical coordinate frame and feed straight into both the alignment pool
56
+ // and the magnetic wall-snap pipeline — letting a roof drawn on the upper
57
+ // floor snap onto the wall corners of the floor below.
58
+ function getBelowLevelWalls(
59
+ currentLevelId: string | null,
60
+ nodes: Readonly<Record<string, AnyNode>>,
61
+ ): WallNode[] {
62
+ if (!currentLevelId) return []
63
+ const currentLevel = nodes[currentLevelId]
64
+ if (currentLevel?.type !== 'level') return []
65
+ const buildingId = resolveBuildingForLevel(currentLevel.id, nodes)
66
+ if (!buildingId) return []
67
+ const building = nodes[buildingId]
68
+ if (building?.type !== 'building') return []
69
+ const currentIndex = (currentLevel as LevelNode).level
70
+ const belowLevel = (building.children ?? [])
71
+ .map((childId) => nodes[childId])
72
+ .filter((node): node is LevelNode => node?.type === 'level' && node.level < currentIndex)
73
+ .sort((a, b) => b.level - a.level)[0]
74
+ return getLevelWalls(belowLevel?.id ?? null, nodes)
75
+ }
76
+
77
+ // Current-level + floor-below walls — the magnetic snap targets the roof draft
78
+ // locks onto (corners, midpoints, crossings, wall bodies), matching the wall
79
+ // tool. Same coordinate frame, so no transform is needed.
80
+ function getRoofSnapWalls(
81
+ currentLevelId: string | null,
82
+ nodes: Readonly<Record<string, AnyNode>>,
83
+ ): WallNode[] {
84
+ return [...getLevelWalls(currentLevelId, nodes), ...getBelowLevelWalls(currentLevelId, nodes)]
85
+ }
86
+
87
+ // Current-level alignment anchors plus the floor-below wall corners.
88
+ function collectRoofAlignmentAnchors(
89
+ nodes: Readonly<Record<string, AnyNode>>,
90
+ currentLevelId: string | null,
91
+ ): AlignmentAnchor[] {
92
+ return [
93
+ ...collectAlignmentAnchors(nodes, '', currentLevelId),
94
+ ...getBelowLevelWalls(currentLevelId, nodes).flatMap((wall) =>
95
+ wallSegmentAnchors(wall.id, wall.start, wall.end, wall.thickness),
96
+ ),
97
+ ]
98
+ }
99
+
26
100
  /**
27
101
  * Creates a roof group with one default gable segment
28
102
  */
@@ -34,6 +108,13 @@ const commitRoofPlacement = (
34
108
  ): AnyNode['id'] => {
35
109
  const { createNode, createNodes, nodes } = useScene.getState()
36
110
 
111
+ // A placed roof preset seeds `toolDefaults.roof` with the flattened
112
+ // subtree params (roofType, pitch, wallHeight, overhang, materials, …)
113
+ // before the tool activates. The footprint (width/depth) and placement
114
+ // come from the drawn rectangle and always win; the segment carries the
115
+ // shape/material params, the roof container picks up the materials.
116
+ const defaults = useEditor.getState().toolDefaults.roof ?? {}
117
+
37
118
  const centerX = (corner1[0] + corner2[0]) / 2
38
119
  const centerZ = (corner1[2] + corner2[2]) / 2
39
120
 
@@ -74,11 +155,12 @@ const commitRoofPlacement = (
74
155
  }
75
156
 
76
157
  const segment = RoofSegmentNode.parse({
77
- width,
78
- depth,
79
158
  wallHeight: DEFAULT_WALL_HEIGHT,
80
- roofHeight: DEFAULT_ROOF_HEIGHT,
159
+ pitch: DEFAULT_PITCH_DEG,
81
160
  roofType: 'gable',
161
+ ...defaults,
162
+ width,
163
+ depth,
82
164
  position: [localX, 0, localZ],
83
165
  })
84
166
 
@@ -93,16 +175,19 @@ const commitRoofPlacement = (
93
175
 
94
176
  // Create the segment first (centered in its new parent)
95
177
  const segment = RoofSegmentNode.parse({
96
- width,
97
- depth,
98
178
  wallHeight: DEFAULT_WALL_HEIGHT,
99
- roofHeight: DEFAULT_ROOF_HEIGHT,
179
+ pitch: DEFAULT_PITCH_DEG,
100
180
  roofType: 'gable',
181
+ ...defaults,
182
+ width,
183
+ depth,
101
184
  position: [0, 0, 0],
102
185
  })
103
186
 
104
- // Create the roof container
187
+ // Create the roof container. Segment-shaped params (roofType, pitch, …) are
188
+ // dropped by the RoofNode schema; surface materials in `defaults` carry over.
105
189
  const roof = RoofNode.parse({
190
+ ...defaults,
106
191
  name,
107
192
  position: [centerX, 0, centerZ],
108
193
  children: [segment.id],
@@ -124,20 +209,178 @@ type PreviewState = {
124
209
  levelY: number
125
210
  }
126
211
 
212
+ function buildRoofGhostGeometry(
213
+ width: number,
214
+ depth: number,
215
+ wallHeight: number,
216
+ pitchDeg: number,
217
+ ) {
218
+ const safeWidth = Math.max(width, 0.1)
219
+ const safeDepth = Math.max(depth, 0.1)
220
+ const halfWidth = safeWidth / 2
221
+ const halfDepth = safeDepth / 2
222
+ const ridgeHeight = wallHeight + Math.tan((pitchDeg * Math.PI) / 180) * halfDepth
223
+
224
+ const vertices = [
225
+ // Front slope
226
+ -halfWidth,
227
+ wallHeight,
228
+ -halfDepth,
229
+ halfWidth,
230
+ wallHeight,
231
+ -halfDepth,
232
+ halfWidth,
233
+ ridgeHeight,
234
+ 0,
235
+
236
+ -halfWidth,
237
+ wallHeight,
238
+ -halfDepth,
239
+ halfWidth,
240
+ ridgeHeight,
241
+ 0,
242
+ -halfWidth,
243
+ ridgeHeight,
244
+ 0,
245
+
246
+ // Back slope
247
+ -halfWidth,
248
+ ridgeHeight,
249
+ 0,
250
+ halfWidth,
251
+ ridgeHeight,
252
+ 0,
253
+ halfWidth,
254
+ wallHeight,
255
+ halfDepth,
256
+
257
+ -halfWidth,
258
+ ridgeHeight,
259
+ 0,
260
+ halfWidth,
261
+ wallHeight,
262
+ halfDepth,
263
+ -halfWidth,
264
+ wallHeight,
265
+ halfDepth,
266
+
267
+ // Left gable
268
+ -halfWidth,
269
+ wallHeight,
270
+ -halfDepth,
271
+ -halfWidth,
272
+ ridgeHeight,
273
+ 0,
274
+ -halfWidth,
275
+ wallHeight,
276
+ halfDepth,
277
+
278
+ // Right gable
279
+ halfWidth,
280
+ wallHeight,
281
+ -halfDepth,
282
+ halfWidth,
283
+ wallHeight,
284
+ halfDepth,
285
+ halfWidth,
286
+ ridgeHeight,
287
+ 0,
288
+ ]
289
+
290
+ const geometry = new BufferGeometry()
291
+ geometry.setAttribute('position', new Float32BufferAttribute(vertices, 3))
292
+ geometry.computeVertexNormals()
293
+ return geometry
294
+ }
295
+
296
+ function buildRoofGhostEdges(width: number, depth: number, wallHeight: number, pitchDeg: number) {
297
+ const safeWidth = Math.max(width, 0.1)
298
+ const safeDepth = Math.max(depth, 0.1)
299
+ const halfWidth = safeWidth / 2
300
+ const halfDepth = safeDepth / 2
301
+ const ridgeHeight = wallHeight + Math.tan((pitchDeg * Math.PI) / 180) * halfDepth
302
+
303
+ const vertices = [
304
+ // Base rectangle
305
+ -halfWidth,
306
+ wallHeight,
307
+ -halfDepth,
308
+ halfWidth,
309
+ wallHeight,
310
+ -halfDepth,
311
+ halfWidth,
312
+ wallHeight,
313
+ -halfDepth,
314
+ halfWidth,
315
+ wallHeight,
316
+ halfDepth,
317
+ halfWidth,
318
+ wallHeight,
319
+ halfDepth,
320
+ -halfWidth,
321
+ wallHeight,
322
+ halfDepth,
323
+ -halfWidth,
324
+ wallHeight,
325
+ halfDepth,
326
+ -halfWidth,
327
+ wallHeight,
328
+ -halfDepth,
329
+
330
+ // Ridge + gable edges
331
+ -halfWidth,
332
+ ridgeHeight,
333
+ 0,
334
+ halfWidth,
335
+ ridgeHeight,
336
+ 0,
337
+ -halfWidth,
338
+ wallHeight,
339
+ -halfDepth,
340
+ -halfWidth,
341
+ ridgeHeight,
342
+ 0,
343
+ -halfWidth,
344
+ ridgeHeight,
345
+ 0,
346
+ -halfWidth,
347
+ wallHeight,
348
+ halfDepth,
349
+ halfWidth,
350
+ wallHeight,
351
+ -halfDepth,
352
+ halfWidth,
353
+ ridgeHeight,
354
+ 0,
355
+ halfWidth,
356
+ ridgeHeight,
357
+ 0,
358
+ halfWidth,
359
+ wallHeight,
360
+ halfDepth,
361
+ ]
362
+
363
+ const geometry = new BufferGeometry()
364
+ geometry.setAttribute('position', new Float32BufferAttribute(vertices, 3))
365
+ return geometry
366
+ }
367
+
127
368
  export const RoofTool: React.FC = () => {
128
369
  const cursorRef = useRef<Group>(null)
129
370
  const outlineRef = useRef<Line>(null!)
130
371
  const currentLevelId = useViewer((state) => state.selection.levelId)
131
372
  const selectedIds = useViewer((state) => state.selection.selectedIds)
132
373
  const setSelection = useViewer((state) => state.setSelection)
133
- const setTool = useEditor((state) => state.setTool)
134
- const setMode = useEditor((state) => state.setMode)
135
374
 
136
375
  const selectedIdsRef = useRef(selectedIds)
137
376
  useEffect(() => {
138
377
  selectedIdsRef.current = selectedIds
139
378
  }, [selectedIds])
140
379
 
380
+ // Clear preset-seeded defaults on deactivation so a later manual roof draw
381
+ // isn't built with a stale preset's parameters. Unmount-only.
382
+ useEffect(() => () => useEditor.getState().setToolDefaults('roof', null), [])
383
+
141
384
  const corner1Ref = useRef<[number, number, number] | null>(null)
142
385
  const previousGridPosRef = useRef<[number, number] | null>(null)
143
386
  const [preview, setPreview] = useState<PreviewState>({
@@ -151,6 +394,40 @@ export const RoofTool: React.FC = () => {
151
394
 
152
395
  outlineRef.current.geometry = new BufferGeometry()
153
396
 
397
+ // Alignment candidates — anchors of every alignable object on the active
398
+ // level plus the wall corners of the floor directly below, so a roof drawn
399
+ // on the upper floor aligns to the walls beneath it. Refreshed after each
400
+ // roof commits. Both corners of the rectangle align.
401
+ let alignmentCandidates = collectRoofAlignmentAnchors(useScene.getState().nodes, currentLevelId)
402
+
403
+ // Resolve a grid:move/click into the drafted corner via the shared surface
404
+ // snap pipeline: magnetic lock onto wall corners / midpoints / crossings /
405
+ // bodies on the active level + floor below (raising the green beacon),
406
+ // falling back to alignment guides, then to the world-grid snap. The same
407
+ // path the slab/ceiling tools use, so the beacon and coloring match. The
408
+ // pipeline reads the snapping mode itself (Shift bypass, magnetic on/off),
409
+ // so this tool never inspects the flags. `levelId` is intentionally omitted
410
+ // so the explicit floor-below `walls` aren't filtered back out.
411
+ const resolveDraftPoint = (event: GridEvent): [number, number] => {
412
+ const rawPoint: [number, number] = [event.localPosition[0], event.localPosition[2]]
413
+ const gridFallback: [number, number] = isGridSnapActive()
414
+ ? snapWorldXZForActiveBuilding(
415
+ event.position[0],
416
+ event.position[2],
417
+ useEditor.getState().gridSnapStep,
418
+ ).local
419
+ : rawPoint
420
+ const nodes = useScene.getState().nodes
421
+ return resolveSurfacePlanPointSnap({
422
+ rawPoint,
423
+ fallbackPoint: gridFallback,
424
+ walls: getRoofSnapWalls(currentLevelId, nodes),
425
+ candidates: alignmentCandidates,
426
+ movingId: '__roof-draft__',
427
+ highlightWalls: true,
428
+ }).point
429
+ }
430
+
154
431
  const updateOutline = (
155
432
  corner1: [number, number, number],
156
433
  corner2: [number, number, number],
@@ -173,8 +450,7 @@ export const RoofTool: React.FC = () => {
173
450
  const onGridMove = (event: GridEvent) => {
174
451
  if (!cursorRef.current) return
175
452
 
176
- const gridX = Math.round(event.localPosition[0] * 2) / 2
177
- const gridZ = Math.round(event.localPosition[2] * 2) / 2
453
+ const [gridX, gridZ] = resolveDraftPoint(event)
178
454
  const y = event.localPosition[1]
179
455
 
180
456
  const cursorPosition: [number, number, number] = [gridX, y, gridZ]
@@ -183,6 +459,7 @@ export const RoofTool: React.FC = () => {
183
459
  cursorRef.current.position.set(gridX, gridY, gridZ)
184
460
 
185
461
  if (
462
+ (isGridSnapActive() || isMagneticSnapActive()) &&
186
463
  corner1Ref.current &&
187
464
  previousGridPosRef.current &&
188
465
  (gridX !== previousGridPosRef.current[0] || gridZ !== previousGridPosRef.current[1])
@@ -206,8 +483,7 @@ export const RoofTool: React.FC = () => {
206
483
  const onGridClick = (event: GridEvent) => {
207
484
  if (!currentLevelId) return
208
485
 
209
- const gridX = Math.round(event.localPosition[0] * 2) / 2
210
- const gridZ = Math.round(event.localPosition[2] * 2) / 2
486
+ const [gridX, gridZ] = resolveDraftPoint(event)
211
487
  const y = event.localPosition[1]
212
488
 
213
489
  if (corner1Ref.current) {
@@ -222,8 +498,11 @@ export const RoofTool: React.FC = () => {
222
498
 
223
499
  corner1Ref.current = null
224
500
  outlineRef.current.visible = false
501
+ alignmentCandidates = collectRoofAlignmentAnchors(useScene.getState().nodes, currentLevelId)
502
+ clearSurfacePlanSnapFeedback()
225
503
  } else {
226
504
  corner1Ref.current = [gridX, y, gridZ]
505
+ sfxEmitter.emit('sfx:structure-build-start')
227
506
  setPreview((prev) => ({
228
507
  ...prev,
229
508
  corner1: corner1Ref.current,
@@ -238,6 +517,7 @@ export const RoofTool: React.FC = () => {
238
517
  outlineRef.current.visible = false
239
518
  setPreview((prev) => ({ ...prev, corner1: null }))
240
519
  }
520
+ clearSurfacePlanSnapFeedback()
241
521
  }
242
522
 
243
523
  emitter.on('grid:move', onGridMove)
@@ -248,6 +528,7 @@ export const RoofTool: React.FC = () => {
248
528
  emitter.off('grid:move', onGridMove)
249
529
  emitter.off('grid:click', onGridClick)
250
530
  emitter.off('tool:cancel', onCancel)
531
+ clearSurfacePlanSnapFeedback()
251
532
 
252
533
  corner1Ref.current = null
253
534
  }
@@ -264,6 +545,34 @@ export const RoofTool: React.FC = () => {
264
545
  return { length, width, centerX, centerZ }
265
546
  }, [corner1, cursorPosition])
266
547
 
548
+ const roofGhostGeometry = useMemo(() => {
549
+ if (!previewDimensions) return null
550
+ return buildRoofGhostGeometry(
551
+ previewDimensions.length,
552
+ previewDimensions.width,
553
+ DEFAULT_WALL_HEIGHT,
554
+ DEFAULT_PITCH_DEG,
555
+ )
556
+ }, [previewDimensions])
557
+
558
+ const roofGhostEdges = useMemo(() => {
559
+ if (!previewDimensions) return null
560
+ return buildRoofGhostEdges(
561
+ previewDimensions.length,
562
+ previewDimensions.width,
563
+ DEFAULT_WALL_HEIGHT,
564
+ DEFAULT_PITCH_DEG,
565
+ )
566
+ }, [previewDimensions])
567
+
568
+ useEffect(
569
+ () => () => {
570
+ roofGhostGeometry?.dispose()
571
+ roofGhostEdges?.dispose()
572
+ },
573
+ [roofGhostEdges, roofGhostGeometry],
574
+ )
575
+
267
576
  return (
268
577
  <group>
269
578
  <CursorSphere ref={cursorRef} />
@@ -297,21 +606,34 @@ export const RoofTool: React.FC = () => {
297
606
  )}
298
607
 
299
608
  {previewDimensions && previewDimensions.length > 0.1 && previewDimensions.width > 0.1 && (
300
- <mesh
609
+ <group
301
610
  layers={EDITOR_LAYER}
302
611
  position={[previewDimensions.centerX, levelY + GRID_OFFSET, previewDimensions.centerZ]}
303
- rotation={[-Math.PI / 2, 0, 0]}
304
612
  >
305
- <planeGeometry args={[previewDimensions.length, previewDimensions.width]} />
306
- <meshBasicMaterial
307
- color="#818cf8"
308
- depthTest={false}
309
- depthWrite={false}
310
- opacity={0.1}
311
- side={DoubleSide}
312
- transparent
313
- />
314
- </mesh>
613
+ {roofGhostGeometry && (
614
+ <mesh geometry={roofGhostGeometry} layers={EDITOR_LAYER} renderOrder={1}>
615
+ <meshBasicMaterial
616
+ color="#818cf8"
617
+ depthTest={false}
618
+ depthWrite={false}
619
+ opacity={0.16}
620
+ side={DoubleSide}
621
+ transparent
622
+ />
623
+ </mesh>
624
+ )}
625
+ {roofGhostEdges && (
626
+ <lineSegments geometry={roofGhostEdges} layers={EDITOR_LAYER} renderOrder={2}>
627
+ <lineBasicMaterial
628
+ color="#818cf8"
629
+ depthTest={false}
630
+ depthWrite={false}
631
+ opacity={0.5}
632
+ transparent
633
+ />
634
+ </lineSegments>
635
+ )}
636
+ </group>
315
637
  )}
316
638
  </group>
317
639
  )
@@ -0,0 +1,88 @@
1
+ export let boxSelectHandled = false
2
+
3
+ let resetTimeout: ReturnType<typeof setTimeout> | null = null
4
+ const suppressedPointerIds = new Set<number>()
5
+ const suppressionCleanups = new Map<number, () => void>()
6
+
7
+ type PointerEventLike = {
8
+ pointerId?: number
9
+ nativeEvent?: PointerEvent | PointerEventLike
10
+ }
11
+
12
+ type SuppressBoxSelectOptions = {
13
+ markHandled?: boolean
14
+ }
15
+
16
+ function pointerIdFor(event: PointerEvent | PointerEventLike): number | null {
17
+ if ('pointerId' in event && typeof event.pointerId === 'number') {
18
+ return event.pointerId
19
+ }
20
+ const nativeEvent = 'nativeEvent' in event ? event.nativeEvent : undefined
21
+ return nativeEvent ? pointerIdFor(nativeEvent) : null
22
+ }
23
+
24
+ export function markBoxSelectHandled() {
25
+ boxSelectHandled = true
26
+ if (resetTimeout) {
27
+ clearTimeout(resetTimeout)
28
+ }
29
+ resetTimeout = setTimeout(() => {
30
+ boxSelectHandled = false
31
+ resetTimeout = null
32
+ }, 50)
33
+ }
34
+
35
+ export function suppressBoxSelectForPointer(
36
+ event: PointerEvent | PointerEventLike,
37
+ options: SuppressBoxSelectOptions = {},
38
+ ) {
39
+ const markHandled = options.markHandled ?? true
40
+ if (markHandled) markBoxSelectHandled()
41
+
42
+ const pointerId = pointerIdFor(event)
43
+ if (pointerId === null || suppressedPointerIds.has(pointerId)) return
44
+
45
+ suppressedPointerIds.add(pointerId)
46
+
47
+ const clear = (releaseEvent?: PointerEvent) => {
48
+ if (releaseEvent && releaseEvent.pointerId !== pointerId) return
49
+ if (markHandled) markBoxSelectHandled()
50
+ suppressedPointerIds.delete(pointerId)
51
+ const cleanup = suppressionCleanups.get(pointerId)
52
+ suppressionCleanups.delete(pointerId)
53
+ cleanup?.()
54
+ }
55
+
56
+ const onPointerUp = (releaseEvent: PointerEvent) => clear(releaseEvent)
57
+ const onPointerCancel = (releaseEvent: PointerEvent) => clear(releaseEvent)
58
+ const onBlur = () => clear()
59
+ // Click-preserving handle interactions need suppression cleared before
60
+ // canvas-level pointerup handlers decide whether to block the follow-up click.
61
+ const releaseListenerOptions = markHandled ? undefined : { capture: true }
62
+ const cleanup = () => {
63
+ window.removeEventListener('pointerup', onPointerUp, releaseListenerOptions)
64
+ window.removeEventListener('pointercancel', onPointerCancel, releaseListenerOptions)
65
+ window.removeEventListener('blur', onBlur)
66
+ }
67
+
68
+ suppressionCleanups.set(pointerId, cleanup)
69
+ window.addEventListener('pointerup', onPointerUp, releaseListenerOptions)
70
+ window.addEventListener('pointercancel', onPointerCancel, releaseListenerOptions)
71
+ window.addEventListener('blur', onBlur)
72
+ }
73
+
74
+ export function isBoxSelectPointerSuppressed(event: PointerEvent | PointerEventLike) {
75
+ const pointerId = pointerIdFor(event)
76
+ return pointerId !== null && suppressedPointerIds.has(pointerId)
77
+ }
78
+
79
+ export function clearBoxSelectHandled() {
80
+ if (resetTimeout) {
81
+ clearTimeout(resetTimeout)
82
+ resetTimeout = null
83
+ }
84
+ boxSelectHandled = false
85
+ for (const cleanup of suppressionCleanups.values()) cleanup()
86
+ suppressionCleanups.clear()
87
+ suppressedPointerIds.clear()
88
+ }