@pascal-app/editor 1.0.3 → 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 (440) hide show
  1. package/package.json +11 -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 +202 -57
  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 +36 -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 +344 -342
  27. package/src/components/editor/footprint-height-handle.tsx +204 -0
  28. package/src/components/editor/group-actions.ts +54 -2
  29. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  30. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  31. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  32. package/src/components/editor/handles/use-handle-drag.ts +269 -197
  33. package/src/components/editor/index.tsx +294 -169
  34. package/src/components/editor/join-walls-pill.tsx +44 -0
  35. package/src/components/editor/node-action-menu.tsx +68 -70
  36. package/src/components/editor/node-arrow-handles.tsx +25 -6
  37. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  38. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  39. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  40. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  41. package/src/components/editor/registry-action-contributions.tsx +80 -1
  42. package/src/components/editor/room-controls.tsx +847 -0
  43. package/src/components/editor/room-floor-highlight.tsx +128 -0
  44. package/src/components/editor/room-handles-2d.tsx +354 -0
  45. package/src/components/editor/room-handles.tsx +675 -0
  46. package/src/components/editor/room-highlight.tsx +225 -0
  47. package/src/components/editor/room-labels.test.ts +357 -0
  48. package/src/components/editor/room-labels.tsx +990 -0
  49. package/src/components/editor/room-transform-ghost.tsx +256 -0
  50. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  51. package/src/components/editor/selection-manager.tsx +451 -317
  52. package/src/components/editor/slab-hole-highlights.tsx +3 -0
  53. package/src/components/editor/snapshot-capture-overlay.tsx +4 -0
  54. package/src/components/editor/snapshot-capture.test.ts +52 -4
  55. package/src/components/editor/snapshot-capture.ts +30 -5
  56. package/src/components/editor/thumbnail-generator.test.ts +308 -0
  57. package/src/components/editor/thumbnail-generator.tsx +1035 -229
  58. package/src/components/editor/use-floorplan-background-placement.ts +31 -11
  59. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  60. package/src/components/editor/views/host-view.tsx +61 -0
  61. package/src/components/editor/views/plugins-view.tsx +670 -0
  62. package/src/components/editor/views/scene-region.tsx +59 -0
  63. package/src/components/editor/views/use-editor-views.ts +57 -0
  64. package/src/components/editor/views/view-bar.tsx +220 -0
  65. package/src/components/editor/wall-justification.test.ts +140 -0
  66. package/src/components/editor/wall-move-side-handles.tsx +43 -142
  67. package/src/components/editor/wall-push-arrow.tsx +92 -0
  68. package/src/components/editor/wall-region-handles.tsx +509 -0
  69. package/src/components/editor/wall-snap-beacon-layer.tsx +3 -0
  70. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  71. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  72. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  73. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  74. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +113 -102
  75. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  76. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  77. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  78. package/src/components/editor-2d/renderers/{floorplan-draft-wall-measurement.tsx → floorplan-draft-measurement.tsx} +8 -8
  79. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  80. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +38 -8
  81. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +291 -227
  82. package/src/components/editor-2d/svg-paths.ts +12 -15
  83. package/src/components/procedural-items/inspector.tsx +40 -15
  84. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  85. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  86. package/src/components/systems/zone/zone-system.test.ts +47 -0
  87. package/src/components/systems/zone/zone-system.tsx +29 -68
  88. package/src/components/tools/item/placement-history.test.tsx +474 -0
  89. package/src/components/tools/item/placement-strategies.ts +8 -4
  90. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  91. package/src/components/tools/item/use-draft-node.ts +166 -95
  92. package/src/components/tools/item/use-placement-coordinator.tsx +93 -46
  93. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  94. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  95. package/src/components/tools/registry/move-registry-node-tool.tsx +3 -1
  96. package/src/components/tools/select/box-select-tool.tsx +2 -12
  97. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  98. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  99. package/src/components/tools/select/select-candidates.test.ts +37 -4
  100. package/src/components/tools/select/select-candidates.ts +15 -11
  101. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  102. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  103. package/src/components/tools/stair/stair-defaults.ts +0 -3
  104. package/src/components/tools/stair/stair-tool.tsx +90 -42
  105. package/src/components/tools/tool-manager.tsx +46 -20
  106. package/src/components/tools/wall/wall-drafting.test.ts +19 -4
  107. package/src/components/tools/zone/zone-tool.tsx +4 -12
  108. package/src/components/ui/action-menu/action-button.tsx +3 -0
  109. package/src/components/ui/action-menu/camera-actions.tsx +2 -1
  110. package/src/components/ui/action-menu/control-modes.tsx +2 -35
  111. package/src/components/ui/action-menu/measurement-control.tsx +3 -10
  112. package/src/components/ui/build-panel/build-panel.tsx +235 -0
  113. package/src/components/ui/command-palette/editor-commands.tsx +4 -30
  114. package/src/components/ui/command-palette/index.tsx +5 -24
  115. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  116. package/src/components/ui/controls/material-paint-panel.tsx +97 -26
  117. package/src/components/ui/controls/material-picker.tsx +4 -1
  118. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  119. package/src/components/ui/controls/select-control.tsx +65 -0
  120. package/src/components/ui/floating-level-selector.tsx +9 -14
  121. package/src/components/ui/helpers/building-helper.tsx +4 -1
  122. package/src/components/ui/helpers/contextual-helper-panel.tsx +154 -70
  123. package/src/components/ui/helpers/helper-manager.test.tsx +69 -0
  124. package/src/components/ui/helpers/helper-manager.tsx +325 -14
  125. package/src/components/ui/helpers/item-helper.tsx +8 -0
  126. package/src/components/ui/helpers/registered-tool-helper.tsx +8 -1
  127. package/src/components/ui/icon-ref.tsx +45 -1
  128. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  129. package/src/components/ui/node-icon-image.tsx +16 -0
  130. package/src/components/ui/panels/collections/collections-popover.tsx +20 -14
  131. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  132. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  133. package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
  134. package/src/components/ui/panels/mobile-selection-bar.tsx +2 -2
  135. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  136. package/src/components/ui/panels/multi-field-value.ts +6 -3
  137. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  138. package/src/components/ui/panels/multi-selection-panel.tsx +43 -4
  139. package/src/components/ui/panels/node-display.ts +8 -3
  140. package/src/components/ui/panels/panel-manager.tsx +103 -68
  141. package/src/components/ui/panels/panel-wrapper.tsx +78 -36
  142. package/src/components/ui/panels/parametric-field-control.tsx +1 -1
  143. package/src/components/ui/panels/parametric-inspector.tsx +4 -4
  144. package/src/components/ui/panels/reference-panel.tsx +2 -3
  145. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  146. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  147. package/src/components/ui/panels/room-name-options.ts +47 -0
  148. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  149. package/src/components/ui/panels/room-panel.tsx +725 -0
  150. package/src/components/ui/panels/shape-choice.tsx +58 -0
  151. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  152. package/src/components/ui/primitives/color-dot.tsx +36 -24
  153. package/src/components/ui/primitives/sidebar.tsx +2 -0
  154. package/src/components/ui/right-stack.test.ts +29 -0
  155. package/src/components/ui/right-stack.tsx +145 -0
  156. package/src/components/ui/sidebar/icon-rail.tsx +5 -0
  157. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +31 -4
  158. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +5 -5
  159. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  160. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  161. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  162. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  163. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  164. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  165. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  166. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  167. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  168. package/src/components/ui/sidebar/panels/site-panel/index.tsx +176 -211
  169. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  170. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  171. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  172. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  173. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  174. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  175. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  176. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  177. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  178. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  179. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +0 -27
  180. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  181. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  182. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  183. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  184. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +0 -2
  185. package/src/components/ui/sidebar/tab-bar.tsx +10 -0
  186. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  187. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  188. package/src/components/viewer/floorplan-preview.tsx +5 -6
  189. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  190. package/src/components/viewer/viewer-rooms.tsx +175 -0
  191. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  192. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  193. package/src/components/viewer/viewer-stage.tsx +2 -2
  194. package/src/components/viewer-zone-system.tsx +3 -8
  195. package/src/hooks/use-auto-save.integration.test.tsx +123 -9
  196. package/src/hooks/use-auto-save.ts +34 -8
  197. package/src/hooks/use-keyboard.test.ts +178 -3
  198. package/src/hooks/use-keyboard.ts +89 -71
  199. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  200. package/src/hooks/use-open-wall-ends.ts +238 -0
  201. package/src/hooks/use-placement-notice.test.ts +48 -0
  202. package/src/hooks/use-placement-notice.ts +42 -0
  203. package/src/hooks/use-selected-room.ts +143 -0
  204. package/src/index.tsx +246 -39
  205. package/src/lib/ceiling-edit-session.test.ts +321 -0
  206. package/src/lib/ceiling-edit-session.ts +206 -0
  207. package/src/lib/ceiling-region-session.test.ts +179 -0
  208. package/src/lib/continuation.test.ts +19 -1
  209. package/src/lib/continuation.ts +25 -4
  210. package/src/lib/door-interaction.ts +14 -0
  211. package/src/lib/editor-level-display.test.ts +96 -0
  212. package/src/lib/editor-level-display.ts +48 -0
  213. package/src/lib/editor-views.ts +144 -0
  214. package/src/lib/export-geometry-wait.test.ts +90 -0
  215. package/src/lib/floor-edit-session.ts +153 -0
  216. package/src/lib/floor-footprints.test.ts +563 -0
  217. package/src/lib/floor-footprints.ts +395 -0
  218. package/src/lib/floor-foundation-support.test.ts +283 -0
  219. package/src/lib/floor-opening-draft.ts +218 -0
  220. package/src/lib/floor-openings.test.ts +216 -0
  221. package/src/lib/floor-region-geometry.ts +191 -0
  222. package/src/lib/floor-region-session.test.ts +485 -0
  223. package/src/lib/floor-region-session.ts +589 -0
  224. package/src/lib/floor-region-snap.ts +101 -0
  225. package/src/lib/floor-step-hover.test.ts +32 -0
  226. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  227. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  228. package/src/lib/floorplan/dimension-drive.ts +479 -0
  229. package/src/lib/floorplan/floorplan-export.test.ts +12 -8
  230. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  231. package/src/lib/floorplan/floorplan-extension.ts +32 -0
  232. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  233. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  234. package/src/lib/floorplan/floorplan-pdfkit-document.ts +339 -18
  235. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +138 -8
  236. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +56 -25
  237. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  238. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  239. package/src/lib/floorplan/geometry.ts +13 -6
  240. package/src/lib/floorplan/index.ts +0 -9
  241. package/src/lib/floorplan/items.ts +5 -2
  242. package/src/lib/floorplan/marks.test.ts +213 -0
  243. package/src/lib/floorplan/marks.ts +238 -0
  244. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  245. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  246. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  247. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  248. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  249. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  250. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  251. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  252. package/src/lib/floorplan/schedules.test.ts +189 -0
  253. package/src/lib/floorplan/schedules.ts +339 -0
  254. package/src/lib/floorplan/sheet-export.test.ts +100 -0
  255. package/src/lib/floorplan/sheet-export.ts +318 -0
  256. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +779 -0
  257. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  258. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  259. package/src/lib/floorplan/site-plan/coverage.ts +232 -0
  260. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  261. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  262. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  263. package/src/lib/floorplan/site-plan/front-edge.ts +156 -0
  264. package/src/lib/floorplan/site-plan/geometry.test.ts +275 -0
  265. package/src/lib/floorplan/site-plan/geometry.ts +557 -0
  266. package/src/lib/floorplan/site-plan/index.ts +71 -0
  267. package/src/lib/floorplan/site-plan/site-annotations.test.ts +366 -0
  268. package/src/lib/floorplan/site-plan/site-annotations.ts +1083 -0
  269. package/src/lib/floorplan/site-plan/site-parts.ts +379 -0
  270. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  271. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  272. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  273. package/src/lib/floorplan/site-plan/yards.test.ts +69 -0
  274. package/src/lib/floorplan/types.ts +1 -27
  275. package/src/lib/fresh-planar-placement.test.ts +36 -0
  276. package/src/lib/fresh-planar-placement.ts +59 -36
  277. package/src/lib/geometry-script/author.ts +152 -0
  278. package/src/lib/geometry-script/client.ts +152 -0
  279. package/src/lib/geometry-script/protocol.ts +12 -0
  280. package/src/lib/gesture-lifecycle.test.ts +595 -0
  281. package/src/lib/gesture-lifecycle.ts +290 -0
  282. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  283. package/src/lib/glb-export.test.ts +485 -0
  284. package/src/lib/glb-export.ts +237 -119
  285. package/src/lib/history.test.ts +6 -0
  286. package/src/lib/history.ts +7 -0
  287. package/src/lib/hud-title.ts +109 -0
  288. package/src/lib/hydration-gated-storage.ts +49 -0
  289. package/src/lib/ifc-export.test.ts +174 -0
  290. package/src/lib/ifc-export.ts +201 -0
  291. package/src/lib/ifc-parts.ts +15 -0
  292. package/src/lib/inspector-expanded.test.ts +44 -0
  293. package/src/lib/inspector-expanded.ts +36 -0
  294. package/src/lib/interaction/scope.ts +18 -0
  295. package/src/lib/keyboard-pan.test.ts +129 -7
  296. package/src/lib/keyboard-pan.ts +161 -12
  297. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  298. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  299. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  300. package/src/lib/lot/clean-ring.test.ts +186 -0
  301. package/src/lib/lot/clean-ring.ts +217 -0
  302. package/src/lib/lot/dossier.test.ts +202 -0
  303. package/src/lib/lot/dossier.ts +498 -0
  304. package/src/lib/lot/drop-in.test.ts +326 -0
  305. package/src/lib/lot/drop-in.ts +407 -0
  306. package/src/lib/lot/index.ts +43 -0
  307. package/src/lib/lot/lot-patch.test.ts +340 -0
  308. package/src/lib/lot/lot-patch.ts +394 -0
  309. package/src/lib/lot/parcel-provider.ts +43 -0
  310. package/src/lib/lot/terrain.test.ts +161 -0
  311. package/src/lib/lot/terrain.ts +266 -0
  312. package/src/lib/material-catalog-model.ts +3 -1
  313. package/src/lib/material-paint-panel-model.ts +9 -5
  314. package/src/lib/material-paint.ts +4 -0
  315. package/src/lib/mezzanine-actions.test.ts +342 -0
  316. package/src/lib/mezzanine-draft.test.ts +501 -0
  317. package/src/lib/mezzanine-draft.ts +278 -0
  318. package/src/lib/mezzanine-messages.ts +26 -0
  319. package/src/lib/mezzanine-selection.test.ts +207 -0
  320. package/src/lib/mezzanine-stairs.ts +46 -0
  321. package/src/lib/model-export.ts +3 -1
  322. package/src/lib/node-action-movement.ts +17 -6
  323. package/src/lib/node-display-label.ts +37 -0
  324. package/src/lib/orbit-floor-clearance.test.ts +165 -0
  325. package/src/lib/orbit-floor-clearance.ts +106 -0
  326. package/src/lib/paint-commit-route.test.ts +37 -0
  327. package/src/lib/paint-commit-route.ts +17 -0
  328. package/src/lib/paint-eyedropper.test.tsx +165 -0
  329. package/src/lib/paint-eyedropper.ts +99 -0
  330. package/src/lib/paint-pass-through.test.ts +111 -0
  331. package/src/lib/paint-pass-through.ts +76 -0
  332. package/src/lib/paint-region-hover.ts +25 -0
  333. package/src/lib/paint-region-mode.ts +154 -0
  334. package/src/lib/paint-regions.test.ts +166 -0
  335. package/src/lib/paint-regions.ts +192 -0
  336. package/src/lib/paint-scope.test.ts +317 -35
  337. package/src/lib/paint-scope.ts +266 -25
  338. package/src/lib/panel-rows.ts +21 -0
  339. package/src/lib/parametric-node-update.ts +1 -1
  340. package/src/lib/plate-paint-affected.test.ts +300 -0
  341. package/src/lib/plate-paint-affected.ts +177 -0
  342. package/src/lib/plugin-chat.ts +50 -0
  343. package/src/lib/plugin-panels.test.ts +25 -0
  344. package/src/lib/plugin-panels.ts +94 -0
  345. package/src/lib/print-shell-compiler.ts +1 -0
  346. package/src/lib/registered-draft-snap.ts +12 -0
  347. package/src/lib/roof-duplication.ts +10 -2
  348. package/src/lib/room-assembly-overlay.test.ts +275 -0
  349. package/src/lib/room-assembly-overlay.ts +266 -0
  350. package/src/lib/room-built-on.test.ts +379 -0
  351. package/src/lib/room-built-on.ts +138 -0
  352. package/src/lib/room-construction-commands.ts +712 -0
  353. package/src/lib/room-construction.test.ts +746 -0
  354. package/src/lib/room-construction.ts +385 -0
  355. package/src/lib/room-controls-phase6.test.ts +630 -0
  356. package/src/lib/room-divide-session.ts +523 -0
  357. package/src/lib/room-handle-drag.ts +1068 -0
  358. package/src/lib/room-name-catalog.test.ts +90 -0
  359. package/src/lib/room-name-catalog.ts +168 -0
  360. package/src/lib/room-push-dimensions.test.ts +311 -0
  361. package/src/lib/room-push-dimensions.ts +251 -0
  362. package/src/lib/room-selection-commands.ts +62 -0
  363. package/src/lib/room-selection.test.ts +973 -0
  364. package/src/lib/room-selection.ts +417 -0
  365. package/src/lib/room-structure-commands.ts +94 -0
  366. package/src/lib/room-structure.test.ts +921 -0
  367. package/src/lib/room-transform-session.ts +570 -0
  368. package/src/lib/room-zone-routing.test.ts +294 -0
  369. package/src/lib/room-zone-routing.ts +197 -0
  370. package/src/lib/scene-clipboard.test.ts +364 -0
  371. package/src/lib/scene-clipboard.ts +229 -48
  372. package/src/lib/scene-material-refs.test.ts +79 -0
  373. package/src/lib/scene-material-refs.ts +79 -0
  374. package/src/lib/scene.test.ts +24 -36
  375. package/src/lib/scene.ts +33 -57
  376. package/src/lib/selection-routing.test.ts +129 -45
  377. package/src/lib/selection-routing.ts +53 -74
  378. package/src/lib/session-writes.test.ts +120 -0
  379. package/src/lib/session-writes.ts +205 -0
  380. package/src/lib/sidebar-panel.ts +62 -0
  381. package/src/lib/site-boundary.test.ts +4 -6
  382. package/src/lib/site-boundary.ts +1 -1
  383. package/src/lib/snapping-mode.test.ts +4 -0
  384. package/src/lib/snapping-mode.ts +8 -1
  385. package/src/lib/terrace-draft.test.ts +145 -0
  386. package/src/lib/terrace-draft.ts +213 -0
  387. package/src/lib/tool-cursor-icon.test.ts +16 -0
  388. package/src/lib/tool-cursor-icon.ts +11 -0
  389. package/src/lib/typing-target.ts +9 -0
  390. package/src/lib/units.ts +77 -39
  391. package/src/lib/vector-edges.test.ts +58 -0
  392. package/src/lib/vector-edges.ts +400 -0
  393. package/src/lib/viewer-selection.test.ts +577 -0
  394. package/src/lib/viewer-selection.ts +569 -0
  395. package/src/lib/wall-draw-variant.test.ts +71 -0
  396. package/src/lib/wall-draw-variant.ts +91 -0
  397. package/src/lib/wall-paint-region-session.ts +246 -0
  398. package/src/lib/wall-paint-region.test.ts +373 -0
  399. package/src/lib/wall-polygon-draft.test.ts +262 -0
  400. package/src/lib/wall-polygon-draft.ts +286 -0
  401. package/src/lib/wall-push.test.ts +453 -0
  402. package/src/lib/wall-region-face.ts +352 -0
  403. package/src/lib/wall-region-handles.test.ts +304 -0
  404. package/src/lib/wall-region-handles.ts +368 -0
  405. package/src/lib/wall-region-list.test.ts +119 -0
  406. package/src/lib/wall-region-list.ts +136 -0
  407. package/src/lib/wall-region-snap.ts +110 -0
  408. package/src/lib/window-interaction.ts +14 -0
  409. package/src/lib/zone-content.ts +4 -1
  410. package/src/lib/zone-name.test.ts +17 -0
  411. package/src/lib/zone-name.ts +17 -0
  412. package/src/store/opens-in-select-mode.test.ts +223 -0
  413. package/src/store/paint-mode.test.ts +188 -0
  414. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  415. package/src/store/tool-mode.test.ts +137 -6
  416. package/src/store/use-delete-confirmation.ts +28 -1
  417. package/src/store/use-drawing-view.test.ts +24 -3
  418. package/src/store/use-drawing-view.ts +29 -1
  419. package/src/store/use-editor.tsx +432 -321
  420. package/src/store/use-floorplan-draft-preview.ts +13 -0
  421. package/src/store/use-floorplan-marquee.ts +12 -2
  422. package/src/store/use-quick-measurement-hud.ts +3 -3
  423. package/src/store/use-stair-build-preview.ts +13 -5
  424. package/src/store/use-wall-move-ghosts.ts +4 -2
  425. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  426. package/src/store/use-wall-region-handles-selection.ts +139 -0
  427. package/src/store/view-layout.test.ts +175 -0
  428. package/src/store/view-layout.ts +196 -0
  429. package/src/test-utils/immediate-animation-frames.ts +16 -0
  430. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  431. package/src/test-utils/selection-harness.ts +82 -0
  432. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  433. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  434. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  435. package/src/components/tools/fence/fence-drafting.ts +0 -266
  436. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  437. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  438. package/src/lib/floorplan/stairs.ts +0 -489
  439. package/src/lib/level-duplication.test.ts +0 -144
  440. package/src/lib/level-duplication.ts +0 -195
