@pascal-app/editor 0.8.0 → 0.9.2

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 (350) hide show
  1. package/package.json +15 -13
  2. package/src/components/editor/alignment-3d-guide-layer.tsx +168 -0
  3. package/src/components/editor/bake-exporter.tsx +47 -0
  4. package/src/components/editor/custom-camera-controls.tsx +795 -28
  5. package/src/components/editor/editor-layout-mobile.tsx +8 -3
  6. package/src/components/editor/editor-layout-v2.tsx +76 -51
  7. package/src/components/editor/export-manager.tsx +69 -32
  8. package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
  9. package/src/components/editor/first-person/build-collider-world.test.ts +182 -0
  10. package/src/components/editor/first-person/build-collider-world.ts +228 -83
  11. package/src/components/editor/first-person-controls.tsx +943 -27
  12. package/src/components/editor/floating-action-menu.tsx +676 -208
  13. package/src/components/editor/floating-building-action-menu.tsx +2 -2
  14. package/src/components/editor/floorplan-background-selection.test.ts +58 -0
  15. package/src/components/editor/floorplan-background-selection.ts +3 -2
  16. package/src/components/editor/floorplan-panel.tsx +6916 -12137
  17. package/src/components/editor/grid.tsx +197 -36
  18. package/src/components/editor/group-actions.ts +457 -0
  19. package/src/components/editor/group-floating-action-menu.tsx +130 -0
  20. package/src/components/editor/group-move-3d.ts +397 -0
  21. package/src/components/editor/group-rotate-handle.tsx +420 -0
  22. package/src/components/editor/group-selection-box-3d.tsx +173 -0
  23. package/src/components/editor/group-transform-shared.test.ts +396 -0
  24. package/src/components/editor/group-transform-shared.ts +489 -0
  25. package/src/components/editor/handles/handle-arrow.tsx +581 -0
  26. package/src/components/editor/handles/use-handle-drag.ts +246 -0
  27. package/src/components/editor/index.tsx +412 -162
  28. package/src/components/editor/measurement-pill.tsx +115 -0
  29. package/src/components/editor/node-action-menu.tsx +14 -1
  30. package/src/components/editor/node-arrow-handles.tsx +1646 -0
  31. package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
  32. package/src/components/editor/quick-measurement-card.tsx +70 -0
  33. package/src/components/editor/quick-measurement-hud.tsx +28 -0
  34. package/src/components/editor/riser-diagram-panel.tsx +137 -0
  35. package/src/components/editor/selection-manager.tsx +1106 -483
  36. package/src/components/editor/site-edge-labels.tsx +45 -21
  37. package/src/components/editor/slab-hole-highlights.tsx +474 -0
  38. package/src/components/editor/snapshot-capture-overlay.tsx +369 -155
  39. package/src/components/editor/three-context-bridge.ts +22 -0
  40. package/src/components/editor/thumbnail-generator.tsx +140 -41
  41. package/src/components/editor/use-floorplan-background-placement.ts +190 -35
  42. package/src/components/editor/use-floorplan-hit-testing.ts +24 -0
  43. package/src/components/editor/use-floorplan-scene-data.ts +19 -7
  44. package/src/components/editor/use-mesh-settle-epoch.ts +26 -0
  45. package/src/components/editor/wall-measurement-label.tsx +14 -19
  46. package/src/components/editor/wall-move-side-handles.tsx +870 -0
  47. package/src/components/editor/wall-opening-highlights.tsx +181 -0
  48. package/src/components/editor/wall-snap-beacon-layer.tsx +304 -0
  49. package/src/components/editor-2d/floorplan-action-menu-layer.tsx +15 -6
  50. package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +142 -0
  51. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +5 -3
  52. package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
  53. package/src/components/editor-2d/floorplan-group-move.tsx +701 -0
  54. package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
  55. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1656 -0
  56. package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
  57. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +392 -0
  58. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +857 -0
  59. package/src/components/editor-2d/floorplan-render-context.tsx +62 -0
  60. package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +77 -0
  61. package/src/components/editor-2d/floorplan-wall-move-ghost-layer.tsx +47 -0
  62. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +266 -0
  63. package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +15 -0
  64. package/src/components/editor-2d/renderers/floorplan-label-angle.ts +14 -0
  65. package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -66
  66. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +62 -0
  67. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +308 -0
  68. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +3325 -0
  69. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +24 -9
  70. package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
  71. package/src/components/editor-2d/svg-paths.ts +10 -2
  72. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +439 -57
  73. package/src/components/systems/ceiling/ceiling-system.tsx +109 -12
  74. package/src/components/systems/roof/roof-edit-system.tsx +1851 -22
  75. package/src/components/systems/selection-affordance-manager.tsx +38 -0
  76. package/src/components/systems/stair/stair-edit-system.tsx +1 -1
  77. package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
  78. package/src/components/systems/zone/zone-system.tsx +42 -13
  79. package/src/components/tools/elevator/elevator-defaults.ts +8 -0
  80. package/src/components/tools/elevator/elevator-tool.tsx +375 -0
  81. package/src/components/tools/elevator/move-elevator-tool.tsx +263 -0
  82. package/src/components/tools/fence/fence-drafting.ts +141 -27
  83. package/src/components/tools/item/move-tool.tsx +45 -105
  84. package/src/components/tools/item/placement-math.test.ts +22 -0
  85. package/src/components/tools/item/placement-math.ts +59 -7
  86. package/src/components/tools/item/placement-strategies.ts +432 -16
  87. package/src/components/tools/item/placement-types.ts +22 -1
  88. package/src/components/tools/item/use-draft-node.ts +51 -2
  89. package/src/components/tools/item/use-placement-coordinator.tsx +1399 -256
  90. package/src/components/tools/registry/move-registry-node-tool.tsx +1064 -0
  91. package/src/components/tools/roof/roof-tool.tsx +350 -28
  92. package/src/components/tools/select/box-select-state.ts +88 -0
  93. package/src/components/tools/select/box-select-tool.tsx +442 -520
  94. package/src/components/tools/select/marquee-geometry.test.ts +161 -0
  95. package/src/components/tools/select/marquee-geometry.ts +155 -0
  96. package/src/components/tools/select/plane-box-select-tool.tsx +570 -0
  97. package/src/components/tools/select/screen-rectangle-selection.ts +84 -0
  98. package/src/components/tools/select/select-candidates.test.ts +109 -0
  99. package/src/components/tools/select/select-candidates.ts +85 -0
  100. package/src/components/tools/shared/affordance-dispatch.ts +30 -0
  101. package/src/components/tools/shared/cursor-sphere.tsx +62 -26
  102. package/src/components/tools/shared/drag-bounding-box.tsx +195 -0
  103. package/src/components/tools/shared/facing-indicator.tsx +87 -0
  104. package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
  105. package/src/components/tools/shared/floor-stack-preview.ts +25 -0
  106. package/src/components/tools/shared/fresh-placement-visibility.ts +61 -0
  107. package/src/components/tools/shared/placement-box-geometry.ts +123 -0
  108. package/src/components/tools/shared/placement-box.tsx +305 -0
  109. package/src/components/tools/shared/polygon-editor.tsx +866 -175
  110. package/src/components/tools/shared/segment-angle.ts +56 -0
  111. package/src/components/tools/site/site-boundary-editor.tsx +520 -16
  112. package/src/components/tools/site/site-flag-model.ts +18 -0
  113. package/src/components/tools/stair/stair-defaults.ts +2 -1
  114. package/src/components/tools/stair/stair-tool.tsx +342 -37
  115. package/src/components/tools/tool-manager.tsx +239 -62
  116. package/src/components/tools/wall/wall-drafting.test.ts +330 -0
  117. package/src/components/tools/wall/wall-drafting.ts +248 -171
  118. package/src/components/tools/wall/wall-snap-geometry.test.ts +123 -0
  119. package/src/components/tools/wall/wall-snap-geometry.ts +331 -0
  120. package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
  121. package/src/components/tools/zone/zone-tool.tsx +86 -82
  122. package/src/components/ui/action-menu/action-button.tsx +21 -1
  123. package/src/components/ui/action-menu/camera-actions.tsx +24 -17
  124. package/src/components/ui/action-menu/control-modes.tsx +40 -170
  125. package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
  126. package/src/components/ui/action-menu/index.tsx +3 -117
  127. package/src/components/ui/action-menu/measurement-control.tsx +188 -0
  128. package/src/components/ui/action-menu/structure-tools.tsx +27 -93
  129. package/src/components/ui/action-menu/view-toggles.tsx +279 -90
  130. package/src/components/ui/command-palette/editor-commands.tsx +20 -15
  131. package/src/components/ui/command-palette/index.tsx +5 -3
  132. package/src/components/ui/controls/material-paint-panel.tsx +152 -0
  133. package/src/components/ui/controls/material-picker.tsx +87 -177
  134. package/src/components/ui/controls/material-properties-editor.tsx +108 -0
  135. package/src/components/ui/controls/metric-control.tsx +142 -46
  136. package/src/components/ui/controls/scene-material-list.tsx +247 -0
  137. package/src/components/ui/controls/slider-control.tsx +88 -38
  138. package/src/components/ui/floating-level-selector.tsx +47 -10
  139. package/src/components/ui/helpers/building-helper.tsx +10 -23
  140. package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
  141. package/src/components/ui/helpers/helper-manager.tsx +235 -23
  142. package/src/components/ui/helpers/item-helper.tsx +28 -32
  143. package/src/components/ui/helpers/registered-tool-helper.tsx +59 -0
  144. package/src/components/ui/helpers/roof-helper.tsx +10 -12
  145. package/src/components/ui/icon-ref.tsx +47 -0
  146. package/src/components/ui/item-catalog/catalog-items.tsx +40 -1
  147. package/src/components/ui/item-catalog/item-catalog.tsx +16 -36
  148. package/src/components/ui/level-duplicate-dialog.tsx +2 -1
  149. package/src/components/ui/panels/node-display.ts +17 -16
  150. package/src/components/ui/panels/panel-manager.tsx +55 -58
  151. package/src/components/ui/panels/panel-wrapper.tsx +238 -12
  152. package/src/components/ui/panels/parametric-inspector.tsx +482 -0
  153. package/src/components/ui/panels/reference-panel.tsx +54 -19
  154. package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
  155. package/src/components/ui/primitives/shortcut-token.tsx +39 -3
  156. package/src/components/ui/primitives/sidebar.tsx +1 -0
  157. package/src/components/ui/scene-loader.tsx +1 -3
  158. package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
  159. package/src/components/ui/sidebar/icon-rail.tsx +50 -31
  160. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +263 -0
  161. package/src/components/ui/sidebar/panels/items-panel/index.tsx +82 -8
  162. package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
  163. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +122 -41
  164. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +66 -7
  165. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +267 -0
  166. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -1
  167. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +9 -5
  168. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +86 -0
  169. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
  170. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
  171. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +92 -0
  172. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +75 -0
  173. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +3 -3
  174. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +86 -0
  175. package/src/components/ui/sidebar/panels/site-panel/index.tsx +42 -21
  176. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
  177. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -1
  178. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +140 -0
  179. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +48 -8
  180. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +132 -0
  181. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +9 -5
  182. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +86 -0
  183. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +7 -1
  184. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
  185. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +112 -39
  186. package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
  187. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
  188. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
  189. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
  190. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +51 -2
  191. package/src/components/ui/sidebar/tab-bar.tsx +75 -1
  192. package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
  193. package/src/components/ui/snap-target-badge.test.tsx +40 -0
  194. package/src/components/ui/snap-target-badge.tsx +88 -0
  195. package/src/components/viewer-overlay.tsx +253 -61
  196. package/src/components/viewer-zone-system.tsx +7 -2
  197. package/src/hooks/use-auto-save.test.ts +14 -0
  198. package/src/hooks/use-auto-save.ts +64 -10
  199. package/src/hooks/use-ceiling-events.ts +176 -0
  200. package/src/hooks/use-drag-action.ts +127 -0
  201. package/src/hooks/use-keyboard.ts +493 -95
  202. package/src/hooks/use-selection.ts +64 -0
  203. package/src/index.tsx +451 -6
  204. package/src/lib/active-placement-surface.ts +35 -0
  205. package/src/lib/ceiling-plan-snap.ts +24 -0
  206. package/src/lib/constants.ts +6 -2
  207. package/src/lib/contextual-help.test.ts +112 -0
  208. package/src/lib/contextual-help.ts +144 -0
  209. package/src/lib/continuation.ts +64 -0
  210. package/src/lib/direct-manipulation.test.ts +192 -0
  211. package/src/lib/direct-manipulation.ts +109 -0
  212. package/src/lib/editor-api.ts +50 -0
  213. package/src/lib/elevator-support.ts +96 -0
  214. package/src/lib/floorplan/apply-alignment.test.ts +25 -0
  215. package/src/lib/floorplan/apply-alignment.ts +117 -0
  216. package/src/lib/floorplan/floorplan-export.tsx +364 -0
  217. package/src/lib/floorplan/geometry.ts +53 -0
  218. package/src/lib/floorplan/index.ts +10 -0
  219. package/src/lib/floorplan/items.ts +5 -2
  220. package/src/lib/floorplan/plan-coords.ts +21 -0
  221. package/src/lib/floorplan/selection-tool.ts +40 -0
  222. package/src/lib/floorplan/stairs.ts +12 -1
  223. package/src/lib/fresh-planar-placement.test.ts +339 -0
  224. package/src/lib/fresh-planar-placement.ts +130 -0
  225. package/src/lib/glb-export.test.ts +441 -0
  226. package/src/lib/glb-export.ts +874 -0
  227. package/src/lib/history.ts +11 -1
  228. package/src/lib/interaction/hot-set.test.ts +133 -0
  229. package/src/lib/interaction/hot-set.ts +67 -0
  230. package/src/lib/interaction/overlay-policy.test.ts +51 -0
  231. package/src/lib/interaction/overlay-policy.ts +59 -0
  232. package/src/lib/interaction/scope.ts +211 -0
  233. package/src/lib/level-duplication.ts +54 -42
  234. package/src/lib/level-selection.ts +2 -2
  235. package/src/lib/material-paint.ts +227 -48
  236. package/src/lib/measurement-kind.test.ts +30 -0
  237. package/src/lib/measurement-kind.ts +19 -0
  238. package/src/lib/measurement-label.test.ts +47 -0
  239. package/src/lib/measurement-label.ts +68 -0
  240. package/src/lib/measurement-parser.ts +116 -0
  241. package/src/lib/measurements.test.ts +183 -0
  242. package/src/lib/measurements.ts +199 -0
  243. package/src/lib/paint-scope.test.ts +454 -0
  244. package/src/lib/paint-scope.ts +461 -0
  245. package/src/lib/placement-drag-release.ts +23 -0
  246. package/src/lib/placement-metadata.ts +29 -0
  247. package/src/lib/planar-cursor-placement.test.ts +100 -0
  248. package/src/lib/planar-cursor-placement.ts +42 -0
  249. package/src/lib/plugin-panels.test.ts +19 -0
  250. package/src/lib/plugin-panels.ts +91 -0
  251. package/src/lib/quick-measurement.test.ts +77 -0
  252. package/src/lib/quick-measurement.ts +109 -0
  253. package/src/lib/roof-duplication.ts +7 -7
  254. package/src/lib/roof-hover-outline-proxy.ts +22 -0
  255. package/src/lib/roof-wall-hit.ts +164 -0
  256. package/src/lib/scene-clipboard.test.ts +196 -0
  257. package/src/lib/scene-clipboard.ts +332 -0
  258. package/src/lib/scene.ts +40 -8
  259. package/src/lib/selection-routing.test.ts +298 -0
  260. package/src/lib/selection-routing.ts +175 -0
  261. package/src/lib/sfx/index.ts +1 -0
  262. package/src/lib/sfx/movement-tick.test.ts +65 -0
  263. package/src/lib/sfx/movement-tick.ts +18 -0
  264. package/src/lib/sfx-bus.ts +69 -10
  265. package/src/lib/sfx-player.test.ts +124 -0
  266. package/src/lib/sfx-player.ts +148 -55
  267. package/src/lib/site-boundary.ts +1 -0
  268. package/src/lib/slab-plan-snap.test.ts +93 -0
  269. package/src/lib/slab-plan-snap.ts +108 -0
  270. package/src/lib/snapping-mode.test.ts +138 -0
  271. package/src/lib/snapping-mode.ts +179 -0
  272. package/src/lib/stair-duplication.ts +4 -4
  273. package/src/lib/stair-levels.test.ts +146 -0
  274. package/src/lib/stair-levels.ts +177 -0
  275. package/src/lib/surface-plan-snap.test.ts +71 -0
  276. package/src/lib/surface-plan-snap.ts +256 -0
  277. package/src/lib/use-linear-display.ts +40 -0
  278. package/src/lib/world-grid-snap.ts +231 -0
  279. package/src/lib/zone-content.ts +130 -0
  280. package/src/store/use-alignment-guides.ts +21 -0
  281. package/src/store/use-direct-manipulation-feedback.ts +20 -0
  282. package/src/store/use-editor.tsx +767 -114
  283. package/src/store/use-facing-pose.ts +44 -0
  284. package/src/store/use-fence-curve-draft.ts +21 -0
  285. package/src/store/use-floorplan-draft-preview.ts +101 -0
  286. package/src/store/use-floorplan-marquee.ts +50 -0
  287. package/src/store/use-interaction-scope.test.ts +186 -0
  288. package/src/store/use-interaction-scope.ts +132 -0
  289. package/src/store/use-measurement-draft.test.ts +530 -0
  290. package/src/store/use-measurement-draft.ts +543 -0
  291. package/src/store/use-opening-guides.ts +41 -0
  292. package/src/store/use-placement-preview.ts +39 -0
  293. package/src/store/use-quick-measurement-hud.test.ts +53 -0
  294. package/src/store/use-quick-measurement-hud.ts +77 -0
  295. package/src/store/use-segment-draft-chain.ts +28 -0
  296. package/src/store/use-stair-build-preview.ts +43 -0
  297. package/src/store/use-wall-move-ghosts.ts +36 -0
  298. package/src/store/use-wall-snap-indicator.ts +34 -0
  299. package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
  300. package/src/components/editor/preset-thumbnail-generator.tsx +0 -125
  301. package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +0 -113
  302. package/src/components/tools/building/move-building-tool.tsx +0 -157
  303. package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +0 -43
  304. package/src/components/tools/ceiling/ceiling-hole-editor.tsx +0 -49
  305. package/src/components/tools/ceiling/ceiling-tool.tsx +0 -465
  306. package/src/components/tools/ceiling/move-ceiling-tool.tsx +0 -264
  307. package/src/components/tools/column/column-tool.tsx +0 -97
  308. package/src/components/tools/column/move-column-tool.tsx +0 -105
  309. package/src/components/tools/door/door-math.ts +0 -110
  310. package/src/components/tools/door/door-tool.tsx +0 -324
  311. package/src/components/tools/door/move-door-tool.tsx +0 -412
  312. package/src/components/tools/fence/curve-fence-tool.tsx +0 -178
  313. package/src/components/tools/fence/fence-tool.tsx +0 -346
  314. package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -441
  315. package/src/components/tools/fence/move-fence-tool.tsx +0 -302
  316. package/src/components/tools/item/item-tool.tsx +0 -26
  317. package/src/components/tools/roof/move-roof-tool.tsx +0 -364
  318. package/src/components/tools/slab/move-slab-tool.tsx +0 -182
  319. package/src/components/tools/slab/slab-boundary-editor.tsx +0 -43
  320. package/src/components/tools/slab/slab-hole-editor.tsx +0 -49
  321. package/src/components/tools/slab/slab-tool.tsx +0 -322
  322. package/src/components/tools/spawn/move-spawn-tool.tsx +0 -101
  323. package/src/components/tools/spawn/spawn-tool.tsx +0 -130
  324. package/src/components/tools/wall/curve-wall-tool.tsx +0 -178
  325. package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -426
  326. package/src/components/tools/wall/move-wall-tool.tsx +0 -358
  327. package/src/components/tools/wall/wall-tool.tsx +0 -332
  328. package/src/components/tools/window/move-window-tool.tsx +0 -447
  329. package/src/components/tools/window/window-math.ts +0 -117
  330. package/src/components/tools/window/window-tool.tsx +0 -332
  331. package/src/components/ui/helpers/ceiling-helper.tsx +0 -20
  332. package/src/components/ui/helpers/slab-helper.tsx +0 -20
  333. package/src/components/ui/helpers/wall-helper.tsx +0 -20
  334. package/src/components/ui/panels/ceiling-panel.tsx +0 -249
  335. package/src/components/ui/panels/column-panel.tsx +0 -759
  336. package/src/components/ui/panels/door-panel.tsx +0 -1299
  337. package/src/components/ui/panels/fence-panel.tsx +0 -222
  338. package/src/components/ui/panels/item-panel.tsx +0 -306
  339. package/src/components/ui/panels/paint-panel.tsx +0 -163
  340. package/src/components/ui/panels/presets/presets-popover.tsx +0 -511
  341. package/src/components/ui/panels/roof-panel.tsx +0 -280
  342. package/src/components/ui/panels/roof-segment-panel.tsx +0 -311
  343. package/src/components/ui/panels/slab-panel.tsx +0 -250
  344. package/src/components/ui/panels/spawn-panel.tsx +0 -161
  345. package/src/components/ui/panels/stair-panel.tsx +0 -577
  346. package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
  347. package/src/components/ui/panels/wall-panel.tsx +0 -273
  348. package/src/components/ui/panels/window-panel.tsx +0 -968
  349. package/src/contexts/presets-context.tsx +0 -121
  350. package/src/hooks/use-contextual-tools.ts +0 -61
