@pascal-app/editor 0.9.1 → 1.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (366) hide show
  1. package/package.json +14 -9
  2. package/src/components/editor/alignment-3d-guide-layer.tsx +10 -2
  3. package/src/components/editor/bake-exporter.tsx +49 -0
  4. package/src/components/editor/bake-thumbnail.tsx +95 -0
  5. package/src/components/editor/camera-dragging-lifecycle.test.ts +49 -0
  6. package/src/components/editor/camera-dragging-lifecycle.ts +41 -0
  7. package/src/components/editor/custom-camera-controls.tsx +332 -199
  8. package/src/components/editor/delete-confirmation-dialog.tsx +54 -0
  9. package/src/components/editor/editor-layout-mobile.tsx +5 -0
  10. package/src/components/editor/editor-layout-v2.tsx +18 -0
  11. package/src/components/editor/elevation-3d-guide-layer.tsx +120 -0
  12. package/src/components/editor/export-manager.tsx +71 -63
  13. package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
  14. package/src/components/editor/first-person/build-collider-world.test.ts +102 -5
  15. package/src/components/editor/first-person/build-collider-world.ts +52 -5
  16. package/src/components/editor/first-person/terrain-collider.test.ts +132 -0
  17. package/src/components/editor/first-person/terrain-collider.ts +212 -0
  18. package/src/components/editor/first-person-controls.tsx +410 -136
  19. package/src/components/editor/floating-action-menu.tsx +435 -64
  20. package/src/components/editor/floating-building-action-menu.tsx +1 -1
  21. package/src/components/editor/floorplan-background-selection.test.ts +72 -0
  22. package/src/components/editor/floorplan-background-selection.ts +7 -20
  23. package/src/components/editor/floorplan-camera-sync.test.ts +249 -0
  24. package/src/components/editor/floorplan-camera-sync.ts +213 -0
  25. package/src/components/editor/floorplan-mode-coordinator.tsx +82 -0
  26. package/src/components/editor/floorplan-navigation-presentation.test.ts +142 -0
  27. package/src/components/editor/floorplan-navigation-presentation.ts +156 -0
  28. package/src/components/editor/floorplan-panel.tsx +2517 -1492
  29. package/src/components/editor/grid.tsx +166 -39
  30. package/src/components/editor/group-actions.ts +597 -0
  31. package/src/components/editor/group-floating-action-menu.tsx +130 -0
  32. package/src/components/editor/group-move-3d.ts +397 -0
  33. package/src/components/editor/group-rotate-handle.tsx +102 -64
  34. package/src/components/editor/group-selection-box-3d.tsx +173 -0
  35. package/src/components/editor/group-transform-shared.test.ts +192 -1
  36. package/src/components/editor/group-transform-shared.ts +240 -7
  37. package/src/components/editor/handles/handle-arrow.tsx +81 -23
  38. package/src/components/editor/handles/handle-drag-history.test.ts +96 -0
  39. package/src/components/editor/handles/handle-drag-history.ts +14 -0
  40. package/src/components/editor/handles/preview-overrides.test.ts +26 -0
  41. package/src/components/editor/handles/preview-overrides.ts +13 -0
  42. package/src/components/editor/handles/resize-snap.test.ts +52 -0
  43. package/src/components/editor/handles/resize-snap.ts +23 -0
  44. package/src/components/editor/handles/use-handle-drag.ts +49 -6
  45. package/src/components/editor/index.tsx +289 -35
  46. package/src/components/editor/measurement-pill.tsx +68 -23
  47. package/src/components/editor/node-action-menu.tsx +14 -1
  48. package/src/components/editor/node-arrow-handles.tsx +409 -180
  49. package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
  50. package/src/components/editor/quick-measurement-card.tsx +76 -0
  51. package/src/components/editor/quick-measurement-hud.tsx +34 -0
  52. package/src/components/editor/riser-diagram-panel.tsx +137 -0
  53. package/src/components/editor/selection-manager.tsx +814 -466
  54. package/src/components/editor/site-edge-labels.tsx +28 -6
  55. package/src/components/editor/slab-hole-highlights.tsx +13 -5
  56. package/src/components/editor/snapshot-capture-overlay.tsx +258 -115
  57. package/src/components/editor/three-context-bridge.ts +22 -0
  58. package/src/components/editor/thumbnail-generator.tsx +118 -253
  59. package/src/components/editor/use-floorplan-background-placement.ts +113 -45
  60. package/src/components/editor/use-floorplan-hit-testing.test.ts +82 -0
  61. package/src/components/editor/use-floorplan-hit-testing.ts +144 -161
  62. package/src/components/editor/use-floorplan-scene-data.ts +5 -5
  63. package/src/components/editor/use-mesh-settle-epoch.ts +27 -0
  64. package/src/components/editor/wall-measurement-label.tsx +10 -28
  65. package/src/components/editor/wall-move-side-handles.tsx +229 -39
  66. package/src/components/editor/wall-opening-highlights.tsx +63 -32
  67. package/src/components/editor/wall-snap-beacon-layer.tsx +162 -6
  68. package/src/components/editor-2d/floorplan-action-menu-layer.tsx +9 -5
  69. package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +2 -1
  70. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +40 -9
  71. package/src/components/editor-2d/floorplan-cursor-indicator-position.test.ts +31 -0
  72. package/src/components/editor-2d/floorplan-cursor-indicator-position.ts +39 -0
  73. package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
  74. package/src/components/editor-2d/floorplan-group-move.tsx +744 -0
  75. package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
  76. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1672 -0
  77. package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
  78. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +72 -0
  79. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +219 -20
  80. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +327 -59
  81. package/src/components/editor-2d/floorplan-render-context.test.ts +27 -0
  82. package/src/components/editor-2d/floorplan-render-context.tsx +113 -8
  83. package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +5 -3
  84. package/src/components/editor-2d/renderers/floorplan-annotation-layout.test.ts +285 -0
  85. package/src/components/editor-2d/renderers/floorplan-annotation-layout.ts +516 -0
  86. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.test.tsx +236 -0
  87. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +618 -0
  88. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +369 -0
  89. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +431 -32
  90. package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +89 -0
  91. package/src/components/editor-2d/renderers/floorplan-label-angle.ts +106 -0
  92. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +94 -26
  93. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +587 -0
  94. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +2280 -578
  95. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +20 -10
  96. package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
  97. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +425 -61
  98. package/src/components/systems/ceiling/ceiling-system.tsx +97 -4
  99. package/src/components/systems/roof/roof-edit-system.tsx +1768 -13
  100. package/src/components/systems/selection-affordance-manager.tsx +38 -0
  101. package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
  102. package/src/components/systems/zone/zone-system.tsx +42 -13
  103. package/src/components/tools/elevator/elevator-tool.tsx +34 -8
  104. package/src/components/tools/elevator/move-elevator-tool.tsx +13 -2
  105. package/src/components/tools/fence/fence-drafting.ts +107 -8
  106. package/src/components/tools/item/move-tool.tsx +17 -13
  107. package/src/components/tools/item/placement-math.test.ts +13 -1
  108. package/src/components/tools/item/placement-math.ts +28 -2
  109. package/src/components/tools/item/placement-strategies.ts +246 -5
  110. package/src/components/tools/item/placement-types.ts +13 -1
  111. package/src/components/tools/item/use-draft-node.ts +146 -57
  112. package/src/components/tools/item/use-placement-coordinator.tsx +813 -125
  113. package/src/components/tools/registry/move-registry-node-tool.tsx +636 -103
  114. package/src/components/tools/roof/roof-tool.tsx +336 -94
  115. package/src/components/tools/select/box-select-state.ts +18 -7
  116. package/src/components/tools/select/box-select-tool.tsx +176 -40
  117. package/src/components/tools/select/marquee-geometry.test.ts +161 -0
  118. package/src/components/tools/select/marquee-geometry.ts +155 -0
  119. package/src/components/tools/select/plane-box-select-tool.tsx +1 -8
  120. package/src/components/tools/select/select-candidates.ts +1 -1
  121. package/src/components/tools/shared/cursor-sphere.tsx +62 -26
  122. package/src/components/tools/shared/drag-bounding-box.tsx +28 -4
  123. package/src/components/tools/shared/facing-indicator.tsx +87 -0
  124. package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
  125. package/src/components/tools/shared/floor-stack-preview.ts +4 -0
  126. package/src/components/tools/shared/horizontal-construction-plane.ts +123 -0
  127. package/src/components/tools/shared/placement-box.tsx +183 -1
  128. package/src/components/tools/shared/pointer-support-cap.test.ts +98 -0
  129. package/src/components/tools/shared/pointer-support-cap.ts +273 -0
  130. package/src/components/tools/shared/polygon-editor.tsx +276 -35
  131. package/src/components/tools/site/site-boundary-editor.tsx +101 -42
  132. package/src/components/tools/site/stroke-pointer-ownership.test.ts +91 -0
  133. package/src/components/tools/site/stroke-pointer-ownership.ts +57 -0
  134. package/src/components/tools/site/terrain-brush-cursor.tsx +247 -0
  135. package/src/components/tools/site/terrain-sculpt-grid.tsx +192 -0
  136. package/src/components/tools/site/terrain-sculpt-tool.tsx +356 -0
  137. package/src/components/tools/stair/stair-click-guard.test.ts +58 -0
  138. package/src/components/tools/stair/stair-click-guard.ts +72 -0
  139. package/src/components/tools/stair/stair-defaults.ts +1 -0
  140. package/src/components/tools/stair/stair-tool.tsx +164 -42
  141. package/src/components/tools/tool-manager.tsx +149 -30
  142. package/src/components/tools/wall/wall-drafting.test.ts +447 -0
  143. package/src/components/tools/wall/wall-drafting.ts +304 -108
  144. package/src/components/tools/wall/wall-snap-geometry.test.ts +52 -0
  145. package/src/components/tools/wall/wall-snap-geometry.ts +137 -9
  146. package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
  147. package/src/components/tools/zone/zone-tool.tsx +98 -88
  148. package/src/components/ui/action-menu/camera-actions.tsx +24 -17
  149. package/src/components/ui/action-menu/control-modes.tsx +44 -94
  150. package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
  151. package/src/components/ui/action-menu/index.tsx +2 -4
  152. package/src/components/ui/action-menu/measurement-control.tsx +267 -0
  153. package/src/components/ui/action-menu/structure-tools.tsx +22 -13
  154. package/src/components/ui/action-menu/view-toggles.tsx +42 -90
  155. package/src/components/ui/command-palette/editor-commands.tsx +16 -3
  156. package/src/components/ui/command-palette/index.tsx +4 -3
  157. package/src/components/ui/controls/material-paint-panel.tsx +93 -18
  158. package/src/components/ui/controls/material-picker.tsx +162 -152
  159. package/src/components/ui/controls/material-properties-editor.tsx +108 -0
  160. package/src/components/ui/controls/metric-control.tsx +118 -44
  161. package/src/components/ui/controls/scene-material-list.tsx +247 -0
  162. package/src/components/ui/controls/slider-control.tsx +88 -38
  163. package/src/components/ui/controls/terrain-sculpt-panel.tsx +193 -0
  164. package/src/components/ui/floating-level-selector.tsx +74 -20
  165. package/src/components/ui/helpers/building-helper.tsx +10 -23
  166. package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
  167. package/src/components/ui/helpers/helper-manager.tsx +273 -14
  168. package/src/components/ui/helpers/item-helper.tsx +28 -32
  169. package/src/components/ui/helpers/registered-tool-helper.tsx +45 -11
  170. package/src/components/ui/helpers/roof-helper.tsx +10 -12
  171. package/src/components/ui/icon-ref.tsx +47 -0
  172. package/src/components/ui/item-catalog/catalog-items.tsx +39 -1
  173. package/src/components/ui/item-catalog/item-catalog.tsx +13 -36
  174. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  175. package/src/components/ui/panels/multi-selection-panel.tsx +57 -0
  176. package/src/components/ui/panels/node-display.ts +17 -17
  177. package/src/components/ui/panels/panel-manager.tsx +36 -14
  178. package/src/components/ui/panels/panel-wrapper.tsx +24 -3
  179. package/src/components/ui/panels/parametric-inspector.tsx +19 -4
  180. package/src/components/ui/panels/reference-panel.tsx +54 -19
  181. package/src/components/ui/panels/selection-breakdown.test.ts +24 -0
  182. package/src/components/ui/panels/selection-breakdown.ts +20 -0
  183. package/src/components/ui/primitives/shortcut-token.tsx +39 -3
  184. package/src/components/ui/primitives/sidebar.tsx +1 -0
  185. package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
  186. package/src/components/ui/sidebar/icon-rail.tsx +50 -31
  187. package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
  188. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +82 -41
  189. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +76 -6
  190. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +12 -4
  191. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +2 -1
  192. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -3
  193. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +10 -7
  194. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
  195. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
  196. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +10 -7
  197. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -1
  198. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +1 -1
  199. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +10 -7
  200. package/src/components/ui/sidebar/panels/site-panel/index.tsx +42 -17
  201. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
  202. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
  203. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +72 -25
  204. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +7 -3
  205. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +12 -8
  206. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +4 -3
  207. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +10 -7
  208. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
  209. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
  210. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +44 -3
  211. package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
  212. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
  213. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
  214. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
  215. package/src/components/ui/sidebar/tab-bar.tsx +42 -28
  216. package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
  217. package/src/components/ui/snap-target-badge.test.tsx +40 -0
  218. package/src/components/ui/snap-target-badge.tsx +88 -0
  219. package/src/components/viewer/viewer-controls-bar.tsx +466 -0
  220. package/src/components/viewer/viewer-scene-header.tsx +235 -0
  221. package/src/components/viewer-overlay.tsx +36 -596
  222. package/src/components/viewer-zone-system.tsx +6 -1
  223. package/src/components/walkthrough-hud.tsx +111 -0
  224. package/src/hooks/use-auto-save.test.ts +14 -0
  225. package/src/hooks/use-auto-save.ts +54 -7
  226. package/src/hooks/use-ceiling-events.ts +3 -2
  227. package/src/hooks/use-drag-action.ts +4 -1
  228. package/src/hooks/use-grid-events.ts +24 -0
  229. package/src/hooks/use-keyboard.ts +438 -70
  230. package/src/index.tsx +355 -9
  231. package/src/lib/active-placement-surface.test.ts +24 -0
  232. package/src/lib/active-placement-surface.ts +42 -0
  233. package/src/lib/camera-pose.test.ts +237 -0
  234. package/src/lib/camera-pose.ts +172 -0
  235. package/src/lib/ceiling-plan-snap.ts +24 -0
  236. package/src/lib/contextual-help.test.ts +112 -0
  237. package/src/lib/contextual-help.ts +144 -0
  238. package/src/lib/continuation.ts +64 -0
  239. package/src/lib/direct-manipulation.test.ts +275 -0
  240. package/src/lib/direct-manipulation.ts +118 -0
  241. package/src/lib/door-interaction.ts +1 -1
  242. package/src/lib/editor-api.ts +23 -35
  243. package/src/lib/elevation-guides.test.ts +107 -0
  244. package/src/lib/elevation-guides.ts +236 -0
  245. package/src/lib/floorplan/annotation-visibility.test.ts +327 -0
  246. package/src/lib/floorplan/annotation-visibility.ts +133 -0
  247. package/src/lib/floorplan/apply-alignment.test.ts +25 -0
  248. package/src/lib/floorplan/apply-alignment.ts +18 -12
  249. package/src/lib/floorplan/drawing-coordination.test.ts +46 -0
  250. package/src/lib/floorplan/drawing-coordination.ts +13 -0
  251. package/src/lib/floorplan/floorplan-export.test.ts +319 -0
  252. package/src/lib/floorplan/floorplan-export.tsx +978 -0
  253. package/src/lib/floorplan/floorplan-extension.test.ts +43 -0
  254. package/src/lib/floorplan/floorplan-extension.ts +181 -0
  255. package/src/lib/floorplan/floorplan-mode.test.ts +120 -0
  256. package/src/lib/floorplan/floorplan-mode.ts +73 -0
  257. package/src/lib/floorplan/floorplan-pdfkit-document.ts +194 -0
  258. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +107 -0
  259. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +680 -0
  260. package/src/lib/floorplan/geometry.ts +52 -0
  261. package/src/lib/floorplan/index.ts +3 -0
  262. package/src/lib/floorplan/items.ts +5 -2
  263. package/src/lib/floorplan/plan-coords.ts +21 -0
  264. package/src/lib/fresh-planar-placement.test.ts +283 -3
  265. package/src/lib/fresh-planar-placement.ts +85 -1
  266. package/src/lib/glb-export.test.ts +556 -0
  267. package/src/lib/glb-export.ts +1071 -0
  268. package/src/lib/ground-surface.ts +95 -0
  269. package/src/lib/history.test.ts +96 -0
  270. package/src/lib/history.ts +31 -0
  271. package/src/lib/interaction/hot-set.test.ts +133 -0
  272. package/src/lib/interaction/hot-set.ts +67 -0
  273. package/src/lib/interaction/overlay-policy.test.ts +52 -0
  274. package/src/lib/interaction/overlay-policy.ts +59 -0
  275. package/src/lib/interaction/scope.ts +243 -0
  276. package/src/lib/level-duplication.test.ts +2 -1
  277. package/src/lib/level-selection.ts +2 -2
  278. package/src/lib/material-paint.ts +36 -49
  279. package/src/lib/measurement-kind.test.ts +30 -0
  280. package/src/lib/measurement-kind.ts +19 -0
  281. package/src/lib/measurement-label.test.ts +47 -0
  282. package/src/lib/measurement-label.ts +68 -0
  283. package/src/lib/measurement-parser.ts +116 -0
  284. package/src/lib/measurements.test.ts +188 -0
  285. package/src/lib/measurements.ts +210 -0
  286. package/src/lib/paint-scope.test.ts +454 -0
  287. package/src/lib/paint-scope.ts +461 -0
  288. package/src/lib/placement-drag-release.ts +23 -0
  289. package/src/lib/planar-cursor-placement.test.ts +57 -0
  290. package/src/lib/plugin-panels.test.ts +19 -0
  291. package/src/lib/plugin-panels.ts +91 -0
  292. package/src/lib/quick-action-feedback.ts +33 -0
  293. package/src/lib/quick-action-nodes.test.ts +94 -0
  294. package/src/lib/quick-action-nodes.ts +47 -0
  295. package/src/lib/quick-measurement.test.ts +77 -0
  296. package/src/lib/quick-measurement.ts +109 -0
  297. package/src/lib/roof-duplication.ts +6 -6
  298. package/src/lib/roof-hover-outline-proxy.ts +22 -0
  299. package/src/lib/roof-wall-hit.ts +164 -0
  300. package/src/lib/scene-clipboard.test.ts +325 -0
  301. package/src/lib/scene-clipboard.ts +310 -21
  302. package/src/lib/scene.ts +28 -5
  303. package/src/lib/selection-routing.test.ts +313 -0
  304. package/src/lib/selection-routing.ts +180 -0
  305. package/src/lib/sfx/index.ts +1 -0
  306. package/src/lib/sfx/movement-tick.test.ts +65 -0
  307. package/src/lib/sfx/movement-tick.ts +18 -0
  308. package/src/lib/sfx-bus.ts +82 -14
  309. package/src/lib/sfx-player.test.ts +266 -0
  310. package/src/lib/sfx-player.ts +262 -69
  311. package/src/lib/site-boundary.test.ts +53 -0
  312. package/src/lib/site-boundary.ts +27 -0
  313. package/src/lib/slab-plan-snap.test.ts +93 -0
  314. package/src/lib/slab-plan-snap.ts +108 -0
  315. package/src/lib/snapping-mode.test.ts +165 -0
  316. package/src/lib/snapping-mode.ts +191 -0
  317. package/src/lib/stair-duplication.test.ts +103 -0
  318. package/src/lib/stair-duplication.ts +58 -86
  319. package/src/lib/stair-levels.ts +2 -0
  320. package/src/lib/surface-plan-snap.test.ts +71 -0
  321. package/src/lib/surface-plan-snap.ts +256 -0
  322. package/src/lib/terrain-sculpt.test.ts +429 -0
  323. package/src/lib/terrain-sculpt.ts +301 -0
  324. package/src/lib/terrain-verb-color.test.ts +51 -0
  325. package/src/lib/terrain-verb-color.ts +58 -0
  326. package/src/lib/touch-gesture-priority.test.ts +45 -0
  327. package/src/lib/touch-gesture-priority.ts +30 -0
  328. package/src/lib/use-linear-display.ts +40 -0
  329. package/src/lib/window-interaction.ts +1 -1
  330. package/src/lib/world-grid-snap.ts +37 -5
  331. package/src/store/camera-pose-store.test.ts +29 -0
  332. package/src/store/camera-pose-store.ts +27 -0
  333. package/src/store/live-draft-preview-stores.test.ts +136 -0
  334. package/src/store/navigation-sync-pose-store.test.ts +34 -0
  335. package/src/store/navigation-sync-pose-store.ts +27 -0
  336. package/src/store/terrain-sculpt-mode.test.ts +223 -0
  337. package/src/store/use-delete-confirmation.ts +27 -0
  338. package/src/store/use-direct-manipulation-feedback.ts +20 -0
  339. package/src/store/use-drawing-view.test.ts +23 -0
  340. package/src/store/use-drawing-view.ts +84 -0
  341. package/src/store/use-editor.tsx +665 -126
  342. package/src/store/use-elevation-guides.ts +40 -0
  343. package/src/store/use-facing-pose.ts +44 -0
  344. package/src/store/use-fence-curve-draft.ts +51 -0
  345. package/src/store/use-first-person-hud.ts +40 -0
  346. package/src/store/use-floorplan-annotation-visibility.ts +60 -0
  347. package/src/store/use-floorplan-draft-preview.ts +151 -0
  348. package/src/store/use-floorplan-marquee.ts +50 -0
  349. package/src/store/use-floorplan-mode.ts +110 -0
  350. package/src/store/use-interaction-scope.test.ts +206 -0
  351. package/src/store/use-interaction-scope.ts +140 -0
  352. package/src/store/use-measurement-draft.test.ts +559 -0
  353. package/src/store/use-measurement-draft.ts +570 -0
  354. package/src/store/use-opening-guides.ts +41 -0
  355. package/src/store/use-path-draft-preview.ts +90 -0
  356. package/src/store/use-placement-preview.ts +11 -3
  357. package/src/store/use-quick-measurement-hud.test.ts +53 -0
  358. package/src/store/use-quick-measurement-hud.ts +77 -0
  359. package/src/store/use-segment-draft-chain.ts +28 -0
  360. package/src/store/use-stair-build-preview.ts +53 -0
  361. package/src/store/use-wall-snap-indicator.ts +2 -0
  362. package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
  363. package/src/components/editor/group-move-handle.tsx +0 -316
  364. package/src/components/ui/panels/paint-panel.tsx +0 -163
  365. package/src/lib/floorplan/selection-tool.ts +0 -271
  366. package/src/lib/level-name.ts +0 -11
