@pascal-app/editor 0.9.1 → 1.0.0-beta.1

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 (366) hide show
  1. package/package.json +14 -9
  2. package/src/components/editor/alignment-3d-guide-layer.tsx +10 -2
  3. package/src/components/editor/bake-exporter.tsx +49 -0
  4. package/src/components/editor/bake-thumbnail.tsx +95 -0
  5. package/src/components/editor/camera-dragging-lifecycle.test.ts +49 -0
  6. package/src/components/editor/camera-dragging-lifecycle.ts +41 -0
  7. package/src/components/editor/custom-camera-controls.tsx +332 -199
  8. package/src/components/editor/delete-confirmation-dialog.tsx +54 -0
  9. package/src/components/editor/editor-layout-mobile.tsx +5 -0
  10. package/src/components/editor/editor-layout-v2.tsx +18 -0
  11. package/src/components/editor/elevation-3d-guide-layer.tsx +120 -0
  12. package/src/components/editor/export-manager.tsx +71 -63
  13. package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
  14. package/src/components/editor/first-person/build-collider-world.test.ts +102 -5
  15. package/src/components/editor/first-person/build-collider-world.ts +52 -5
  16. package/src/components/editor/first-person/terrain-collider.test.ts +132 -0
  17. package/src/components/editor/first-person/terrain-collider.ts +212 -0
  18. package/src/components/editor/first-person-controls.tsx +410 -136
  19. package/src/components/editor/floating-action-menu.tsx +435 -64
  20. package/src/components/editor/floating-building-action-menu.tsx +1 -1
  21. package/src/components/editor/floorplan-background-selection.test.ts +72 -0
  22. package/src/components/editor/floorplan-background-selection.ts +7 -20
  23. package/src/components/editor/floorplan-camera-sync.test.ts +249 -0
  24. package/src/components/editor/floorplan-camera-sync.ts +213 -0
  25. package/src/components/editor/floorplan-mode-coordinator.tsx +82 -0
  26. package/src/components/editor/floorplan-navigation-presentation.test.ts +142 -0
  27. package/src/components/editor/floorplan-navigation-presentation.ts +156 -0
  28. package/src/components/editor/floorplan-panel.tsx +2517 -1492
  29. package/src/components/editor/grid.tsx +166 -39
  30. package/src/components/editor/group-actions.ts +597 -0
  31. package/src/components/editor/group-floating-action-menu.tsx +130 -0
  32. package/src/components/editor/group-move-3d.ts +397 -0
  33. package/src/components/editor/group-rotate-handle.tsx +102 -64
  34. package/src/components/editor/group-selection-box-3d.tsx +173 -0
  35. package/src/components/editor/group-transform-shared.test.ts +192 -1
  36. package/src/components/editor/group-transform-shared.ts +240 -7
  37. package/src/components/editor/handles/handle-arrow.tsx +81 -23
  38. package/src/components/editor/handles/handle-drag-history.test.ts +96 -0
  39. package/src/components/editor/handles/handle-drag-history.ts +14 -0
  40. package/src/components/editor/handles/preview-overrides.test.ts +26 -0
  41. package/src/components/editor/handles/preview-overrides.ts +13 -0
  42. package/src/components/editor/handles/resize-snap.test.ts +52 -0
  43. package/src/components/editor/handles/resize-snap.ts +23 -0
  44. package/src/components/editor/handles/use-handle-drag.ts +49 -6
  45. package/src/components/editor/index.tsx +289 -35
  46. package/src/components/editor/measurement-pill.tsx +68 -23
  47. package/src/components/editor/node-action-menu.tsx +14 -1
  48. package/src/components/editor/node-arrow-handles.tsx +409 -180
  49. package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
  50. package/src/components/editor/quick-measurement-card.tsx +76 -0
  51. package/src/components/editor/quick-measurement-hud.tsx +34 -0
  52. package/src/components/editor/riser-diagram-panel.tsx +137 -0
  53. package/src/components/editor/selection-manager.tsx +814 -466
  54. package/src/components/editor/site-edge-labels.tsx +28 -6
  55. package/src/components/editor/slab-hole-highlights.tsx +13 -5
  56. package/src/components/editor/snapshot-capture-overlay.tsx +258 -115
  57. package/src/components/editor/three-context-bridge.ts +22 -0
  58. package/src/components/editor/thumbnail-generator.tsx +118 -253
  59. package/src/components/editor/use-floorplan-background-placement.ts +113 -45
  60. package/src/components/editor/use-floorplan-hit-testing.test.ts +82 -0
  61. package/src/components/editor/use-floorplan-hit-testing.ts +144 -161
  62. package/src/components/editor/use-floorplan-scene-data.ts +5 -5
  63. package/src/components/editor/use-mesh-settle-epoch.ts +27 -0
  64. package/src/components/editor/wall-measurement-label.tsx +10 -28
  65. package/src/components/editor/wall-move-side-handles.tsx +229 -39
  66. package/src/components/editor/wall-opening-highlights.tsx +63 -32
  67. package/src/components/editor/wall-snap-beacon-layer.tsx +162 -6
  68. package/src/components/editor-2d/floorplan-action-menu-layer.tsx +9 -5
  69. package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +2 -1
  70. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +40 -9
  71. package/src/components/editor-2d/floorplan-cursor-indicator-position.test.ts +31 -0
  72. package/src/components/editor-2d/floorplan-cursor-indicator-position.ts +39 -0
  73. package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
  74. package/src/components/editor-2d/floorplan-group-move.tsx +744 -0
  75. package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
  76. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1672 -0
  77. package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
  78. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +72 -0
  79. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +219 -20
  80. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +327 -59
  81. package/src/components/editor-2d/floorplan-render-context.test.ts +27 -0
  82. package/src/components/editor-2d/floorplan-render-context.tsx +113 -8
  83. package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +5 -3
  84. package/src/components/editor-2d/renderers/floorplan-annotation-layout.test.ts +285 -0
  85. package/src/components/editor-2d/renderers/floorplan-annotation-layout.ts +516 -0
  86. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.test.tsx +236 -0
  87. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +618 -0
  88. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +369 -0
  89. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +431 -32
  90. package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +89 -0
  91. package/src/components/editor-2d/renderers/floorplan-label-angle.ts +106 -0
  92. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +94 -26
  93. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +587 -0
  94. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +2280 -578
  95. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +20 -10
  96. package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
  97. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +425 -61
  98. package/src/components/systems/ceiling/ceiling-system.tsx +97 -4
  99. package/src/components/systems/roof/roof-edit-system.tsx +1768 -13
  100. package/src/components/systems/selection-affordance-manager.tsx +38 -0
  101. package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
  102. package/src/components/systems/zone/zone-system.tsx +42 -13
  103. package/src/components/tools/elevator/elevator-tool.tsx +34 -8
  104. package/src/components/tools/elevator/move-elevator-tool.tsx +13 -2
  105. package/src/components/tools/fence/fence-drafting.ts +107 -8
  106. package/src/components/tools/item/move-tool.tsx +17 -13
  107. package/src/components/tools/item/placement-math.test.ts +13 -1
  108. package/src/components/tools/item/placement-math.ts +28 -2
  109. package/src/components/tools/item/placement-strategies.ts +246 -5
  110. package/src/components/tools/item/placement-types.ts +13 -1
  111. package/src/components/tools/item/use-draft-node.ts +146 -57
  112. package/src/components/tools/item/use-placement-coordinator.tsx +813 -125
  113. package/src/components/tools/registry/move-registry-node-tool.tsx +636 -103
  114. package/src/components/tools/roof/roof-tool.tsx +336 -94
  115. package/src/components/tools/select/box-select-state.ts +18 -7
  116. package/src/components/tools/select/box-select-tool.tsx +176 -40
  117. package/src/components/tools/select/marquee-geometry.test.ts +161 -0
  118. package/src/components/tools/select/marquee-geometry.ts +155 -0
  119. package/src/components/tools/select/plane-box-select-tool.tsx +1 -8
  120. package/src/components/tools/select/select-candidates.ts +1 -1
  121. package/src/components/tools/shared/cursor-sphere.tsx +62 -26
  122. package/src/components/tools/shared/drag-bounding-box.tsx +28 -4
  123. package/src/components/tools/shared/facing-indicator.tsx +87 -0
  124. package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
  125. package/src/components/tools/shared/floor-stack-preview.ts +4 -0
  126. package/src/components/tools/shared/horizontal-construction-plane.ts +123 -0
  127. package/src/components/tools/shared/placement-box.tsx +183 -1
  128. package/src/components/tools/shared/pointer-support-cap.test.ts +98 -0
  129. package/src/components/tools/shared/pointer-support-cap.ts +273 -0
  130. package/src/components/tools/shared/polygon-editor.tsx +276 -35
  131. package/src/components/tools/site/site-boundary-editor.tsx +101 -42
  132. package/src/components/tools/site/stroke-pointer-ownership.test.ts +91 -0
  133. package/src/components/tools/site/stroke-pointer-ownership.ts +57 -0
  134. package/src/components/tools/site/terrain-brush-cursor.tsx +247 -0
  135. package/src/components/tools/site/terrain-sculpt-grid.tsx +192 -0
  136. package/src/components/tools/site/terrain-sculpt-tool.tsx +356 -0
  137. package/src/components/tools/stair/stair-click-guard.test.ts +58 -0
  138. package/src/components/tools/stair/stair-click-guard.ts +72 -0
  139. package/src/components/tools/stair/stair-defaults.ts +1 -0
  140. package/src/components/tools/stair/stair-tool.tsx +164 -42
  141. package/src/components/tools/tool-manager.tsx +149 -30
  142. package/src/components/tools/wall/wall-drafting.test.ts +447 -0
  143. package/src/components/tools/wall/wall-drafting.ts +304 -108
  144. package/src/components/tools/wall/wall-snap-geometry.test.ts +52 -0
  145. package/src/components/tools/wall/wall-snap-geometry.ts +137 -9
  146. package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
  147. package/src/components/tools/zone/zone-tool.tsx +98 -88
  148. package/src/components/ui/action-menu/camera-actions.tsx +24 -17
  149. package/src/components/ui/action-menu/control-modes.tsx +44 -94
  150. package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
  151. package/src/components/ui/action-menu/index.tsx +2 -4
  152. package/src/components/ui/action-menu/measurement-control.tsx +267 -0
  153. package/src/components/ui/action-menu/structure-tools.tsx +22 -13
  154. package/src/components/ui/action-menu/view-toggles.tsx +42 -90
  155. package/src/components/ui/command-palette/editor-commands.tsx +16 -3
  156. package/src/components/ui/command-palette/index.tsx +4 -3
  157. package/src/components/ui/controls/material-paint-panel.tsx +93 -18
  158. package/src/components/ui/controls/material-picker.tsx +162 -152
  159. package/src/components/ui/controls/material-properties-editor.tsx +108 -0
  160. package/src/components/ui/controls/metric-control.tsx +118 -44
  161. package/src/components/ui/controls/scene-material-list.tsx +247 -0
  162. package/src/components/ui/controls/slider-control.tsx +88 -38
  163. package/src/components/ui/controls/terrain-sculpt-panel.tsx +193 -0
  164. package/src/components/ui/floating-level-selector.tsx +74 -20
  165. package/src/components/ui/helpers/building-helper.tsx +10 -23
  166. package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
  167. package/src/components/ui/helpers/helper-manager.tsx +273 -14
  168. package/src/components/ui/helpers/item-helper.tsx +28 -32
  169. package/src/components/ui/helpers/registered-tool-helper.tsx +45 -11
  170. package/src/components/ui/helpers/roof-helper.tsx +10 -12
  171. package/src/components/ui/icon-ref.tsx +47 -0
  172. package/src/components/ui/item-catalog/catalog-items.tsx +39 -1
  173. package/src/components/ui/item-catalog/item-catalog.tsx +13 -36
  174. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  175. package/src/components/ui/panels/multi-selection-panel.tsx +57 -0
  176. package/src/components/ui/panels/node-display.ts +17 -17
  177. package/src/components/ui/panels/panel-manager.tsx +36 -14
  178. package/src/components/ui/panels/panel-wrapper.tsx +24 -3
  179. package/src/components/ui/panels/parametric-inspector.tsx +19 -4
  180. package/src/components/ui/panels/reference-panel.tsx +54 -19
  181. package/src/components/ui/panels/selection-breakdown.test.ts +24 -0
  182. package/src/components/ui/panels/selection-breakdown.ts +20 -0
  183. package/src/components/ui/primitives/shortcut-token.tsx +39 -3
  184. package/src/components/ui/primitives/sidebar.tsx +1 -0
  185. package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
  186. package/src/components/ui/sidebar/icon-rail.tsx +50 -31
  187. package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
  188. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +82 -41
  189. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +76 -6
  190. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +12 -4
  191. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +2 -1
  192. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -3
  193. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +10 -7
  194. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
  195. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
  196. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +10 -7
  197. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -1
  198. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +1 -1
  199. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +10 -7
  200. package/src/components/ui/sidebar/panels/site-panel/index.tsx +42 -17
  201. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
  202. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
  203. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +72 -25
  204. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +7 -3
  205. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +12 -8
  206. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +4 -3
  207. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +10 -7
  208. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
  209. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
  210. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +44 -3
  211. package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
  212. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
  213. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
  214. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
  215. package/src/components/ui/sidebar/tab-bar.tsx +42 -28
  216. package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
  217. package/src/components/ui/snap-target-badge.test.tsx +40 -0
  218. package/src/components/ui/snap-target-badge.tsx +88 -0
  219. package/src/components/viewer/viewer-controls-bar.tsx +466 -0
  220. package/src/components/viewer/viewer-scene-header.tsx +235 -0
  221. package/src/components/viewer-overlay.tsx +36 -596
  222. package/src/components/viewer-zone-system.tsx +6 -1
  223. package/src/components/walkthrough-hud.tsx +111 -0
  224. package/src/hooks/use-auto-save.test.ts +14 -0
  225. package/src/hooks/use-auto-save.ts +54 -7
  226. package/src/hooks/use-ceiling-events.ts +3 -2
  227. package/src/hooks/use-drag-action.ts +4 -1
  228. package/src/hooks/use-grid-events.ts +24 -0
  229. package/src/hooks/use-keyboard.ts +438 -70
  230. package/src/index.tsx +355 -9
  231. package/src/lib/active-placement-surface.test.ts +24 -0
  232. package/src/lib/active-placement-surface.ts +42 -0
  233. package/src/lib/camera-pose.test.ts +237 -0
  234. package/src/lib/camera-pose.ts +172 -0
  235. package/src/lib/ceiling-plan-snap.ts +24 -0
  236. package/src/lib/contextual-help.test.ts +112 -0
  237. package/src/lib/contextual-help.ts +144 -0
  238. package/src/lib/continuation.ts +64 -0
  239. package/src/lib/direct-manipulation.test.ts +275 -0
  240. package/src/lib/direct-manipulation.ts +118 -0
  241. package/src/lib/door-interaction.ts +1 -1
  242. package/src/lib/editor-api.ts +23 -35
  243. package/src/lib/elevation-guides.test.ts +107 -0
  244. package/src/lib/elevation-guides.ts +236 -0
  245. package/src/lib/floorplan/annotation-visibility.test.ts +327 -0
  246. package/src/lib/floorplan/annotation-visibility.ts +133 -0
  247. package/src/lib/floorplan/apply-alignment.test.ts +25 -0
  248. package/src/lib/floorplan/apply-alignment.ts +18 -12
  249. package/src/lib/floorplan/drawing-coordination.test.ts +46 -0
  250. package/src/lib/floorplan/drawing-coordination.ts +13 -0
  251. package/src/lib/floorplan/floorplan-export.test.ts +319 -0
  252. package/src/lib/floorplan/floorplan-export.tsx +978 -0
  253. package/src/lib/floorplan/floorplan-extension.test.ts +43 -0
  254. package/src/lib/floorplan/floorplan-extension.ts +181 -0
  255. package/src/lib/floorplan/floorplan-mode.test.ts +120 -0
  256. package/src/lib/floorplan/floorplan-mode.ts +73 -0
  257. package/src/lib/floorplan/floorplan-pdfkit-document.ts +194 -0
  258. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +107 -0
  259. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +680 -0
  260. package/src/lib/floorplan/geometry.ts +52 -0
  261. package/src/lib/floorplan/index.ts +3 -0
  262. package/src/lib/floorplan/items.ts +5 -2
  263. package/src/lib/floorplan/plan-coords.ts +21 -0
  264. package/src/lib/fresh-planar-placement.test.ts +283 -3
  265. package/src/lib/fresh-planar-placement.ts +85 -1
  266. package/src/lib/glb-export.test.ts +556 -0
  267. package/src/lib/glb-export.ts +1071 -0
  268. package/src/lib/ground-surface.ts +95 -0
  269. package/src/lib/history.test.ts +96 -0
  270. package/src/lib/history.ts +31 -0
  271. package/src/lib/interaction/hot-set.test.ts +133 -0
  272. package/src/lib/interaction/hot-set.ts +67 -0
  273. package/src/lib/interaction/overlay-policy.test.ts +52 -0
  274. package/src/lib/interaction/overlay-policy.ts +59 -0
  275. package/src/lib/interaction/scope.ts +243 -0
  276. package/src/lib/level-duplication.test.ts +2 -1
  277. package/src/lib/level-selection.ts +2 -2
  278. package/src/lib/material-paint.ts +36 -49
  279. package/src/lib/measurement-kind.test.ts +30 -0
  280. package/src/lib/measurement-kind.ts +19 -0
  281. package/src/lib/measurement-label.test.ts +47 -0
  282. package/src/lib/measurement-label.ts +68 -0
  283. package/src/lib/measurement-parser.ts +116 -0
  284. package/src/lib/measurements.test.ts +188 -0
  285. package/src/lib/measurements.ts +210 -0
  286. package/src/lib/paint-scope.test.ts +454 -0
  287. package/src/lib/paint-scope.ts +461 -0
  288. package/src/lib/placement-drag-release.ts +23 -0
  289. package/src/lib/planar-cursor-placement.test.ts +57 -0
  290. package/src/lib/plugin-panels.test.ts +19 -0
  291. package/src/lib/plugin-panels.ts +91 -0
  292. package/src/lib/quick-action-feedback.ts +33 -0
  293. package/src/lib/quick-action-nodes.test.ts +94 -0
  294. package/src/lib/quick-action-nodes.ts +47 -0
  295. package/src/lib/quick-measurement.test.ts +77 -0
  296. package/src/lib/quick-measurement.ts +109 -0
  297. package/src/lib/roof-duplication.ts +6 -6
  298. package/src/lib/roof-hover-outline-proxy.ts +22 -0
  299. package/src/lib/roof-wall-hit.ts +164 -0
  300. package/src/lib/scene-clipboard.test.ts +325 -0
  301. package/src/lib/scene-clipboard.ts +310 -21
  302. package/src/lib/scene.ts +28 -5
  303. package/src/lib/selection-routing.test.ts +313 -0
  304. package/src/lib/selection-routing.ts +180 -0
  305. package/src/lib/sfx/index.ts +1 -0
  306. package/src/lib/sfx/movement-tick.test.ts +65 -0
  307. package/src/lib/sfx/movement-tick.ts +18 -0
  308. package/src/lib/sfx-bus.ts +82 -14
  309. package/src/lib/sfx-player.test.ts +266 -0
  310. package/src/lib/sfx-player.ts +262 -69
  311. package/src/lib/site-boundary.test.ts +53 -0
  312. package/src/lib/site-boundary.ts +27 -0
  313. package/src/lib/slab-plan-snap.test.ts +93 -0
  314. package/src/lib/slab-plan-snap.ts +108 -0
  315. package/src/lib/snapping-mode.test.ts +165 -0
  316. package/src/lib/snapping-mode.ts +191 -0
  317. package/src/lib/stair-duplication.test.ts +103 -0
  318. package/src/lib/stair-duplication.ts +58 -86
  319. package/src/lib/stair-levels.ts +2 -0
  320. package/src/lib/surface-plan-snap.test.ts +71 -0
  321. package/src/lib/surface-plan-snap.ts +256 -0
  322. package/src/lib/terrain-sculpt.test.ts +429 -0
  323. package/src/lib/terrain-sculpt.ts +301 -0
  324. package/src/lib/terrain-verb-color.test.ts +51 -0
  325. package/src/lib/terrain-verb-color.ts +58 -0
  326. package/src/lib/touch-gesture-priority.test.ts +45 -0
  327. package/src/lib/touch-gesture-priority.ts +30 -0
  328. package/src/lib/use-linear-display.ts +40 -0
  329. package/src/lib/window-interaction.ts +1 -1
  330. package/src/lib/world-grid-snap.ts +37 -5
  331. package/src/store/camera-pose-store.test.ts +29 -0
  332. package/src/store/camera-pose-store.ts +27 -0
  333. package/src/store/live-draft-preview-stores.test.ts +136 -0
  334. package/src/store/navigation-sync-pose-store.test.ts +34 -0
  335. package/src/store/navigation-sync-pose-store.ts +27 -0
  336. package/src/store/terrain-sculpt-mode.test.ts +223 -0
  337. package/src/store/use-delete-confirmation.ts +27 -0
  338. package/src/store/use-direct-manipulation-feedback.ts +20 -0
  339. package/src/store/use-drawing-view.test.ts +23 -0
  340. package/src/store/use-drawing-view.ts +84 -0
  341. package/src/store/use-editor.tsx +665 -126
  342. package/src/store/use-elevation-guides.ts +40 -0
  343. package/src/store/use-facing-pose.ts +44 -0
  344. package/src/store/use-fence-curve-draft.ts +51 -0
  345. package/src/store/use-first-person-hud.ts +40 -0
  346. package/src/store/use-floorplan-annotation-visibility.ts +60 -0
  347. package/src/store/use-floorplan-draft-preview.ts +151 -0
  348. package/src/store/use-floorplan-marquee.ts +50 -0
  349. package/src/store/use-floorplan-mode.ts +110 -0
  350. package/src/store/use-interaction-scope.test.ts +206 -0
  351. package/src/store/use-interaction-scope.ts +140 -0
  352. package/src/store/use-measurement-draft.test.ts +559 -0
  353. package/src/store/use-measurement-draft.ts +570 -0
  354. package/src/store/use-opening-guides.ts +41 -0
  355. package/src/store/use-path-draft-preview.ts +90 -0
  356. package/src/store/use-placement-preview.ts +11 -3
  357. package/src/store/use-quick-measurement-hud.test.ts +53 -0
  358. package/src/store/use-quick-measurement-hud.ts +77 -0
  359. package/src/store/use-segment-draft-chain.ts +28 -0
  360. package/src/store/use-stair-build-preview.ts +53 -0
  361. package/src/store/use-wall-snap-indicator.ts +2 -0
  362. package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
  363. package/src/components/editor/group-move-handle.tsx +0 -316
  364. package/src/components/ui/panels/paint-panel.tsx +0 -163
  365. package/src/lib/floorplan/selection-tool.ts +0 -271
  366. package/src/lib/level-name.ts +0 -11