@@ -1,11 +1,15 @@
1
1
  import {
2
2
  type AnyNodeId,
3
3
  type AssetInput,
4
+ beginSceneHistoryDraft,
4
5
  ItemNode,
5
6
  resolveSupportSlabPatch,
7
+ runSceneHistoryDraftWrite,
6
8
  type SurfaceRejectReason,
9
+ sceneHistoryDraftRevertUpdates,
7
10
  sceneRegistry,
8
11
  useScene,
12
+ withSceneHistoryDraftSuspended,
9
13
  } from '@pascal-app/core'
10
14
  import { beginPerfAction, commitPerfAction, useViewer } from '@pascal-app/viewer'
11
15
  import { useCallback, useMemo, useRef } from 'react'
@@ -24,6 +28,51 @@ import useInteractionScope, {
24
28
  import usePlacementPreview from '../../../store/use-placement-preview'
25
29
  import { stripTransient } from './placement-math'
26
30
 
31
+ function releaseHistoryDraft(end: { current: (() => void) | null }): void {
32
+ end.current?.()
33
+ end.current = null
34
+ }
35
+
36
+ /**
37
+ * A placement's own write: under a short pause, and the carried draft's fields it changes stay
38
+ * recorded as they were before the carry (core's `runSceneHistoryDraftWrite`).
39
+ */
40
+ export const pausedDraftWrite = runSceneHistoryDraftWrite
41
+
42
+ /**
43
+ * Puts back what the carry still holds on an adopted item (its own writes, core's
44
+ * `sceneHistoryDraftRevertUpdates`), as a carry write: a position, name or host a collaborator
45
+ * wrote meanwhile stays. A host deleted mid-carry is never a parent again (nor its face, roof
46
+ * or surface): the item stays on its live parent, else the level. Returns its parent after.
47
+ */
48
+ function restoreOriginalState(
49
+ id: AnyNodeId,
50
+ original: OriginalState,
51
+ draftParentId: string | null | undefined,
52
+ ): string | null {
53
+ const nodes = useScene.getState().nodes
54
+ const exists = (parentId: string | null | undefined): parentId is string =>
55
+ Boolean(parentId && nodes[parentId as AnyNodeId])
56
+ const hostGone = original.parentId != null && !exists(original.parentId)
57
+ const updates = sceneHistoryDraftRevertUpdates([id])
58
+ const own = updates.find((update) => update.id === id)
59
+ if (hostGone && own) {
60
+ for (const key of ['roofSegmentId', 'roofFace', 'blockFaceId']) delete own.data[key]
61
+ }
62
+ const liveParentId = nodes[id]?.parentId
63
+ if (!exists(liveParentId)) {
64
+ const fallback = [draftParentId, useViewer.getState().selection.levelId].find(exists)
65
+ if (fallback) {
66
+ if (own) own.data.parentId = fallback
67
+ else updates.push({ id, data: { parentId: fallback } })
68
+ }
69
+ }
70
+ if (updates.length > 0) {
71
+ pausedDraftWrite(() => useScene.getState().updateNodes(updates as never))
72
+ }
73
+ return useScene.getState().nodes[id]?.parentId ?? null
74
+ }
75
+
27
76
  interface OriginalState {
28
77
  surfaceId: string | null
29
78
  position: [number, number, number]
@@ -38,6 +87,9 @@ interface OriginalState {
38
87
  metadata: ItemNode['metadata']
39
88
  }
40
89
 
90
+ /** What a duplicate keeps from the item it copies. */
91
+ export type ItemCopy = Pick<ItemNode, 'slots' | 'collectionIds'>
92
+
41
93
  export interface DraftNodeHandle {
42
94
  updateSurface: (data: Partial<ItemNode>, surfaceId: string | null) => void
43
95
  /** Current draft item, or null */
@@ -45,17 +97,19 @@ export interface DraftNodeHandle {
45
97
  /** Whether the current draft was adopted (move mode) vs created (create mode) */
46
98
  readonly isAdopted: boolean
47
99
  /** Create a new draft item at the given position. Returns the created node or null.
48
- * `slots` seeds painted slot overrides so duplicates keep their materials. */
100
+ * `copied` is the item a duplicate copies: it keeps its painted slots and collections. */
49
101
  create: (
50
102
  gridPosition: Vector3,
51
103
  asset: AssetInput,
52
104
  rotation?: [number, number, number],
53
105
  scale?: [number, number, number],
54
- slots?: ItemNode['slots'],
106
+ copied?: ItemCopy,
55
107
  ) => ItemNode | null
56
108
  /** Take ownership of an existing scene node as the draft (for move mode). */
57
109
  adopt: (node: ItemNode) => void
58
110
  /** Commit the current draft. Create mode: delete+recreate. Move mode: update in place.
111
+ * The final write is the one undo step, recorded only while no owner pauses history:
112
+ * a caller holding a pause lifts it around this call (`SceneHistoryPauseSession.commitStep`).
59
113
  * `supportElevationCap` (floor commits) is the pointer-decided surface
60
114
  * elevation — it caps the persisted `supportSlabId` election so the
61
115
  * commit lands on the surface the cursor pointed at. */
@@ -74,7 +128,11 @@ export interface DraftNodeHandle {
74
128
 
75
129
  /**
76
130
  * Hook that manages the lifecycle of a transient (draft) item node.
77
- * Handles temporal pause/resume for undo/redo isolation.
131
+ * The draft is registered with core's history drafts from create/adopt until
132
+ * commit/destroy, so history records it as absent (created) or as it was
133
+ * (adopted) and no draft write becomes an undo step; the draft's own writes
134
+ * also run under a short balanced pause. An adopted `commit` suspends the registration for
135
+ * its tracked write and ends it only after success, so a rejected drop can still be retried.
78
136
  *
79
137
  * Supports two modes:
80
138
  * - Create mode (via `create()`): draft is a new transient node. Commit = delete+recreate (undo removes node).
@@ -85,6 +143,7 @@ export function useDraftNode(): DraftNodeHandle {
85
143
  const adoptedRef = useRef(false)
86
144
  const ownsSubtreeRef = useRef(false)
87
145
  const originalStateRef = useRef<OriginalState | null>(null)
146
+ const endHistoryDraftRef = useRef<(() => void) | null>(null)
88
147
 
89
148
  const create = useCallback(
90
149
  (
@@ -92,7 +151,7 @@ export function useDraftNode(): DraftNodeHandle {
92
151
  asset: AssetInput,
93
152
  rotation?: [number, number, number],
94
153
  scale?: [number, number, number],
95
- slots?: ItemNode['slots'],
154
+ copied?: ItemCopy,
96
155
  ): ItemNode | null => {
97
156
  const currentLevelId = useViewer.getState().selection.levelId
98
157
  if (!currentLevelId) return null
@@ -105,10 +164,24 @@ export function useDraftNode(): DraftNodeHandle {
105
164
  asset,
106
165
  parentId: currentLevelId,
107
166
  metadata: { isTransient: true },
108
- ...(slots ? { slots } : {}),
167
+ ...(copied?.slots ? { slots: copied.slots } : {}),
168
+ ...(copied?.collectionIds ? { collectionIds: copied.collectionIds } : {}),
109
169
  })
110
170
 
111
- useScene.getState().createNode(node, currentLevelId)
171
+ releaseHistoryDraft(endHistoryDraftRef)
172
+ endHistoryDraftRef.current = beginSceneHistoryDraft(node.id, null)
173
+ try {
174
+ pausedDraftWrite(() => useScene.getState().createNode(node, currentLevelId))
175
+ } catch (error) {
176
+ try {
177
+ if (useScene.getState().nodes[node.id]) {
178
+ pausedDraftWrite(() => useScene.getState().deleteNode(node.id))
179
+ }
180
+ } finally {
181
+ releaseHistoryDraft(endHistoryDraftRef)
182
+ }
183
+ throw error
184
+ }
112
185
  usePlacementPreview
113
186
  .getState()
114
187
  .set(node, useScene.getState().nodes[currentLevelId as AnyNodeId] ?? null)
@@ -122,6 +195,11 @@ export function useDraftNode(): DraftNodeHandle {
122
195
  )
123
196
 
124
197
  const adopt = useCallback((node: ItemNode): void => {
198
+ releaseHistoryDraft(endHistoryDraftRef)
199
+ endHistoryDraftRef.current = beginSceneHistoryDraft(
200
+ node.id,
201
+ isFreshPlacementMetadata(node.metadata) ? null : node,
202
+ )
125
203
  ownsSubtreeRef.current =
126
204
  useInteractionScope.getState().adoptSubtree(node.id) || isInteractionSubtreeDraft(node.id)
127
205
  // Save original state so destroy() can restore it
@@ -149,9 +227,11 @@ export function useDraftNode(): DraftNodeHandle {
149
227
  adoptedRef.current = true
150
228
 
151
229
  // Mark as transient so it renders as a draft
152
- useScene.getState().updateNode(node.id, {
153
- metadata: { ...meta, isTransient: true },
154
- })
230
+ pausedDraftWrite(() =>
231
+ useScene.getState().updateNode(node.id, {
232
+ metadata: { ...meta, isTransient: true },
233
+ }),
234
+ )
155
235
  usePlacementPreview
156
236
  .getState()
157
237
  .set(
@@ -172,6 +252,10 @@ export function useDraftNode(): DraftNodeHandle {
172
252
  ): string | null => {
173
253
  const draft = draftRef.current
174
254
  if (!draft) return null
255
+ if (finalUpdate.parentId && !useScene.getState().nodes[finalUpdate.parentId as AnyNodeId]) {
256
+ options?.onReject?.('no-surface')
257
+ return null
258
+ }
175
259
 
176
260
  const surfaceId = surfaceAttachmentId(useScene.getState().nodes[draft.id] ?? draft)
177
261
  const stored = surfaceFramePose(
@@ -181,6 +265,10 @@ export function useDraftNode(): DraftNodeHandle {
181
265
  true,
182
266
  )
183
267
  finalUpdate = { ...finalUpdate, ...stored }
268
+ // The drop ends the carry on the item's live metadata: placement strategies hand in the
269
+ // adoption-time snapshot, which would drop anything an agent wrote meanwhile.
270
+ const liveDraft = useScene.getState().nodes[draft.id]
271
+ if (liveDraft) finalUpdate.metadata = stripTransient(liveDraft.metadata)
184
272
  if (isFreshPlacementMetadata(originalStateRef.current?.metadata)) {
185
273
  const effectiveNode = ItemNode.parse({ ...draft, ...finalUpdate })
186
274
  const id = commitFreshPlacementSubtree(
@@ -196,6 +284,7 @@ export function useDraftNode(): DraftNodeHandle {
196
284
  options?.onReject,
197
285
  )
198
286
  if (!id) return null
287
+ releaseHistoryDraft(endHistoryDraftRef)
199
288
  if (usePlacementPreview.getState().node?.id === draft.id) {
200
289
  usePlacementPreview.getState().clear()
201
290
  }
@@ -207,31 +296,15 @@ export function useDraftNode(): DraftNodeHandle {
207
296
  if (adoptedRef.current) {
208
297
  // Move mode: update in place (single undoable action)
209
298
  const { parentId: newParentId, ...updateProps } = finalUpdate
210
- const parentId =
211
- newParentId ??
212
- originalStateRef.current?.parentId ??
213
- useViewer.getState().selection.levelId
214
299
  const original = originalStateRef.current!
300
+ const nodesNow = useScene.getState().nodes
301
+ const restoredParentId = restoreOriginalState(draft.id, original, draft.parentId)
302
+ const parentId =
303
+ [newParentId, restoredParentId].find((id) => id && nodesNow[id as AnyNodeId]) ??
304
+ restoredParentId
215
305
 
216
- // Restore original state while paused — so the undo baseline is clean
217
- updateSurfaceNode(
218
- draft.id,
219
- {
220
- position: original.position,
221
- rotation: original.rotation,
222
- side: original.side,
223
- parentId: original.parentId,
224
- roofSegmentId: original.roofSegmentId,
225
- roofFace: original.roofFace,
226
- blockFaceId: original.blockFaceId,
227
- metadata: original.metadata,
228
- },
229
- original.surfaceId,
230
- )
231
-
232
- // Resume → tracked update (undo reverts to original)
233
- useScene.temporal.getState().resume()
234
-
306
+ // The original is restored above (a carry write), so the one tracked write below has
307
+ // the true baseline as its undo state.
235
308
  const effectiveNode = ItemNode.parse({
236
309
  ...draft,
237
310
  ...updateProps,
@@ -239,34 +312,35 @@ export function useDraftNode(): DraftNodeHandle {
239
312
  metadata: updateProps.metadata ?? stripTransient(draft.metadata),
240
313
  })
241
314
 
242
- updateSurfaceNode(
243
- draft.id,
244
- {
245
- position: updateProps.position ?? draft.position,
246
- rotation: updateProps.rotation ?? draft.rotation,
247
- side: updateProps.side ?? draft.side,
248
- metadata: updateProps.metadata ?? stripTransient(draft.metadata),
249
- parentId: parentId as string,
250
- // Forward the roof host explicitly: strategies set it on every
251
- // commit (segment id on a roof face, undefined elsewhere), and
252
- // dropping it here strands the item in the roof frame without
253
- // the segment transform.
254
- roofSegmentId: updateProps.roofSegmentId,
255
- roofFace: updateProps.roofFace,
256
- blockFaceId: updateProps.blockFaceId,
257
- // Only when the strategy decided about wallId (roof commits clear
258
- // it) — floor/ceiling commits never managed the field.
259
- ...('wallId' in updateProps ? { wallId: updateProps.wallId } : {}),
260
- ...resolveSupportSlabPatch(effectiveNode, useScene.getState().nodes, {
261
- maxElevation: options?.supportElevationCap,
262
- preferredSlabId: options?.preferredSupportSlabId,
263
- pinSupport: options?.pinSupport,
264
- }),
265
- },
266
- surfaceId,
267
- )
268
-
269
- useScene.temporal.getState().pause()
315
+ withSceneHistoryDraftSuspended(draft.id, () => {
316
+ updateSurfaceNode(
317
+ draft.id,
318
+ {
319
+ position: updateProps.position ?? draft.position,
320
+ rotation: updateProps.rotation ?? draft.rotation,
321
+ side: updateProps.side ?? draft.side,
322
+ metadata: updateProps.metadata ?? stripTransient(draft.metadata),
323
+ parentId: parentId as string,
324
+ // Forward the roof host explicitly: strategies set it on every
325
+ // commit (segment id on a roof face, undefined elsewhere), and
326
+ // dropping it here strands the item in the roof frame without
327
+ // the segment transform.
328
+ roofSegmentId: updateProps.roofSegmentId,
329
+ roofFace: updateProps.roofFace,
330
+ blockFaceId: updateProps.blockFaceId,
331
+ // Only when the strategy decided about wallId (roof commits clear
332
+ // it) — floor/ceiling commits never managed the field.
333
+ ...('wallId' in updateProps ? { wallId: updateProps.wallId } : {}),
334
+ ...resolveSupportSlabPatch(effectiveNode, useScene.getState().nodes, {
335
+ maxElevation: options?.supportElevationCap,
336
+ preferredSlabId: options?.preferredSupportSlabId,
337
+ pinSupport: options?.pinSupport,
338
+ }),
339
+ },
340
+ surfaceId,
341
+ )
342
+ })
343
+ releaseHistoryDraft(endHistoryDraftRef)
270
344
 
271
345
  const id = draft.id
272
346
  if (usePlacementPreview.getState().node?.id === id) {
@@ -278,20 +352,19 @@ export function useDraftNode(): DraftNodeHandle {
278
352
  return id
279
353
  }
280
354
 
281
- // Create mode: delete draft (paused), resume, create fresh node (tracked), re-pause
355
+ // Create mode: delete the draft (paused), then create the fresh node (the tracked write)
282
356
  const { parentId: newParentId, ...updateProps } = finalUpdate
283
357
  const parentId = (newParentId ?? useViewer.getState().selection.levelId) as AnyNodeId
284
358
  if (!parentId) return null
285
359
 
286
360
  beginPerfAction('place:item', draft.id)
287
- // Delete draft while paused (invisible to undo)
288
- updateSurfaceNode(draft.id, {}, null)
289
- useScene.getState().deleteNode(draft.id)
361
+ pausedDraftWrite(() => {
362
+ updateSurfaceNode(draft.id, {}, null)
363
+ useScene.getState().deleteNode(draft.id)
364
+ })
365
+ releaseHistoryDraft(endHistoryDraftRef)
290
366
  draftRef.current = null
291
367
 
292
- // Briefly resume → create fresh node (the single undoable action)
293
- useScene.temporal.getState().resume()
294
-
295
368
  const finalNode = ItemNode.parse({
296
369
  name: draft.name,
297
370
  asset: draft.asset,
@@ -299,8 +372,9 @@ export function useDraftNode(): DraftNodeHandle {
299
372
  rotation: updateProps.rotation ?? draft.rotation,
300
373
  scale: updateProps.scale ?? draft.scale,
301
374
  side: updateProps.side ?? draft.side,
302
- // Carry painted slot overrides so a duplicated item keeps its materials.
375
+ // A duplicated item keeps its painted slots and its collections.
303
376
  ...(draft.slots ? { slots: draft.slots } : {}),
377
+ ...(draft.collectionIds ? { collectionIds: draft.collectionIds } : {}),
304
378
  // Roof host — see the move-mode commit above for why this must be
305
379
  // forwarded explicitly.
306
380
  roofSegmentId: updateProps.roofSegmentId,
@@ -331,9 +405,6 @@ export function useDraftNode(): DraftNodeHandle {
331
405
  usePlacementPreview.getState().clear()
332
406
  }
333
407
 
334
- // Re-pause for next draft cycle
335
- useScene.temporal.getState().pause()
336
-
337
408
  adoptedRef.current = false
338
409
  originalStateRef.current = null
339
410
  commitPerfAction()
@@ -347,6 +418,7 @@ export function useDraftNode(): DraftNodeHandle {
347
418
 
348
419
  const draftId = draftRef.current.id
349
420
  if (ownsSubtreeRef.current) {
421
+ releaseHistoryDraft(endHistoryDraftRef)
350
422
  draftRef.current = null
351
423
  adoptedRef.current = false
352
424
  originalStateRef.current = null
@@ -358,7 +430,8 @@ export function useDraftNode(): DraftNodeHandle {
358
430
  // `useScene.updateNodes` before unmounting the legacy mover, and
359
431
  // an unconditional restore here would wipe that commit. By
360
432
  // comparing the live state to the snapshot we took in `adopt()`,
361
- // we let an external committer's write stick.
433
+ // we let an external committer's write stick. A preview after a rejected drop may
434
+ // lack the transient marker, but its still-owned position must be restored on cancel.
362
435
  const original = originalStateRef.current
363
436
  const id = draftRef.current.id
364
437
  const live = useScene.getState().nodes[id as AnyNodeId] as ItemNode | undefined
@@ -366,10 +439,14 @@ export function useDraftNode(): DraftNodeHandle {
366
439
  const externallyMoved =
367
440
  !live?.metadata?.isTransient &&
368
441
  !!livePosition &&
442
+ !sceneHistoryDraftRevertUpdates([id]).some(
443
+ (update) => update.id === id && 'position' in update.data,
444
+ ) &&
369
445
  (livePosition[0] !== original.position[0] ||
370
446
  livePosition[1] !== original.position[1] ||
371
447
  livePosition[2] !== original.position[2])
372
448
  if (externallyMoved) {
449
+ releaseHistoryDraft(endHistoryDraftRef)
373
450
  draftRef.current = null
374
451
  adoptedRef.current = false
375
452
  originalStateRef.current = null
@@ -379,36 +456,28 @@ export function useDraftNode(): DraftNodeHandle {
379
456
  return
380
457
  }
381
458
 
382
- updateSurfaceNode(
383
- id,
384
- {
385
- position: original.position,
386
- rotation: original.rotation,
387
- side: original.side,
388
- parentId: original.parentId,
389
- roofSegmentId: original.roofSegmentId,
390
- roofFace: original.roofFace,
391
- blockFaceId: original.blockFaceId,
392
- metadata: original.metadata,
393
- },
394
- original.surfaceId,
395
- )
459
+ restoreOriginalState(id, original, draftRef.current.parentId)
396
460
 
397
461
  // Also reset the Three.js mesh directly — the store update triggers a React
398
462
  // re-render but the mesh position was mutated by useFrame and may not reset
399
463
  // until the next render cycle, leaving a visual glitch.
400
464
  const mesh = sceneRegistry.nodes.get(id as AnyNodeId)
401
- if (mesh) {
402
- mesh.position.set(original.position[0], original.position[1], original.position[2])
403
- mesh.rotation.y = original.rotation[1] ?? 0
465
+ const restored = useScene.getState().nodes[id as AnyNodeId] as ItemNode | undefined
466
+ if (mesh && restored) {
467
+ mesh.position.set(restored.position[0], restored.position[1], restored.position[2])
468
+ mesh.rotation.y = restored.rotation[1] ?? 0
404
469
  mesh.visible = true
405
470
  }
406
471
  } else {
407
472
  // Create mode: delete the transient node
408
- updateSurfaceNode(draftRef.current.id, {}, null)
409
- useScene.getState().deleteNode(draftRef.current.id)
473
+ const id = draftRef.current.id
474
+ pausedDraftWrite(() => {
475
+ updateSurfaceNode(id, {}, null)
476
+ useScene.getState().deleteNode(id)
477
+ })
410
478
  }
411
479
 
480
+ releaseHistoryDraft(endHistoryDraftRef)
412
481
  draftRef.current = null
413
482
  adoptedRef.current = false
414
483
  originalStateRef.current = null
@@ -421,10 +490,12 @@ export function useDraftNode(): DraftNodeHandle {
421
490
  const draft = draftRef.current
422
491
  if (!draft) return
423
492
  const pose = { ...draft, ...data }
424
- updateSurfaceNode(
425
- draft.id,
426
- { ...data, ...surfaceFramePose(pose.parentId, surfaceId, pose, true) },
427
- surfaceId,
493
+ pausedDraftWrite(() =>
494
+ updateSurfaceNode(
495
+ draft.id,
496
+ { ...data, ...surfaceFramePose(pose.parentId, surfaceId, pose, true) },
497
+ surfaceId,
498
+ ),
428
499
  )
429
500
  }, [])
430
501