@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
@@ -3,12 +3,16 @@ import {
3
3
  type AlignmentAnchor,
4
4
  type AnyNode,
5
5
  type AnyNodeId,
6
+ beginSceneHistoryPauseSession,
6
7
  type CeilingEvent,
8
+ canHostSurfaceChild,
7
9
  collectAlignmentAnchors,
8
10
  emitter,
9
11
  findLevelAncestorId,
10
12
  type GridEvent,
11
13
  getScaledDimensions,
14
+ getWallBodyCenterOffset,
15
+ getWallLocalFaceZ,
12
16
  type ItemEvent,
13
17
  movingFootprintAnchors,
14
18
  type NodeEvent,
@@ -24,7 +28,6 @@ import {
24
28
  useScene,
25
29
  useSpatialQuery,
26
30
  type WallEvent,
27
- type WallNode,
28
31
  } from '@pascal-app/core'
29
32
  import { useViewer } from '@pascal-app/viewer'
30
33
  import { Html } from '@react-three/drei'
@@ -48,12 +51,13 @@ import {
48
51
  } from '../../../lib/active-placement-surface'
49
52
  import { EDITOR_LAYER } from '../../../lib/constants'
50
53
  import { formatLinearMeasurement } from '../../../lib/measurements'
54
+ import { isFreshPlacementMetadata } from '../../../lib/placement-metadata'
51
55
  import { createMovementSfxTick } from '../../../lib/sfx/movement-tick'
52
56
  import { sfxEmitter } from '../../../lib/sfx-bus'
53
57
  import {
54
58
  surfaceAttachmentId,
55
59
  surfaceFramePose,
56
- updateSurfaceNode,
60
+ updateSurfaceNode as writeSurfaceNode,
57
61
  } from '../../../lib/surface-attachment'
58
62
  import {
59
63
  projectAlignmentGuidesWorldToActiveBuildingLocal,
@@ -66,6 +70,7 @@ import useEditor, {
66
70
  isMagneticSnapActive,
67
71
  } from '../../../store/use-editor'
68
72
  import useFacingPose from '../../../store/use-facing-pose'
73
+ import { getMovingNode } from '../../../store/use-interaction-scope'
69
74
  import usePlacementPreview from '../../../store/use-placement-preview'
70
75
  import {
71
76
  createItemSurfaceGridDispatch,
@@ -80,11 +85,13 @@ import {
80
85
  } from '../shared/placement-box-geometry'
81
86
  import {
82
87
  type PointerSupportSurface,
83
- resolvePointerSupportElevation,
84
88
  resolvePointerSupportSurface,
85
89
  } from '../shared/pointer-support-cap'
86
90
  import { createShelfStickiness } from '../shared/shelf-stickiness'
87
- import { createSurfaceRejectionFeedback } from '../shared/surface-rejection'
91
+ import {
92
+ createSurfaceEventOwnership,
93
+ createSurfaceRejectionFeedback,
94
+ } from '../shared/surface-rejection'
88
95
  import { shouldCreateFloorDraft } from './draft-creation'
89
96
  import { commitFaceHostClick, resolveFaceHostPreviewCommit } from './face-host-commit'
90
97
  import {
@@ -112,7 +119,7 @@ import {
112
119
  } from './placement-strategies'
113
120
  import { resolveItemPlacementSurfaceNormal } from './placement-surface'
114
121
  import type { PlacementState, TransitionResult } from './placement-types'
115
- import type { DraftNodeHandle } from './use-draft-node'
122
+ import { type DraftNodeHandle, type ItemCopy, pausedDraftWrite } from './use-draft-node'
116
123
 
117
124
  const DEFAULT_DIMENSIONS: [number, number, number] = [1, 1, 1]
118
125
 
@@ -258,20 +265,20 @@ basePlaneMaterial.opacityNode = radialOpacity
258
265
 
259
266
  export interface PlacementCoordinatorConfig {
260
267
  asset: AssetInput | null
268
+ /** Scale to use when lazily creating a draft (e.g. for wall/ceiling duplicates). Defaults to [1,1,1]. */
269
+ defaultScale?: [number, number, number]
261
270
  draftNode: DraftNodeHandle
262
271
  initDraft: (gridPosition: Vector3) => void
263
- onCommitted: () => boolean
264
- onCancel?: () => void
265
272
  initialState?: PlacementState
266
- /** Scale to use when lazily creating a draft (e.g. for wall/ceiling duplicates). Defaults to [1,1,1]. */
267
- defaultScale?: [number, number, number]
268
- /** Painted slot overrides to seed onto a lazily-created draft (wall/ceiling
269
- * duplicates) so the duplicate keeps its materials. */
270
- slots?: ItemNode['slots']
273
+ onCancel?: () => void
274
+ onCommitted: () => boolean
271
275
  /** Move-mode sessions keep the grabbed item offset from the first surface hit
272
276
  * (floor / wall / ceiling / item-surface / shelf) instead of snapping the
273
277
  * item's origin under the cursor. */
274
278
  preserveDragOffset?: boolean
279
+ /** The item a duplicate copies, for a lazily-created draft (wall/ceiling
280
+ * duplicates) to keep its materials and collections. */
281
+ copied?: ItemCopy
275
282
  }
276
283
 
277
284
  export function usePlacementCoordinator(config: PlacementCoordinatorConfig): React.ReactNode {
@@ -524,7 +531,14 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
524
531
 
525
532
  useEffect(() => {
526
533
  if (!asset) return
527
- useScene.temporal.getState().pause()
534
+ // History is paused only around the placement's own writes, never for the whole carry:
535
+ // the draft stays out of history snapshots (core's history-drafts), so writes others make
536
+ // mid-carry record as their own steps, with space detection. In split view the 2D move
537
+ // overlay co-owns the gesture (keyed by the moving node), and the drop's commitStep lifts
538
+ // its pause too.
539
+ const movingNodeId = getMovingNode()?.id
540
+ const updateSurfaceNode: typeof writeSurfaceNode = (...args) =>
541
+ pausedDraftWrite(() => writeSurfaceNode(...args))
528
542
 
529
543
  const validators = { canPlaceOnFloor, canPlaceOnWall, canPlaceOnCeiling }
530
544
 
@@ -597,13 +611,13 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
597
611
  wasAdopted: boolean,
598
612
  repeat: () => void,
599
613
  ) => {
614
+ if (!committedId) return
600
615
  if (configRef.current.onCommitted()) {
601
616
  repeat()
602
617
  return
603
618
  }
604
619
 
605
620
  useAlignmentGuides.getState().clear()
606
- useScene.temporal.getState().resume()
607
621
  if (committedId) {
608
622
  useViewer.getState().setSelection({ selectedIds: [committedId as AnyNodeId] })
609
623
  }
@@ -626,12 +640,27 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
626
640
  ) => {
627
641
  const draftId = draftNode.current?.id ?? null
628
642
  const wasAdopted = draftNode.isAdopted
629
- const finalId = draftNode.commit(nodeUpdate, options)
630
- if (draftId) {
643
+ const drop = beginSceneHistoryPauseSession(useScene, { gesture: movingNodeId })
644
+ let finalId: string | null
645
+ try {
646
+ finalId = drop.commitStep(() =>
647
+ draftNode.commit(nodeUpdate, {
648
+ ...options,
649
+ onReject: (reason) => {
650
+ feedback.reject(reason)
651
+ edgeMaterial.color.setHex(0xef_44_44)
652
+ basePlaneMaterial.color.setHex(0xef_44_44)
653
+ },
654
+ }),
655
+ )
656
+ } finally {
657
+ drop.end()
658
+ }
659
+ if (finalId && draftId) {
631
660
  useLiveTransforms.getState().clear(draftId)
632
661
  useLiveNodeOverrides.getState().clearFields(draftId, faceHostClearFields(draftNode.current))
633
662
  }
634
- return { committedId: finalId ?? draftId, wasAdopted }
663
+ return { committedId: finalId, wasAdopted }
635
664
  }
636
665
 
637
666
  const faceHostClearFields = (draft: ItemNode | null | undefined): Array<keyof ItemNode> => {
@@ -651,6 +680,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
651
680
  }
652
681
 
653
682
  const surfacePointer = createItemSurfacePointerArbitration()
683
+ const surfaceOwnership = createSurfaceEventOwnership()
654
684
  const revalidate = (): boolean => {
655
685
  const fits = checkCanPlace(getContext(), validators)
656
686
  if (
@@ -681,11 +711,22 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
681
711
  return placeable
682
712
  }
683
713
 
684
- const surfaceContext = (event: NodeEvent<AnyNode>) => {
714
+ const surfaceContext = (event: NodeEvent<AnyNode>, generic = false) => {
685
715
  lastSurfaceEvent = event
686
716
  return {
687
717
  ...getContext(),
688
718
  onSurfaceReject: (reason: SurfaceRejectReason) => {
719
+ // A rejected generic acquisition leaves the paired pointer's floor route in charge.
720
+ if (
721
+ generic &&
722
+ placementState.current.surfaceItemId !== event.node.id &&
723
+ reason !== 'surface-cutout' &&
724
+ reason !== 'surface-occupied'
725
+ ) {
726
+ return
727
+ }
728
+ surfaceOwnership.claim(event.node.id, event.nativeEvent.nativeEvent ?? event.nativeEvent)
729
+ event.stopPropagation()
689
730
  if (
690
731
  (reason === 'footprint-outside-surface' ||
691
732
  reason === 'footprint-exceeds-host' ||
@@ -695,7 +736,9 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
695
736
  ) {
696
737
  feedback.clear()
697
738
  detachItemSurfaceToFloor(event as ItemEvent)
698
- surfacePointer.hit(event.node.id, event.nativeEvent.nativeEvent ?? event.nativeEvent)
739
+ // A generic host exit yields positioning to this pointer's paired floor event.
740
+ if (!generic)
741
+ surfacePointer.hit(event.node.id, event.nativeEvent.nativeEvent ?? event.nativeEvent)
699
742
  return
700
743
  }
701
744
  feedback.reject(reason, event.nativeEvent.nativeEvent ?? event.nativeEvent)
@@ -746,6 +789,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
746
789
 
747
790
  const applyTransition = (result: TransitionResult, event?: NodeEvent<AnyNode>) => {
748
791
  if (event) {
792
+ surfaceOwnership.claim(event.node.id, event.nativeEvent.nativeEvent ?? event.nativeEvent)
749
793
  surfacePointer.hit(event.node.id, event.nativeEvent.nativeEvent ?? event.nativeEvent)
750
794
  tickSurfaceMovementSfx(result.cursorPosition)
751
795
  }
@@ -805,16 +849,15 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
805
849
  asset,
806
850
  initRotation,
807
851
  configRef.current.defaultScale,
808
- configRef.current.slots,
852
+ configRef.current.copied,
809
853
  )
810
854
 
811
855
  const draft = draftNode.current
812
856
  if (draft) {
813
857
  Object.assign(draft, result.nodeUpdate)
814
858
  // One-time setup: put node in the right parent so it renders correctly
815
- if (result.surfaceId !== undefined)
816
- draftNode.updateSurface(result.nodeUpdate, result.surfaceId)
817
- else updateSurfaceNode(draft.id, result.nodeUpdate)
859
+ if (result.surfaceId === undefined) updateSurfaceNode(draft.id, result.nodeUpdate)
860
+ else draftNode.updateSurface(result.nodeUpdate, result.surfaceId)
818
861
  disableDraftRaycastNow()
819
862
  }
820
863
 
@@ -1054,7 +1097,9 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
1054
1097
  // perspective-skewed along the ray whenever the plane sits on a
1055
1098
  // different storey than the pointed surface (the skew is what made
1056
1099
  // a drag over a deck-above-a-floor hop between the two surfaces).
1057
- const pointed = resolvePointerSupportSurface(cameraRef.current, event.position)
1100
+ const pointed = resolvePointerSupportSurface(cameraRef.current, event.position, {
1101
+ pointerRay: event.nativeEvent.ray,
1102
+ })
1058
1103
  pointerSupportCapRef.current = pointed?.elevation ?? null
1059
1104
  pointerSupportSurfaceRef.current = pointed
1060
1105
  const surfaceEvent: GridEvent =
@@ -1247,7 +1292,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
1247
1292
  asset,
1248
1293
  currentRotation,
1249
1294
  configRef.current.defaultScale,
1250
- configRef.current.slots,
1295
+ configRef.current.copied,
1251
1296
  )
1252
1297
  const previewBounds = expandBoundsToGrid(
1253
1298
  getFallbackPreviewBounds(draftNode.current, asset, asset.attachTo),
@@ -1398,12 +1443,13 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
1398
1443
  const rot = result.nodeUpdate?.rotation
1399
1444
  if (rot) mesh.rotation.y = rot[1]
1400
1445
 
1401
- // Push wall-side items out by half the parent wall's thickness
1402
- if (asset.attachTo === 'wall-side' && placementState.current.wallId) {
1446
+ if (placementState.current.wallId) {
1403
1447
  const parentWall = useScene.getState().nodes[placementState.current.wallId as AnyNodeId]
1404
1448
  if (parentWall?.type === 'wall') {
1405
- const wallThickness = (parentWall as WallNode).thickness ?? 0.1
1406
- mesh.position.z = (wallThickness / 2) * (draft.side === 'front' ? 1 : -1)
1449
+ mesh.position.z =
1450
+ asset.attachTo === 'wall-side'
1451
+ ? getWallLocalFaceZ(parentWall, draft.side === 'front' ? 'a' : 'b')
1452
+ : draft.position[2] + getWallBodyCenterOffset(parentWall)
1407
1453
  }
1408
1454
  }
1409
1455
  }
@@ -1571,7 +1617,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
1571
1617
  const segment =
1572
1618
  useScene.getState().nodes[placementState.current.roofSegmentId as AnyNodeId]
1573
1619
  if (segment?.type === 'roof-segment') {
1574
- mesh.position.z = (segment.wallThickness ?? 0.1) / 2
1620
+ mesh.position.z = getWallLocalFaceZ({ thickness: segment.wallThickness ?? 0.1 }, 'a')
1575
1621
  }
1576
1622
  }
1577
1623
  const rot = result.nodeUpdate?.rotation
@@ -1611,10 +1657,12 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
1611
1657
  applyTransition(result)
1612
1658
  const draft = draftNode.current
1613
1659
  if (draft) {
1614
- useScene.getState().updateNode(draft.id, {
1615
- parentId: result.nodeUpdate.parentId as string,
1616
- roofSegmentId: undefined,
1617
- })
1660
+ pausedDraftWrite(() =>
1661
+ useScene.getState().updateNode(draft.id, {
1662
+ parentId: result.nodeUpdate.parentId as string,
1663
+ roofSegmentId: undefined,
1664
+ }),
1665
+ )
1618
1666
  }
1619
1667
  } else {
1620
1668
  // Create mode: destroy transient and reset state
@@ -1729,9 +1777,11 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
1729
1777
  }
1730
1778
  if (draftNode.isAdopted) {
1731
1779
  applyTransition(result)
1732
- if (draft) useScene.getState().updateNode(draft.id, result.nodeUpdate)
1780
+ if (draft)
1781
+ pausedDraftWrite(() => useScene.getState().updateNode(draft.id, result.nodeUpdate))
1733
1782
  } else {
1734
1783
  draftNode.destroy()
1784
+ gridPosition.current.set(...result.gridPosition)
1735
1785
  Object.assign(placementState.current, result.stateUpdate)
1736
1786
  }
1737
1787
  }
@@ -1744,11 +1794,10 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
1744
1794
  // Landing back on the floor: refresh the pointer surface cap from
1745
1795
  // this event's world hit so the first floor position already targets
1746
1796
  // the aimed-at surface (not a deck above it).
1747
- pointerSupportCapRef.current = resolvePointerSupportElevation(cameraRef.current, [
1748
- event.position[0],
1749
- event.position[1],
1750
- event.position[2],
1751
- ])
1797
+ pointerSupportCapRef.current =
1798
+ resolvePointerSupportSurface(cameraRef.current, event.position, {
1799
+ pointerRay: event.nativeEvent.ray,
1800
+ })?.elevation ?? null
1752
1801
  // Coming back from a host: forget the floor grab too, so the item
1753
1802
  // centers under the cursor instead of restoring the pre-drag offset —
1754
1803
  // and landing on the floor is "anchoring elsewhere", so a later return
@@ -1823,7 +1872,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
1823
1872
  revalidate()
1824
1873
  }
1825
1874
 
1826
- const onItemEnter = (event: NodeEvent<AnyNode>) => {
1875
+ const onItemEnter = (event: NodeEvent<AnyNode>, generic = false) => {
1827
1876
  if (event.node.type === 'cabinet' && placementState.current.surfaceItemId === event.node.id) {
1828
1877
  onItemMove(event)
1829
1878
  return
@@ -1834,7 +1883,9 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
1834
1883
  }
1835
1884
  if (event.node.id === draftNode.current?.id) return
1836
1885
  has3DPointerDrivenMoveRef.current = true
1837
- const result = itemSurfaceStrategy.enter(surfaceContext(event), event)
1886
+ // An authored object's underside takes ceiling items (a pendant under a vault).
1887
+ if (enterFaceHost(event)) return
1888
+ const result = itemSurfaceStrategy.enter(surfaceContext(event, generic), event)
1838
1889
  if (!result) return
1839
1890
  feedback.clear()
1840
1891
 
@@ -1849,12 +1900,23 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
1849
1900
  }
1850
1901
  }
1851
1902
 
1852
- const onItemMove = (event: NodeEvent<AnyNode>) => {
1903
+ const onItemMove = (event: NodeEvent<AnyNode>, generic = false) => {
1853
1904
  if (event.node.id === draftNode.current?.id) return
1905
+ if (
1906
+ placementState.current.surface === 'block-face' &&
1907
+ placementState.current.blockId === event.node.id
1908
+ ) {
1909
+ onFaceHostMove(event)
1910
+ return
1911
+ }
1912
+ if (placementState.current.surface !== 'block-face' && enterFaceHost(event)) {
1913
+ releaseCommit = () => onFaceHostClick(event)
1914
+ return
1915
+ }
1854
1916
  releaseCommit = () => onItemClick(event)
1855
1917
  has3DPointerDrivenMoveRef.current = true
1856
1918
  if (!cursorGroupRef.current) return
1857
- const ctx = surfaceContext(event)
1919
+ const ctx = surfaceContext(event, generic)
1858
1920
  if (event.node.type === 'cabinet') {
1859
1921
  lastRawPos.current.set(...event.position)
1860
1922
  surfacePointer.hit(event.node.id, event.nativeEvent.nativeEvent ?? event.nativeEvent)
@@ -1879,6 +1941,8 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
1879
1941
  event,
1880
1942
  )
1881
1943
 
1944
+ // An unclaimed generic mesh must let its owning ancestor receive the same hit.
1945
+ if (generic && !enterResult) return
1882
1946
  event.stopPropagation()
1883
1947
  if (enterResult) {
1884
1948
  applyTransition(enterResult, event)
@@ -1892,7 +1956,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
1892
1956
  }
1893
1957
 
1894
1958
  if (!draftNode.current) {
1895
- const enterResult = itemSurfaceStrategy.enter(surfaceContext(event), event)
1959
+ const enterResult = itemSurfaceStrategy.enter(surfaceContext(event, generic), event)
1896
1960
  if (!enterResult) return
1897
1961
  event.stopPropagation()
1898
1962
  ensureDraft(enterResult)
@@ -1900,9 +1964,9 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
1900
1964
  }
1901
1965
 
1902
1966
  const surfaceWorld =
1903
- ctx.state.surfaceItemId !== null
1904
- ? resolveHostSurfaceWorld(ctx.state.surfaceItemId, event.position)
1905
- : null
1967
+ ctx.state.surfaceItemId === null
1968
+ ? null
1969
+ : resolveHostSurfaceWorld(ctx.state.surfaceItemId, event.position)
1906
1970
  const itemMoveEvent = surfaceWorld ? { ...event, position: surfaceWorld } : event
1907
1971
  lastRawPos.current.set(
1908
1972
  itemMoveEvent.position[0],
@@ -1949,6 +2013,13 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
1949
2013
  const onItemLeave = (event: NodeEvent<AnyNode>) => {
1950
2014
  if (event.node.type === 'cabinet') return
1951
2015
  if (event.node.id === draftNode.current?.id) return
2016
+ if (
2017
+ placementState.current.surface === 'block-face' &&
2018
+ placementState.current.blockId === event.node.id
2019
+ ) {
2020
+ onFaceHostLeave(event)
2021
+ return
2022
+ }
1952
2023
  if (placementState.current.surface !== 'item-surface') return
1953
2024
 
1954
2025
  event.stopPropagation()
@@ -1964,6 +2035,13 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
1964
2035
  const onItemClick = (event: NodeEvent<AnyNode>) => {
1965
2036
  gridDispatch.flush()
1966
2037
  if (feedback.reason) return
2038
+ if (
2039
+ placementState.current.surface === 'block-face' &&
2040
+ placementState.current.blockId === event.node.id
2041
+ ) {
2042
+ onFaceHostClick(event)
2043
+ return
2044
+ }
1967
2045
  // Click on the draft item itself. R3F dispatches click events to
1968
2046
  // the closest intersected mesh only — when the draft is hovering
1969
2047
  // on a host (shelf / table / etc.) the draft's mesh is *above*
@@ -2007,12 +2085,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
2007
2085
  // the host's own click event is blocked by the cursor preview.
2008
2086
  if (ctx.state.surface === 'item-surface' && ctx.state.surfaceItemId) {
2009
2087
  const hostNode = useScene.getState().nodes[ctx.state.surfaceItemId as AnyNodeId]
2010
- if (
2011
- hostNode &&
2012
- (hostNode.type === 'item' ||
2013
- hostNode.type === 'cabinet' ||
2014
- hostNode.type === 'procedural-item')
2015
- ) {
2088
+ if (hostNode) {
2016
2089
  const synthetic = { ...event, node: hostNode } as ItemEvent
2017
2090
  const result = itemSurfaceStrategy.click(ctx, synthetic)
2018
2091
  if (result) {
@@ -2283,9 +2356,9 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
2283
2356
  return
2284
2357
  }
2285
2358
  const shelfWorld =
2286
- ctx.state.shelfId !== null
2287
- ? resolveHostSurfaceWorld(ctx.state.shelfId, event.position)
2288
- : null
2359
+ ctx.state.shelfId === null
2360
+ ? null
2361
+ : resolveHostSurfaceWorld(ctx.state.shelfId, event.position)
2289
2362
  const shelfMoveEvent = shelfWorld ? { ...event, position: shelfWorld } : event
2290
2363
  const result = shelfSurfaceStrategy.move(ctx, shelfMoveEvent)
2291
2364
  if (!result) return
@@ -2386,7 +2459,49 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
2386
2459
  const currentRotation = draft.rotation
2387
2460
  // Round to the nearest 45° then step, matching the placed-item R/T.
2388
2461
  const newRotationY = steppedRotation(currentRotation[1] ?? 0, rotationDir)
2389
- draft.rotation = [currentRotation[0], newRotationY, currentRotation[2]]
2462
+ const nextRotation: [number, number, number] = [
2463
+ currentRotation[0],
2464
+ newRotationY,
2465
+ currentRotation[2],
2466
+ ]
2467
+ const surface = placementState.current.surface
2468
+ const surfaceMesh =
2469
+ surface === 'item-surface' && placementState.current.surfaceItemId
2470
+ ? sceneRegistry.nodes.get(placementState.current.surfaceItemId)
2471
+ : undefined
2472
+ let surfacePosition: [number, number, number] | undefined
2473
+ if (surfaceMesh) {
2474
+ const localPos = surfaceMesh.worldToLocal(lastRawPos.current.clone())
2475
+ const [dimX, , dimZ] = getScaledDimensions(draft)
2476
+ const swapDims = Math.abs(Math.sin(newRotationY)) > 0.9
2477
+ surfacePosition = [
2478
+ snapToGrid(localPos.x, swapDims ? dimZ : dimX),
2479
+ gridPosition.current.y,
2480
+ snapToGrid(localPos.z, swapDims ? dimX : dimZ),
2481
+ ]
2482
+ }
2483
+ if (surfaceAttachmentId(draft)) {
2484
+ let occupied = false
2485
+ checkCanPlace(
2486
+ {
2487
+ ...getContext(),
2488
+ draftItem: { ...draft, rotation: nextRotation },
2489
+ gridPosition: surfacePosition
2490
+ ? new Vector3(...surfacePosition)
2491
+ : gridPosition.current,
2492
+ onSurfaceReject: (reason) => {
2493
+ occupied = reason === 'surface-occupied'
2494
+ },
2495
+ },
2496
+ validators,
2497
+ )
2498
+ if (occupied) {
2499
+ feedback.reject('surface-occupied')
2500
+ revalidate()
2501
+ return
2502
+ }
2503
+ }
2504
+ draft.rotation = nextRotation
2390
2505
 
2391
2506
  // Rotate the building-local cursor by the same delta as the host-local
2392
2507
  // draft. This preserves the host's composed yaw for items resting on a
@@ -2398,7 +2513,6 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
2398
2513
  if (mesh) mesh.rotation.y = newRotationY
2399
2514
 
2400
2515
  // Re-snap position immediately with updated rotation (dimX/dimZ may swap at 90°)
2401
- const surface = placementState.current.surface
2402
2516
  if (surface === 'floor' || surface === 'ceiling') {
2403
2517
  const dims = getScaledDimensions(draft)
2404
2518
  const [dimX, , dimZ] = dims
@@ -2428,15 +2542,8 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
2428
2542
  }
2429
2543
  }
2430
2544
  } else if (surface === 'item-surface' && placementState.current.surfaceItemId) {
2431
- const surfaceMesh = sceneRegistry.nodes.get(placementState.current.surfaceItemId)
2432
- if (surfaceMesh) {
2433
- const localPos = surfaceMesh.worldToLocal(lastRawPos.current.clone())
2434
- const dims = getScaledDimensions(draft)
2435
- const [dimX, , dimZ] = dims
2436
- const swapDims = Math.abs(Math.sin(newRotationY)) > 0.9
2437
- const x = snapToGrid(localPos.x, swapDims ? dimZ : dimX)
2438
- const z = snapToGrid(localPos.z, swapDims ? dimX : dimZ)
2439
- const y = gridPosition.current.y
2545
+ if (surfaceMesh && surfacePosition) {
2546
+ const [x, y, z] = surfacePosition
2440
2547
  gridPosition.current.set(x, y, z)
2441
2548
  draft.position = [x, y, z]
2442
2549
  const worldSnapped = surfaceMesh.localToWorld(new Vector3(x, y, z))
@@ -2553,7 +2660,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
2553
2660
  if (event.button !== 2) return
2554
2661
  const down = rightDown
2555
2662
  rightDown = null
2556
- if (!down || !configRef.current.onCancel) return
2663
+ if (!(down && configRef.current.onCancel)) return
2557
2664
  const movedSq = (event.clientX - down.x) ** 2 + (event.clientY - down.y) ** 2
2558
2665
  const elapsed = performance.now() - down.t
2559
2666
  if (movedSq <= RIGHT_CLICK_CANCEL_MAX_MOVE_PX ** 2 && elapsed <= RIGHT_CLICK_CANCEL_MAX_MS) {
@@ -2594,7 +2701,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
2594
2701
  const unsubDraftWatch = useScene.subscribe((state) => {
2595
2702
  if (tearingDown) return
2596
2703
  const draft = draftNode.current
2597
- if (draft === null) return
2704
+ if (draft === null || isFreshPlacementMetadata(draft.metadata)) return
2598
2705
  if (draft.id in state.nodes) return
2599
2706
 
2600
2707
  queueMicrotask(() => {
@@ -2602,48 +2709,79 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
2602
2709
  const draft = draftNode.current
2603
2710
  if (draft === null) return
2604
2711
  if (draft.id in useScene.getState().nodes) return
2605
- // Temporal is paused during placement, createNode won't be tracked
2606
- useScene.getState().createNode(draft, draft.parentId as AnyNodeId)
2712
+ pausedDraftWrite(() => useScene.getState().createNode(draft, draft.parentId as AnyNodeId))
2607
2713
  })
2608
2714
  })
2609
2715
 
2610
2716
  // ---- Subscribe ----
2611
2717
 
2718
+ const specializedKinds = new Set<string>()
2719
+ const subscribeSpecialized = <E,>(key: string, handler: (event: E) => void) => {
2720
+ specializedKinds.add(key.slice(0, key.lastIndexOf(':')))
2721
+ emitter.on(key as never, handler as never)
2722
+ }
2723
+ const genericRoute = (phase: 'enter' | 'move' | 'click' | 'leave') => (event: NodeEvent) => {
2724
+ if (specializedKinds.has(event.node.type)) return
2725
+ if (
2726
+ !surfaceOwnership.allows(event.node.id, event.nativeEvent.nativeEvent ?? event.nativeEvent)
2727
+ )
2728
+ return
2729
+ if (nodeRegistry.get(event.node.type)?.capabilities.faceHost) {
2730
+ const handlers = {
2731
+ enter: onFaceHostEnter,
2732
+ move: onFaceHostMove,
2733
+ click: onFaceHostClick,
2734
+ leave: onFaceHostLeave,
2735
+ }
2736
+ handlers[phase](event)
2737
+ return
2738
+ }
2739
+ if (!canHostSurfaceChild(event.node, 'item', draftNode.current?.id)) return
2740
+ if (phase === 'leave' && placementState.current.surfaceItemId !== event.node.id) return
2741
+ if (phase === 'enter') onItemEnter(event, true)
2742
+ else if (phase === 'move') onItemMove(event, true)
2743
+ else if (phase === 'click') onItemClick(event)
2744
+ else onItemLeave(event)
2745
+ }
2746
+ const onNodeEnter = genericRoute('enter')
2747
+ const onNodeMove = genericRoute('move')
2748
+ const onNodeClick = genericRoute('click')
2749
+ const onNodeLeave = genericRoute('leave')
2612
2750
  const gridDispatch = createItemSurfaceGridDispatch(onGridMove)
2613
2751
  emitter.on('grid:move', gridDispatch.schedule)
2614
2752
  emitter.on('grid:click', onGridClick)
2615
- emitter.on('item:enter', onItemEnter)
2616
- emitter.on('item:move', onItemMove)
2617
- emitter.on('item:leave', onItemLeave)
2618
- emitter.on('item:click', onItemClick)
2619
- emitter.on('wall:enter', onWallEnter)
2620
- emitter.on('wall:move', onWallMove)
2621
- emitter.on('wall:click', onWallClick)
2622
- emitter.on('wall:leave', onWallLeave)
2623
- emitter.on('roof:enter', onRoofWallEnter)
2624
- emitter.on('roof:move', onRoofWallMove)
2625
- emitter.on('roof:click', onRoofWallClick)
2626
- emitter.on('roof:leave', onRoofWallLeave)
2627
- emitter.on('node:enter', onFaceHostEnter)
2628
- emitter.on('node:move', onFaceHostMove)
2629
- emitter.on('node:click', onFaceHostClick)
2630
- emitter.on('node:leave', onFaceHostLeave)
2631
- emitter.on('ceiling:enter', onCeilingEnter)
2632
- emitter.on('ceiling:move', onCeilingMove)
2633
- emitter.on('ceiling:click', onCeilingClick)
2634
- emitter.on('ceiling:leave', onCeilingLeave)
2635
- emitter.on('cabinet:enter', onItemEnter)
2636
- emitter.on('procedural-item:enter' as never, onItemEnter)
2637
- emitter.on('cabinet:move', onItemMove)
2638
- emitter.on('procedural-item:move' as never, onItemMove)
2639
- emitter.on('cabinet:click', onItemClick)
2640
- emitter.on('procedural-item:click' as never, onItemClick)
2641
- emitter.on('cabinet:leave', onItemLeave)
2642
- emitter.on('procedural-item:leave' as never, onItemLeave)
2643
- emitter.on('shelf:enter', onShelfEnter)
2644
- emitter.on('shelf:move', onShelfMove)
2645
- emitter.on('shelf:click', onShelfClick)
2646
- emitter.on('shelf:leave', onShelfLeave)
2753
+ subscribeSpecialized('item:enter', onItemEnter)
2754
+ subscribeSpecialized('item:move', onItemMove)
2755
+ subscribeSpecialized('item:leave', onItemLeave)
2756
+ subscribeSpecialized('item:click', onItemClick)
2757
+ subscribeSpecialized('wall:enter', onWallEnter)
2758
+ subscribeSpecialized('wall:move', onWallMove)
2759
+ subscribeSpecialized('wall:click', onWallClick)
2760
+ subscribeSpecialized('wall:leave', onWallLeave)
2761
+ subscribeSpecialized('roof:enter', onRoofWallEnter)
2762
+ subscribeSpecialized('roof:move', onRoofWallMove)
2763
+ subscribeSpecialized('roof:click', onRoofWallClick)
2764
+ subscribeSpecialized('roof:leave', onRoofWallLeave)
2765
+ emitter.on('node:enter', onNodeEnter)
2766
+ emitter.on('node:move', onNodeMove)
2767
+ emitter.on('node:click', onNodeClick)
2768
+ emitter.on('node:leave', onNodeLeave)
2769
+ subscribeSpecialized('ceiling:enter', onCeilingEnter)
2770
+ subscribeSpecialized('ceiling:move', onCeilingMove)
2771
+ subscribeSpecialized('ceiling:click', onCeilingClick)
2772
+ subscribeSpecialized('ceiling:leave', onCeilingLeave)
2773
+ subscribeSpecialized('cabinet:enter', onItemEnter)
2774
+ subscribeSpecialized('procedural-item:enter', onItemEnter)
2775
+ subscribeSpecialized('cabinet:move', onItemMove)
2776
+ subscribeSpecialized('procedural-item:move', onItemMove)
2777
+ subscribeSpecialized('cabinet:click', onItemClick)
2778
+ subscribeSpecialized('procedural-item:click', onItemClick)
2779
+ subscribeSpecialized('cabinet:leave', onItemLeave)
2780
+ subscribeSpecialized('procedural-item:leave', onItemLeave)
2781
+ subscribeSpecialized('shelf:enter', onShelfEnter)
2782
+ subscribeSpecialized('shelf:move', onShelfMove)
2783
+ subscribeSpecialized('shelf:click', onShelfClick)
2784
+ subscribeSpecialized('shelf:leave', onShelfLeave)
2647
2785
 
2648
2786
  // A floor placement commits at the tracked floor cursor (`gridPosition`),
2649
2787
  // which keeps following the floor even when the click ray lands on a wall
@@ -2673,7 +2811,6 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
2673
2811
  useLiveNodeOverrides.getState().clearFields(draftNode.current.id, ['rotation'])
2674
2812
  }
2675
2813
  draftNode.destroy()
2676
- useScene.temporal.getState().resume()
2677
2814
  gridDispatch.cancel()
2678
2815
  emitter.off('grid:move', gridDispatch.schedule)
2679
2816
  emitter.off('grid:click', onGridClick)
@@ -2689,10 +2826,10 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
2689
2826
  emitter.off('roof:move', onRoofWallMove)
2690
2827
  emitter.off('roof:click', onRoofWallClick)
2691
2828
  emitter.off('roof:leave', onRoofWallLeave)
2692
- emitter.off('node:enter', onFaceHostEnter)
2693
- emitter.off('node:move', onFaceHostMove)
2694
- emitter.off('node:click', onFaceHostClick)
2695
- emitter.off('node:leave', onFaceHostLeave)
2829
+ emitter.off('node:enter', onNodeEnter)
2830
+ emitter.off('node:move', onNodeMove)
2831
+ emitter.off('node:click', onNodeClick)
2832
+ emitter.off('node:leave', onNodeLeave)
2696
2833
  emitter.off('ceiling:enter', onCeilingEnter)
2697
2834
  emitter.off('ceiling:move', onCeilingMove)
2698
2835
  emitter.off('ceiling:click', onCeilingClick)
@@ -2764,12 +2901,15 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
2764
2901
  draftParent?.type === 'shelf' ||
2765
2902
  draftParent?.type === 'cabinet' ||
2766
2903
  draftParent?.type === 'procedural-item' ||
2904
+ (draftParent && canHostSurfaceChild(draftParent, draft.type, draft.id)) ||
2767
2905
  (draftParent &&
2768
2906
  nodeRegistry.get(draftParent.type)?.capabilities.faceHost?.currentFaceId(draft))
2769
2907
  )
2770
2908
  return
2771
2909
  draft.parentId = viewerLevelId
2772
- useScene.getState().updateNode(draft.id as AnyNodeId, { parentId: viewerLevelId })
2910
+ pausedDraftWrite(() =>
2911
+ useScene.getState().updateNode(draft.id as AnyNodeId, { parentId: viewerLevelId }),
2912
+ )
2773
2913
  }, [viewerLevelId, draftNode, asset])
2774
2914
 
2775
2915
  // Restore the draft mesh's raycast when the coordinator unmounts (tool change).
@@ -2782,8 +2922,8 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
2782
2922
  // instead of drawing an inline triangle. Only this coordinator publishes — a
2783
2923
  // moving existing node has no draft here, so the grid reads that case straight
2784
2924
  // off the node's mesh. Cleared when idle.
2785
- const surfaceNormalRef = useRef(new Vector3(0, 1, 0))
2786
2925
  const surfaceWorldPointRef = useRef(new Vector3())
2926
+ const surfaceNormalRef = useRef(new Vector3(0, 1, 0))
2787
2927
  const facingForwardRef = useRef(new Vector3(0, 0, 1))
2788
2928
  const facingQuatRef = useRef(new Quaternion())
2789
2929
  const ghostSurfaceQuatRef = useRef(new Quaternion())