@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,12 +1,15 @@
1
1
  'use client'
2
2
 
3
3
  import {
4
+ type AnyNode,
4
5
  type AnyNodeId,
5
- DEFAULT_WALL_HEIGHT,
6
6
  type FenceNode,
7
+ getWallBaseElevationForNodes,
7
8
  getWallCurveFrameAt,
9
+ getWallEffectiveHeightForNodes,
8
10
  getWallThickness,
9
11
  isCurvedWall,
12
+ MIN_WALL_HEIGHT,
10
13
  sceneRegistry,
11
14
  useLiveNodeOverrides,
12
15
  useScene,
@@ -32,12 +35,27 @@ import {
32
35
  } from 'three'
33
36
  import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js'
34
37
  import { MeshBasicNodeMaterial } from 'three/webgpu'
38
+ import {
39
+ clearStructuralElevationGuide,
40
+ publishStructuralElevationGuide,
41
+ resolveStructuralElevationSnap,
42
+ } from '../../lib/elevation-guides'
43
+ import { isHistoryShortcut } from '../../lib/history'
44
+ import { endpointReshapeScope } from '../../lib/interaction/scope'
35
45
  import { sfxEmitter } from '../../lib/sfx-bus'
36
- import useEditor from '../../store/use-editor'
46
+ import useEditor, { isGridSnapActive, isMagneticSnapActive } from '../../store/use-editor'
47
+ import useInteractionScope, {
48
+ useEndpointReshape,
49
+ useIsCurveReshape,
50
+ useMovingNode,
51
+ } from '../../store/use-interaction-scope'
37
52
  import { suppressBoxSelectForPointer } from '../tools/select/box-select-state'
53
+ import { resolveResizeSnapValue } from './handles/resize-snap'
54
+ import { type HandleDragControls, useHandleDrag } from './handles/use-handle-drag'
38
55
  import {
39
56
  createArrowHitAreaGeometry,
40
57
  createEndpointHitAreaGeometry,
58
+ HandleArrow,
41
59
  InvisibleHandleHitArea,
42
60
  NO_RAYCAST,
43
61
  useInvisibleHitAreaMaterial,
@@ -48,13 +66,12 @@ const HANDLE_MIN_OFFSET = 0.33
48
66
  const HANDLE_MIN_HEIGHT = 0.4
49
67
  const HANDLE_TOP_INSET = 0.08
50
68
  const HEIGHT_HANDLE_OFFSET = 0.26
51
- const MIN_WALL_HEIGHT = 0.5
52
69
  const ARROW_COLOR = '#8381ed'
53
70
  const ARROW_HOVER_COLOR = '#a5b4fc'
54
71
  // Match the door arrows: scale the rendered chevron down to ~two-thirds
55
72
  // so the in-world handles read as a single UI family.
56
73
  const ARROW_SCALE = 0.65
57
- const CORNER_HEX_RADIUS = 0.16
74
+ const CORNER_HEX_RADIUS = 0.11
58
75
  const CORNER_DASH_SIZE = 0.1
59
76
  const CORNER_GAP_SIZE = 0.07
60
77
  const CORNER_DASH_THICKNESS = 0.006
@@ -118,11 +135,9 @@ export function WallMoveSideHandles() {
118
135
  const selectedIds = useViewer((state) => state.selection.selectedIds)
119
136
  const mode = useEditor((state) => state.mode)
120
137
  const isFloorplanHovered = useEditor((state) => state.isFloorplanHovered)
121
- const movingNode = useEditor((state) => state.movingNode)
122
- const movingWallEndpoint = useEditor((state) => state.movingWallEndpoint)
123
- const movingFenceEndpoint = useEditor((state) => state.movingFenceEndpoint)
124
- const curvingWall = useEditor((state) => state.curvingWall)
125
- const curvingFence = useEditor((state) => state.curvingFence)
138
+ const movingNode = useMovingNode()
139
+ const endpointReshape = useEndpointReshape()
140
+ const isCurveReshape = useIsCurveReshape()
126
141
 
127
142
  const selectedId = selectedIds.length === 1 ? selectedIds[0] : null
128
143
  // Fence side-move / height / corner-pickers now flow through the
@@ -140,10 +155,8 @@ export function WallMoveSideHandles() {
140
155
  !isFloorplanHovered &&
141
156
  mode !== 'delete' &&
142
157
  !movingNode &&
143
- !movingWallEndpoint &&
144
- !movingFenceEndpoint &&
145
- !curvingWall &&
146
- !curvingFence
158
+ !endpointReshape &&
159
+ !isCurveReshape
147
160
 
148
161
  if (!shouldRender || !selectedNode) return null
149
162
 
@@ -151,6 +164,7 @@ export function WallMoveSideHandles() {
151
164
  }
152
165
 
153
166
  function WallMoveSideHandlesForWall({ wall }: { wall: WallNode }) {
167
+ const nodes = useScene((state) => state.nodes)
154
168
  // Merge the in-flight drag override so every handle (side-move arrows,
155
169
  // height arrow, corner leaders) tracks the live height in real time
156
170
  // during a height drag — the scene store stays at the pre-drag value
@@ -194,24 +208,34 @@ function WallMoveSideHandlesForWall({ wall }: { wall: WallNode }) {
194
208
  }
195
209
  }, [wall.parentId])
196
210
 
197
- const handles = useMemo(() => getWallMoveHandles(effectiveWall), [effectiveWall])
211
+ const baseElevation = getWallBaseElevationForNodes(effectiveWall, nodes)
212
+ const handles = useMemo(() => getWallMoveHandles(effectiveWall, nodes), [effectiveWall, nodes])
198
213
 
199
214
  if (!levelObject || handles.length === 0) return null
200
215
 
201
216
  return createPortal(
202
- <group>
203
- {handles.map((handle) => (
204
- <WallMoveArrowHandle handle={handle} key={handle.key} wall={effectiveWall} />
205
- ))}
206
- <WallHeightArrowHandle wall={effectiveWall} />
207
- <WallCornerLeaderHandle endpoint="start" wall={effectiveWall} />
208
- <WallCornerLeaderHandle endpoint="end" wall={effectiveWall} />
209
- </group>,
217
+ <>
218
+ <group position={[0, baseElevation, 0]}>
219
+ {handles.map((handle) => (
220
+ <WallMoveArrowHandle handle={handle} key={handle.key} wall={effectiveWall} />
221
+ ))}
222
+ <WallHeightArrowHandle wall={effectiveWall} />
223
+ <WallCornerLeaderHandle endpoint="start" wall={effectiveWall} />
224
+ <WallCornerLeaderHandle endpoint="end" wall={effectiveWall} />
225
+ </group>
226
+ <WallBaseElevationHandle
227
+ baseElevation={baseElevation}
228
+ levelObject={levelObject}
229
+ wall={effectiveWall}
230
+ />
231
+ </>,
210
232
  levelObject,
211
233
  )
212
234
  }
213
235
 
214
236
  function buildDashedVerticalGeometry(height: number) {
237
+ if (!(Number.isFinite(height) && height > 0)) return new BufferGeometry()
238
+
215
239
  // Build each dash as a thin cylinder section so thickness is
216
240
  // controllable — native `lineSegments` lock to 1px on WebGL/WebGPU.
217
241
  const dashes: BufferGeometry[] = []
@@ -224,7 +248,10 @@ function buildDashedVerticalGeometry(height: number) {
224
248
  dashes.push(cylinder)
225
249
  y = end + CORNER_GAP_SIZE
226
250
  }
227
- const merged = mergeGeometries(dashes, false) ?? new BufferGeometry()
251
+ const merged =
252
+ dashes.length > 0
253
+ ? (mergeGeometries(dashes, false) ?? new BufferGeometry())
254
+ : new BufferGeometry()
228
255
  for (const dash of dashes) dash.dispose()
229
256
  return merged
230
257
  }
@@ -241,7 +268,7 @@ function WallCornerLeaderHandle({ wall, endpoint }: { wall: WallNode; endpoint:
241
268
  const corner = endpoint === 'start' ? wall.start : wall.end
242
269
  const x = corner[0]
243
270
  const z = corner[1]
244
- const wallHeight = wall.height ?? DEFAULT_WALL_HEIGHT
271
+ const wallHeight = getWallEffectiveHeightForNodes(wall, useScene.getState().nodes)
245
272
 
246
273
  const dashedGeometry = useMemo(() => buildDashedVerticalGeometry(wallHeight), [wallHeight])
247
274
  const hitGeometry = useMemo(() => createEndpointHitAreaGeometry(CORNER_HEX_RADIUS), [])
@@ -329,11 +356,12 @@ function WallCornerLeaderHandle({ wall, endpoint }: { wall: WallNode; endpoint:
329
356
  }, [])
330
357
 
331
358
  const activateEndpointMove = (event: ThreeEvent<PointerEvent>) => {
359
+ if (event.button !== 0) return
332
360
  event.stopPropagation()
333
361
  suppressBoxSelectForPointer(event)
334
362
  sfxEmitter.emit('sfx:item-pick')
335
363
  document.body.style.cursor = 'grabbing'
336
- useEditor.getState().setMovingWallEndpoint({ wall, endpoint })
364
+ useInteractionScope.getState().begin(endpointReshapeScope(wall.id, endpoint))
337
365
  }
338
366
 
339
367
  return (
@@ -377,6 +405,155 @@ function WallCornerLeaderHandle({ wall, endpoint }: { wall: WallNode; endpoint:
377
405
  )
378
406
  }
379
407
 
408
+ function WallBaseElevationHandle({
409
+ wall,
410
+ baseElevation,
411
+ levelObject,
412
+ }: {
413
+ wall: WallNode
414
+ baseElevation: number
415
+ levelObject: Object3D
416
+ }) {
417
+ const [isHovered, setIsHovered] = useState(false)
418
+ const [isDragging, setIsDragging] = useState(false)
419
+ const { camera } = useThree()
420
+ const zoom = camera instanceof OrthographicCamera ? 1 / camera.zoom : 1
421
+ const curveFrame = isCurvedWall(wall) ? getWallCurveFrameAt(wall, 0.5) : null
422
+ const midpoint: [number, number] = curveFrame
423
+ ? [curveFrame.point.x, curveFrame.point.y]
424
+ : [(wall.start[0] + wall.end[0]) / 2, (wall.start[1] + wall.end[1]) / 2]
425
+ const elevationGuideSource = {
426
+ nodeId: wall.id,
427
+ levelId: wall.parentId,
428
+ anchor: midpoint,
429
+ }
430
+ const leaderBottom = Math.min(0, baseElevation)
431
+ const leaderHeight = Math.abs(baseElevation)
432
+ const dashedGeometry = useMemo(
433
+ () => (leaderHeight > 0.0001 ? buildDashedVerticalGeometry(leaderHeight) : null),
434
+ [leaderHeight],
435
+ )
436
+ const dashMaterial = useMemo(
437
+ () =>
438
+ new MeshBasicNodeMaterial({
439
+ color: new Color(ARROW_COLOR),
440
+ transparent: true,
441
+ opacity: 0.85,
442
+ depthTest: false,
443
+ depthWrite: false,
444
+ }),
445
+ [],
446
+ )
447
+
448
+ const isActive = isHovered || isDragging
449
+ useEffect(() => {
450
+ dashMaterial.color.set(isActive ? ARROW_HOVER_COLOR : ARROW_COLOR)
451
+ }, [dashMaterial, isActive])
452
+ useEffect(() => () => dashedGeometry?.dispose(), [dashedGeometry])
453
+ useEffect(() => () => dashMaterial.dispose(), [dashMaterial])
454
+ const dragControls = useMemo<HandleDragControls>(
455
+ () => ({
456
+ onStart: () => {},
457
+ onEnd: () => {},
458
+ }),
459
+ [],
460
+ )
461
+
462
+ const activateElevationMove = useHandleDrag({
463
+ kind: 'drag',
464
+ cursor: 'ns-resize',
465
+ dragControls,
466
+ handleIndex: 0,
467
+ node: wall,
468
+ rideObject: levelObject,
469
+ setIsDragging,
470
+ onStart: ({ event, initialNode, intersectPlane, nodeId, sceneApi }) => {
471
+ if (initialNode.type !== 'wall') return null
472
+
473
+ levelObject.updateWorldMatrix(true, false)
474
+ const initialBase = getWallBaseElevationForNodes(initialNode, sceneApi.nodes())
475
+ const supportBase = initialBase - (initialNode.supportOffset ?? 0)
476
+ const initialHeight = getWallEffectiveHeightForNodes(initialNode, sceneApi.nodes())
477
+ const midpointWorld = new Vector3(midpoint[0], initialBase, midpoint[1]).applyMatrix4(
478
+ levelObject.matrixWorld,
479
+ )
480
+ const planeNormal = new Vector3().subVectors(camera.position, midpointWorld).setY(0)
481
+ if (planeNormal.lengthSq() === 0) return null
482
+ planeNormal.normalize()
483
+ const plane = new Plane().setFromNormalAndCoplanarPoint(planeNormal, midpointWorld)
484
+ const initialHit = new Vector3()
485
+ if (
486
+ !intersectPlane(event.nativeEvent.clientX, event.nativeEvent.clientY, plane, initialHit)
487
+ ) {
488
+ return null
489
+ }
490
+ const initialPointerY = levelObject.worldToLocal(initialHit).y
491
+
492
+ return {
493
+ onBegin: () => {
494
+ useInteractionScope.getState().begin({ kind: 'handle-drag', nodeId, handle: 'elevation' })
495
+ },
496
+ onEnd: () => {
497
+ useInteractionScope.getState().endIf((scope) => scope.kind === 'handle-drag')
498
+ clearStructuralElevationGuide(initialNode.id)
499
+ },
500
+ move: ({ event: moveEvent, intersectPlane: intersectMovePlane }) => {
501
+ const hit = new Vector3()
502
+ if (!intersectMovePlane(moveEvent.clientX, moveEvent.clientY, plane, hit)) return null
503
+ const pointerY = levelObject.worldToLocal(hit).y
504
+ const nextBase = resolveResizeSnapValue({
505
+ rawValue: initialBase + pointerY - initialPointerY,
506
+ gridSnapEnabled: true,
507
+ gridSnapActive: isGridSnapActive(),
508
+ gridSnapStep: useEditor.getState().gridSnapStep,
509
+ magneticSnapActive: isMagneticSnapActive(),
510
+ magneticSnap: (value) =>
511
+ resolveStructuralElevationSnap(elevationGuideSource, value, sceneApi.nodes()),
512
+ })
513
+ publishStructuralElevationGuide(elevationGuideSource, nextBase, sceneApi.nodes())
514
+ if (Math.abs(nextBase - initialBase) < 1e-6) {
515
+ return {
516
+ supportOffset: initialNode.supportOffset,
517
+ height: initialNode.height,
518
+ }
519
+ }
520
+ const nextOffset = nextBase - supportBase
521
+ return {
522
+ supportOffset: Math.abs(nextOffset) < 1e-6 ? undefined : nextOffset,
523
+ height: initialHeight,
524
+ }
525
+ },
526
+ }
527
+ },
528
+ })
529
+
530
+ return (
531
+ <>
532
+ {dashedGeometry ? (
533
+ <mesh
534
+ frustumCulled={false}
535
+ geometry={dashedGeometry}
536
+ material={dashMaterial}
537
+ position={[midpoint[0], leaderBottom, midpoint[1]]}
538
+ renderOrder={1001}
539
+ />
540
+ ) : null}
541
+ <HandleArrow
542
+ cursor="ns-resize"
543
+ hover={isActive}
544
+ hoverScale={1.25}
545
+ onHoverChange={setIsHovered}
546
+ onPointerDown={activateElevationMove}
547
+ placement={{
548
+ position: [midpoint[0], baseElevation, midpoint[1]],
549
+ baseScale: zoom,
550
+ }}
551
+ shape="tracker"
552
+ />
553
+ </>
554
+ )
555
+ }
556
+
380
557
  function WallHeightArrowHandle({ wall }: { wall: WallNode }) {
381
558
  const [isHovered, setIsHovered] = useState(false)
382
559
  const arrowGeometry = useMemo(() => createArrowHandleGeometry(), [])
@@ -429,10 +606,11 @@ function WallHeightArrowHandle({ wall }: { wall: WallNode }) {
429
606
  const wallAngle = Math.atan2(-dirZ, dirX)
430
607
  // `wall` is the override-merged effective wall (see
431
608
  // WallMoveSideHandlesForWall), so this height is already live during a drag.
432
- const wallHeight = wall.height ?? DEFAULT_WALL_HEIGHT
609
+ const wallHeight = getWallEffectiveHeightForNodes(wall, useScene.getState().nodes)
433
610
  const handleY = wallHeight + HEIGHT_HANDLE_OFFSET
434
611
 
435
612
  const activateHeightResize = (event: ThreeEvent<PointerEvent>) => {
613
+ if (event.button !== 0) return
436
614
  event.stopPropagation()
437
615
  suppressBoxSelectForPointer(event)
438
616
  const levelObject = wall.parentId ? sceneRegistry.nodes.get(wall.parentId) : null
@@ -461,14 +639,16 @@ function WallHeightArrowHandle({ wall }: { wall: WallNode }) {
461
639
  const hit = new Vector3()
462
640
  if (!raycaster.ray.intersectPlane(plane, hit)) return
463
641
 
464
- const initialHeight = wall.height ?? DEFAULT_WALL_HEIGHT
642
+ // Dragging the top makes the wall custom-height; seed from the resolved
643
+ // effective height so a plane-bound wall's drag starts at its real top.
644
+ const initialHeight = getWallEffectiveHeightForNodes(wall, useScene.getState().nodes)
465
645
  const initialY = hit.y
466
646
  const wallId = wall.id as AnyNodeId
467
647
  let pendingHeight = initialHeight
468
648
 
469
649
  document.body.style.cursor = 'ns-resize'
470
650
  sfxEmitter.emit('sfx:item-pick')
471
- useEditor.getState().setActiveHandleDrag({ nodeId: wallId, label: 'height' })
651
+ useInteractionScope.getState().begin({ kind: 'handle-drag', nodeId: wallId, handle: 'height' })
472
652
  // Suppress R3F node pointer events until pointerup completes so the
473
653
  // synthesized click doesn't reroute selection to whatever mesh sits
474
654
  // under the cursor at release.
@@ -494,11 +674,12 @@ function WallHeightArrowHandle({ wall }: { wall: WallNode }) {
494
674
  window.removeEventListener('pointermove', onMove)
495
675
  window.removeEventListener('pointerup', onUp)
496
676
  window.removeEventListener('pointercancel', onCancel)
677
+ window.removeEventListener('keydown', onKeyDown, true)
497
678
  if (document.body.style.cursor === 'ns-resize') {
498
679
  document.body.style.cursor = ''
499
680
  }
500
681
  useScene.temporal.getState().resume()
501
- useEditor.getState().setActiveHandleDrag(null)
682
+ useInteractionScope.getState().endIf((sc) => sc.kind === 'handle-drag')
502
683
  useViewer.getState().setInputDragging(false)
503
684
  dragCleanupRef.current = null
504
685
  }
@@ -523,10 +704,21 @@ function WallHeightArrowHandle({ wall }: { wall: WallNode }) {
523
704
  cleanup()
524
705
  }
525
706
 
707
+ // Escape / ⌘Z abort the drag — capture phase so they win over the global
708
+ // use-keyboard arms (⌘Z must never history-jump under a live pointer).
709
+ const onKeyDown = (e: KeyboardEvent) => {
710
+ if (e.key !== 'Escape' && !isHistoryShortcut(e)) return
711
+ e.preventDefault()
712
+ e.stopPropagation()
713
+ swallowNextClick()
714
+ onCancel()
715
+ }
716
+
526
717
  dragCleanupRef.current = cleanup
527
718
  window.addEventListener('pointermove', onMove)
528
719
  window.addEventListener('pointerup', onUp)
529
720
  window.addEventListener('pointercancel', onCancel)
721
+ window.addEventListener('keydown', onKeyDown, true)
530
722
  }
531
723
 
532
724
  return (
@@ -605,16 +797,15 @@ function WallMoveArrowHandle({ wall, handle }: { wall: WallNode; handle: WallMov
605
797
  useEffect(() => () => arrowMaterial.dispose(), [arrowMaterial])
606
798
 
607
799
  const activateWallMove = (event: ThreeEvent<PointerEvent>) => {
800
+ if (event.button !== 0) return
608
801
  event.stopPropagation()
609
802
  suppressBoxSelectForPointer(event)
610
803
  document.body.style.cursor = 'grabbing'
611
804
 
612
805
  sfxEmitter.emit('sfx:item-pick')
613
806
  useEditor.getState().setMovingNode(wall)
614
- useEditor.getState().setMovingWallEndpoint(null)
615
- useEditor.getState().setMovingFenceEndpoint(null)
616
- useEditor.getState().setCurvingWall(null)
617
- useEditor.getState().setCurvingFence(null)
807
+ useInteractionScope.getState().endIf((s) => s.kind === 'reshaping' && s.reshape === 'endpoint')
808
+ useInteractionScope.getState().endIf((s) => s.kind === 'reshaping' && s.reshape === 'curve')
618
809
  // Keep the wall selected so it stays the active item once the move
619
810
  // commits; the `!movingNode` guard on the handles hides them mid-drag.
620
811
  }
@@ -696,16 +887,15 @@ function FenceMoveArrowHandle({ fence, handle }: { fence: FenceNode; handle: Wal
696
887
  useEffect(() => () => arrowMaterial.dispose(), [arrowMaterial])
697
888
 
698
889
  const activateFenceMove = (event: ThreeEvent<PointerEvent>) => {
890
+ if (event.button !== 0) return
699
891
  event.stopPropagation()
700
892
  suppressBoxSelectForPointer(event)
701
893
  document.body.style.cursor = 'grabbing'
702
894
 
703
895
  sfxEmitter.emit('sfx:item-pick')
704
896
  useEditor.getState().setMovingNode(fence)
705
- useEditor.getState().setMovingWallEndpoint(null)
706
- useEditor.getState().setMovingFenceEndpoint(null)
707
- useEditor.getState().setCurvingWall(null)
708
- useEditor.getState().setCurvingFence(null)
897
+ useInteractionScope.getState().endIf((s) => s.kind === 'reshaping' && s.reshape === 'endpoint')
898
+ useInteractionScope.getState().endIf((s) => s.kind === 'reshaping' && s.reshape === 'curve')
709
899
  // Keep the fence selected so it stays active once the move commits.
710
900
  }
711
901
 
@@ -743,7 +933,7 @@ function FenceMoveArrowHandle({ fence, handle }: { fence: FenceNode; handle: Wal
743
933
  )
744
934
  }
745
935
 
746
- function getWallMoveHandles(wall: WallNode): WallMoveHandle[] {
936
+ function getWallMoveHandles(wall: WallNode, nodes: Record<string, AnyNode>): WallMoveHandle[] {
747
937
  const dx = wall.end[0] - wall.start[0]
748
938
  const dz = wall.end[1] - wall.start[1]
749
939
  const length = Math.hypot(dx, dz)
@@ -759,7 +949,7 @@ function getWallMoveHandles(wall: WallNode): WallMoveHandle[] {
759
949
  const midpoint: [number, number] = frame
760
950
  ? [frame.point.x, frame.point.y]
761
951
  : [(wall.start[0] + wall.end[0]) / 2, (wall.start[1] + wall.end[1]) / 2]
762
- const wallHeight = wall.height ?? DEFAULT_WALL_HEIGHT
952
+ const wallHeight = getWallEffectiveHeightForNodes(wall, nodes)
763
953
  const handleHeight = Math.max(wallHeight - HANDLE_TOP_INSET, HANDLE_MIN_HEIGHT)
764
954
  const offset = Math.max(getWallThickness(wall) / 2 + HANDLE_OFFSET, HANDLE_MIN_OFFSET)
765
955
 
@@ -3,8 +3,16 @@
3
3
  import { type AnyNodeId, sceneRegistry, useLiveNodeOverrides, useScene } from '@pascal-app/core'
4
4
  import { useViewer } from '@pascal-app/viewer'
5
5
  import { createPortal, useFrame, useThree } from '@react-three/fiber'
6
- import { useEffect, useMemo, useRef } from 'react'
7
- import { BoxGeometry, type BufferGeometry, EdgesGeometry, type Group, Vector3 } from 'three'
6
+ import { useEffect, useLayoutEffect, useMemo, useRef } from 'react'
7
+ import {
8
+ BoxGeometry,
9
+ type BufferGeometry,
10
+ EdgesGeometry,
11
+ type Group,
12
+ type Mesh,
13
+ MeshBasicMaterial,
14
+ Vector3,
15
+ } from 'three'
8
16
  import { LineBasicNodeMaterial, MeshBasicNodeMaterial } from 'three/webgpu'
9
17
  import { EDITOR_LAYER } from '../../lib/constants'
10
18
 
@@ -41,6 +49,13 @@ const fillMaterial = new MeshBasicNodeMaterial({
41
49
  depthWrite: false,
42
50
  })
43
51
 
52
+ // Keep the cutout in the render list for the outliner's override material
53
+ // without letting its source material alter the normal scene pass.
54
+ const outlineProxyMaterial = new MeshBasicMaterial({
55
+ colorWrite: false,
56
+ depthWrite: false,
57
+ })
58
+
44
59
  function makeOutlineGeometry(width: number, height: number, depth: number): BufferGeometry {
45
60
  const box = new BoxGeometry(width + PAD, height + PAD, depth + PAD)
46
61
  const edges = new EdgesGeometry(box)
@@ -60,26 +75,60 @@ function makeOutlineGeometry(width: number, height: number, depth: number): Buff
60
75
  */
61
76
  export function WallOpeningHighlights() {
62
77
  const selectedIds = useViewer((state) => state.selection.selectedIds)
78
+ const hoveredId = useViewer((state) => state.hoveredId)
63
79
  const { scene } = useThree()
80
+ const outlineProxyIds = Array.from(new Set(hoveredId ? [...selectedIds, hoveredId] : selectedIds))
64
81
 
65
- if (selectedIds.length === 0) return null
82
+ if (selectedIds.length === 0 && !hoveredId) return null
66
83
 
67
- return createPortal(
84
+ return (
68
85
  <>
69
- {selectedIds.map((id) => (
70
- <SelectionOpeningHighlights key={id} selectedId={id} />
86
+ {outlineProxyIds.map((id) => (
87
+ <OpeningOutlineProxy key={id} openingId={id} />
71
88
  ))}
72
- </>,
73
- scene,
89
+ {selectedIds.length > 0 &&
90
+ createPortal(
91
+ <>
92
+ {selectedIds.map((id) => (
93
+ <SelectionOpeningHighlights key={id} selectedId={id} />
94
+ ))}
95
+ </>,
96
+ scene,
97
+ )}
98
+ </>
74
99
  )
75
100
  }
76
101
 
77
- // Resolves a selected node into the opening highlight(s) to draw:
78
- // - a selected wall a hint over each door / window it hosts ("editable
79
- // child here").
80
- // - a directly-selected frameless opening (a `door` whose `openingKind` is
81
- // `'opening'`) a fill over its own cutout, so the selection reads as
82
- // occupied even though the opening renders no geometry of its own.
102
+ function OpeningOutlineProxy({ openingId }: { openingId: string }) {
103
+ const node = useScene((state) => state.nodes[openingId as AnyNodeId])
104
+ const geometryRevision = useViewer((state) => state.geometryRevision)
105
+ const proxyRef = useRef<Mesh | null>(null)
106
+ const isFramelessOpening = node?.type === 'door' && node.openingKind === 'opening'
107
+
108
+ useLayoutEffect(() => {
109
+ void geometryRevision
110
+ if (!isFramelessOpening) return
111
+ const root = sceneRegistry.nodes.get(openingId as AnyNodeId)
112
+ const proxy = root?.getObjectByName('cutout') as Mesh | undefined
113
+ if (!proxy) return
114
+
115
+ proxy.material = outlineProxyMaterial
116
+ proxy.visible = true
117
+ proxyRef.current = proxy
118
+
119
+ return () => {
120
+ proxy.visible = false
121
+ proxyRef.current = null
122
+ }
123
+ }, [geometryRevision, isFramelessOpening, openingId])
124
+
125
+ useFrame(() => {
126
+ if (proxyRef.current) proxyRef.current.visible = true
127
+ })
128
+
129
+ return null
130
+ }
131
+
83
132
  function SelectionOpeningHighlights({ selectedId }: { selectedId: string }) {
84
133
  const node = useScene((state) => state.nodes[selectedId as AnyNodeId])
85
134
 
@@ -94,27 +143,9 @@ function SelectionOpeningHighlights({ selectedId }: { selectedId: string }) {
94
143
  )
95
144
  }
96
145
 
97
- if (node?.type === 'door' && node.openingKind === 'opening') {
98
- return <SelectedOpeningHighlight openingId={selectedId} parentId={node.parentId} />
99
- }
100
-
101
146
  return null
102
147
  }
103
148
 
104
- // A frameless opening selected on its own. Pulls the cutout depth from its
105
- // host wall's thickness so the fill block matches the wall it sits in.
106
- function SelectedOpeningHighlight({
107
- openingId,
108
- parentId,
109
- }: {
110
- openingId: string
111
- parentId: string | null
112
- }) {
113
- const parent = useScene((state) => (parentId ? state.nodes[parentId as AnyNodeId] : undefined))
114
- const depth = parent?.type === 'wall' ? (parent.thickness ?? 0.1) : 0.1
115
- return <OpeningHighlight depth={depth} openingId={openingId} />
116
- }
117
-
118
149
  function OpeningHighlight({ openingId, depth }: { openingId: string; depth: number }) {
119
150
  const node = useScene((state) => state.nodes[openingId as AnyNodeId])
120
151
  // Resize arrows publish width/height to the live-override store during the