@pascal-app/editor 1.0.1 → 1.0.4

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