@@ -0,0 +1,196 @@
1
+ import { beforeEach, describe, expect, test } from 'bun:test'
2
+ import {
3
+ type AnyNode,
4
+ type AnyNodeId,
5
+ CabinetModuleNode,
6
+ type CabinetModuleNode as CabinetModuleNodeType,
7
+ CabinetNode,
8
+ type CabinetNode as CabinetNodeType,
9
+ type LevelNode,
10
+ MeasurementNode,
11
+ useScene,
12
+ WallNode,
13
+ } from '@pascal-app/core'
14
+ import { useViewer } from '@pascal-app/viewer'
15
+ import {
16
+ copySelectedNodesToEditorClipboard,
17
+ getEditorClipboardSnapshot,
18
+ pasteEditorClipboardToLevel,
19
+ } from './scene-clipboard'
20
+
21
+ const sourceLevelId = 'level_clipboard-source' as LevelNode['id']
22
+ const targetLevelId = 'level_clipboard-target' as LevelNode['id']
23
+ const runId = 'cabinet_clipboard-run' as CabinetNodeType['id']
24
+ const leftModuleId = 'cabinet-module_clipboard-left' as CabinetModuleNodeType['id']
25
+ const rightModuleId = 'cabinet-module_clipboard-right' as CabinetModuleNodeType['id']
26
+
27
+ function makeLevel(id: AnyNodeId, children: AnyNodeId[] = []): AnyNode {
28
+ return {
29
+ id,
30
+ type: 'level',
31
+ object: 'node',
32
+ visible: true,
33
+ name: '',
34
+ metadata: {},
35
+ position: [0, 0, 0],
36
+ rotation: 0,
37
+ parentId: null,
38
+ level: 0,
39
+ children,
40
+ } as unknown as AnyNode
41
+ }
42
+
43
+ function seedCabinetRun() {
44
+ const sourceLevel = makeLevel(sourceLevelId, [runId])
45
+ const targetLevel = makeLevel(targetLevelId)
46
+ const run = CabinetNode.parse({
47
+ id: runId,
48
+ parentId: sourceLevelId,
49
+ position: [1, 0, 2],
50
+ rotation: 0,
51
+ children: [leftModuleId, rightModuleId],
52
+ withCountertop: true,
53
+ showPlinth: true,
54
+ })
55
+ const leftModule = CabinetModuleNode.parse({
56
+ id: leftModuleId,
57
+ parentId: runId,
58
+ position: [-0.45, 0.1, 0],
59
+ width: 0.9,
60
+ showPlinth: false,
61
+ withCountertop: false,
62
+ })
63
+ const rightModule = CabinetModuleNode.parse({
64
+ id: rightModuleId,
65
+ parentId: runId,
66
+ position: [0.45, 0.1, 0],
67
+ width: 0.9,
68
+ showPlinth: false,
69
+ withCountertop: false,
70
+ })
71
+
72
+ useScene.setState({
73
+ nodes: {
74
+ [sourceLevel.id]: sourceLevel,
75
+ [targetLevel.id]: targetLevel,
76
+ [run.id]: run as AnyNode,
77
+ [leftModule.id]: leftModule as AnyNode,
78
+ [rightModule.id]: rightModule as AnyNode,
79
+ },
80
+ rootNodeIds: [sourceLevel.id, targetLevel.id],
81
+ } as never)
82
+ useViewer.getState().setSelection({
83
+ levelId: sourceLevelId,
84
+ selectedIds: [],
85
+ })
86
+ }
87
+
88
+ function isPastedCabinetRun(node: AnyNode): node is CabinetNodeType {
89
+ return node.type === 'cabinet' && node.id !== runId
90
+ }
91
+
92
+ function pastedCabinetRun() {
93
+ return Object.values(useScene.getState().nodes).find(isPastedCabinetRun)
94
+ }
95
+
96
+ describe('scene clipboard', () => {
97
+ beforeEach(() => {
98
+ seedCabinetRun()
99
+ useScene.temporal.getState().clear()
100
+ })
101
+
102
+ test('copies a selected cabinet run as one subtree instead of independent modules', () => {
103
+ const copied = copySelectedNodesToEditorClipboard([runId, leftModuleId, rightModuleId])
104
+
105
+ expect(copied).toBe(true)
106
+ expect(getEditorClipboardSnapshot()?.rootIds).toEqual([runId])
107
+
108
+ const result = pasteEditorClipboardToLevel(targetLevelId)
109
+ expect(result?.pastedIds).toHaveLength(1)
110
+
111
+ const pastedRun = pastedCabinetRun()
112
+ expect(pastedRun).toBeDefined()
113
+ expect(pastedRun?.parentId).toBe(targetLevelId)
114
+ expect(pastedRun?.children).toHaveLength(2)
115
+
116
+ for (const childId of pastedRun?.children ?? []) {
117
+ const child = useScene.getState().nodes[childId as AnyNodeId]
118
+ expect(child?.type).toBe('cabinet-module')
119
+ expect(child?.parentId).toBe(pastedRun?.id)
120
+ }
121
+
122
+ const sourceRun = useScene.getState().nodes[runId]
123
+ expect(sourceRun?.type).toBe('cabinet')
124
+ expect((sourceRun as CabinetNodeType | undefined)?.children).toEqual([
125
+ leftModuleId,
126
+ rightModuleId,
127
+ ] satisfies CabinetModuleNodeType['id'][])
128
+ })
129
+
130
+ test('promotes a complete module selection to the cabinet run before copying', () => {
131
+ const copied = copySelectedNodesToEditorClipboard([leftModuleId, rightModuleId])
132
+
133
+ expect(copied).toBe(true)
134
+ expect(getEditorClipboardSnapshot()?.rootIds).toEqual([runId])
135
+
136
+ const result = pasteEditorClipboardToLevel(targetLevelId)
137
+ expect(result?.pastedIds).toHaveLength(1)
138
+ expect(pastedCabinetRun()?.children).toHaveLength(2)
139
+ })
140
+
141
+ test('remaps a measurement association when its host is copied with it', () => {
142
+ const wall = WallNode.parse({
143
+ id: 'wall_clipboard-host',
144
+ type: 'wall',
145
+ parentId: sourceLevelId,
146
+ start: [0, 0],
147
+ end: [3, 0],
148
+ })
149
+ const measurement = MeasurementNode.parse({
150
+ id: 'measurement_clipboard-associated',
151
+ type: 'measurement',
152
+ parentId: sourceLevelId,
153
+ measurement: {
154
+ kind: 'distance',
155
+ points: [
156
+ {
157
+ kind: 'feature',
158
+ reference: { nodeId: wall.id, featureId: 'wall:face:left', parameters: { t: 0 } },
159
+ fallback: [0, 0, 0],
160
+ },
161
+ [3, 0, 0],
162
+ ],
163
+ },
164
+ })
165
+ useScene.setState((state) => ({
166
+ nodes: {
167
+ ...state.nodes,
168
+ [sourceLevelId]: makeLevel(sourceLevelId, [wall.id, measurement.id]),
169
+ [wall.id]: wall,
170
+ [measurement.id]: measurement,
171
+ },
172
+ }))
173
+
174
+ expect(copySelectedNodesToEditorClipboard([wall.id, measurement.id])).toBe(true)
175
+ expect(pasteEditorClipboardToLevel(targetLevelId)?.pastedIds).toHaveLength(2)
176
+
177
+ const pastedWall = Object.values(useScene.getState().nodes).find(
178
+ (node) => node.type === 'wall' && node.id !== wall.id,
179
+ )
180
+ const pastedMeasurement = Object.values(useScene.getState().nodes).find(
181
+ (node) => node.type === 'measurement' && node.id !== measurement.id,
182
+ )
183
+ expect(pastedWall?.type).toBe('wall')
184
+ expect(pastedMeasurement?.type).toBe('measurement')
185
+ if (
186
+ pastedWall?.type !== 'wall' ||
187
+ pastedMeasurement?.type !== 'measurement' ||
188
+ pastedMeasurement.measurement.kind !== 'distance'
189
+ ) {
190
+ return
191
+ }
192
+ const anchor = pastedMeasurement.measurement.points[0]
193
+ expect(Array.isArray(anchor)).toBe(false)
194
+ if (!Array.isArray(anchor)) expect(anchor.reference.nodeId).toBe(pastedWall.id)
195
+ })
196
+ })
@@ -0,0 +1,332 @@
1
+ import {
2
+ AnyNode,
3
+ type AnyNodeId,
4
+ generateId,
5
+ type LevelNode,
6
+ remapMeasurementReferences,
7
+ type StairNode,
8
+ useScene,
9
+ } from '@pascal-app/core'
10
+ import { useViewer } from '@pascal-app/viewer'
11
+
12
+ type ClipboardPayload = {
13
+ copiedAt: number
14
+ nodes: AnyNode[]
15
+ rootIds: AnyNodeId[]
16
+ }
17
+
18
+ type PasteResult = {
19
+ pastedIds: AnyNodeId[]
20
+ skippedIds: AnyNodeId[]
21
+ }
22
+
23
+ const COPYABLE_ROOT_TYPES = new Set<AnyNode['type']>([
24
+ 'wall',
25
+ 'fence',
26
+ 'column',
27
+ 'item',
28
+ 'slab',
29
+ 'ceiling',
30
+ 'roof',
31
+ 'stair',
32
+ 'spawn',
33
+ 'zone',
34
+ 'cabinet',
35
+ 'cabinet-module',
36
+ 'measurement',
37
+ ])
38
+
39
+ let clipboardPayload: ClipboardPayload | null = null
40
+ const subscribers = new Set<() => void>()
41
+
42
+ function notifySubscribers() {
43
+ for (const subscriber of subscribers) {
44
+ subscriber()
45
+ }
46
+ }
47
+
48
+ export function subscribeEditorClipboard(subscriber: () => void) {
49
+ subscribers.add(subscriber)
50
+ return () => {
51
+ subscribers.delete(subscriber)
52
+ }
53
+ }
54
+
55
+ export function getEditorClipboardSnapshot() {
56
+ return clipboardPayload
57
+ }
58
+
59
+ export function hasEditorClipboard() {
60
+ return !!clipboardPayload && clipboardPayload.rootIds.length > 0
61
+ }
62
+
63
+ function extractIdPrefix(id: string) {
64
+ const underscoreIndex = id.indexOf('_')
65
+ return underscoreIndex === -1 ? 'node' : id.slice(0, underscoreIndex)
66
+ }
67
+
68
+ function collectSubtreeIds(
69
+ nodes: Record<AnyNodeId, AnyNode>,
70
+ rootId: AnyNodeId,
71
+ ids: Set<AnyNodeId>,
72
+ ) {
73
+ if (ids.has(rootId)) return
74
+ const node = nodes[rootId]
75
+ if (!node) return
76
+ ids.add(rootId)
77
+
78
+ if ('children' in node && Array.isArray(node.children)) {
79
+ for (const childId of node.children as AnyNodeId[]) {
80
+ collectSubtreeIds(nodes, childId, ids)
81
+ }
82
+ }
83
+ }
84
+
85
+ function hasSelectedAncestor(
86
+ nodes: Record<AnyNodeId, AnyNode>,
87
+ id: AnyNodeId,
88
+ selectedIds: Set<AnyNodeId>,
89
+ ) {
90
+ let parentId = nodes[id]?.parentId as AnyNodeId | null
91
+
92
+ while (parentId) {
93
+ if (selectedIds.has(parentId)) return true
94
+ parentId = nodes[parentId]?.parentId as AnyNodeId | null
95
+ }
96
+
97
+ return false
98
+ }
99
+
100
+ function isLevelChildRoot(nodes: Record<AnyNodeId, AnyNode>, node: AnyNode) {
101
+ const parentId = node.parentId as AnyNodeId | null
102
+ if (!parentId) return true
103
+ return nodes[parentId]?.type === 'level'
104
+ }
105
+
106
+ function getPromotedCabinetRunId(
107
+ nodes: Record<AnyNodeId, AnyNode>,
108
+ node: AnyNode,
109
+ selectedIds: Set<AnyNodeId>,
110
+ ) {
111
+ if (node.type !== 'cabinet-module') return null
112
+
113
+ const parentId = node.parentId as AnyNodeId | null
114
+ const parent = parentId ? nodes[parentId] : null
115
+ if (parent?.type !== 'cabinet') return null
116
+ if (!parentId) return null
117
+ if (selectedIds.has(parentId)) return parentId
118
+
119
+ const siblingIds = Array.isArray(parent.children) ? (parent.children as AnyNodeId[]) : []
120
+ const hasOnlySelectedModules =
121
+ siblingIds.length > 0 &&
122
+ siblingIds.every((childId) => {
123
+ const child = nodes[childId]
124
+ return child?.type === 'cabinet-module' && selectedIds.has(childId)
125
+ })
126
+
127
+ return hasOnlySelectedModules ? parentId : null
128
+ }
129
+
130
+ function getPasteTargetLevel(targetLevelId?: AnyNodeId) {
131
+ const scene = useScene.getState()
132
+ const resolvedLevelId =
133
+ targetLevelId ?? (useViewer.getState().selection.levelId as AnyNodeId | null)
134
+ if (!resolvedLevelId) return null
135
+
136
+ const level = scene.nodes[resolvedLevelId]
137
+ return level?.type === 'level' ? level : null
138
+ }
139
+
140
+ function getNextLevelId(level: LevelNode, nodes: Record<AnyNodeId, AnyNode>) {
141
+ const parentId = level.parentId as AnyNodeId | null
142
+ if (!parentId) return null
143
+
144
+ const building = nodes[parentId]
145
+ if (building?.type !== 'building') return null
146
+
147
+ const siblingLevels = building.children
148
+ .map((childId) => nodes[childId as AnyNodeId])
149
+ .filter((node): node is LevelNode => node?.type === 'level')
150
+
151
+ return (
152
+ siblingLevels
153
+ .filter((candidate) => candidate.level > level.level)
154
+ .sort((a, b) => a.level - b.level)[0]?.id ?? null
155
+ )
156
+ }
157
+
158
+ function remapNodeReferences(
159
+ node: AnyNode,
160
+ oldId: AnyNodeId,
161
+ targetLevel: LevelNode,
162
+ idMap: Map<AnyNodeId, AnyNodeId>,
163
+ rootIds: Set<AnyNodeId>,
164
+ nodes: Record<AnyNodeId, AnyNode>,
165
+ ) {
166
+ const clone = JSON.parse(JSON.stringify(node)) as AnyNode
167
+ ;(clone as Record<string, unknown>).id = idMap.get(oldId)
168
+
169
+ if (rootIds.has(oldId)) {
170
+ clone.parentId = targetLevel.id
171
+ } else if (clone.parentId && typeof clone.parentId === 'string') {
172
+ clone.parentId = idMap.get(clone.parentId as AnyNodeId) ?? clone.parentId
173
+ }
174
+
175
+ if ('children' in clone && Array.isArray(clone.children)) {
176
+ ;(clone as Record<string, unknown>).children = (clone.children as AnyNodeId[])
177
+ .map((childId) => idMap.get(childId))
178
+ .filter((childId): childId is AnyNodeId => !!childId)
179
+ }
180
+
181
+ if ('wallId' in clone && typeof clone.wallId === 'string') {
182
+ const nextWallId = idMap.get(clone.wallId as AnyNodeId)
183
+ if (nextWallId) {
184
+ ;(clone as Record<string, unknown>).wallId = nextWallId
185
+ } else {
186
+ delete (clone as Record<string, unknown>).wallId
187
+ }
188
+ }
189
+
190
+ if (clone.type === 'stair') {
191
+ const nextLevelId = getNextLevelId(targetLevel, nodes)
192
+ ;(clone as StairNode).fromLevelId = targetLevel.id
193
+ ;(clone as StairNode).toLevelId = nextLevelId
194
+ }
195
+
196
+ if (clone.type === 'measurement') {
197
+ clone.measurement = remapMeasurementReferences(clone.measurement, idMap)
198
+ }
199
+
200
+ const metadata =
201
+ clone.metadata && typeof clone.metadata === 'object' && !Array.isArray(clone.metadata)
202
+ ? { ...(clone.metadata as Record<string, unknown>) }
203
+ : {}
204
+ delete metadata.isNew
205
+ delete metadata.isTransient
206
+ ;(clone as Record<string, unknown>).metadata = metadata
207
+
208
+ return AnyNode.parse(clone)
209
+ }
210
+
211
+ function buildClipboardPayload(ids: AnyNodeId[]): ClipboardPayload | null {
212
+ const scene = useScene.getState()
213
+ const selectedIdSet = new Set(ids)
214
+ const promotedIds = ids.map((id) => {
215
+ const node = scene.nodes[id]
216
+ return node ? (getPromotedCabinetRunId(scene.nodes, node, selectedIdSet) ?? id) : id
217
+ })
218
+ const rootIds = Array.from(new Set(promotedIds)).filter((id) => {
219
+ const node = scene.nodes[id]
220
+ return (
221
+ node &&
222
+ COPYABLE_ROOT_TYPES.has(node.type) &&
223
+ isLevelChildRoot(scene.nodes, node) &&
224
+ !hasSelectedAncestor(scene.nodes, id, selectedIdSet)
225
+ )
226
+ })
227
+
228
+ if (rootIds.length === 0) {
229
+ return null
230
+ }
231
+
232
+ const subtreeIds = new Set<AnyNodeId>()
233
+ for (const rootId of rootIds) {
234
+ collectSubtreeIds(scene.nodes, rootId, subtreeIds)
235
+ }
236
+
237
+ return {
238
+ copiedAt: Date.now(),
239
+ nodes: [...subtreeIds]
240
+ .map((id) => scene.nodes[id])
241
+ .filter((node): node is AnyNode => !!node)
242
+ .map((node) => JSON.parse(JSON.stringify(node)) as AnyNode),
243
+ rootIds,
244
+ }
245
+ }
246
+
247
+ export function copySelectedNodesToEditorClipboard(selectedIds?: AnyNodeId[]) {
248
+ const ids = selectedIds ?? (useViewer.getState().selection.selectedIds as AnyNodeId[])
249
+ const payload = buildClipboardPayload(ids)
250
+ if (!payload) return false
251
+
252
+ clipboardPayload = payload
253
+ notifySubscribers()
254
+
255
+ return true
256
+ }
257
+
258
+ /**
259
+ * Clone the given nodes (subtrees included, ids remapped) onto the target /
260
+ * active level in place — the same copy + paste pipeline in one step, WITHOUT
261
+ * touching the user's editor clipboard. Selects the clones. Used by the group
262
+ * action menu's Duplicate.
263
+ */
264
+ export function duplicateNodesToLevel(
265
+ ids: AnyNodeId[],
266
+ targetLevelId?: AnyNodeId,
267
+ ): PasteResult | null {
268
+ const payload = buildClipboardPayload(ids)
269
+ if (!payload) return null
270
+ return applyClipboardPayloadToLevel(payload, targetLevelId)
271
+ }
272
+
273
+ export function pasteEditorClipboardToLevel(targetLevelId?: AnyNodeId): PasteResult | null {
274
+ if (!clipboardPayload) return null
275
+ return applyClipboardPayloadToLevel(clipboardPayload, targetLevelId)
276
+ }
277
+
278
+ function applyClipboardPayloadToLevel(
279
+ payload: ClipboardPayload,
280
+ targetLevelId?: AnyNodeId,
281
+ ): PasteResult | null {
282
+ const targetLevel = getPasteTargetLevel(targetLevelId)
283
+ if (!targetLevel) return null
284
+
285
+ const scene = useScene.getState()
286
+ const idMap = new Map<AnyNodeId, AnyNodeId>()
287
+
288
+ for (const node of payload.nodes) {
289
+ idMap.set(node.id as AnyNodeId, generateId(extractIdPrefix(node.id)) as AnyNodeId)
290
+ }
291
+
292
+ const rootIdSet = new Set(payload.rootIds)
293
+ const pastedNodes: AnyNode[] = []
294
+ const skippedIds: AnyNodeId[] = []
295
+
296
+ for (const node of payload.nodes) {
297
+ try {
298
+ pastedNodes.push(
299
+ remapNodeReferences(node, node.id as AnyNodeId, targetLevel, idMap, rootIdSet, scene.nodes),
300
+ )
301
+ } catch (error) {
302
+ console.error('Failed to paste copied node', node.id, error)
303
+ skippedIds.push(node.id as AnyNodeId)
304
+ }
305
+ }
306
+
307
+ if (pastedNodes.length === 0) {
308
+ return { pastedIds: [], skippedIds }
309
+ }
310
+
311
+ scene.createNodes(
312
+ pastedNodes.map((node) => ({
313
+ node,
314
+ parentId: (node.parentId as AnyNodeId | null) ?? undefined,
315
+ })),
316
+ )
317
+
318
+ const pastedNodeIds = new Set(pastedNodes.map((node) => node.id as AnyNodeId))
319
+ const pastedRootIds = payload.rootIds
320
+ .map((rootId) => idMap.get(rootId))
321
+ .filter((id): id is AnyNodeId => !!id && pastedNodeIds.has(id))
322
+
323
+ useViewer.getState().setSelection({
324
+ levelId: targetLevel.id,
325
+ selectedIds: pastedRootIds,
326
+ })
327
+
328
+ return {
329
+ pastedIds: pastedRootIds,
330
+ skippedIds,
331
+ }
332
+ }
package/src/lib/scene.ts CHANGED
@@ -1,16 +1,28 @@
1
1
  'use client'
