@pascal-app/editor 1.0.1 → 1.0.4

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 (498) hide show
  1. package/package.json +16 -9
  2. package/src/components/editor/action-menu-button.tsx +83 -0
  3. package/src/components/editor/bake-thumbnail.tsx +315 -4
  4. package/src/components/editor/capture-level-picker.tsx +72 -0
  5. package/src/components/editor/custom-camera-controls.tsx +221 -128
  6. package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
  7. package/src/components/editor/duplicate-node.ts +244 -0
  8. package/src/components/editor/editor-layout-mobile.tsx +37 -25
  9. package/src/components/editor/editor-layout-v2.tsx +131 -58
  10. package/src/components/editor/export-manager.tsx +34 -8
  11. package/src/components/editor/first-person/build-collider-world.test.ts +51 -0
  12. package/src/components/editor/first-person/build-collider-world.ts +6 -2
  13. package/src/components/editor/first-person/mechanism-targets.test.ts +75 -0
  14. package/src/components/editor/first-person/mechanism-targets.ts +36 -0
  15. package/src/components/editor/first-person-controls.tsx +205 -2
  16. package/src/components/editor/floating-action-menu.tsx +63 -207
  17. package/src/components/editor/floor-aware-camera-controls.test.ts +442 -0
  18. package/src/components/editor/floor-aware-camera-controls.ts +324 -0
  19. package/src/components/editor/floor-openings-overlay.tsx +243 -0
  20. package/src/components/editor/floor-region-controls.tsx +846 -0
  21. package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
  22. package/src/components/editor/floor-surface.test.ts +165 -0
  23. package/src/components/editor/floor-surface.ts +182 -0
  24. package/src/components/editor/floorplan-background-selection.test.ts +11 -2
  25. package/src/components/editor/floorplan-background-selection.ts +1 -3
  26. package/src/components/editor/floorplan-panel.tsx +536 -487
  27. package/src/components/editor/footprint-height-handle.tsx +204 -0
  28. package/src/components/editor/grid.tsx +31 -7
  29. package/src/components/editor/group-actions.ts +59 -11
  30. package/src/components/editor/group-floating-action-menu.tsx +19 -9
  31. package/src/components/editor/group-move-3d.ts +4 -6
  32. package/src/components/editor/group-rotate-handle.tsx +81 -35
  33. package/src/components/editor/group-selection-box-3d.tsx +25 -17
  34. package/src/components/editor/group-transform-shared.test.ts +126 -1
  35. package/src/components/editor/group-transform-shared.ts +79 -73
  36. package/src/components/editor/handles/handle-arrow.tsx +72 -2
  37. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  38. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  39. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  40. package/src/components/editor/handles/use-handle-drag.ts +272 -131
  41. package/src/components/editor/index.tsx +316 -176
  42. package/src/components/editor/join-walls-pill.tsx +44 -0
  43. package/src/components/editor/node-action-menu.tsx +70 -70
  44. package/src/components/editor/node-arrow-handles.tsx +167 -14
  45. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  46. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  47. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  48. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  49. package/src/components/editor/registry-action-contributions.tsx +132 -0
  50. package/src/components/editor/room-controls.tsx +847 -0
  51. package/src/components/editor/room-floor-highlight.tsx +128 -0
  52. package/src/components/editor/room-handles-2d.tsx +354 -0
  53. package/src/components/editor/room-handles.tsx +675 -0
  54. package/src/components/editor/room-highlight.tsx +225 -0
  55. package/src/components/editor/room-labels.test.ts +357 -0
  56. package/src/components/editor/room-labels.tsx +990 -0
  57. package/src/components/editor/room-transform-ghost.tsx +256 -0
  58. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  59. package/src/components/editor/selection-manager.tsx +451 -317
  60. package/src/components/editor/slab-hole-highlights.tsx +3 -0
  61. package/src/components/editor/snapshot-capture-overlay.tsx +4 -0
  62. package/src/components/editor/snapshot-capture.test.ts +52 -4
  63. package/src/components/editor/snapshot-capture.ts +30 -5
  64. package/src/components/editor/thumbnail-generator.test.ts +308 -0
  65. package/src/components/editor/thumbnail-generator.tsx +1035 -229
  66. package/src/components/editor/use-floorplan-background-placement.ts +31 -11
  67. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  68. package/src/components/editor/views/host-view.tsx +61 -0
  69. package/src/components/editor/views/plugins-view.tsx +670 -0
  70. package/src/components/editor/views/scene-region.tsx +59 -0
  71. package/src/components/editor/views/use-editor-views.ts +57 -0
  72. package/src/components/editor/views/view-bar.tsx +220 -0
  73. package/src/components/editor/wall-justification.test.ts +140 -0
  74. package/src/components/editor/wall-move-side-handles.tsx +114 -163
  75. package/src/components/editor/wall-opening-highlights.tsx +2 -3
  76. package/src/components/editor/wall-push-arrow.tsx +92 -0
  77. package/src/components/editor/wall-region-handles.tsx +509 -0
  78. package/src/components/editor/wall-snap-beacon-layer.tsx +3 -0
  79. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  80. package/src/components/editor-2d/floorplan-group-move.tsx +17 -24
  81. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  82. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  83. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +33 -12
  84. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  85. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +242 -126
  86. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  87. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  88. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  89. package/src/components/editor-2d/renderers/floorplan-draft-measurement.tsx +166 -0
  90. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  91. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +87 -8
  92. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +355 -245
  93. package/src/components/editor-2d/svg-paths.ts +12 -15
  94. package/src/components/procedural-items/inspector.tsx +40 -15
  95. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  96. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  97. package/src/components/systems/zone/zone-system.test.ts +47 -0
  98. package/src/components/systems/zone/zone-system.tsx +31 -66
  99. package/src/components/tools/__fixtures__/hosting.ts +10 -0
  100. package/src/components/tools/item/item-surface-placement.test.ts +3 -9
  101. package/src/components/tools/item/item-surface-strategy-resolver.test.ts +3 -9
  102. package/src/components/tools/item/placement-history.test.tsx +474 -0
  103. package/src/components/tools/item/placement-strategies.ts +63 -47
  104. package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +3 -3
  105. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  106. package/src/components/tools/item/use-draft-node.ts +207 -97
  107. package/src/components/tools/item/use-placement-coordinator.tsx +251 -111
  108. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  109. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  110. package/src/components/tools/registry/item-surface-move.test.ts +10 -2
  111. package/src/components/tools/registry/item-surface-move.ts +46 -24
  112. package/src/components/tools/registry/item-surface-resolver.test.ts +2 -2
  113. package/src/components/tools/registry/move-registry-node-tool.tsx +26 -10
  114. package/src/components/tools/select/box-select-tool.tsx +2 -12
  115. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  116. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  117. package/src/components/tools/select/select-candidates.test.ts +37 -4
  118. package/src/components/tools/select/select-candidates.ts +15 -11
  119. package/src/components/tools/shared/affordance-dispatch.test.ts +24 -0
  120. package/src/components/tools/shared/affordance-dispatch.ts +13 -0
  121. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  122. package/src/components/tools/shared/draft-measurement-label.tsx +38 -0
  123. package/src/components/tools/shared/pointer-support-cap.test.ts +27 -0
  124. package/src/components/tools/shared/pointer-support-cap.ts +17 -5
  125. package/src/components/tools/shared/surface-rejection.test.tsx +5 -4
  126. package/src/components/tools/shared/surface-rejection.tsx +14 -0
  127. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  128. package/src/components/tools/stair/stair-defaults.ts +0 -3
  129. package/src/components/tools/stair/stair-tool.tsx +90 -42
  130. package/src/components/tools/tool-manager.tsx +82 -23
  131. package/src/components/tools/wall/wall-drafting.test.ts +19 -4
  132. package/src/components/tools/zone/zone-tool.tsx +4 -12
  133. package/src/components/ui/action-menu/action-button.tsx +3 -0
  134. package/src/components/ui/action-menu/camera-actions.tsx +2 -1
  135. package/src/components/ui/action-menu/control-modes.tsx +2 -35
  136. package/src/components/ui/action-menu/measurement-control.tsx +3 -10
  137. package/src/components/ui/build-panel/build-panel.tsx +235 -0
  138. package/src/components/ui/command-palette/editor-commands.tsx +4 -30
  139. package/src/components/ui/command-palette/index.tsx +5 -24
  140. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  141. package/src/components/ui/controls/material-paint-panel.tsx +101 -91
  142. package/src/components/ui/controls/material-picker.tsx +8 -67
  143. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  144. package/src/components/ui/controls/select-control.tsx +65 -0
  145. package/src/components/ui/controls/slider-control.tsx +76 -31
  146. package/src/components/ui/floating-level-selector.tsx +9 -14
  147. package/src/components/ui/helpers/building-helper.tsx +4 -1
  148. package/src/components/ui/helpers/contextual-helper-panel.tsx +154 -70
  149. package/src/components/ui/helpers/helper-manager.test.tsx +69 -0
  150. package/src/components/ui/helpers/helper-manager.tsx +348 -19
  151. package/src/components/ui/helpers/item-helper.tsx +8 -0
  152. package/src/components/ui/helpers/registered-tool-helper.tsx +10 -33
  153. package/src/components/ui/icon-ref.tsx +45 -1
  154. package/src/components/ui/item-catalog/item-catalog.tsx +5 -30
  155. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  156. package/src/components/ui/node-icon-image.tsx +16 -0
  157. package/src/components/ui/panels/collections/collections-popover.tsx +20 -14
  158. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  159. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  160. package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
  161. package/src/components/ui/panels/mobile-selection-bar.tsx +2 -2
  162. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  163. package/src/components/ui/panels/multi-field-value.ts +6 -3
  164. package/src/components/ui/panels/multi-height-mode.tsx +27 -21
  165. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  166. package/src/components/ui/panels/multi-selection-panel.tsx +43 -4
  167. package/src/components/ui/panels/node-display.ts +8 -3
  168. package/src/components/ui/panels/panel-manager.tsx +103 -68
  169. package/src/components/ui/panels/panel-wrapper.tsx +78 -36
  170. package/src/components/ui/panels/parametric-field-control.tsx +1 -1
  171. package/src/components/ui/panels/parametric-inspector.tsx +7 -19
  172. package/src/components/ui/panels/reference-panel.tsx +2 -3
  173. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  174. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  175. package/src/components/ui/panels/room-name-options.ts +47 -0
  176. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  177. package/src/components/ui/panels/room-panel.tsx +725 -0
  178. package/src/components/ui/panels/shape-choice.tsx +58 -0
  179. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  180. package/src/components/ui/primitives/color-dot.tsx +36 -24
  181. package/src/components/ui/primitives/shortcut-token.tsx +4 -0
  182. package/src/components/ui/primitives/sidebar.tsx +2 -0
  183. package/src/components/ui/right-stack.test.ts +29 -0
  184. package/src/components/ui/right-stack.tsx +145 -0
  185. package/src/components/ui/sidebar/icon-rail.tsx +5 -0
  186. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +2 -2
  187. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +615 -372
  188. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +7 -7
  189. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +1 -1
  190. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  191. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  192. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  193. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  194. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  195. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  196. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  197. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  198. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  199. package/src/components/ui/sidebar/panels/site-panel/index.tsx +176 -211
  200. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  201. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  202. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  203. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  204. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  205. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  206. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  207. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  208. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  209. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  210. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +0 -27
  211. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  212. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  213. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  214. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  215. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +0 -2
  216. package/src/components/ui/sidebar/tab-bar.tsx +10 -0
  217. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  218. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  219. package/src/components/viewer/floorplan-preview-visibility.test.ts +35 -0
  220. package/src/components/viewer/floorplan-preview-visibility.ts +19 -0
  221. package/src/components/viewer/floorplan-preview.tsx +6 -18
  222. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  223. package/src/components/viewer/viewer-rooms.tsx +175 -0
  224. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  225. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  226. package/src/components/viewer/viewer-stage.tsx +2 -2
  227. package/src/components/viewer-zone-system.tsx +9 -9
  228. package/src/hooks/use-auto-save.integration.test.tsx +311 -0
  229. package/src/hooks/use-auto-save.ts +55 -14
  230. package/src/hooks/use-ceiling-events.test.ts +1 -0
  231. package/src/hooks/use-ceiling-events.ts +2 -0
  232. package/src/hooks/use-grid-events.test.ts +2 -0
  233. package/src/hooks/use-grid-events.ts +16 -11
  234. package/src/hooks/use-keyboard.test.ts +182 -0
  235. package/src/hooks/use-keyboard.ts +122 -91
  236. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  237. package/src/hooks/use-open-wall-ends.ts +238 -0
  238. package/src/hooks/use-placement-notice.test.ts +48 -0
  239. package/src/hooks/use-placement-notice.ts +42 -0
  240. package/src/hooks/use-selected-room.ts +143 -0
  241. package/src/index.tsx +306 -39
  242. package/src/lib/catalog-panel-model.ts +39 -0
  243. package/src/lib/catalog-selection.test.ts +27 -0
  244. package/src/lib/catalog-selection.ts +8 -0
  245. package/src/lib/ceiling-edit-session.test.ts +321 -0
  246. package/src/lib/ceiling-edit-session.ts +206 -0
  247. package/src/lib/ceiling-region-session.test.ts +179 -0
  248. package/src/lib/contextual-help.test.ts +27 -1
  249. package/src/lib/contextual-help.ts +16 -9
  250. package/src/lib/continuation.test.ts +19 -1
  251. package/src/lib/continuation.ts +25 -4
  252. package/src/lib/door-interaction.ts +14 -0
  253. package/src/lib/editor-level-display.test.ts +96 -0
  254. package/src/lib/editor-level-display.ts +48 -0
  255. package/src/lib/editor-views.ts +144 -0
  256. package/src/lib/export-geometry-wait.test.ts +90 -0
  257. package/src/lib/floor-edit-session.ts +153 -0
  258. package/src/lib/floor-footprints.test.ts +563 -0
  259. package/src/lib/floor-footprints.ts +395 -0
  260. package/src/lib/floor-foundation-support.test.ts +283 -0
  261. package/src/lib/floor-opening-draft.ts +218 -0
  262. package/src/lib/floor-openings.test.ts +216 -0
  263. package/src/lib/floor-region-geometry.ts +191 -0
  264. package/src/lib/floor-region-session.test.ts +485 -0
  265. package/src/lib/floor-region-session.ts +589 -0
  266. package/src/lib/floor-region-snap.ts +101 -0
  267. package/src/lib/floor-step-hover.test.ts +32 -0
  268. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  269. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  270. package/src/lib/floorplan/dimension-drive.ts +479 -0
  271. package/src/lib/floorplan/floorplan-export.test.ts +15 -9
  272. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  273. package/src/lib/floorplan/floorplan-extension.ts +48 -0
  274. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  275. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  276. package/src/lib/floorplan/floorplan-pdfkit-document.ts +339 -18
  277. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +138 -8
  278. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +56 -25
  279. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  280. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  281. package/src/lib/floorplan/geometry.ts +13 -6
  282. package/src/lib/floorplan/index.ts +0 -9
  283. package/src/lib/floorplan/items.ts +5 -2
  284. package/src/lib/floorplan/marks.test.ts +213 -0
  285. package/src/lib/floorplan/marks.ts +238 -0
  286. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  287. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  288. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  289. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  290. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  291. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  292. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  293. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  294. package/src/lib/floorplan/schedules.test.ts +189 -0
  295. package/src/lib/floorplan/schedules.ts +339 -0
  296. package/src/lib/floorplan/sheet-export.test.ts +100 -0
  297. package/src/lib/floorplan/sheet-export.ts +318 -0
  298. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +779 -0
  299. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  300. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  301. package/src/lib/floorplan/site-plan/coverage.ts +232 -0
  302. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  303. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  304. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  305. package/src/lib/floorplan/site-plan/front-edge.ts +156 -0
  306. package/src/lib/floorplan/site-plan/geometry.test.ts +275 -0
  307. package/src/lib/floorplan/site-plan/geometry.ts +557 -0
  308. package/src/lib/floorplan/site-plan/index.ts +71 -0
  309. package/src/lib/floorplan/site-plan/site-annotations.test.ts +366 -0
  310. package/src/lib/floorplan/site-plan/site-annotations.ts +1083 -0
  311. package/src/lib/floorplan/site-plan/site-parts.ts +379 -0
  312. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  313. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  314. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  315. package/src/lib/floorplan/site-plan/yards.test.ts +69 -0
  316. package/src/lib/floorplan/types.ts +1 -27
  317. package/src/lib/fresh-planar-placement.test.ts +36 -0
  318. package/src/lib/fresh-planar-placement.ts +191 -36
  319. package/src/lib/geometry-script/author.ts +152 -0
  320. package/src/lib/geometry-script/client.ts +152 -0
  321. package/src/lib/geometry-script/protocol.ts +12 -0
  322. package/src/lib/gesture-lifecycle.test.ts +595 -0
  323. package/src/lib/gesture-lifecycle.ts +290 -0
  324. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  325. package/src/lib/glb-export.test.ts +544 -16
  326. package/src/lib/glb-export.ts +256 -125
  327. package/src/lib/grid-frame.test.ts +19 -0
  328. package/src/lib/grid-frame.ts +18 -0
  329. package/src/lib/history.test.ts +6 -0
  330. package/src/lib/history.ts +7 -0
  331. package/src/lib/hud-title.ts +109 -0
  332. package/src/lib/hydration-gated-storage.ts +49 -0
  333. package/src/lib/ifc-export.test.ts +174 -0
  334. package/src/lib/ifc-export.ts +201 -0
  335. package/src/lib/ifc-parts.ts +15 -0
  336. package/src/lib/inspector-expanded.test.ts +44 -0
  337. package/src/lib/inspector-expanded.ts +36 -0
  338. package/src/lib/interaction/scope.ts +27 -1
  339. package/src/lib/keyboard-pan.test.ts +184 -0
  340. package/src/lib/keyboard-pan.ts +238 -0
  341. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  342. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  343. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  344. package/src/lib/lot/clean-ring.test.ts +186 -0
  345. package/src/lib/lot/clean-ring.ts +217 -0
  346. package/src/lib/lot/dossier.test.ts +202 -0
  347. package/src/lib/lot/dossier.ts +498 -0
  348. package/src/lib/lot/drop-in.test.ts +326 -0
  349. package/src/lib/lot/drop-in.ts +407 -0
  350. package/src/lib/lot/index.ts +43 -0
  351. package/src/lib/lot/lot-patch.test.ts +340 -0
  352. package/src/lib/lot/lot-patch.ts +394 -0
  353. package/src/lib/lot/parcel-provider.ts +43 -0
  354. package/src/lib/lot/terrain.test.ts +161 -0
  355. package/src/lib/lot/terrain.ts +266 -0
  356. package/src/lib/material-catalog-model.ts +74 -0
  357. package/src/lib/material-paint-panel-model.ts +80 -0
  358. package/src/lib/material-paint.ts +4 -0
  359. package/src/lib/mezzanine-actions.test.ts +342 -0
  360. package/src/lib/mezzanine-draft.test.ts +501 -0
  361. package/src/lib/mezzanine-draft.ts +278 -0
  362. package/src/lib/mezzanine-messages.ts +26 -0
  363. package/src/lib/mezzanine-selection.test.ts +207 -0
  364. package/src/lib/mezzanine-stairs.ts +46 -0
  365. package/src/lib/model-export.ts +3 -1
  366. package/src/lib/node-action-movement.ts +17 -6
  367. package/src/lib/node-display-label.ts +37 -0
  368. package/src/lib/orbit-floor-clearance.test.ts +165 -0
  369. package/src/lib/orbit-floor-clearance.ts +106 -0
  370. package/src/lib/overlay-transform.test.ts +28 -0
  371. package/src/lib/overlay-transform.ts +12 -0
  372. package/src/lib/paint-commit-route.test.ts +37 -0
  373. package/src/lib/paint-commit-route.ts +17 -0
  374. package/src/lib/paint-eyedropper.test.tsx +165 -0
  375. package/src/lib/paint-eyedropper.ts +99 -0
  376. package/src/lib/paint-pass-through.test.ts +111 -0
  377. package/src/lib/paint-pass-through.ts +76 -0
  378. package/src/lib/paint-region-hover.ts +25 -0
  379. package/src/lib/paint-region-mode.ts +154 -0
  380. package/src/lib/paint-regions.test.ts +166 -0
  381. package/src/lib/paint-regions.ts +192 -0
  382. package/src/lib/paint-scope.test.ts +317 -35
  383. package/src/lib/paint-scope.ts +266 -25
  384. package/src/lib/panel-rows.ts +96 -0
  385. package/src/lib/panel-tool-options.test.ts +34 -0
  386. package/src/lib/panel-tool-options.ts +46 -0
  387. package/src/lib/parametric-node-update.ts +37 -0
  388. package/src/lib/plate-paint-affected.test.ts +300 -0
  389. package/src/lib/plate-paint-affected.ts +177 -0
  390. package/src/lib/plugin-chat.ts +50 -0
  391. package/src/lib/plugin-panels.test.ts +25 -0
  392. package/src/lib/plugin-panels.ts +94 -0
  393. package/src/lib/print-shell-compiler.ts +1 -0
  394. package/src/lib/registered-draft-snap.ts +12 -0
  395. package/src/lib/roof-duplication.ts +10 -2
  396. package/src/lib/room-assembly-overlay.test.ts +275 -0
  397. package/src/lib/room-assembly-overlay.ts +266 -0
  398. package/src/lib/room-built-on.test.ts +379 -0
  399. package/src/lib/room-built-on.ts +138 -0
  400. package/src/lib/room-construction-commands.ts +712 -0
  401. package/src/lib/room-construction.test.ts +746 -0
  402. package/src/lib/room-construction.ts +385 -0
  403. package/src/lib/room-controls-phase6.test.ts +630 -0
  404. package/src/lib/room-divide-session.ts +523 -0
  405. package/src/lib/room-handle-drag.ts +1068 -0
  406. package/src/lib/room-name-catalog.test.ts +90 -0
  407. package/src/lib/room-name-catalog.ts +168 -0
  408. package/src/lib/room-push-dimensions.test.ts +311 -0
  409. package/src/lib/room-push-dimensions.ts +251 -0
  410. package/src/lib/room-selection-commands.ts +62 -0
  411. package/src/lib/room-selection.test.ts +973 -0
  412. package/src/lib/room-selection.ts +417 -0
  413. package/src/lib/room-structure-commands.ts +94 -0
  414. package/src/lib/room-structure.test.ts +921 -0
  415. package/src/lib/room-transform-session.ts +570 -0
  416. package/src/lib/room-zone-routing.test.ts +294 -0
  417. package/src/lib/room-zone-routing.ts +197 -0
  418. package/src/lib/scene-clipboard.test.ts +364 -0
  419. package/src/lib/scene-clipboard.ts +229 -48
  420. package/src/lib/scene-material-refs.test.ts +79 -0
  421. package/src/lib/scene-material-refs.ts +79 -0
  422. package/src/lib/scene.test.ts +87 -0
  423. package/src/lib/scene.ts +39 -45
  424. package/src/lib/selection-routing.test.ts +129 -45
  425. package/src/lib/selection-routing.ts +53 -74
  426. package/src/lib/session-writes.test.ts +120 -0
  427. package/src/lib/session-writes.ts +205 -0
  428. package/src/lib/sidebar-panel.ts +62 -0
  429. package/src/lib/site-boundary.test.ts +4 -6
  430. package/src/lib/site-boundary.ts +1 -1
  431. package/src/lib/snapping-mode.test.ts +17 -2
  432. package/src/lib/snapping-mode.ts +19 -7
  433. package/src/lib/spatial-drag-plane.test.ts +53 -0
  434. package/src/lib/spatial-drag-plane.ts +19 -0
  435. package/src/lib/spatial-pointer-input.test.ts +72 -0
  436. package/src/lib/spatial-pointer-input.ts +62 -0
  437. package/src/lib/spatial-snap.test.ts +47 -0
  438. package/src/lib/terrace-draft.test.ts +145 -0
  439. package/src/lib/terrace-draft.ts +213 -0
  440. package/src/lib/terrain-panel-model.ts +117 -0
  441. package/src/lib/tool-cursor-icon.test.ts +16 -0
  442. package/src/lib/tool-cursor-icon.ts +11 -0
  443. package/src/lib/typing-target.ts +9 -0
  444. package/src/lib/units.ts +77 -39
  445. package/src/lib/vector-edges.test.ts +58 -0
  446. package/src/lib/vector-edges.ts +400 -0
  447. package/src/lib/viewer-selection.test.ts +577 -0
  448. package/src/lib/viewer-selection.ts +569 -0
  449. package/src/lib/wall-draw-variant.test.ts +71 -0
  450. package/src/lib/wall-draw-variant.ts +91 -0
  451. package/src/lib/wall-paint-region-session.ts +246 -0
  452. package/src/lib/wall-paint-region.test.ts +373 -0
  453. package/src/lib/wall-polygon-draft.test.ts +262 -0
  454. package/src/lib/wall-polygon-draft.ts +286 -0
  455. package/src/lib/wall-push.test.ts +453 -0
  456. package/src/lib/wall-region-face.ts +352 -0
  457. package/src/lib/wall-region-handles.test.ts +304 -0
  458. package/src/lib/wall-region-handles.ts +368 -0
  459. package/src/lib/wall-region-list.test.ts +119 -0
  460. package/src/lib/wall-region-list.ts +136 -0
  461. package/src/lib/wall-region-snap.ts +110 -0
  462. package/src/lib/window-interaction.ts +14 -0
  463. package/src/lib/world-grid-snap.test.ts +48 -0
  464. package/src/lib/world-grid-snap.ts +9 -5
  465. package/src/lib/zone-content.ts +4 -1
  466. package/src/lib/zone-name.test.ts +17 -0
  467. package/src/lib/zone-name.ts +17 -0
  468. package/src/store/opens-in-select-mode.test.ts +223 -0
  469. package/src/store/paint-mode.test.ts +188 -0
  470. package/src/store/rotation-snapping.test.ts +53 -0
  471. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  472. package/src/store/tool-mode.test.ts +137 -6
  473. package/src/store/use-delete-confirmation.ts +28 -1
  474. package/src/store/use-drawing-view.test.ts +24 -3
  475. package/src/store/use-drawing-view.ts +29 -1
  476. package/src/store/use-editor.tsx +442 -323
  477. package/src/store/use-floorplan-draft-preview.ts +23 -1
  478. package/src/store/use-floorplan-marquee.ts +12 -2
  479. package/src/store/use-interaction-scope.ts +155 -6
  480. package/src/store/use-quick-measurement-hud.ts +3 -3
  481. package/src/store/use-stair-build-preview.ts +13 -5
  482. package/src/store/use-wall-move-ghosts.ts +4 -2
  483. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  484. package/src/store/use-wall-region-handles-selection.ts +139 -0
  485. package/src/store/view-layout.test.ts +175 -0
  486. package/src/store/view-layout.ts +196 -0
  487. package/src/test-utils/immediate-animation-frames.ts +16 -0
  488. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  489. package/src/test-utils/selection-harness.ts +82 -0
  490. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  491. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  492. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  493. package/src/components/tools/fence/fence-drafting.ts +0 -266
  494. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  495. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  496. package/src/lib/floorplan/stairs.ts +0 -489
  497. package/src/lib/level-duplication.test.ts +0 -144
  498. package/src/lib/level-duplication.ts +0 -195