@@ -1,4 +1,5 @@
1
1
  import {
2
+ type AlignmentAnchor,
2
3
  type AnyNode,
3
4
  type AnyNodeId,
4
5
  collectAlignmentAnchors,
@@ -7,33 +8,94 @@ import {
7
8
  type LevelNode,
8
9
  RoofNode,
9
10
  RoofSegmentNode,
11
+ resolveBuildingForLevel,
10
12
  sceneRegistry,
11
- snapScalar,
12
13
  useScene,
14
+ type WallNode,
15
+ wallSegmentAnchors,
13
16
  } from '@pascal-app/core'
14
- import { useAlignmentGuides } from '@pascal-app/editor'
17
+ import { clearSurfacePlanSnapFeedback, resolveSurfacePlanPointSnap } from '@pascal-app/editor'
15
18
  import { useViewer } from '@pascal-app/viewer'
16
19
  import { useEffect, useMemo, useRef, useState } from 'react'
17
20
  import * as THREE from 'three'
18
- 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'
19
29
  import { markToolCancelConsumed } from '../../../hooks/use-keyboard'
20
30
  import { EDITOR_LAYER } from '../../../lib/constants'
21
31
  import { sfxEmitter } from '../../../lib/sfx-bus'
22
- import {
23
- resolveAlignmentForActiveBuilding,
24
- snapWorldXZForActiveBuilding,
25
- } from '../../../lib/world-grid-snap'
26
- import useEditor from '../../../store/use-editor'
32
+ import { snapWorldXZForActiveBuilding } from '../../../lib/world-grid-snap'
33
+ import useEditor, { isGridSnapActive, isMagneticSnapActive } from '../../../store/use-editor'
34
+ import { useFloorplanDraftPreview } from '../../../store/use-floorplan-draft-preview'
27
35
  import { CursorSphere } from '../shared/cursor-sphere'
28
36
 
29
37
  const DEFAULT_WALL_HEIGHT = 0.5
30
38
  const DEFAULT_PITCH_DEG = 40
31
39
  const GRID_OFFSET = 0.02
32
- /** Figma-style alignment-snap threshold (meters), matching the move tools. */
33
- const ALIGNMENT_THRESHOLD_M = 0.08
34
40
 
35
- function snapToActiveGrid(value: number): number {
36
- return snapScalar(value, useEditor.getState().gridSnapStep)
41
+ // Walls that are direct children of a level.
42
+ function getLevelWalls(
43
+ levelId: string | null,
44
+ nodes: Readonly<Record<string, AnyNode>>,
45
+ ): WallNode[] {
46
+ if (!levelId) return []
47
+ const levelNode = nodes[levelId]
48
+ if (levelNode?.type !== 'level') return []
49
+ return (levelNode as LevelNode).children
50
+ .map((childId) => nodes[childId])
51
+ .filter((node): node is WallNode => node?.type === 'wall')
52
+ }
53
+
54
+ // Walls on the level directly beneath the active one. Levels share the same
55
+ // local XZ origin (they only differ in world Y), so these walls live in the
56
+ // identical coordinate frame and feed straight into both the alignment pool
57
+ // and the magnetic wall-snap pipeline — letting a roof drawn on the upper
58
+ // floor snap onto the wall corners of the floor below.
59
+ function getBelowLevelWalls(
60
+ currentLevelId: string | null,
61
+ nodes: Readonly<Record<string, AnyNode>>,
62
+ ): WallNode[] {
63
+ if (!currentLevelId) return []
64
+ const currentLevel = nodes[currentLevelId]
65
+ if (currentLevel?.type !== 'level') return []
66
+ const buildingId = resolveBuildingForLevel(currentLevel.id, nodes)
67
+ if (!buildingId) return []
68
+ const building = nodes[buildingId]
69
+ if (building?.type !== 'building') return []
70
+ const currentIndex = (currentLevel as LevelNode).level
71
+ const belowLevel = (building.children ?? [])
72
+ .map((childId) => nodes[childId])
73
+ .filter((node): node is LevelNode => node?.type === 'level' && node.level < currentIndex)
74
+ .sort((a, b) => b.level - a.level)[0]
75
+ return getLevelWalls(belowLevel?.id ?? null, nodes)
76
+ }
77
+
78
+ // Current-level + floor-below walls — the magnetic snap targets the roof draft
79
+ // locks onto (corners, midpoints, crossings, wall bodies), matching the wall
80
+ // tool. Same coordinate frame, so no transform is needed.
81
+ function getRoofSnapWalls(
82
+ currentLevelId: string | null,
83
+ nodes: Readonly<Record<string, AnyNode>>,
84
+ ): WallNode[] {
85
+ return [...getLevelWalls(currentLevelId, nodes), ...getBelowLevelWalls(currentLevelId, nodes)]
86
+ }
87
+
88
+ // Current-level alignment anchors plus the floor-below wall corners.
89
+ function collectRoofAlignmentAnchors(
90
+ nodes: Readonly<Record<string, AnyNode>>,
91
+ currentLevelId: string | null,
92
+ ): AlignmentAnchor[] {
93
+ return [
94
+ ...collectAlignmentAnchors(nodes, '', currentLevelId),
95
+ ...getBelowLevelWalls(currentLevelId, nodes).flatMap((wall) =>
96
+ wallSegmentAnchors(wall.id, wall.start, wall.end, wall.thickness),
97
+ ),
98
+ ]
37
99
  }
38
100
 
39
101
  /**
@@ -148,14 +210,168 @@ type PreviewState = {
148
210
  levelY: number
149
211
  }
150
212
 
213
+ function buildRoofGhostGeometry(
214
+ width: number,
215
+ depth: number,
216
+ wallHeight: number,
217
+ pitchDeg: number,
218
+ ) {
219
+ const safeWidth = Math.max(width, 0.1)
220
+ const safeDepth = Math.max(depth, 0.1)
221
+ const halfWidth = safeWidth / 2
222
+ const halfDepth = safeDepth / 2
223
+ const ridgeHeight = wallHeight + Math.tan((pitchDeg * Math.PI) / 180) * halfDepth
224
+
225
+ const vertices = [
226
+ // Front slope
227
+ -halfWidth,
228
+ wallHeight,
229
+ -halfDepth,
230
+ halfWidth,
231
+ wallHeight,
232
+ -halfDepth,
233
+ halfWidth,
234
+ ridgeHeight,
235
+ 0,
236
+
237
+ -halfWidth,
238
+ wallHeight,
239
+ -halfDepth,
240
+ halfWidth,
241
+ ridgeHeight,
242
+ 0,
243
+ -halfWidth,
244
+ ridgeHeight,
245
+ 0,
246
+
247
+ // Back slope
248
+ -halfWidth,
249
+ ridgeHeight,
250
+ 0,
251
+ halfWidth,
252
+ ridgeHeight,
253
+ 0,
254
+ halfWidth,
255
+ wallHeight,
256
+ halfDepth,
257
+
258
+ -halfWidth,
259
+ ridgeHeight,
260
+ 0,
261
+ halfWidth,
262
+ wallHeight,
263
+ halfDepth,
264
+ -halfWidth,
265
+ wallHeight,
266
+ halfDepth,
267
+
268
+ // Left gable
269
+ -halfWidth,
270
+ wallHeight,
271
+ -halfDepth,
272
+ -halfWidth,
273
+ ridgeHeight,
274
+ 0,
275
+ -halfWidth,
276
+ wallHeight,
277
+ halfDepth,
278
+
279
+ // Right gable
280
+ halfWidth,
281
+ wallHeight,
282
+ -halfDepth,
283
+ halfWidth,
284
+ wallHeight,
285
+ halfDepth,
286
+ halfWidth,
287
+ ridgeHeight,
288
+ 0,
289
+ ]
290
+
291
+ const geometry = new BufferGeometry()
292
+ geometry.setAttribute('position', new Float32BufferAttribute(vertices, 3))
293
+ geometry.computeVertexNormals()
294
+ return geometry
295
+ }
296
+
297
+ function buildRoofGhostEdges(width: number, depth: number, wallHeight: number, pitchDeg: number) {
298
+ const safeWidth = Math.max(width, 0.1)
299
+ const safeDepth = Math.max(depth, 0.1)
300
+ const halfWidth = safeWidth / 2
301
+ const halfDepth = safeDepth / 2
302
+ const ridgeHeight = wallHeight + Math.tan((pitchDeg * Math.PI) / 180) * halfDepth
303
+
304
+ const vertices = [
305
+ // Base rectangle
306
+ -halfWidth,
307
+ wallHeight,
308
+ -halfDepth,
309
+ halfWidth,
310
+ wallHeight,
311
+ -halfDepth,
312
+ halfWidth,
313
+ wallHeight,
314
+ -halfDepth,
315
+ halfWidth,
316
+ wallHeight,
317
+ halfDepth,
318
+ halfWidth,
319
+ wallHeight,
320
+ halfDepth,
321
+ -halfWidth,
322
+ wallHeight,
323
+ halfDepth,
324
+ -halfWidth,
325
+ wallHeight,
326
+ halfDepth,
327
+ -halfWidth,
328
+ wallHeight,
329
+ -halfDepth,
330
+
331
+ // Ridge + gable edges
332
+ -halfWidth,
333
+ ridgeHeight,
334
+ 0,
335
+ halfWidth,
336
+ ridgeHeight,
337
+ 0,
338
+ -halfWidth,
339
+ wallHeight,
340
+ -halfDepth,
341
+ -halfWidth,
342
+ ridgeHeight,
343
+ 0,
344
+ -halfWidth,
345
+ ridgeHeight,
346
+ 0,
347
+ -halfWidth,
348
+ wallHeight,
349
+ halfDepth,
350
+ halfWidth,
351
+ wallHeight,
352
+ -halfDepth,
353
+ halfWidth,
354
+ ridgeHeight,
355
+ 0,
356
+ halfWidth,
357
+ ridgeHeight,
358
+ 0,
359
+ halfWidth,
360
+ wallHeight,
361
+ halfDepth,
362
+ ]
363
+
364
+ const geometry = new BufferGeometry()
365
+ geometry.setAttribute('position', new Float32BufferAttribute(vertices, 3))
366
+ return geometry
367
+ }
368
+
151
369
  export const RoofTool: React.FC = () => {
152
370
  const cursorRef = useRef<Group>(null)
153
371
  const outlineRef = useRef<Line>(null!)
154
372
  const currentLevelId = useViewer((state) => state.selection.levelId)
155
373
  const selectedIds = useViewer((state) => state.selection.selectedIds)
156
374
  const setSelection = useViewer((state) => state.setSelection)
157
- const setTool = useEditor((state) => state.setTool)
158
- const setMode = useEditor((state) => state.setMode)
159
375
 
160
376
  const selectedIdsRef = useRef(selectedIds)
161
377
  useEffect(() => {
@@ -179,43 +395,38 @@ export const RoofTool: React.FC = () => {
179
395
 
180
396
  outlineRef.current.geometry = new BufferGeometry()
181
397
 
182
- // Alignment candidates — anchors of every alignable object; refreshed
183
- // after each roof commits. Both corners of the rectangle align.
184
- let alignmentCandidates = collectAlignmentAnchors(useScene.getState().nodes, '', currentLevelId)
185
- // Snap the drafted corner onto another object's nearest real anchor and
186
- // publish the guide. The probe is the RAW cursor, NOT the 0.5m-grid-snapped
187
- // point: resolving against the grid point would only ever catch anchors
188
- // that happen to sit on a grid line, so off-grid items (furniture, angled
189
- // walls) would never surface a guide. The matched axis locks exactly to the
190
- // candidate's coordinate; the other axis keeps its grid snap. Alt bypasses.
191
- const alignPoint = (
192
- gridX: number,
193
- gridZ: number,
194
- rawX: number,
195
- rawZ: number,
196
- bypass: boolean,
197
- ): [number, number] => {
198
- if (bypass || alignmentCandidates.length === 0) {
199
- useAlignmentGuides.getState().clear()
200
- return [gridX, gridZ]
201
- }
202
- const ar = resolveAlignmentForActiveBuilding({
203
- moving: [{ nodeId: '__roof-draft__', kind: 'corner', x: rawX, z: rawZ }],
398
+ // Alignment candidates — anchors of every alignable object on the active
399
+ // level plus the wall corners of the floor directly below, so a roof drawn
400
+ // on the upper floor aligns to the walls beneath it. Refreshed after each
401
+ // roof commits. Both corners of the rectangle align.
402
+ let alignmentCandidates = collectRoofAlignmentAnchors(useScene.getState().nodes, currentLevelId)
403
+
404
+ // Resolve a grid:move/click into the drafted corner via the shared surface
405
+ // snap pipeline: magnetic lock onto wall corners / midpoints / crossings /
406
+ // bodies on the active level + floor below (raising the green beacon),
407
+ // falling back to alignment guides, then to the world-grid snap. The same
408
+ // path the slab/ceiling tools use, so the beacon and coloring match. The
409
+ // pipeline reads the snapping mode itself (Shift bypass, magnetic on/off),
410
+ // so this tool never inspects the flags. `levelId` is intentionally omitted
411
+ // so the explicit floor-below `walls` aren't filtered back out.
412
+ const resolveDraftPoint = (event: GridEvent): [number, number] => {
413
+ const rawPoint: [number, number] = [event.localPosition[0], event.localPosition[2]]
414
+ const gridFallback: [number, number] = isGridSnapActive()
415
+ ? snapWorldXZForActiveBuilding(
416
+ event.position[0],
417
+ event.position[2],
418
+ useEditor.getState().gridSnapStep,
419
+ ).local
420
+ : rawPoint
421
+ const nodes = useScene.getState().nodes
422
+ return resolveSurfacePlanPointSnap({
423
+ rawPoint,
424
+ fallbackPoint: gridFallback,
425
+ walls: getRoofSnapWalls(currentLevelId, nodes),
204
426
  candidates: alignmentCandidates,
205
- threshold: ALIGNMENT_THRESHOLD_M,
206
- })
207
- if (ar.guides.length === 0) {
208
- useAlignmentGuides.getState().clear()
209
- return [gridX, gridZ]
210
- }
211
- useAlignmentGuides.getState().set(ar.guides)
212
- let x = gridX
213
- let z = gridZ
214
- for (const guide of ar.guides) {
215
- if (guide.axis === 'x') x = guide.coord
216
- else z = guide.coord
217
- }
218
- return [x, z]
427
+ movingId: '__roof-draft__',
428
+ highlightWalls: true,
429
+ }).point
219
430
  }
220
431
 
221
432
  const updateOutline = (
@@ -240,20 +451,7 @@ export const RoofTool: React.FC = () => {
240
451
  const onGridMove = (event: GridEvent) => {
241
452
  if (!cursorRef.current) return
242
453
 
243
- // World-grid snap projected into building-local; rotated buildings
244
- // used to drag every roof corner off the visible grid.
245
- const snapped = snapWorldXZForActiveBuilding(
246
- event.position[0],
247
- event.position[2],
248
- useEditor.getState().gridSnapStep,
249
- ).local
250
- const [gridX, gridZ] = alignPoint(
251
- snapped[0],
252
- snapped[1],
253
- event.localPosition[0],
254
- event.localPosition[2],
255
- event.nativeEvent?.altKey === true,
256
- )
454
+ const [gridX, gridZ] = resolveDraftPoint(event)
257
455
  const y = event.localPosition[1]
258
456
 
259
457
  const cursorPosition: [number, number, number] = [gridX, y, gridZ]
@@ -262,6 +460,7 @@ export const RoofTool: React.FC = () => {
262
460
  cursorRef.current.position.set(gridX, gridY, gridZ)
263
461
 
264
462
  if (
463
+ (isGridSnapActive() || isMagneticSnapActive()) &&
265
464
  corner1Ref.current &&
266
465
  previousGridPosRef.current &&
267
466
  (gridX !== previousGridPosRef.current[0] || gridZ !== previousGridPosRef.current[1])
@@ -278,6 +477,9 @@ export const RoofTool: React.FC = () => {
278
477
  })
279
478
 
280
479
  if (corner1Ref.current) {
480
+ const draftPreview = useFloorplanDraftPreview.getState()
481
+ draftPreview.setRoofDraftStart([corner1Ref.current[0], corner1Ref.current[2]])
482
+ draftPreview.setRoofDraftEnd([gridX, gridZ])
281
483
  updateOutline(corner1Ref.current, cursorPosition)
282
484
  }
283
485
  }
@@ -285,20 +487,7 @@ export const RoofTool: React.FC = () => {
285
487
  const onGridClick = (event: GridEvent) => {
286
488
  if (!currentLevelId) return
287
489
 
288
- // World-grid snap projected into building-local; rotated buildings
289
- // used to drag every roof corner off the visible grid.
290
- const snapped = snapWorldXZForActiveBuilding(
291
- event.position[0],
292
- event.position[2],
293
- useEditor.getState().gridSnapStep,
294
- ).local
295
- const [gridX, gridZ] = alignPoint(
296
- snapped[0],
297
- snapped[1],
298
- event.localPosition[0],
299
- event.localPosition[2],
300
- event.nativeEvent?.altKey === true,
301
- )
490
+ const [gridX, gridZ] = resolveDraftPoint(event)
302
491
  const y = event.localPosition[1]
303
492
 
304
493
  if (corner1Ref.current) {
@@ -312,11 +501,17 @@ export const RoofTool: React.FC = () => {
312
501
  setSelection({ selectedIds: [roofId as AnyNode['id']] })
313
502
 
314
503
  corner1Ref.current = null
504
+ const draftPreview = useFloorplanDraftPreview.getState()
505
+ draftPreview.setRoofDraftStart(null)
506
+ draftPreview.setRoofDraftEnd(null)
315
507
  outlineRef.current.visible = false
316
- alignmentCandidates = collectAlignmentAnchors(useScene.getState().nodes, '', currentLevelId)
317
- useAlignmentGuides.getState().clear()
508
+ alignmentCandidates = collectRoofAlignmentAnchors(useScene.getState().nodes, currentLevelId)
509
+ clearSurfacePlanSnapFeedback()
318
510
  } else {
319
511
  corner1Ref.current = [gridX, y, gridZ]
512
+ const draftPreview = useFloorplanDraftPreview.getState()
513
+ draftPreview.setRoofDraftStart([gridX, gridZ])
514
+ draftPreview.setRoofDraftEnd([gridX, gridZ])
320
515
  sfxEmitter.emit('sfx:structure-build-start')
321
516
  setPreview((prev) => ({
322
517
  ...prev,
@@ -329,10 +524,13 @@ export const RoofTool: React.FC = () => {
329
524
  if (corner1Ref.current) {
330
525
  markToolCancelConsumed()
331
526
  corner1Ref.current = null
527
+ const draftPreview = useFloorplanDraftPreview.getState()
528
+ draftPreview.setRoofDraftStart(null)
529
+ draftPreview.setRoofDraftEnd(null)
332
530
  outlineRef.current.visible = false
333
531
  setPreview((prev) => ({ ...prev, corner1: null }))
334
532
  }
335
- useAlignmentGuides.getState().clear()
533
+ clearSurfacePlanSnapFeedback()
336
534
  }
337
535
 
338
536
  emitter.on('grid:move', onGridMove)
@@ -343,9 +541,12 @@ export const RoofTool: React.FC = () => {
343
541
  emitter.off('grid:move', onGridMove)
344
542
  emitter.off('grid:click', onGridClick)
345
543
  emitter.off('tool:cancel', onCancel)
346
- useAlignmentGuides.getState().clear()
544
+ clearSurfacePlanSnapFeedback()
347
545
 
348
546
  corner1Ref.current = null
547
+ const draftPreview = useFloorplanDraftPreview.getState()
548
+ draftPreview.setRoofDraftStart(null)
549
+ draftPreview.setRoofDraftEnd(null)
349
550
  }
350
551
  }, [currentLevelId, setSelection])
351
552
 
@@ -360,6 +561,34 @@ export const RoofTool: React.FC = () => {
360
561
  return { length, width, centerX, centerZ }
361
562
  }, [corner1, cursorPosition])
362
563
 
564
+ const roofGhostGeometry = useMemo(() => {
565
+ if (!previewDimensions) return null
566
+ return buildRoofGhostGeometry(
567
+ previewDimensions.length,
568
+ previewDimensions.width,
569
+ DEFAULT_WALL_HEIGHT,
570
+ DEFAULT_PITCH_DEG,
571
+ )
572
+ }, [previewDimensions])
573
+
574
+ const roofGhostEdges = useMemo(() => {
575
+ if (!previewDimensions) return null
576
+ return buildRoofGhostEdges(
577
+ previewDimensions.length,
578
+ previewDimensions.width,
579
+ DEFAULT_WALL_HEIGHT,
580
+ DEFAULT_PITCH_DEG,
581
+ )
582
+ }, [previewDimensions])
583
+
584
+ useEffect(
585
+ () => () => {
586
+ roofGhostGeometry?.dispose()
587
+ roofGhostEdges?.dispose()
588
+ },
589
+ [roofGhostEdges, roofGhostGeometry],
590
+ )
591
+
363
592
  return (
364
593
  <group>
365
594
  <CursorSphere ref={cursorRef} />
@@ -393,21 +622,34 @@ export const RoofTool: React.FC = () => {
393
622
  )}
394
623
 
395
624
  {previewDimensions && previewDimensions.length > 0.1 && previewDimensions.width > 0.1 && (
396
- <mesh
625
+ <group
397
626
  layers={EDITOR_LAYER}
398
627
  position={[previewDimensions.centerX, levelY + GRID_OFFSET, previewDimensions.centerZ]}
399
- rotation={[-Math.PI / 2, 0, 0]}
400
628
  >
401
- <planeGeometry args={[previewDimensions.length, previewDimensions.width]} />
402
- <meshBasicMaterial
403
- color="#818cf8"
404
- depthTest={false}
405
- depthWrite={false}
406
- opacity={0.1}
407
- side={DoubleSide}
408
- transparent
409
- />
410
- </mesh>
629
+ {roofGhostGeometry && (
630
+ <mesh geometry={roofGhostGeometry} layers={EDITOR_LAYER} renderOrder={1}>
631
+ <meshBasicMaterial
632
+ color="#818cf8"
633
+ depthTest={false}
634
+ depthWrite={false}
635
+ opacity={0.16}
636
+ side={DoubleSide}
637
+ transparent
638
+ />
639
+ </mesh>
640
+ )}
641
+ {roofGhostEdges && (
642
+ <lineSegments geometry={roofGhostEdges} layers={EDITOR_LAYER} renderOrder={2}>
643
+ <lineBasicMaterial
644
+ color="#818cf8"
645
+ depthTest={false}
646
+ depthWrite={false}
647
+ opacity={0.5}
648
+ transparent
649
+ />
650
+ </lineSegments>
651
+ )}
652
+ </group>
411
653
  )}
412
654
  </group>
413
655
  )
@@ -9,6 +9,10 @@ type PointerEventLike = {
9
9
  nativeEvent?: PointerEvent | PointerEventLike
10
10
  }
11
11
 
12
+ type SuppressBoxSelectOptions = {
13
+ markHandled?: boolean
14
+ }
15
+
12
16
  function pointerIdFor(event: PointerEvent | PointerEventLike): number | null {
13
17
  if ('pointerId' in event && typeof event.pointerId === 'number') {
14
18
  return event.pointerId
@@ -28,8 +32,12 @@ export function markBoxSelectHandled() {
28
32
  }, 50)
29
33
  }
30
34
 
31
- export function suppressBoxSelectForPointer(event: PointerEvent | PointerEventLike) {
32
- markBoxSelectHandled()
35
+ export function suppressBoxSelectForPointer(
36
+ event: PointerEvent | PointerEventLike,
37
+ options: SuppressBoxSelectOptions = {},
38
+ ) {
39
+ const markHandled = options.markHandled ?? true
40
+ if (markHandled) markBoxSelectHandled()
33
41
 
34
42
  const pointerId = pointerIdFor(event)
35
43
  if (pointerId === null || suppressedPointerIds.has(pointerId)) return
@@ -38,7 +46,7 @@ export function suppressBoxSelectForPointer(event: PointerEvent | PointerEventLi
38
46
 
39
47
  const clear = (releaseEvent?: PointerEvent) => {
40
48
  if (releaseEvent && releaseEvent.pointerId !== pointerId) return
41
- markBoxSelectHandled()
49
+ if (markHandled) markBoxSelectHandled()
42
50
  suppressedPointerIds.delete(pointerId)
43
51
  const cleanup = suppressionCleanups.get(pointerId)
44
52
  suppressionCleanups.delete(pointerId)
@@ -48,15 +56,18 @@ export function suppressBoxSelectForPointer(event: PointerEvent | PointerEventLi
48
56
  const onPointerUp = (releaseEvent: PointerEvent) => clear(releaseEvent)
49
57
  const onPointerCancel = (releaseEvent: PointerEvent) => clear(releaseEvent)
50
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 }
51
62
  const cleanup = () => {
52
- window.removeEventListener('pointerup', onPointerUp)
53
- window.removeEventListener('pointercancel', onPointerCancel)
63
+ window.removeEventListener('pointerup', onPointerUp, releaseListenerOptions)
64
+ window.removeEventListener('pointercancel', onPointerCancel, releaseListenerOptions)
54
65
  window.removeEventListener('blur', onBlur)
55
66
  }
56
67
 
57
68
  suppressionCleanups.set(pointerId, cleanup)
58
- window.addEventListener('pointerup', onPointerUp)
59
- window.addEventListener('pointercancel', onPointerCancel)
69
+ window.addEventListener('pointerup', onPointerUp, releaseListenerOptions)
70
+ window.addEventListener('pointercancel', onPointerCancel, releaseListenerOptions)
60
71
  window.addEventListener('blur', onBlur)
61
72
  }
62
73