2
2
 
3
- import { resolveLevelId, sceneRegistry, useScene } from '@pascal-app/core'
3
+ import {
4
+ clearSceneHistory,
5
+ nodeRegistry,
6
+ resolveLevelId,
7
+ sceneRegistry,
8
+ useScene,
9
+ } from '@pascal-app/core'
4
10
  import { useViewer } from '@pascal-app/viewer'
5
11
  import useEditor, {
6
12
  hasCustomPersistedEditorUiState,
7
13
  normalizePersistedEditorUiState,
8
14
  type PersistedEditorUiState,
9
15
  } from '../store/use-editor'
16
+ import { editorHostPanelRegistry } from './plugin-panels'
10
17
 
11
18
  export type SceneGraph = {
12
19
  nodes: Record<string, unknown>
13
20
  rootNodeIds: string[]
21
+ // Document-level scene state that travels with the graph. Optional so older
22
+ // payloads (and callers that only build nodes) stay valid.
23
+ collections?: Record<string, unknown>
24
+ materials?: Record<string, unknown>
25
+ installedPlugins?: string[]
14
26
  }
15
27
 
16
28
  type PersistedSelectionPath = {
@@ -220,7 +232,11 @@ function getValidatedSelectionForScene(
220
232
 
221
233
  const selectedIds = selection.selectedIds.filter((id) => {
222
234
  const node = sceneNodes[id]
223
- return Boolean(node) && resolveLevelId(node, sceneNodes) === levelId
235
+ if (!node) return false
236
+ if (resolveLevelId(node, sceneNodes) === levelId) return true
237
+
238
+ const def = nodeRegistry.get(node.type)
239
+ return def?.floorplanScope === 'building' && node.parentId === buildingId
224
240
  })
225
241
 
226
242
  return {
@@ -283,7 +299,11 @@ export function syncEditorSelectionFromCurrentScene() {
283
299
 
284
300
  if (shouldRestoreEditorUiState) {
285
301
  if (restoredSelection) {
286
- useViewer.getState().setSelection(restoredSelection)
302
+ // PersistedSelectionPath carries plain `string` ids (read from
303
+ // localStorage, no branded-template-literal guarantee). The viewer's
304
+ // SelectionPath expects branded ids. The runtime values match the
305
+ // brand; the cast bridges the static gap.
306
+ useViewer.getState().setSelection(restoredSelection as never)
287
307
  useEditor.setState(
288
308
  restoredEditorUiState.phase === 'site'
289
309
  ? (selectionDrivenEditorUiState ?? restoredEditorUiState)
@@ -305,7 +325,7 @@ export function syncEditorSelectionFromCurrentScene() {
305
325
  }
306
326
 
307
327
  if (restoredSelection) {
308
- useViewer.getState().setSelection(restoredSelection)
328
+ useViewer.getState().setSelection(restoredSelection as never)
309
329
  if (selectionDrivenEditorUiState) {
310
330
  useEditor.setState(selectionDrivenEditorUiState)
311
331
  }
@@ -347,10 +367,9 @@ function resetEditorInteractionState() {
347
367
  structureLayer: 'elements',
348
368
  catalogCategory: null,
349
369
  selectedItem: null,
350
- movingNode: null,
351
370
  selectedReferenceId: null,
352
371
  spaces: {},
353
- editingHole: null,
372
+ hoveredHole: null,
354
373
  isPreviewMode: false,
355
374
  })
356
375
  }
@@ -364,13 +383,26 @@ function hasUsableSceneGraph(sceneGraph?: SceneGraph | null): sceneGraph is Scen
364
383
  }
365
384
 
366
385
  export function applySceneGraphToEditor(sceneGraph?: SceneGraph | null) {
386
+ const defaultInstalledPlugins = editorHostPanelRegistry.getDefaultInstalledPluginIds()
367
387
  if (hasUsableSceneGraph(sceneGraph)) {
368
- const { nodes, rootNodeIds } = sceneGraph
369
- useScene.getState().setScene(nodes as any, rootNodeIds as any)
388
+ const { nodes, rootNodeIds, collections, materials, installedPlugins } = sceneGraph
389
+ useScene.getState().setScene(nodes as any, rootNodeIds as any, {
390
+ collections: collections as any,
391
+ materials: materials as any,
392
+ installedPlugins: installedPlugins ?? defaultInstalledPlugins,
393
+ hasExplicitPluginInstallState: installedPlugins !== undefined,
394
+ })
370
395
  } else {
371
396
  useScene.getState().clearScene()
397
+ useScene.getState().setInstalledPlugins(defaultInstalledPlugins, { explicit: false })
372
398
  }
373
399
 
400
+ // The loaded scene is the undo floor. Loading records history entries of
401
+ // its own (`unloadScene` + `setScene`/`clearScene` are tracked writes), so
402
+ // without this reset a few Ctrl+Z presses could step past the load into the
403
+ // pre-load — often empty — state and wipe the whole project.
404
+ clearSceneHistory()
405
+
374
406
  syncEditorSelectionFromCurrentScene()
375
407
  }
376
408