@@ -2,7 +2,12 @@ import {
2
2
  AnyNode,
3
3
  type AnyNodeId,
4
4
  generateId,
5
+ generateSceneMaterialId,
5
6
  type LevelNode,
7
+ nodeRegistry,
8
+ remapMeasurementReferences,
9
+ SceneMaterial,
10
+ type SceneMaterialId,
6
11
  type StairNode,
7
12
  useScene,
8
13
  } from '@pascal-app/core'
@@ -10,18 +15,25 @@ import { useViewer } from '@pascal-app/viewer'
10
15
 
11
16
  type ClipboardPayload = {
12
17
  copiedAt: number
18
+ materials: SceneMaterial[]
13
19
  nodes: AnyNode[]
14
20
  rootIds: AnyNodeId[]
15
21
  }
16
22
 
17
- type PasteResult = {
23
+ export type PasteResult = {
24
+ createdMaterialIds: SceneMaterialId[]
18
25
  pastedIds: AnyNodeId[]
19
26
  skippedIds: AnyNodeId[]
20
27
  }
21
28
 
29
+ const SYSTEM_CLIPBOARD_KIND = 'pascal.scene-nodes'
30
+ const SYSTEM_CLIPBOARD_VERSION = 1
31
+
22
32
  const COPYABLE_ROOT_TYPES = new Set<AnyNode['type']>([
23
33
  'wall',
24
34
  'fence',
35
+ 'door',
36
+ 'window',
25
37
  'column',
26
38
  'item',
27
39
  'slab',
@@ -30,9 +42,13 @@ const COPYABLE_ROOT_TYPES = new Set<AnyNode['type']>([
30
42
  'stair',
31
43
  'spawn',
32
44
  'zone',
45
+ 'cabinet',
46
+ 'cabinet-module',
47
+ 'measurement',
33
48
  ])
34
49
 
35
50
  let clipboardPayload: ClipboardPayload | null = null
51
+ let pendingSystemClipboardWrite: Promise<boolean> | null = null
36
52
  const subscribers = new Set<() => void>()
37
53
 
38
54
  function notifySubscribers() {
@@ -93,10 +109,53 @@ function hasSelectedAncestor(
93
109
  return false
94
110
  }
95
111
 
96
- function isLevelChildRoot(nodes: Record<AnyNodeId, AnyNode>, node: AnyNode) {
112
+ function isClipboardRoot(
113
+ nodes: Record<AnyNodeId, AnyNode>,
114
+ node: AnyNode,
115
+ allowHostedOpening: boolean,
116
+ ) {
97
117
  const parentId = node.parentId as AnyNodeId | null
98
118
  if (!parentId) return true
99
- return nodes[parentId]?.type === 'level'
119
+ const parent = nodes[parentId]
120
+ if (parent?.type === 'level') return true
121
+ if (
122
+ allowHostedOpening &&
123
+ (node.type === 'door' || node.type === 'window') &&
124
+ (parent?.type === 'wall' || parent?.type === 'roof-segment')
125
+ ) {
126
+ return true
127
+ }
128
+ return parent?.type === 'building' && nodeRegistry.get(node.type)?.floorplanScope === 'building'
129
+ }
130
+
131
+ function isCopyableRootType(node: AnyNode) {
132
+ if (COPYABLE_ROOT_TYPES.has(node.type)) return true
133
+ const definition = nodeRegistry.get(node.type)
134
+ return !!definition && definition.capabilities?.duplicable !== false
135
+ }
136
+
137
+ function getPromotedCabinetRunId(
138
+ nodes: Record<AnyNodeId, AnyNode>,
139
+ node: AnyNode,
140
+ selectedIds: Set<AnyNodeId>,
141
+ ) {
142
+ if (node.type !== 'cabinet-module') return null
143
+
144
+ const parentId = node.parentId as AnyNodeId | null
145
+ const parent = parentId ? nodes[parentId] : null
146
+ if (parent?.type !== 'cabinet') return null
147
+ if (!parentId) return null
148
+ if (selectedIds.has(parentId)) return parentId
149
+
150
+ const siblingIds = Array.isArray(parent.children) ? (parent.children as AnyNodeId[]) : []
151
+ const hasOnlySelectedModules =
152
+ siblingIds.length > 0 &&
153
+ siblingIds.every((childId) => {
154
+ const child = nodes[childId]
155
+ return child?.type === 'cabinet-module' && selectedIds.has(childId)
156
+ })
157
+
158
+ return hasOnlySelectedModules ? parentId : null
100
159
  }
101
160
 
102
161
  function getPasteTargetLevel(targetLevelId?: AnyNodeId) {
@@ -114,7 +173,7 @@ function getNextLevelId(level: LevelNode, nodes: Record<AnyNodeId, AnyNode>) {
114
173
  if (!parentId) return null
115
174
 
116
175
  const building = nodes[parentId]
117
- if (!building || building.type !== 'building') return null
176
+ if (building?.type !== 'building') return null
118
177
 
119
178
  const siblingLevels = building.children
120
179
  .map((childId) => nodes[childId as AnyNodeId])
@@ -132,14 +191,28 @@ function remapNodeReferences(
132
191
  oldId: AnyNodeId,
133
192
  targetLevel: LevelNode,
134
193
  idMap: Map<AnyNodeId, AnyNodeId>,
194
+ materialIdMap: Map<SceneMaterialId, SceneMaterialId>,
135
195
  rootIds: Set<AnyNodeId>,
136
196
  nodes: Record<AnyNodeId, AnyNode>,
137
197
  ) {
138
- const clone = JSON.parse(JSON.stringify(node)) as AnyNode
198
+ const clone = remapSceneMaterialReferences(
199
+ JSON.parse(JSON.stringify(node)),
200
+ materialIdMap,
201
+ ) as AnyNode
139
202
  ;(clone as Record<string, unknown>).id = idMap.get(oldId)
140
203
 
141
204
  if (rootIds.has(oldId)) {
142
- clone.parentId = targetLevel.id
205
+ const buildingId = targetLevel.parentId as AnyNodeId | null
206
+ clone.parentId =
207
+ nodeRegistry.get(node.type)?.floorplanScope === 'building' &&
208
+ buildingId &&
209
+ nodes[buildingId]?.type === 'building'
210
+ ? buildingId
211
+ : targetLevel.id
212
+ if (clone.type === 'door' || clone.type === 'window') {
213
+ delete clone.roofSegmentId
214
+ delete clone.roofFace
215
+ }
143
216
  } else if (clone.parentId && typeof clone.parentId === 'string') {
144
217
  clone.parentId = idMap.get(clone.parentId as AnyNodeId) ?? clone.parentId
145
218
  }
@@ -165,6 +238,10 @@ function remapNodeReferences(
165
238
  ;(clone as StairNode).toLevelId = nextLevelId
166
239
  }
167
240
 
241
+ if (clone.type === 'measurement') {
242
+ clone.measurement = remapMeasurementReferences(clone.measurement, idMap)
243
+ }
244
+
168
245
  const metadata =
169
246
  clone.metadata && typeof clone.metadata === 'object' && !Array.isArray(clone.metadata)
170
247
  ? { ...(clone.metadata as Record<string, unknown>) }
@@ -176,22 +253,63 @@ function remapNodeReferences(
176
253
  return AnyNode.parse(clone)
177
254
  }
178
255
 
179
- export function copySelectedNodesToEditorClipboard(selectedIds?: AnyNodeId[]) {
256
+ function remapSceneMaterialReferences(
257
+ value: unknown,
258
+ materialIdMap: Map<SceneMaterialId, SceneMaterialId>,
259
+ ): unknown {
260
+ if (typeof value === 'string' && value.startsWith('scene:')) {
261
+ const oldId = value.slice('scene:'.length) as SceneMaterialId
262
+ const nextId = materialIdMap.get(oldId)
263
+ return nextId ? `scene:${nextId}` : value
264
+ }
265
+ if (Array.isArray(value)) {
266
+ return value.map((entry) => remapSceneMaterialReferences(entry, materialIdMap))
267
+ }
268
+ if (value && typeof value === 'object') {
269
+ return Object.fromEntries(
270
+ Object.entries(value).map(([key, entry]) => [
271
+ key,
272
+ remapSceneMaterialReferences(entry, materialIdMap),
273
+ ]),
274
+ )
275
+ }
276
+ return value
277
+ }
278
+
279
+ function collectReferencedSceneMaterialIds(value: unknown, ids: Set<SceneMaterialId>) {
280
+ if (typeof value === 'string' && value.startsWith('scene:')) {
281
+ ids.add(value.slice('scene:'.length) as SceneMaterialId)
282
+ return
283
+ }
284
+ if (Array.isArray(value)) {
285
+ for (const entry of value) collectReferencedSceneMaterialIds(entry, ids)
286
+ return
287
+ }
288
+ if (value && typeof value === 'object') {
289
+ for (const entry of Object.values(value)) collectReferencedSceneMaterialIds(entry, ids)
290
+ }
291
+ }
292
+
293
+ function buildClipboardPayload(ids: AnyNodeId[]): ClipboardPayload | null {
180
294
  const scene = useScene.getState()
181
- const ids = selectedIds ?? (useViewer.getState().selection.selectedIds as AnyNodeId[])
182
295
  const selectedIdSet = new Set(ids)
183
- const rootIds = ids.filter((id) => {
296
+ const allowHostedOpening = ids.length === 1
297
+ const promotedIds = ids.map((id) => {
298
+ const node = scene.nodes[id]
299
+ return node ? (getPromotedCabinetRunId(scene.nodes, node, selectedIdSet) ?? id) : id
300
+ })
301
+ const rootIds = Array.from(new Set(promotedIds)).filter((id) => {
184
302
  const node = scene.nodes[id]
185
303
  return (
186
304
  node &&
187
- COPYABLE_ROOT_TYPES.has(node.type) &&
188
- isLevelChildRoot(scene.nodes, node) &&
305
+ isCopyableRootType(node) &&
306
+ isClipboardRoot(scene.nodes, node, allowHostedOpening) &&
189
307
  !hasSelectedAncestor(scene.nodes, id, selectedIdSet)
190
308
  )
191
309
  })
192
310
 
193
311
  if (rootIds.length === 0) {
194
- return false
312
+ return null
195
313
  }
196
314
 
197
315
  const subtreeIds = new Set<AnyNodeId>()
@@ -199,30 +317,189 @@ export function copySelectedNodesToEditorClipboard(selectedIds?: AnyNodeId[]) {
199
317
  collectSubtreeIds(scene.nodes, rootId, subtreeIds)
200
318
  }
201
319
 
202
- clipboardPayload = {
320
+ const copiedNodes = [...subtreeIds]
321
+ .map((id) => scene.nodes[id])
322
+ .filter((node): node is AnyNode => !!node)
323
+ .map((node) => JSON.parse(JSON.stringify(node)) as AnyNode)
324
+ const materialIds = new Set<SceneMaterialId>()
325
+ collectReferencedSceneMaterialIds(copiedNodes, materialIds)
326
+
327
+ return {
203
328
  copiedAt: Date.now(),
204
- nodes: [...subtreeIds]
205
- .map((id) => scene.nodes[id])
206
- .filter((node): node is AnyNode => !!node)
207
- .map((node) => JSON.parse(JSON.stringify(node)) as AnyNode),
329
+ materials: [...materialIds]
330
+ .map((id) => scene.materials[id])
331
+ .filter((material): material is SceneMaterial => !!material)
332
+ .map((material) => JSON.parse(JSON.stringify(material)) as SceneMaterial),
333
+ nodes: copiedNodes,
208
334
  rootIds,
209
335
  }
336
+ }
337
+
338
+ function serializeClipboardPayload(payload: ClipboardPayload) {
339
+ return JSON.stringify({
340
+ kind: SYSTEM_CLIPBOARD_KIND,
341
+ version: SYSTEM_CLIPBOARD_VERSION,
342
+ payload,
343
+ })
344
+ }
345
+
346
+ function parseClipboardPayload(text: string): ClipboardPayload | null {
347
+ try {
348
+ const envelope = JSON.parse(text) as {
349
+ kind?: unknown
350
+ payload?: unknown
351
+ version?: unknown
352
+ }
353
+ if (
354
+ envelope.kind !== SYSTEM_CLIPBOARD_KIND ||
355
+ envelope.version !== SYSTEM_CLIPBOARD_VERSION ||
356
+ !envelope.payload ||
357
+ typeof envelope.payload !== 'object'
358
+ ) {
359
+ return null
360
+ }
361
+
362
+ const candidate = envelope.payload as {
363
+ copiedAt?: unknown
364
+ materials?: unknown
365
+ nodes?: unknown
366
+ rootIds?: unknown
367
+ }
368
+ if (
369
+ typeof candidate.copiedAt !== 'number' ||
370
+ !Array.isArray(candidate.nodes) ||
371
+ !Array.isArray(candidate.rootIds) ||
372
+ !candidate.rootIds.every((id) => typeof id === 'string')
373
+ ) {
374
+ return null
375
+ }
376
+
377
+ const nodes = candidate.nodes.map((node) => AnyNode.safeParse(node))
378
+ if (nodes.some((result) => !result.success)) return null
379
+ const materials = Array.isArray(candidate.materials)
380
+ ? candidate.materials.map((material) => SceneMaterial.safeParse(material))
381
+ : []
382
+ if (materials.some((result) => !result.success)) return null
383
+
384
+ const parsedNodes = nodes.filter((result) => result.success).map((result) => result.data)
385
+ const nodeIds = new Set(parsedNodes.map((node) => node.id))
386
+ const rootIds = candidate.rootIds as AnyNodeId[]
387
+ if (rootIds.length === 0 || rootIds.some((id) => !nodeIds.has(id))) return null
388
+
389
+ return {
390
+ copiedAt: candidate.copiedAt,
391
+ materials: materials.filter((result) => result.success).map((result) => result.data),
392
+ nodes: parsedNodes,
393
+ rootIds,
394
+ }
395
+ } catch {
396
+ return null
397
+ }
398
+ }
399
+
400
+ function writeSystemClipboard(payload: ClipboardPayload) {
401
+ if (typeof navigator === 'undefined' || !navigator.clipboard?.writeText) {
402
+ pendingSystemClipboardWrite = null
403
+ return
404
+ }
405
+ pendingSystemClipboardWrite = navigator.clipboard
406
+ .writeText(serializeClipboardPayload(payload))
407
+ .then(
408
+ () => true,
409
+ () => false,
410
+ )
411
+ }
412
+
413
+ export function copySelectedNodesToEditorClipboard(selectedIds?: AnyNodeId[]) {
414
+ const ids = selectedIds ?? (useViewer.getState().selection.selectedIds as AnyNodeId[])
415
+ const payload = buildClipboardPayload(ids)
416
+ if (!payload) return false
417
+
418
+ clipboardPayload = payload
210
419
  notifySubscribers()
420
+ writeSystemClipboard(payload)
211
421
 
212
422
  return true
213
423
  }
214
424
 
425
+ export async function readEditorClipboardFromSystem() {
426
+ const pendingWrite = pendingSystemClipboardWrite
427
+ pendingSystemClipboardWrite = null
428
+ if (pendingWrite && !(await pendingWrite)) {
429
+ return hasEditorClipboard()
430
+ }
431
+
432
+ if (typeof navigator === 'undefined' || !navigator.clipboard?.readText) {
433
+ return hasEditorClipboard()
434
+ }
435
+
436
+ let text: string
437
+ try {
438
+ text = await navigator.clipboard.readText()
439
+ } catch {
440
+ return hasEditorClipboard()
441
+ }
442
+
443
+ const payload = parseClipboardPayload(text)
444
+ if (!payload) return false
445
+ clipboardPayload = payload
446
+ notifySubscribers()
447
+ return true
448
+ }
449
+
450
+ /**
451
+ * Clone the given nodes (subtrees included, ids remapped) onto the target /
452
+ * active level in place — the same copy + paste pipeline in one step, WITHOUT
453
+ * touching the user's editor clipboard. Selects the clones. Used by the group
454
+ * action menu's Duplicate.
455
+ */
456
+ export function duplicateNodesToLevel(
457
+ ids: AnyNodeId[],
458
+ targetLevelId?: AnyNodeId,
459
+ ): PasteResult | null {
460
+ const payload = buildClipboardPayload(ids)
461
+ if (!payload) return null
462
+ return applyClipboardPayloadToLevel(payload, targetLevelId)
463
+ }
464
+
215
465
  export function pasteEditorClipboardToLevel(targetLevelId?: AnyNodeId): PasteResult | null {
216
- const payload = clipboardPayload
466
+ if (!clipboardPayload) return null
467
+ return applyClipboardPayloadToLevel(clipboardPayload, targetLevelId)
468
+ }
469
+
470
+ export async function pasteSystemEditorClipboardToLevel(
471
+ targetLevelId?: AnyNodeId,
472
+ ): Promise<PasteResult | null> {
473
+ if (!(await readEditorClipboardFromSystem())) return null
474
+ return pasteEditorClipboardToLevel(targetLevelId)
475
+ }
476
+
477
+ function applyClipboardPayloadToLevel(
478
+ payload: ClipboardPayload,
479
+ targetLevelId?: AnyNodeId,
480
+ ): PasteResult | null {
217
481
  const targetLevel = getPasteTargetLevel(targetLevelId)
218
- if (!payload || !targetLevel) return null
482
+ if (!targetLevel) return null
219
483
 
220
484
  const scene = useScene.getState()
221
485
  const idMap = new Map<AnyNodeId, AnyNodeId>()
486
+ const materialIdMap = new Map<SceneMaterialId, SceneMaterialId>()
487
+ const materialsToCreate: SceneMaterial[] = []
222
488
 
223
489
  for (const node of payload.nodes) {
224
490
  idMap.set(node.id as AnyNodeId, generateId(extractIdPrefix(node.id)) as AnyNodeId)
225
491
  }
492
+ for (const material of payload.materials) {
493
+ const oldId = material.id as SceneMaterialId
494
+ const existing = scene.materials[oldId]
495
+ if (existing && JSON.stringify(existing) === JSON.stringify(material)) {
496
+ materialIdMap.set(oldId, oldId)
497
+ continue
498
+ }
499
+ const nextId = existing ? generateSceneMaterialId() : oldId
500
+ materialIdMap.set(oldId, nextId)
501
+ materialsToCreate.push({ ...material, id: nextId })
502
+ }
226
503
 
227
504
  const rootIdSet = new Set(payload.rootIds)
228
505
  const pastedNodes: AnyNode[] = []
@@ -231,7 +508,15 @@ export function pasteEditorClipboardToLevel(targetLevelId?: AnyNodeId): PasteRes
231
508
  for (const node of payload.nodes) {
232
509
  try {
233
510
  pastedNodes.push(
234
- remapNodeReferences(node, node.id as AnyNodeId, targetLevel, idMap, rootIdSet, scene.nodes),
511
+ remapNodeReferences(
512
+ node,
513
+ node.id as AnyNodeId,
514
+ targetLevel,
515
+ idMap,
516
+ materialIdMap,
517
+ rootIdSet,
518
+ scene.nodes,
519
+ ),
235
520
  )
236
521
  } catch (error) {
237
522
  console.error('Failed to paste copied node', node.id, error)
@@ -240,9 +525,12 @@ export function pasteEditorClipboardToLevel(targetLevelId?: AnyNodeId): PasteRes
240
525
  }
241
526
 
242
527
  if (pastedNodes.length === 0) {
243
- return { pastedIds: [], skippedIds }
528
+ return { createdMaterialIds: [], pastedIds: [], skippedIds }
244
529
  }
245
530
 
531
+ for (const material of materialsToCreate) {
532
+ scene.addSceneMaterial(material)
533
+ }
246
534
  scene.createNodes(
247
535
  pastedNodes.map((node) => ({
248
536
  node,
@@ -261,6 +549,7 @@ export function pasteEditorClipboardToLevel(targetLevelId?: AnyNodeId): PasteRes
261
549
  })
262
550
 
263
551
  return {
552
+ createdMaterialIds: materialsToCreate.map((material) => material.id as SceneMaterialId),
264
553
  pastedIds: pastedRootIds,
265
554
  skippedIds,
266
555
  }
package/src/lib/scene.ts CHANGED
@@ -1,16 +1,28 @@
1
1
  'use client'
2
2
 
3
- import { nodeRegistry, 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 = {
@@ -355,10 +367,8 @@ function resetEditorInteractionState() {
355
367
  structureLayer: 'elements',
356
368
  catalogCategory: null,
357
369
  selectedItem: null,
358
- movingNode: null,
359
370
  selectedReferenceId: null,
360
371
  spaces: {},
361
- editingHole: null,
362
372
  hoveredHole: null,
363
373
  isPreviewMode: false,
364
374
  })
@@ -373,13 +383,26 @@ function hasUsableSceneGraph(sceneGraph?: SceneGraph | null): sceneGraph is Scen
373
383
  }
374
384
 
375
385
  export function applySceneGraphToEditor(sceneGraph?: SceneGraph | null) {
386
+ const defaultInstalledPlugins = editorHostPanelRegistry.getDefaultInstalledPluginIds()
376
387
  if (hasUsableSceneGraph(sceneGraph)) {
377
- const { nodes, rootNodeIds } = sceneGraph
378
- 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
+ })
379
395
  } else {
380
396
  useScene.getState().clearScene()
397
+ useScene.getState().setInstalledPlugins(defaultInstalledPlugins, { explicit: false })
381
398
  }
382
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
+
383
406
  syncEditorSelectionFromCurrentScene()
384
407
  }
385
408