@@ -1,7 +1,6 @@
1
1
  'use client'
2
2
 
3
3
  import {
4
- type AnyNode,
5
4
  type AnyNodeId,
6
5
  type FenceNode,
7
6
  getFenceCenterlineFrameAt,
@@ -10,6 +9,7 @@ import {
10
9
  getWallCurveFrameAt,
11
10
  getWallCurveLength,
12
11
  getWallEffectiveHeightForNodes,
12
+ getWallLocalFaceZ,
13
13
  getWallThickness,
14
14
  isCurvedWall,
15
15
  MIN_WALL_HEIGHT,
@@ -48,7 +48,10 @@ import {
48
48
  } from '../../lib/elevation-guides'
49
49
  import { isHistoryShortcut } from '../../lib/history'
50
50
  import { endpointReshapeScope } from '../../lib/interaction/scope'
51
+ import { useRoomHandleDrag, wallPushHandles } from '../../lib/room-handle-drag'
51
52
  import { sfxEmitter } from '../../lib/sfx-bus'
53
+ import { intersectSpatialDragPlane, spatialDragLocalY } from '../../lib/spatial-drag-plane'
54
+ import { getSpatialPointerId, spatialPointerInput } from '../../lib/spatial-pointer-input'
52
55
  import useEditor, { isGridSnapActive, isMagneticSnapActive } from '../../store/use-editor'
53
56
  import useInteractionScope, {
54
57
  useEndpointReshape,
@@ -67,6 +70,7 @@ import {
67
70
  NO_RAYCAST,
68
71
  useInvisibleHitAreaMaterial,
69
72
  } from './node-arrow-handles'
73
+ import { WallPushArrow } from './wall-push-arrow'
70
74
 
71
75
  const HANDLE_OFFSET = 0.27
72
76
  const HANDLE_MIN_OFFSET = 0.33
@@ -147,6 +151,7 @@ export function WallMoveSideHandles() {
147
151
  const movingNode = useMovingNode()
148
152
  const endpointReshape = useEndpointReshape()
149
153
  const isCurveReshape = useIsCurveReshape()
154
+ const isPushing = useRoomHandleDrag((state) => state.drag?.kind === 'push')
150
155
 
151
156
  const selectedId = selectedIds.length === 1 ? selectedIds[0] : null
152
157
  // Walls still use this legacy handle renderer. Fences retain their registry
@@ -162,9 +167,10 @@ export function WallMoveSideHandles() {
162
167
  mode !== 'delete' &&
163
168
  !movingNode &&
164
169
  !endpointReshape &&
165
- !isCurveReshape
170
+ !isCurveReshape &&
171
+ !isPushing
166
172
 
167
- if (!shouldRender || !selectedNode) return null
173
+ if (!(shouldRender && selectedNode)) return null
168
174
 
169
175
  return selectedNode.type === 'wall' ? (
170
176
  <WallMoveSideHandlesForWall wall={selectedNode} />
@@ -219,16 +225,17 @@ function WallMoveSideHandlesForWall({ wall }: { wall: WallNode }) {
219
225
  }, [wall.parentId])
220
226
 
221
227
  const baseElevation = getWallBaseElevationForNodes(effectiveWall, nodes)
222
- const handles = useMemo(() => getWallMoveHandles(effectiveWall, nodes), [effectiveWall, nodes])
228
+ const handles = useMemo(() => wallPushHandles(effectiveWall, nodes), [effectiveWall, nodes])
223
229
 
224
- if (!levelObject || handles.length === 0) return null
230
+ const levelId = wall.parentId
231
+ if (!(levelObject && levelId) || handles.length === 0) return null
225
232
 
226
233
  return createPortal(
227
234
  <>
235
+ {handles.map((handle) => (
236
+ <WallPushArrow handle={handle} key={handle.key} levelId={levelId} />
237
+ ))}
228
238
  <group position={[0, baseElevation, 0]}>
229
- {handles.map((handle) => (
230
- <WallMoveArrowHandle handle={handle} key={handle.key} wall={effectiveWall} />
231
- ))}
232
239
  <WallHeightArrowHandle wall={effectiveWall} />
233
240
  <StructureThicknessHandle
234
241
  baseElevation={baseElevation}
@@ -292,7 +299,7 @@ function StructureThicknessHandle({
292
299
  ? (node.height ?? 1.8)
293
300
  : getWallEffectiveHeightForNodes(node, useScene.getState().nodes)
294
301
  const outward = new Vector2(frame.normal.x * side, frame.normal.y * side)
295
- const faceOffset = thickness / 2 + 0.006
302
+ const faceOffset = getWallLocalFaceZ({ ...node, thickness }, side > 0 ? 'a' : 'b') * side + 0.006
296
303
  const position: [number, number, number] = [
297
304
  frame.point.x + outward.x * faceOffset,
298
305
  structureHeight / 2,
@@ -369,11 +376,18 @@ function StructureThicknessHandle({
369
376
  initialNode.type === 'fence'
370
377
  ? (initialNode.thickness ?? 0.08)
371
378
  : getWallThickness(initialNode)
379
+ const initialFaceOffset =
380
+ getWallLocalFaceZ(
381
+ { ...initialNode, thickness: initialStructureThickness },
382
+ side > 0 ? 'a' : 'b',
383
+ ) *
384
+ side +
385
+ 0.006
372
386
  const minimumThickness = initialNode.type === 'fence' ? 0.03 : MIN_WALL_THICKNESS
373
387
  const localOrigin = new Vector3(
374
- initialFrame.point.x + localAxis.x * (initialStructureThickness / 2 + 0.006),
388
+ initialFrame.point.x + localAxis.x * initialFaceOffset,
375
389
  baseElevation + initialStructureHeight / 2,
376
- initialFrame.point.y + localAxis.z * (initialStructureThickness / 2 + 0.006),
390
+ initialFrame.point.y + localAxis.z * initialFaceOffset,
377
391
  )
378
392
  const worldOrigin = localOrigin.clone().applyMatrix4(rideObject.matrixWorld)
379
393
  const worldAxisEnd = localOrigin.clone().add(localAxis).applyMatrix4(rideObject.matrixWorld)
@@ -639,13 +653,14 @@ function WallCornerLeaderHandle({ wall, endpoint }: { wall: WallNode; endpoint:
639
653
  }
640
654
  })
641
655
 
642
- useEffect(() => {
643
- return () => {
656
+ useEffect(
657
+ () => () => {
644
658
  if (document.body.style.cursor === 'grab' || document.body.style.cursor === 'grabbing') {
645
659
  document.body.style.cursor = ''
646
660
  }
647
- }
648
- }, [])
661
+ },
662
+ [],
663
+ )
649
664
 
650
665
  const activateEndpointMove = (event: ThreeEvent<PointerEvent>) => {
651
666
  if (event.button !== 0) return
@@ -769,7 +784,9 @@ function WallBaseElevationHandle({
769
784
  const midpointWorld = new Vector3(midpoint[0], initialBase, midpoint[1]).applyMatrix4(
770
785
  levelObject.matrixWorld,
771
786
  )
772
- const planeNormal = new Vector3().subVectors(camera.position, midpointWorld).setY(0)
787
+ const planeNormal = new Vector3()
788
+ .subVectors(camera.getWorldPosition(new Vector3()), midpointWorld)
789
+ .setY(0)
773
790
  if (planeNormal.lengthSq() === 0) return null
774
791
  planeNormal.normalize()
775
792
  const plane = new Plane().setFromNormalAndCoplanarPoint(planeNormal, midpointWorld)
@@ -873,14 +890,15 @@ function WallHeightArrowHandle({ wall }: { wall: WallNode }) {
873
890
  arrowMaterial.color.set(isHovered ? ARROW_HOVER_COLOR : ARROW_COLOR)
874
891
  }, [arrowMaterial, isHovered])
875
892
 
876
- useEffect(() => {
877
- return () => {
893
+ useEffect(
894
+ () => () => {
878
895
  if (document.body.style.cursor === 'ns-resize') {
879
896
  document.body.style.cursor = ''
880
897
  }
881
898
  dragCleanupRef.current?.()
882
- }
883
- }, [])
899
+ },
900
+ [],
901
+ )
884
902
 
885
903
  useEffect(() => () => arrowGeometry.dispose(), [arrowGeometry])
886
904
  useEffect(() => () => hitGeometry.dispose(), [hitGeometry])
@@ -889,7 +907,7 @@ function WallHeightArrowHandle({ wall }: { wall: WallNode }) {
889
907
  // Sit on the visual centre of the wall — for curved walls that's the
890
908
  // arc apex at t=0.5, not the chord midpoint. Use the curve tangent for
891
909
  // the yaw so the arrow's local frame matches the wall direction at the
892
- // apex, consistent with `getWallMoveHandles`.
910
+ // apex, consistent with `wallPushHandles`.
893
911
  const curveFrame = isCurvedWall(wall) ? getWallCurveFrameAt(wall, 0.5) : null
894
912
  const midX = curveFrame ? curveFrame.point.x : (wall.start[0] + wall.end[0]) / 2
895
913
  const midZ = curveFrame ? curveFrame.point.y : (wall.start[1] + wall.end[1]) / 2
@@ -908,16 +926,12 @@ function WallHeightArrowHandle({ wall }: { wall: WallNode }) {
908
926
  const levelObject = wall.parentId ? sceneRegistry.nodes.get(wall.parentId) : null
909
927
  if (!levelObject) return
910
928
 
911
- // Vertical plane through the wall midpoint whose normal points toward
912
- // the camera (projected to horizontal). Raycasting against it converts
913
- // pointer movement into a world-space Y value.
929
+ levelObject.updateWorldMatrix(true, false)
930
+ const worldToLocal = levelObject.matrixWorld.clone().invert()
914
931
  const midpointWorld = new Vector3(midX, 0, midZ).applyMatrix4(levelObject.matrixWorld)
915
- const planeNormal = new Vector3().subVectors(camera.position, midpointWorld).setY(0)
916
- if (planeNormal.lengthSq() === 0) return
917
- planeNormal.normalize()
918
- const plane = new Plane().setFromNormalAndCoplanarPoint(planeNormal, midpointWorld)
919
-
920
932
  const ndc = new Vector2()
933
+ const spatialPointerId = getSpatialPointerId(event.nativeEvent)
934
+ const spatialRay = spatialPointerId ? event.ray.clone() : null
921
935
  const setNDC = (clientX: number, clientY: number) => {
922
936
  const rect = gl.domElement.getBoundingClientRect()
923
937
  ndc.set(
@@ -926,17 +940,39 @@ function WallHeightArrowHandle({ wall }: { wall: WallNode }) {
926
940
  )
927
941
  }
928
942
 
929
- setNDC(event.nativeEvent.clientX, event.nativeEvent.clientY)
930
- raycaster.setFromCamera(ndc, camera)
931
- const hit = new Vector3()
932
- if (!raycaster.ray.intersectPlane(plane, hit)) return
943
+ let plane: Plane | null = null
944
+ let initialY: number
945
+ if (spatialRay) {
946
+ const worldUp = new Vector3(0, 1, 0).transformDirection(levelObject.matrixWorld)
947
+ const axisPoint = new Vector3()
948
+ const spatialRayLocalY = (ray: Ray) => {
949
+ const axisParameter = closestAxisParameterToRay(midpointWorld, worldUp, ray)
950
+ axisPoint.copy(midpointWorld).addScaledVector(worldUp, axisParameter)
951
+ return spatialDragLocalY(axisPoint, worldToLocal)
952
+ }
953
+ initialY = spatialRayLocalY(spatialRay)
954
+ } else {
955
+ // A camera-facing vertical plane gives desktop pointer movement a stable
956
+ // world-space height. Spatial pointers use the closest point on the
957
+ // wall's vertical axis instead, which remains defined for vertical rays.
958
+ const planeNormal = new Vector3()
959
+ .subVectors(camera.getWorldPosition(new Vector3()), midpointWorld)
960
+ .setY(0)
961
+ if (planeNormal.lengthSq() === 0) return
962
+ plane = new Plane().setFromNormalAndCoplanarPoint(planeNormal.normalize(), midpointWorld)
963
+ setNDC(event.nativeEvent.clientX, event.nativeEvent.clientY)
964
+ raycaster.setFromCamera(ndc, camera)
965
+ const hit = new Vector3()
966
+ if (!intersectSpatialDragPlane(raycaster.ray, plane, hit)) return
967
+ initialY = spatialDragLocalY(hit, worldToLocal)
968
+ }
933
969
 
934
970
  // Dragging the top makes the wall custom-height; seed from the resolved
935
971
  // effective height so a plane-bound wall's drag starts at its real top.
936
972
  const initialHeight = getWallEffectiveHeightForNodes(wall, useScene.getState().nodes)
937
- const initialY = hit.y
938
973
  const wallId = wall.id as AnyNodeId
939
974
  let pendingHeight = initialHeight
975
+ let releaseSpatialCapture: (() => void) | null = null
940
976
 
941
977
  document.body.style.cursor = 'ns-resize'
942
978
  sfxEmitter.emit('sfx:item-pick')
@@ -954,9 +990,23 @@ function WallHeightArrowHandle({ wall }: { wall: WallNode }) {
954
990
  const onMove = (e: PointerEvent) => {
955
991
  setNDC(e.clientX, e.clientY)
956
992
  raycaster.setFromCamera(ndc, camera)
957
- const intersection = new Vector3()
958
- if (!raycaster.ray.intersectPlane(plane, intersection)) return
959
- const newHeight = Math.max(MIN_WALL_HEIGHT, initialHeight + (intersection.y - initialY))
993
+ applyRay(raycaster.ray)
994
+ }
995
+ const applyRay = (ray: Ray) => {
996
+ let currentY: number
997
+ if (spatialRay) {
998
+ const worldUp = new Vector3(0, 1, 0).transformDirection(levelObject.matrixWorld)
999
+ const axisParameter = closestAxisParameterToRay(midpointWorld, worldUp, ray)
1000
+ const axisPoint = midpointWorld.clone().addScaledVector(worldUp, axisParameter)
1001
+ currentY = spatialDragLocalY(axisPoint, worldToLocal)
1002
+ } else {
1003
+ if (!plane) return
1004
+ const intersection = new Vector3()
1005
+ if (!intersectSpatialDragPlane(ray, plane, intersection)) return
1006
+ currentY = spatialDragLocalY(intersection, worldToLocal)
1007
+ }
1008
+ const newHeight = Math.max(MIN_WALL_HEIGHT, initialHeight + (currentY - initialY))
1009
+ if (Math.abs(newHeight - pendingHeight) < 1e-6) return
960
1010
  pendingHeight = newHeight
961
1011
  useLiveNodeOverrides.getState().set(wallId, { height: newHeight })
962
1012
  useScene.getState().markDirty(wallId)
@@ -967,6 +1017,8 @@ function WallHeightArrowHandle({ wall }: { wall: WallNode }) {
967
1017
  window.removeEventListener('pointerup', onUp)
968
1018
  window.removeEventListener('pointercancel', onCancel)
969
1019
  window.removeEventListener('keydown', onKeyDown, true)
1020
+ releaseSpatialCapture?.()
1021
+ releaseSpatialCapture = null
970
1022
  if (document.body.style.cursor === 'ns-resize') {
971
1023
  document.body.style.cursor = ''
972
1024
  }
@@ -981,6 +1033,7 @@ function WallHeightArrowHandle({ wall }: { wall: WallNode }) {
981
1033
  // Commit: write the final override-merged value to zustand once
982
1034
  // (tracked, undoable), then drop the override so the renderer
983
1035
  // falls back to the scene store.
1036
+ useScene.temporal.getState().resume()
984
1037
  if (pendingHeight !== initialHeight) {
985
1038
  useScene.getState().updateNode(wallId, { height: pendingHeight })
986
1039
  }
@@ -1006,10 +1059,21 @@ function WallHeightArrowHandle({ wall }: { wall: WallNode }) {
1006
1059
  onCancel()
1007
1060
  }
1008
1061
 
1009
- dragCleanupRef.current = cleanup
1010
- window.addEventListener('pointermove', onMove)
1011
- window.addEventListener('pointerup', onUp)
1012
- window.addEventListener('pointercancel', onCancel)
1062
+ dragCleanupRef.current = onCancel
1063
+ if (spatialPointerId && spatialRay) {
1064
+ releaseSpatialCapture = spatialPointerInput.capture(spatialPointerId, {
1065
+ onMove: (ray) => {
1066
+ spatialRay.copy(ray)
1067
+ applyRay(spatialRay)
1068
+ },
1069
+ onRelease: onUp,
1070
+ onCancel,
1071
+ })
1072
+ } else {
1073
+ window.addEventListener('pointermove', onMove)
1074
+ window.addEventListener('pointerup', onUp)
1075
+ window.addEventListener('pointercancel', onCancel)
1076
+ }
1013
1077
  window.addEventListener('keydown', onKeyDown, true)
1014
1078
  }
1015
1079
 
@@ -1035,7 +1099,10 @@ function WallHeightArrowHandle({ wall }: { wall: WallNode }) {
1035
1099
  scale={baseScale}
1036
1100
  />
1037
1101
  <mesh
1038
- // Geometry-as-prop + frustumCulled={false} — see WallMoveArrowHandle.
1102
+ // Pass geometry as a prop (not `<primitive attach="geometry">`): with
1103
+ // `frustumCulled={false}` the attach path draws R3F's empty default
1104
+ // geometry on the first frame and WebGPU flags "Vertex buffer slot 0
1105
+ // ... was not set".
1039
1106
  frustumCulled={false}
1040
1107
  geometry={arrowGeometry}
1041
1108
  material={arrowMaterial}
@@ -1048,7 +1115,7 @@ function WallHeightArrowHandle({ wall }: { wall: WallNode }) {
1048
1115
  )
1049
1116
  }
1050
1117
 
1051
- function WallMoveArrowHandle({ wall, handle }: { wall: WallNode; handle: WallMoveHandle }) {
1118
+ function FenceMoveArrowHandle({ fence, handle }: { fence: FenceNode; handle: WallMoveHandle }) {
1052
1119
  const [isHovered, setIsHovered] = useState(false)
1053
1120
  const arrowGeometry = useMemo(() => createArrowHandleGeometry(), [])
1054
1121
  const hitGeometry = useMemo(() => createArrowHitAreaGeometry(), [])
@@ -1068,7 +1135,6 @@ function WallMoveArrowHandle({ wall, handle }: { wall: WallNode; handle: WallMov
1068
1135
  const { camera } = useThree()
1069
1136
 
1070
1137
  const zoom = camera instanceof OrthographicCamera ? 1 / camera.zoom : 1
1071
-
1072
1138
  const baseScale = zoom * ARROW_SCALE
1073
1139
  const scale = (isHovered ? 1.12 : 1) * baseScale
1074
1140
 
@@ -1076,103 +1142,14 @@ function WallMoveArrowHandle({ wall, handle }: { wall: WallNode; handle: WallMov
1076
1142
  arrowMaterial.color.set(isHovered ? ARROW_HOVER_COLOR : ARROW_COLOR)
1077
1143
  }, [arrowMaterial, isHovered])
1078
1144
 
1079
- useEffect(() => {
1080
- return () => {
1145
+ useEffect(
1146
+ () => () => {
1081
1147
  if (document.body.style.cursor === 'grab' || document.body.style.cursor === 'grabbing') {
1082
1148
  document.body.style.cursor = ''
1083
1149
  }
1084
- }
1085
- }, [])
1086
-
1087
- useEffect(() => () => arrowGeometry.dispose(), [arrowGeometry])
1088
- useEffect(() => () => hitGeometry.dispose(), [hitGeometry])
1089
- useEffect(() => () => arrowMaterial.dispose(), [arrowMaterial])
1090
-
1091
- const activateWallMove = (event: ThreeEvent<PointerEvent>) => {
1092
- if (event.button !== 0) return
1093
- event.stopPropagation()
1094
- suppressBoxSelectForPointer(event)
1095
- document.body.style.cursor = 'grabbing'
1096
-
1097
- sfxEmitter.emit('sfx:item-pick')
1098
- useEditor.getState().setMovingNode(wall)
1099
- useInteractionScope.getState().endIf((s) => s.kind === 'reshaping' && s.reshape === 'endpoint')
1100
- useInteractionScope.getState().endIf((s) => s.kind === 'reshaping' && s.reshape === 'curve')
1101
- // Keep the wall selected so it stays the active item once the move
1102
- // commits; the `!movingNode` guard on the handles hides them mid-drag.
1103
- }
1104
-
1105
- return (
1106
- <group position={handle.position} rotation={[0, handle.rotationY, 0]}>
1107
- <InvisibleHandleHitArea
1108
- geometry={hitGeometry}
1109
- material={hitMaterial}
1110
- onPointerDown={activateWallMove}
1111
- onPointerEnter={(event) => {
1112
- event.stopPropagation()
1113
- setIsHovered(true)
1114
- document.body.style.cursor = 'grab'
1115
- }}
1116
- onPointerLeave={(event) => {
1117
- event.stopPropagation()
1118
- setIsHovered(false)
1119
- if (document.body.style.cursor === 'grab') {
1120
- document.body.style.cursor = ''
1121
- }
1122
- }}
1123
- scale={baseScale}
1124
- />
1125
- <mesh
1126
- // Pass geometry as a prop (not `<primitive attach="geometry">`)
1127
- // so the mesh is never rendered with R3F's default empty
1128
- // `BufferGeometry`. Combined with `frustumCulled={false}`, the
1129
- // primitive-attach path emits a `Draw(0, 1, 0, 0)` on the first
1130
- // frame and WebGPU flags "Vertex buffer slot 0 ... was not set".
1131
- frustumCulled={false}
1132
- geometry={arrowGeometry}
1133
- material={arrowMaterial}
1134
- raycast={NO_RAYCAST}
1135
- renderOrder={1002}
1136
- scale={scale}
1137
- />
1138
- </group>
1139
- )
1140
- }
1141
-
1142
- function FenceMoveArrowHandle({ fence, handle }: { fence: FenceNode; handle: WallMoveHandle }) {
1143
- const [isHovered, setIsHovered] = useState(false)
1144
- const arrowGeometry = useMemo(() => createArrowHandleGeometry(), [])
1145
- const hitGeometry = useMemo(() => createArrowHitAreaGeometry(), [])
1146
- const hitMaterial = useInvisibleHitAreaMaterial()
1147
- const arrowMaterial = useMemo(
1148
- () =>
1149
- new MeshBasicNodeMaterial({
1150
- color: new Color(ARROW_COLOR),
1151
- side: DoubleSide,
1152
- depthTest: false,
1153
- depthWrite: false,
1154
- transparent: true,
1155
- opacity: 1,
1156
- }),
1150
+ },
1157
1151
  [],
1158
1152
  )
1159
- const { camera } = useThree()
1160
-
1161
- const zoom = camera instanceof OrthographicCamera ? 1 / camera.zoom : 1
1162
- const baseScale = zoom * ARROW_SCALE
1163
- const scale = (isHovered ? 1.12 : 1) * baseScale
1164
-
1165
- useEffect(() => {
1166
- arrowMaterial.color.set(isHovered ? ARROW_HOVER_COLOR : ARROW_COLOR)
1167
- }, [arrowMaterial, isHovered])
1168
-
1169
- useEffect(() => {
1170
- return () => {
1171
- if (document.body.style.cursor === 'grab' || document.body.style.cursor === 'grabbing') {
1172
- document.body.style.cursor = ''
1173
- }
1174
- }
1175
- }, [])
1176
1153
 
1177
1154
  useEffect(() => () => arrowGeometry.dispose(), [arrowGeometry])
1178
1155
  useEffect(() => () => hitGeometry.dispose(), [hitGeometry])
@@ -1212,7 +1189,7 @@ function FenceMoveArrowHandle({ fence, handle }: { fence: FenceNode; handle: Wal
1212
1189
  scale={baseScale}
1213
1190
  />
1214
1191
  <mesh
1215
- // Pass geometry as a prop — see WallMoveArrowHandle for the
1192
+ // Pass geometry as a prop — see WallHeightArrowHandle for the
1216
1193
  // WebGPU "Vertex buffer slot 0 ... was not set" rationale.
1217
1194
  frustumCulled={false}
1218
1195
  geometry={arrowGeometry}
@@ -1225,32 +1202,6 @@ function FenceMoveArrowHandle({ fence, handle }: { fence: FenceNode; handle: Wal
1225
1202
  )
1226
1203
  }
1227
1204
 
1228
- function getWallMoveHandles(wall: WallNode, nodes: Record<string, AnyNode>): WallMoveHandle[] {
1229
- const dx = wall.end[0] - wall.start[0]
1230
- const dz = wall.end[1] - wall.start[1]
1231
- const length = Math.hypot(dx, dz)
1232
-
1233
- if (length < 1e-6) {
1234
- return []
1235
- }
1236
-
1237
- const frame = isCurvedWall(wall) ? getWallCurveFrameAt(wall, 0.5) : null
1238
- const normal: [number, number] = frame
1239
- ? [frame.normal.x, frame.normal.y]
1240
- : [-dz / length, dx / length]
1241
- const midpoint: [number, number] = frame
1242
- ? [frame.point.x, frame.point.y]
1243
- : [(wall.start[0] + wall.end[0]) / 2, (wall.start[1] + wall.end[1]) / 2]
1244
- const wallHeight = getWallEffectiveHeightForNodes(wall, nodes)
1245
- const handleHeight = Math.max(wallHeight - HANDLE_TOP_INSET, HANDLE_MIN_HEIGHT)
1246
- const offset = Math.max(getWallThickness(wall) / 2 + HANDLE_OFFSET, HANDLE_MIN_OFFSET)
1247
-
1248
- return [
1249
- buildWallMoveHandle('front', midpoint, normal, offset, handleHeight),
1250
- buildWallMoveHandle('back', midpoint, [-normal[0], -normal[1]], offset, handleHeight),
1251
- ]
1252
- }
1253
-
1254
1205
  function WallMoveSideHandlesForFence({ fence }: { fence: FenceNode }) {
1255
1206
  const [levelObject, setLevelObject] = useState<Object3D | null>(() =>
1256
1207
  fence.parentId ? (sceneRegistry.nodes.get(fence.parentId) ?? null) : null,
@@ -11,10 +11,10 @@ import {
11
11
  type Group,
12
12
  type Mesh,
13
13
  MeshBasicMaterial,
14
- Vector3,
15
14
  } from 'three'
16
15
  import { LineBasicNodeMaterial, MeshBasicNodeMaterial } from 'three/webgpu'
17
16
  import { EDITOR_LAYER } from '../../lib/constants'
17
+ import { copyOverlayWorldTransform } from '../../lib/overlay-transform'
18
18
 
19
19
  // How far the outline sits outside the opening's own extents, so it reads as
20
20
  // a frame *around* the opening rather than coinciding with its edges.
@@ -23,7 +23,6 @@ const PAD = 0.05
23
23
  const ACCENT = 0x83_81_ed
24
24
 
25
25
  const NO_RAYCAST = () => null
26
- const scratchScale = new Vector3()
27
26
 
28
27
  // Indigo accent matching the resize-arrow handles — deliberately distinct
29
28
  // from the white selection outline so the highlight reads as "editable child
@@ -182,7 +181,7 @@ function OpeningHighlight({ openingId, depth }: { openingId: string; depth: numb
182
181
  return
183
182
  }
184
183
  group.visible = true
185
- obj.matrixWorld.decompose(group.position, group.quaternion, scratchScale)
184
+ copyOverlayWorldTransform(obj, group)
186
185
  })
187
186
 
188
187
  if (!isOpening || !outlineGeometry || !fillGeometry) return null
@@ -0,0 +1,92 @@
1
+ 'use client'
2
+
3
+ import { sceneRegistry } from '@pascal-app/core'
4
+ import { type ThreeEvent, useThree } from '@react-three/fiber'
5
+ import { useState } from 'react'
6
+ import { OrthographicCamera, Plane, type Ray, Vector2, Vector3 } from 'three'
7
+ import { runWallPushDrag, type WallPushHandle } from '../../lib/room-handle-drag'
8
+ import { getSpatialPointerId } from '../../lib/spatial-pointer-input'
9
+ import { suppressBoxSelectForPointer } from '../tools/select/box-select-state'
10
+ import { ARROW_SCALE, HandleArrow } from './handles/handle-arrow'
11
+
12
+ /**
13
+ * The push/pull arrow of a wall stretch — a selected wall's side arrows and a
14
+ * selected room's boundary arrows are this one component, driven by the one
15
+ * `runWallPushDrag`. Rendered in the level frame. `zoneId` binds a room's
16
+ * arrow to its room.
17
+ */
18
+ export function WallPushArrow({
19
+ handle,
20
+ levelId,
21
+ zoneId,
22
+ }: {
23
+ handle: WallPushHandle
24
+ levelId: string
25
+ zoneId?: string
26
+ }) {
27
+ const [hover, setHover] = useState(false)
28
+ const { camera, raycaster, gl } = useThree()
29
+ const zoom = camera instanceof OrthographicCamera ? 1 / camera.zoom : 1
30
+ const onPointerDown = (event: ThreeEvent<PointerEvent>) => {
31
+ if (event.button !== 0) return
32
+ event.stopPropagation()
33
+ suppressBoxSelectForPointer(event)
34
+ const level = sceneRegistry.nodes.get(levelId)
35
+ if (!level) return
36
+ level.updateWorldMatrix(true, false)
37
+ const toLocal = level.matrixWorld.clone().invert()
38
+ const origin = new Vector3(handle.position[0], handle.height, handle.position[1]).applyMatrix4(
39
+ level.matrixWorld,
40
+ )
41
+ const up = new Vector3(0, 1, 0).transformDirection(level.matrixWorld)
42
+ const plane = new Plane().setFromNormalAndCoplanarPoint(up, origin)
43
+ const along = (ray: Ray) => {
44
+ const hit = ray.intersectPlane(plane, new Vector3())
45
+ if (!hit) return null
46
+ const local = hit.applyMatrix4(toLocal)
47
+ return (
48
+ (local.x - handle.position[0]) * handle.outward[0] +
49
+ (local.z - handle.position[1]) * handle.outward[1]
50
+ )
51
+ }
52
+ const rayAt = (clientX: number, clientY: number) => {
53
+ const rect = gl.domElement.getBoundingClientRect()
54
+ raycaster.setFromCamera(
55
+ new Vector2(
56
+ ((clientX - rect.left) / rect.width) * 2 - 1,
57
+ -((clientY - rect.top) / rect.height) * 2 + 1,
58
+ ),
59
+ camera,
60
+ )
61
+ return raycaster.ray
62
+ }
63
+ const spatialPointerId = getSpatialPointerId(event.nativeEvent)
64
+ const from = along(
65
+ spatialPointerId ? event.ray : rayAt(event.nativeEvent.clientX, event.nativeEvent.clientY),
66
+ )
67
+ if (from === null) return
68
+ runWallPushDrag({
69
+ handle,
70
+ levelId,
71
+ zoneId,
72
+ from,
73
+ along: (clientX, clientY) => along(rayAt(clientX, clientY)),
74
+ spatial: spatialPointerId ? { pointerId: spatialPointerId, along } : undefined,
75
+ })
76
+ }
77
+ return (
78
+ <HandleArrow
79
+ cursor="grab"
80
+ hover={hover}
81
+ onHoverChange={setHover}
82
+ onPointerDown={onPointerDown}
83
+ placement={{
84
+ position: [handle.position[0], handle.height, handle.position[1]],
85
+ rotation: [0, Math.atan2(-handle.outward[1], handle.outward[0]), 0],
86
+ baseScale: zoom * ARROW_SCALE,
87
+ }}
88
+ shape="chevron"
89
+ thin
90
+ />
91
+ )
92
+ }