@pascal-app/core 0.9.2 → 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 (339) hide show
  1. package/README.md +17 -10
  2. package/dist/events/bus.d.ts +21 -2
  3. package/dist/events/bus.d.ts.map +1 -1
  4. package/dist/hooks/spatial-grid/floor-placed-elevation.d.ts +17 -1
  5. package/dist/hooks/spatial-grid/floor-placed-elevation.d.ts.map +1 -1
  6. package/dist/hooks/spatial-grid/floor-placed-elevation.js +61 -8
  7. package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts +116 -66
  8. package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts.map +1 -1
  9. package/dist/hooks/spatial-grid/spatial-grid-manager.js +316 -441
  10. package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts +46 -1
  11. package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts.map +1 -1
  12. package/dist/hooks/spatial-grid/spatial-grid-sync.js +193 -12
  13. package/dist/hooks/spatial-grid/support-host-id.d.ts +3 -0
  14. package/dist/hooks/spatial-grid/support-host-id.d.ts.map +1 -0
  15. package/dist/hooks/spatial-grid/support-host-id.js +2 -0
  16. package/dist/hooks/spatial-grid/support-host-patch.d.ts +51 -0
  17. package/dist/hooks/spatial-grid/support-host-patch.d.ts.map +1 -0
  18. package/dist/hooks/spatial-grid/support-host-patch.js +164 -0
  19. package/dist/index.d.ts +23 -11
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +22 -10
  22. package/dist/lib/measurement-geometry.d.ts +4 -0
  23. package/dist/lib/measurement-geometry.d.ts.map +1 -1
  24. package/dist/lib/measurement-geometry.js +27 -10
  25. package/dist/lib/slab-polygon.d.ts.map +1 -1
  26. package/dist/lib/slab-polygon.js +33 -1
  27. package/dist/lib/space-detection.d.ts +15 -4
  28. package/dist/lib/space-detection.d.ts.map +1 -1
  29. package/dist/lib/space-detection.js +174 -88
  30. package/dist/lib/terrain-brush.d.ts +163 -0
  31. package/dist/lib/terrain-brush.d.ts.map +1 -0
  32. package/dist/lib/terrain-brush.js +359 -0
  33. package/dist/lib/terrain-codec.d.ts +51 -0
  34. package/dist/lib/terrain-codec.d.ts.map +1 -0
  35. package/dist/lib/terrain-codec.js +167 -0
  36. package/dist/lib/terrain-field.d.ts +150 -0
  37. package/dist/lib/terrain-field.d.ts.map +1 -0
  38. package/dist/lib/terrain-field.js +248 -0
  39. package/dist/lib/terrain-raycast.d.ts +44 -0
  40. package/dist/lib/terrain-raycast.d.ts.map +1 -0
  41. package/dist/lib/terrain-raycast.js +188 -0
  42. package/dist/lib/terrain-source.d.ts +60 -0
  43. package/dist/lib/terrain-source.d.ts.map +1 -0
  44. package/dist/lib/terrain-source.js +90 -0
  45. package/dist/lib/terrain-support.d.ts +60 -0
  46. package/dist/lib/terrain-support.d.ts.map +1 -0
  47. package/dist/lib/terrain-support.js +150 -0
  48. package/dist/lib/zone-quantities.d.ts +1 -1
  49. package/dist/lib/zone-quantities.d.ts.map +1 -1
  50. package/dist/lib/zone-quantities.js +311 -66
  51. package/dist/material-library.d.ts +9 -1
  52. package/dist/material-library.d.ts.map +1 -1
  53. package/dist/material-library.js +56 -4
  54. package/dist/registry/handles.d.ts +16 -1
  55. package/dist/registry/handles.d.ts.map +1 -1
  56. package/dist/registry/index.d.ts +1 -1
  57. package/dist/registry/index.d.ts.map +1 -1
  58. package/dist/registry/subtree.d.ts.map +1 -1
  59. package/dist/registry/subtree.js +5 -2
  60. package/dist/registry/types.d.ts +64 -1
  61. package/dist/registry/types.d.ts.map +1 -1
  62. package/dist/schema/index.d.ts +6 -3
  63. package/dist/schema/index.d.ts.map +1 -1
  64. package/dist/schema/index.js +6 -3
  65. package/dist/schema/nodes/cabinet.d.ts +2 -0
  66. package/dist/schema/nodes/cabinet.d.ts.map +1 -1
  67. package/dist/schema/nodes/cabinet.js +4 -2
  68. package/dist/schema/nodes/ceiling.d.ts +1 -1
  69. package/dist/schema/nodes/ceiling.d.ts.map +1 -1
  70. package/dist/schema/nodes/ceiling.js +7 -1
  71. package/dist/schema/nodes/column.d.ts +1 -0
  72. package/dist/schema/nodes/column.d.ts.map +1 -1
  73. package/dist/schema/nodes/column.js +2 -0
  74. package/dist/schema/nodes/construction-dimension.d.ts +195 -0
  75. package/dist/schema/nodes/construction-dimension.d.ts.map +1 -0
  76. package/dist/schema/nodes/construction-dimension.js +151 -0
  77. package/dist/schema/nodes/door.d.ts +29 -0
  78. package/dist/schema/nodes/door.d.ts.map +1 -1
  79. package/dist/schema/nodes/door.js +20 -0
  80. package/dist/schema/nodes/duct-terminal.d.ts +1 -0
  81. package/dist/schema/nodes/duct-terminal.d.ts.map +1 -1
  82. package/dist/schema/nodes/duct-terminal.js +2 -0
  83. package/dist/schema/nodes/fence.d.ts +2 -0
  84. package/dist/schema/nodes/fence.d.ts.map +1 -1
  85. package/dist/schema/nodes/fence.js +8 -0
  86. package/dist/schema/nodes/hvac-equipment.d.ts +1 -0
  87. package/dist/schema/nodes/hvac-equipment.d.ts.map +1 -1
  88. package/dist/schema/nodes/hvac-equipment.js +2 -0
  89. package/dist/schema/nodes/item.d.ts +1 -0
  90. package/dist/schema/nodes/item.d.ts.map +1 -1
  91. package/dist/schema/nodes/item.js +13 -0
  92. package/dist/schema/nodes/level.d.ts +2 -1
  93. package/dist/schema/nodes/level.d.ts.map +1 -1
  94. package/dist/schema/nodes/level.js +12 -33
  95. package/dist/schema/nodes/shelf.d.ts +1 -0
  96. package/dist/schema/nodes/shelf.d.ts.map +1 -1
  97. package/dist/schema/nodes/shelf.js +2 -0
  98. package/dist/schema/nodes/site.d.ts +9 -0
  99. package/dist/schema/nodes/site.d.ts.map +1 -1
  100. package/dist/schema/nodes/site.js +8 -6
  101. package/dist/schema/nodes/slab.d.ts +5 -0
  102. package/dist/schema/nodes/slab.d.ts.map +1 -1
  103. package/dist/schema/nodes/slab.js +14 -2
  104. package/dist/schema/nodes/spawn.d.ts +1 -0
  105. package/dist/schema/nodes/spawn.d.ts.map +1 -1
  106. package/dist/schema/nodes/spawn.js +2 -0
  107. package/dist/schema/nodes/stair.d.ts +3 -1
  108. package/dist/schema/nodes/stair.d.ts.map +1 -1
  109. package/dist/schema/nodes/stair.js +8 -1
  110. package/dist/schema/nodes/structural-grid.d.ts +27 -0
  111. package/dist/schema/nodes/structural-grid.d.ts.map +1 -0
  112. package/dist/schema/nodes/structural-grid.js +17 -0
  113. package/dist/schema/nodes/wall.d.ts +5 -2
  114. package/dist/schema/nodes/wall.d.ts.map +1 -1
  115. package/dist/schema/nodes/wall.js +13 -4
  116. package/dist/schema/nodes/window.d.ts +29 -0
  117. package/dist/schema/nodes/window.d.ts.map +1 -1
  118. package/dist/schema/nodes/window.js +18 -0
  119. package/dist/schema/nodes/zone.d.ts +20 -0
  120. package/dist/schema/nodes/zone.d.ts.map +1 -1
  121. package/dist/schema/nodes/zone.js +15 -0
  122. package/dist/schema/terrain.d.ts +28 -0
  123. package/dist/schema/terrain.d.ts.map +1 -0
  124. package/dist/schema/terrain.js +30 -0
  125. package/dist/schema/types.d.ts +211 -3
  126. package/dist/schema/types.d.ts.map +1 -1
  127. package/dist/schema/types.js +4 -0
  128. package/dist/services/__fixtures__/wall-plane-top-boundary-repro.d.ts +80 -0
  129. package/dist/services/__fixtures__/wall-plane-top-boundary-repro.d.ts.map +1 -0
  130. package/dist/services/__fixtures__/wall-plane-top-boundary-repro.js +89 -0
  131. package/dist/services/index.d.ts +2 -1
  132. package/dist/services/index.d.ts.map +1 -1
  133. package/dist/services/index.js +2 -1
  134. package/dist/services/level-height.d.ts +8 -9
  135. package/dist/services/level-height.d.ts.map +1 -1
  136. package/dist/services/level-height.js +48 -16
  137. package/dist/services/storey.d.ts +103 -0
  138. package/dist/services/storey.d.ts.map +1 -0
  139. package/dist/services/storey.js +296 -0
  140. package/dist/store/actions/node-actions.d.ts.map +1 -1
  141. package/dist/store/actions/node-actions.js +47 -3
  142. package/dist/store/history-control.d.ts +20 -0
  143. package/dist/store/history-control.d.ts.map +1 -1
  144. package/dist/store/history-control.js +110 -13
  145. package/dist/store/use-live-terrain.d.ts +65 -0
  146. package/dist/store/use-live-terrain.d.ts.map +1 -0
  147. package/dist/store/use-live-terrain.js +31 -0
  148. package/dist/store/use-live-transforms.d.ts +8 -0
  149. package/dist/store/use-live-transforms.d.ts.map +1 -1
  150. package/dist/store/use-scene.d.ts +31 -2
  151. package/dist/store/use-scene.d.ts.map +1 -1
  152. package/dist/store/use-scene.js +638 -8
  153. package/dist/systems/elevator/elevator-opening-system.d.ts +1 -0
  154. package/dist/systems/elevator/elevator-opening-system.d.ts.map +1 -1
  155. package/dist/systems/elevator/elevator-opening-system.js +27 -21
  156. package/dist/systems/elevator/elevator-service.d.ts +0 -2
  157. package/dist/systems/elevator/elevator-service.d.ts.map +1 -1
  158. package/dist/systems/elevator/elevator-service.js +2 -24
  159. package/dist/systems/slab/slab-placement.d.ts +11 -0
  160. package/dist/systems/slab/slab-placement.d.ts.map +1 -0
  161. package/dist/systems/slab/slab-placement.js +14 -0
  162. package/dist/systems/slab/slab-support.d.ts +137 -0
  163. package/dist/systems/slab/slab-support.d.ts.map +1 -0
  164. package/dist/systems/slab/slab-support.js +557 -0
  165. package/dist/systems/stair/stair-opening-preview.d.ts +1060 -935
  166. package/dist/systems/stair/stair-opening-preview.d.ts.map +1 -1
  167. package/dist/systems/stair/stair-opening-sync.d.ts.map +1 -1
  168. package/dist/systems/stair/stair-opening-sync.js +18 -15
  169. package/dist/systems/stair/stair-opening-system.d.ts.map +1 -1
  170. package/dist/systems/stair/stair-opening-system.js +32 -4
  171. package/dist/systems/stair/stair-rise.d.ts +22 -0
  172. package/dist/systems/stair/stair-rise.d.ts.map +1 -0
  173. package/dist/systems/stair/stair-rise.js +79 -0
  174. package/dist/systems/wall/wall-curve.d.ts +10 -0
  175. package/dist/systems/wall/wall-curve.d.ts.map +1 -1
  176. package/dist/systems/wall/wall-curve.js +1 -1
  177. package/dist/systems/wall/wall-mitering.js +6 -6
  178. package/dist/systems/wall/wall-top.d.ts +40 -0
  179. package/dist/systems/wall/wall-top.d.ts.map +1 -0
  180. package/dist/systems/wall/wall-top.js +46 -0
  181. package/dist/utils/clone-scene-graph.d.ts.map +1 -1
  182. package/dist/utils/clone-scene-graph.js +34 -3
  183. package/dist/utils/heal-scene-graph.d.ts +1 -1
  184. package/dist/utils/heal-scene-graph.d.ts.map +1 -1
  185. package/dist/utils/heal-scene-graph.js +25 -9
  186. package/dist/validation/validate-build-json.d.ts +2 -0
  187. package/dist/validation/validate-build-json.d.ts.map +1 -1
  188. package/dist/validation/validate-build-json.js +59 -3
  189. package/package.json +2 -2
  190. package/dist/hooks/scene-registry/scene-registry.test.d.ts +0 -2
  191. package/dist/hooks/scene-registry/scene-registry.test.d.ts.map +0 -1
  192. package/dist/hooks/scene-registry/scene-registry.test.js +0 -26
  193. package/dist/hooks/spatial-grid/floor-placed-elevation.test.d.ts +0 -2
  194. package/dist/hooks/spatial-grid/floor-placed-elevation.test.d.ts.map +0 -1
  195. package/dist/hooks/spatial-grid/floor-placed-elevation.test.js +0 -366
  196. package/dist/hooks/spatial-grid/wall-slab-overlap.test.d.ts +0 -2
  197. package/dist/hooks/spatial-grid/wall-slab-overlap.test.d.ts.map +0 -1
  198. package/dist/hooks/spatial-grid/wall-slab-overlap.test.js +0 -363
  199. package/dist/lib/measurement-geometry.test.d.ts +0 -2
  200. package/dist/lib/measurement-geometry.test.d.ts.map +0 -1
  201. package/dist/lib/measurement-geometry.test.js +0 -102
  202. package/dist/lib/slab-polygon.test.d.ts +0 -2
  203. package/dist/lib/slab-polygon.test.d.ts.map +0 -1
  204. package/dist/lib/slab-polygon.test.js +0 -635
  205. package/dist/lib/space-detection-pause.test.d.ts +0 -2
  206. package/dist/lib/space-detection-pause.test.d.ts.map +0 -1
  207. package/dist/lib/space-detection-pause.test.js +0 -42
  208. package/dist/lib/space-detection.test.d.ts +0 -2
  209. package/dist/lib/space-detection.test.d.ts.map +0 -1
  210. package/dist/lib/space-detection.test.js +0 -376
  211. package/dist/lib/zone-quantities.test.d.ts +0 -2
  212. package/dist/lib/zone-quantities.test.d.ts.map +0 -1
  213. package/dist/lib/zone-quantities.test.js +0 -82
  214. package/dist/registry/registry.test.d.ts +0 -2
  215. package/dist/registry/registry.test.d.ts.map +0 -1
  216. package/dist/registry/registry.test.js +0 -196
  217. package/dist/registry/relations-resolver.test.d.ts +0 -2
  218. package/dist/registry/relations-resolver.test.d.ts.map +0 -1
  219. package/dist/registry/relations-resolver.test.js +0 -183
  220. package/dist/registry/scene-api.test.d.ts +0 -2
  221. package/dist/registry/scene-api.test.d.ts.map +0 -1
  222. package/dist/registry/scene-api.test.js +0 -183
  223. package/dist/registry/subtree.test.d.ts +0 -2
  224. package/dist/registry/subtree.test.d.ts.map +0 -1
  225. package/dist/registry/subtree.test.js +0 -163
  226. package/dist/schema/asset-url.test.d.ts +0 -2
  227. package/dist/schema/asset-url.test.d.ts.map +0 -1
  228. package/dist/schema/asset-url.test.js +0 -134
  229. package/dist/schema/material.test.d.ts +0 -2
  230. package/dist/schema/material.test.d.ts.map +0 -1
  231. package/dist/schema/material.test.js +0 -43
  232. package/dist/schema/nodes/measurement.test.d.ts +0 -2
  233. package/dist/schema/nodes/measurement.test.d.ts.map +0 -1
  234. package/dist/schema/nodes/measurement.test.js +0 -139
  235. package/dist/schema/nodes/ridge-vent-defaults.test.d.ts +0 -2
  236. package/dist/schema/nodes/ridge-vent-defaults.test.d.ts.map +0 -1
  237. package/dist/schema/nodes/ridge-vent-defaults.test.js +0 -205
  238. package/dist/schema/nodes/roof-segment-shape.test.d.ts +0 -2
  239. package/dist/schema/nodes/roof-segment-shape.test.d.ts.map +0 -1
  240. package/dist/schema/nodes/roof-segment-shape.test.js +0 -175
  241. package/dist/schema/nodes/roof-segment-surface.test.d.ts +0 -2
  242. package/dist/schema/nodes/roof-segment-surface.test.d.ts.map +0 -1
  243. package/dist/schema/nodes/roof-segment-surface.test.js +0 -48
  244. package/dist/schema/nodes/roof-segment-trim.test.d.ts +0 -2
  245. package/dist/schema/nodes/roof-segment-trim.test.d.ts.map +0 -1
  246. package/dist/schema/nodes/roof-segment-trim.test.js +0 -107
  247. package/dist/schema/nodes/roof-segment-walls.test.d.ts +0 -2
  248. package/dist/schema/nodes/roof-segment-walls.test.d.ts.map +0 -1
  249. package/dist/schema/nodes/roof-segment-walls.test.js +0 -92
  250. package/dist/schema/nodes/wall.test.d.ts +0 -2
  251. package/dist/schema/nodes/wall.test.d.ts.map +0 -1
  252. package/dist/schema/nodes/wall.test.js +0 -208
  253. package/dist/services/alignment-anchors.test.d.ts +0 -2
  254. package/dist/services/alignment-anchors.test.d.ts.map +0 -1
  255. package/dist/services/alignment-anchors.test.js +0 -383
  256. package/dist/services/alignment.test.d.ts +0 -2
  257. package/dist/services/alignment.test.d.ts.map +0 -1
  258. package/dist/services/alignment.test.js +0 -92
  259. package/dist/services/drag-session.test.d.ts +0 -2
  260. package/dist/services/drag-session.test.d.ts.map +0 -1
  261. package/dist/services/drag-session.test.js +0 -219
  262. package/dist/services/hosting.test.d.ts +0 -2
  263. package/dist/services/hosting.test.d.ts.map +0 -1
  264. package/dist/services/hosting.test.js +0 -230
  265. package/dist/services/movement.test.d.ts +0 -2
  266. package/dist/services/movement.test.d.ts.map +0 -1
  267. package/dist/services/movement.test.js +0 -133
  268. package/dist/services/opening-guides.test.d.ts +0 -2
  269. package/dist/services/opening-guides.test.d.ts.map +0 -1
  270. package/dist/services/opening-guides.test.js +0 -208
  271. package/dist/services/port-connectivity.test.d.ts +0 -2
  272. package/dist/services/port-connectivity.test.d.ts.map +0 -1
  273. package/dist/services/port-connectivity.test.js +0 -297
  274. package/dist/services/riser-diagram.test.d.ts +0 -2
  275. package/dist/services/riser-diagram.test.d.ts.map +0 -1
  276. package/dist/services/riser-diagram.test.js +0 -66
  277. package/dist/services/single-undo-dance.test.d.ts +0 -2
  278. package/dist/services/single-undo-dance.test.d.ts.map +0 -1
  279. package/dist/services/single-undo-dance.test.js +0 -210
  280. package/dist/services/snap.test.d.ts +0 -2
  281. package/dist/services/snap.test.d.ts.map +0 -1
  282. package/dist/services/snap.test.js +0 -137
  283. package/dist/services/system-graph.test.d.ts +0 -2
  284. package/dist/services/system-graph.test.d.ts.map +0 -1
  285. package/dist/services/system-graph.test.js +0 -138
  286. package/dist/services/validate-dwv.test.d.ts +0 -2
  287. package/dist/services/validate-dwv.test.d.ts.map +0 -1
  288. package/dist/services/validate-dwv.test.js +0 -109
  289. package/dist/store/actions/node-mutation-sanitize.test.d.ts +0 -2
  290. package/dist/store/actions/node-mutation-sanitize.test.d.ts.map +0 -1
  291. package/dist/store/actions/node-mutation-sanitize.test.js +0 -147
  292. package/dist/store/actions/reparent.test.d.ts +0 -2
  293. package/dist/store/actions/reparent.test.d.ts.map +0 -1
  294. package/dist/store/actions/reparent.test.js +0 -212
  295. package/dist/store/actions/ridge-vent-update.test.d.ts +0 -2
  296. package/dist/store/actions/ridge-vent-update.test.d.ts.map +0 -1
  297. package/dist/store/actions/ridge-vent-update.test.js +0 -264
  298. package/dist/store/use-scene-dirty-tracking.test.d.ts +0 -2
  299. package/dist/store/use-scene-dirty-tracking.test.d.ts.map +0 -1
  300. package/dist/store/use-scene-dirty-tracking.test.js +0 -68
  301. package/dist/store/use-scene-elevator-migration.test.d.ts +0 -2
  302. package/dist/store/use-scene-elevator-migration.test.d.ts.map +0 -1
  303. package/dist/store/use-scene-elevator-migration.test.js +0 -109
  304. package/dist/store/use-scene-measurement-roundtrip.test.d.ts +0 -2
  305. package/dist/store/use-scene-measurement-roundtrip.test.d.ts.map +0 -1
  306. package/dist/store/use-scene-measurement-roundtrip.test.js +0 -97
  307. package/dist/store/use-scene-plugins.test.d.ts +0 -2
  308. package/dist/store/use-scene-plugins.test.d.ts.map +0 -1
  309. package/dist/store/use-scene-plugins.test.js +0 -51
  310. package/dist/store/use-scene-wall-slot-migration.test.d.ts +0 -2
  311. package/dist/store/use-scene-wall-slot-migration.test.d.ts.map +0 -1
  312. package/dist/store/use-scene-wall-slot-migration.test.js +0 -210
  313. package/dist/store/use-scene-window-migration.test.d.ts +0 -2
  314. package/dist/store/use-scene-window-migration.test.d.ts.map +0 -1
  315. package/dist/store/use-scene-window-migration.test.js +0 -85
  316. package/dist/systems/elevator/elevator-opening-sync.test.d.ts +0 -2
  317. package/dist/systems/elevator/elevator-opening-sync.test.d.ts.map +0 -1
  318. package/dist/systems/elevator/elevator-opening-sync.test.js +0 -146
  319. package/dist/systems/elevator/elevator-runtime.test.d.ts +0 -2
  320. package/dist/systems/elevator/elevator-runtime.test.d.ts.map +0 -1
  321. package/dist/systems/elevator/elevator-runtime.test.js +0 -64
  322. package/dist/systems/fence/fence-spline.test.d.ts +0 -2
  323. package/dist/systems/fence/fence-spline.test.d.ts.map +0 -1
  324. package/dist/systems/fence/fence-spline.test.js +0 -136
  325. package/dist/systems/stair/stair-opening-preview.test.d.ts +0 -2
  326. package/dist/systems/stair/stair-opening-preview.test.d.ts.map +0 -1
  327. package/dist/systems/stair/stair-opening-preview.test.js +0 -80
  328. package/dist/systems/stair/stair-opening-sync.test.d.ts +0 -2
  329. package/dist/systems/stair/stair-opening-sync.test.d.ts.map +0 -1
  330. package/dist/systems/stair/stair-opening-sync.test.js +0 -485
  331. package/dist/systems/wall/wall-mitering.test.d.ts +0 -2
  332. package/dist/systems/wall/wall-mitering.test.d.ts.map +0 -1
  333. package/dist/systems/wall/wall-mitering.test.js +0 -63
  334. package/dist/utils/clone-scene-graph.test.d.ts +0 -2
  335. package/dist/utils/clone-scene-graph.test.d.ts.map +0 -1
  336. package/dist/utils/clone-scene-graph.test.js +0 -62
  337. package/dist/utils/heal-scene-graph.test.d.ts +0 -2
  338. package/dist/utils/heal-scene-graph.test.d.ts.map +0 -1
  339. package/dist/utils/heal-scene-graph.test.js +0 -88
@@ -16,10 +16,17 @@ import { getEffectiveStairSurfaceMaterial, StairNode as StairNodeSchema, } from
16
16
  import { StairSegmentNode as StairSegmentNodeSchema } from '../schema/nodes/stair-segment';
17
17
  import { getEffectiveWallSurfaceMaterial } from '../schema/nodes/wall';
18
18
  import { WindowNode as WindowNodeSchema } from '../schema/nodes/window';
19
- import { generateSceneMaterialId, } from '../schema/scene-material';
19
+ import { generateSceneMaterialId, SceneMaterial, } from '../schema/scene-material';
20
+ import { AnyNode as AnyNodeSchema } from '../schema/types';
21
+ import { deriveLegacyLevelHeight } from '../services/level-height';
22
+ import { getCeilingClampBound } from '../services/storey';
23
+ import { computeWallSlabSupport } from '../systems/slab/slab-support';
24
+ import { DEFAULT_WALL_HEIGHT } from '../systems/wall/wall-footprint';
20
25
  import { healSceneNodes } from '../utils/heal-scene-graph';
21
26
  import * as nodeActions from './actions/node-actions';
22
- import { resetSceneHistoryPauseDepth } from './history-control';
27
+ import { areSceneSnapshotsEqual, getSceneHistoryPauseDepth, notifySceneCommit, pauseSceneHistory, resetSceneHistoryPauseDepth, resumeSceneHistory, } from './history-control';
28
+ import useLiveNodeOverrides from './use-live-node-overrides';
29
+ import useLiveTransforms from './use-live-transforms';
23
30
  function getFiniteNumber(value, fallback) {
24
31
  return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
25
32
  }
@@ -52,6 +59,7 @@ function getVector3(value, fallback) {
52
59
  ];
53
60
  }
54
61
  function normalizeStairNode(node) {
62
+ const hasTotalRise = 'totalRise' in node;
55
63
  const sanitized = {
56
64
  ...node,
57
65
  position: getVector3(node.position, [0, 0, 0]),
@@ -62,7 +70,7 @@ function normalizeStairNode(node) {
62
70
  slabOpeningMode: getEnumValue(node.slabOpeningMode, ['none', 'destination'], 'none'),
63
71
  openingOffset: getFiniteNumber(node.openingOffset, 0),
64
72
  width: getFiniteNumber(node.width, 1),
65
- totalRise: getFiniteNumber(node.totalRise, 2.5),
73
+ totalRise: hasTotalRise ? getFiniteNumber(node.totalRise, 2.5) : undefined,
66
74
  stepCount: getFiniteNumber(node.stepCount, 10),
67
75
  thickness: getFiniteNumber(node.thickness, 0.25),
68
76
  fillToFloor: getBoolean(node.fillToFloor, true),
@@ -77,7 +85,15 @@ function normalizeStairNode(node) {
77
85
  children: getStringArray(node.children),
78
86
  };
79
87
  const parsed = StairNodeSchema.safeParse(sanitized);
80
- return parsed.success ? parsed.data : null;
88
+ if (!parsed.success)
89
+ return null;
90
+ if (hasTotalRise)
91
+ return parsed.data;
92
+ // Absent `totalRise` means "rise derives from the storey height" and must
93
+ // survive the load: safeParse echoes the sanitized explicit-undefined key,
94
+ // which would flip `'totalRise' in node` checks — strip it back off.
95
+ const { totalRise: _totalRise, ...rest } = parsed.data;
96
+ return rest;
81
97
  }
82
98
  function normalizeStairSegmentNode(node) {
83
99
  const sanitized = {
@@ -434,11 +450,74 @@ function migrateRoofSurfaceMaterials(node) {
434
450
  }
435
451
  return next;
436
452
  }
453
+ function migrateConstructionDimension(node) {
454
+ const drawingOverrides = Array.isArray(node.drawingOverrides) ? node.drawingOverrides : [];
455
+ const hasLegacyDrawingOverride = drawingOverrides.some((entry) => entry &&
456
+ typeof entry === 'object' &&
457
+ !Array.isArray(entry) &&
458
+ entry.presentation === 'reference');
459
+ if (!('reference' in node || 'referenceStyle' in node || hasLegacyDrawingOverride))
460
+ return node;
461
+ const { reference: _reference, referenceStyle: _referenceStyle, ...dimension } = node;
462
+ return {
463
+ ...dimension,
464
+ drawingOverrides: drawingOverrides.map((entry) => {
465
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry))
466
+ return entry;
467
+ return entry.presentation === 'reference' ? { ...entry, presentation: 'shown' } : entry;
468
+ }),
469
+ };
470
+ }
471
+ function removeRetiredDrawingSheets(nodes) {
472
+ const retiredIds = new Set(Object.entries(nodes)
473
+ .filter(([, node]) => node?.type === 'drawing-sheet')
474
+ .map(([id]) => id));
475
+ if (retiredIds.size === 0)
476
+ return;
477
+ for (const id of retiredIds)
478
+ delete nodes[id];
479
+ for (const [id, node] of Object.entries(nodes)) {
480
+ if (!Array.isArray(node?.children))
481
+ continue;
482
+ const children = getStringArray(node.children);
483
+ if (!children.some((childId) => retiredIds.has(childId)))
484
+ continue;
485
+ nodes[id] = {
486
+ ...node,
487
+ children: children.filter((childId) => !retiredIds.has(childId)),
488
+ };
489
+ }
490
+ }
491
+ function migrateWallAssembly(node) {
492
+ if (!Object.hasOwn(node, 'assemblyLayers'))
493
+ return node;
494
+ const assemblyThickness = Array.isArray(node.assemblyLayers)
495
+ ? node.assemblyLayers.reduce((total, layer) => {
496
+ if (!(layer && typeof layer === 'object'))
497
+ return total;
498
+ const thickness = layer.thickness;
499
+ return typeof thickness === 'number' && Number.isFinite(thickness) && thickness > 0
500
+ ? total + thickness
501
+ : total;
502
+ }, 0)
503
+ : 0;
504
+ const { assemblyLayers: _assemblyLayers, ...wall } = node;
505
+ return assemblyThickness > 0 ? { ...wall, thickness: assemblyThickness } : wall;
506
+ }
507
+ // Walls whose top lands within this of the storey plane become plane-bound;
508
+ // ceilings whose stored height lands within this of their clamp bound become
509
+ // follows-mode (step 3f) — same census-backed threshold for both.
510
+ // From a prod census: the 0.15-short "hole pattern" (default 2.5 walls next to
511
+ // a taller wall) must snap to the plane, while intentional 0.20-short walls
512
+ // (2.5 under a 2.7 plane, 2.3 under a 2.5 plane) must keep their explicit
513
+ // height — hence 0.20 with a strictly-less-than comparison.
514
+ const PLANE_BOUND_EPSILON = 0.2;
437
515
  function migrateNodes(nodes) {
438
516
  // Repair pre-existing corruption (null children, zero-length walls) before
439
517
  // any per-type migration runs, so already-saved scenes load cleanly.
440
518
  const { nodes: healed } = healSceneNodes(nodes);
441
519
  const patchedNodes = { ...healed };
520
+ removeRetiredDrawingSheets(patchedNodes);
442
521
  // Scene materials minted while moving legacy wall fields onto `node.slots`;
443
522
  // merged into the scene material map by the caller (`setScene`).
444
523
  const mintedMaterials = {};
@@ -530,6 +609,9 @@ function migrateNodes(nodes) {
530
609
  patchedNodes[id] = normalized;
531
610
  }
532
611
  }
612
+ if (node.type === 'construction-dimension') {
613
+ patchedNodes[id] = migrateConstructionDimension(node);
614
+ }
533
615
  if (node.type === 'stair') {
534
616
  const normalized = normalizeStairNode(migrateStairSurfaceMaterials(node));
535
617
  if (normalized) {
@@ -544,7 +626,7 @@ function migrateNodes(nodes) {
544
626
  }
545
627
  }
546
628
  if (node.type === 'wall') {
547
- patchedNodes[id] = migrateWallSurfaceMaterials(patchedNodes[id], mintedMaterials);
629
+ patchedNodes[id] = migrateWallSurfaceMaterials(migrateWallAssembly(patchedNodes[id]), mintedMaterials);
548
630
  }
549
631
  // Cabinet v2→v3: node-level `doorStyle` was dead (geometry reads only the
550
632
  // per-compartment `doorType`) and was removed from both cabinet schemas;
@@ -709,6 +791,154 @@ function migrateNodes(nodes) {
709
791
  patchedNodes[id] = normalized;
710
792
  }
711
793
  }
794
+ // Pass 3: vertical building model.
795
+ // A level without `height` marks a scene saved before the vertical model
796
+ // landed. Computed before this pass mutates anything: the stair-rise
797
+ // cleanup below must never run on already-migrated scenes.
798
+ const isLegacyScene = Object.values(patchedNodes).some((node) => node?.type === 'level' && !('height' in node));
799
+ // 3a. Ordinal renumber — always runs, per building (idempotent
800
+ // self-healing; MCP's create-level historically wrote its elevation PARAM
801
+ // into the ordinal, so fractional/duplicate ordinals exist in the wild).
802
+ const buildingNodes = Object.values(patchedNodes).filter((node) => node?.type === 'building');
803
+ const levelsByBuilding = new Map();
804
+ for (const [id, node] of Object.entries(patchedNodes)) {
805
+ if (node?.type !== 'level')
806
+ continue;
807
+ // Mirrors the building resolution in services/storey.ts: an explicit
808
+ // parentId pointing at a building wins, membership in a building's
809
+ // children array is the legacy fallback, and unresolvable levels share
810
+ // one orphan bucket.
811
+ const buildingId = buildingNodes.find((building) => building.id === node.parentId)?.id ??
812
+ buildingNodes.find((building) => getStringArray(building.children).includes(id))?.id ??
813
+ null;
814
+ const bucket = levelsByBuilding.get(buildingId) ?? [];
815
+ bucket.push({ id, ordinal: getFiniteNumber(node.level, 0) });
816
+ levelsByBuilding.set(buildingId, bucket);
817
+ }
818
+ for (const bucket of levelsByBuilding.values()) {
819
+ // Anchored at zero on purpose: ordinals are semantic — `level < 0`
820
+ // renders "Basement N" and `level === 0` is the ground-floor default —
821
+ // so negatives compact upward toward −1 and non-negatives compact down
822
+ // to 0. A blind 0..n renumber would rename basements.
823
+ const sorted = [...bucket].sort((a, b) => a.ordinal - b.ordinal);
824
+ const negativeCount = sorted.filter((entry) => entry.ordinal < 0).length;
825
+ sorted.forEach((entry, index) => {
826
+ const nextOrdinal = index - negativeCount;
827
+ const current = patchedNodes[entry.id];
828
+ if (current.level !== nextOrdinal) {
829
+ patchedNodes[entry.id] = { ...current, level: nextOrdinal };
830
+ }
831
+ });
832
+ }
833
+ // 3b. Stored storey heights: materialize the legacy stacked height verbatim
834
+ // (never rounded or snapped — snapping would move existing buildings).
835
+ // All planes derive before any wall height below mutates.
836
+ const legacyLevelIds = Object.entries(patchedNodes)
837
+ .filter(([, node]) => node?.type === 'level' && !('height' in node))
838
+ .map(([id]) => id);
839
+ const derivedHeights = new Map();
840
+ for (const levelId of legacyLevelIds) {
841
+ derivedHeights.set(levelId, deriveLegacyLevelHeight(levelId, patchedNodes));
842
+ }
843
+ for (const levelId of legacyLevelIds) {
844
+ const plane = derivedHeights.get(levelId);
845
+ const level = patchedNodes[levelId];
846
+ patchedNodes[levelId] = { ...level, height: plane };
847
+ // 3c. Wall-top classification against the just-written plane, using the
848
+ // same slab-support election as deriveLegacyLevelHeight (call shape
849
+ // mirrored from services/level-height.ts). Walls whose top meets the
850
+ // plane drop their explicit height and follow the level from now on;
851
+ // walls ending short (or tall) keep an explicit height — materializing
852
+ // the 2.5 default onto absent-height walls that end short of the plane.
853
+ const children = getStringArray(level.children)
854
+ .map((childId) => patchedNodes[childId])
855
+ .filter((child) => child !== undefined);
856
+ const slabs = children.filter((child) => child.type === 'slab');
857
+ const walls = children.filter((child) => child.type === 'wall');
858
+ for (const wall of walls) {
859
+ const electedBase = computeWallSlabSupport({
860
+ start: wall.start,
861
+ end: wall.end,
862
+ curveOffset: wall.curveOffset,
863
+ thickness: wall.thickness,
864
+ }, slabs, walls).elevation;
865
+ const effectiveHeight = wall.height ?? DEFAULT_WALL_HEIGHT;
866
+ const top = Math.max(0, electedBase) + effectiveHeight;
867
+ if (Math.abs(plane - top) < PLANE_BOUND_EPSILON) {
868
+ if ('height' in wall) {
869
+ const { height: _height, ...planeBound } = wall;
870
+ patchedNodes[wall.id] = planeBound;
871
+ }
872
+ }
873
+ else {
874
+ patchedNodes[wall.id] = { ...wall, height: effectiveHeight };
875
+ }
876
+ }
877
+ }
878
+ // 3d. Stair rise: on legacy scenes a totalRise of exactly 2.5 is the old
879
+ // schema default, not a user choice — drop it so the rise derives from the
880
+ // storey height. Gated on isLegacyScene because on a post-migration scene
881
+ // a stored 2.5 IS a deliberately typed value and must survive reloads.
882
+ if (isLegacyScene) {
883
+ for (const [id, node] of Object.entries(patchedNodes)) {
884
+ if (node?.type !== 'stair')
885
+ continue;
886
+ if (node.totalRise !== 2.5)
887
+ continue;
888
+ const { totalRise: _totalRise, ...derivedRise } = node;
889
+ patchedNodes[id] = derivedRise;
890
+ }
891
+ }
892
+ // 3e. Slab placement/thickness split. `elevation` stays the walking surface;
893
+ // the new `thickness` grows downward so the solid occupies
894
+ // [elevation − thickness, elevation]. Legacy solids extruded [0, elevation],
895
+ // so thickness = elevation EXACTLY (including degenerate 0 — MIN_SLAB_THICKNESS
896
+ // applies to edits only, never here) keeps the occupied interval identical.
897
+ // Legacy pools (elevation < 0) become explicit `recessed` intent with
898
+ // elevation unchanged. Gated per slab on a missing `thickness` — the
899
+ // migration output is cast, so schema defaults never materialize on load.
900
+ for (const [id, node] of Object.entries(patchedNodes)) {
901
+ if (node?.type !== 'slab' || 'thickness' in node)
902
+ continue;
903
+ const elevation = getFiniteNumber(node.elevation, 0.05);
904
+ patchedNodes[id] =
905
+ elevation < 0
906
+ ? { ...node, thickness: 0.05, recessed: true }
907
+ : { ...node, thickness: elevation };
908
+ }
909
+ // 3f. Ceiling follows-mode classification (the ceiling mirror of 3c; runs
910
+ // after 3b/3e so the clamp bound sees stored level heights and split slab
911
+ // thicknesses). A stored ceiling height within PLANE_BOUND_EPSILON of its
912
+ // clamp bound (min(storey plane, covering-slab underside) − margin, via
913
+ // getCeilingClampBound) is the legacy default tracking the level top, not
914
+ // a choice — drop it so the ceiling follows the level from now on.
915
+ // autoFromWalls ceilings always convert: their height was derived by the
916
+ // space-detection sync, never user intent. Gated on isLegacyScene, which
917
+ // is exact — nothing shipped between the level-height migration and this
918
+ // one — and makes the step idempotent. Known accepted edge: a
919
+ // post-migration user typing a custom height exactly equal to the bound
920
+ // keeps it (the gate prevents re-classification on later loads).
921
+ if (isLegacyScene) {
922
+ for (const [id, node] of Object.entries(patchedNodes)) {
923
+ if (node?.type !== 'ceiling' || !('height' in node))
924
+ continue;
925
+ const dropHeight = () => {
926
+ const { height: _height, ...follows } = node;
927
+ patchedNodes[id] = follows;
928
+ };
929
+ if (node.autoFromWalls === true) {
930
+ dropHeight();
931
+ continue;
932
+ }
933
+ if (typeof node.parentId !== 'string')
934
+ continue;
935
+ const bound = getCeilingClampBound(node.parentId, patchedNodes, Array.isArray(node.polygon) ? node.polygon : []);
936
+ const stored = getFiniteNumber(node.height, Number.NaN);
937
+ if (Number.isFinite(bound) && Math.abs(stored - bound) < PLANE_BOUND_EPSILON) {
938
+ dropHeight();
939
+ }
940
+ }
941
+ }
712
942
  return { nodes: patchedNodes, mintedMaterials };
713
943
  }
714
944
  function getNodeChildIds(node) {
@@ -759,6 +989,10 @@ function collectReachableNodeIds(nodes, rootNodeIds) {
759
989
  }
760
990
  return reachable;
761
991
  }
992
+ function sceneHistorySnapshotFromState(state) {
993
+ const { nodes, rootNodeIds, collections, materials, installedPlugins } = state;
994
+ return { nodes, rootNodeIds, collections, materials, installedPlugins };
995
+ }
762
996
  const useScene = create()(temporal((set, get) => ({
763
997
  // 1. Flat dictionary of all nodes
764
998
  nodes: {},
@@ -870,6 +1104,7 @@ const useScene = create()(temporal((set, get) => ({
870
1104
  const level0 = LevelNode.parse({
871
1105
  level: 0,
872
1106
  children: [],
1107
+ height: 2.5,
873
1108
  });
874
1109
  const building = BuildingNode.parse({
875
1110
  children: [level0.id],
@@ -1032,13 +1267,408 @@ const useScene = create()(temporal((set, get) => ({
1032
1267
  });
1033
1268
  },
1034
1269
  }), {
1035
- partialize: (state) => {
1036
- const { nodes, rootNodeIds, collections, materials, installedPlugins } = state;
1037
- return { nodes, rootNodeIds, collections, materials, installedPlugins };
1270
+ partialize: (state) => sceneHistorySnapshotFromState(state),
1271
+ equality: (pastState, currentState) => areSceneSnapshotsEqual(pastState, currentState),
1272
+ onSave: (pastState, currentState) => {
1273
+ notifySceneCommit({
1274
+ origin: 'local',
1275
+ before: sceneHistorySnapshotFromState(pastState),
1276
+ current: sceneHistorySnapshotFromState(currentState),
1277
+ });
1038
1278
  },
1039
1279
  limit: 50, // Limit to last 50 actions
1040
1280
  }));
1041
1281
  export default useScene;
1282
+ let sceneReadOnlyLeaseCount = 0;
1283
+ let sceneReadOnlyLeaseBaseline = false;
1284
+ export function acquireSceneReadOnlyLease() {
1285
+ if (sceneReadOnlyLeaseCount === 0) {
1286
+ sceneReadOnlyLeaseBaseline = useScene.getState().readOnly;
1287
+ }
1288
+ sceneReadOnlyLeaseCount += 1;
1289
+ useScene.setState({ readOnly: true });
1290
+ let released = false;
1291
+ return () => {
1292
+ if (released)
1293
+ return;
1294
+ released = true;
1295
+ sceneReadOnlyLeaseCount = Math.max(0, sceneReadOnlyLeaseCount - 1);
1296
+ if (sceneReadOnlyLeaseCount > 0)
1297
+ return;
1298
+ useScene.setState({ readOnly: sceneReadOnlyLeaseBaseline });
1299
+ sceneReadOnlyLeaseBaseline = false;
1300
+ };
1301
+ }
1302
+ function sceneOperationPatchLiveConflictIds(beforeState, changes) {
1303
+ const ids = new Set();
1304
+ const addNodeAndParent = (node) => {
1305
+ if (!node)
1306
+ return;
1307
+ ids.add(node.id);
1308
+ if (node.parentId)
1309
+ ids.add(node.parentId);
1310
+ };
1311
+ for (const { id, data } of changes.nodeUpdates) {
1312
+ ids.add(id);
1313
+ if (Object.hasOwn(data, 'parentId')) {
1314
+ const currentParentId = beforeState.nodes[id]?.parentId;
1315
+ if (currentParentId)
1316
+ ids.add(currentParentId);
1317
+ if (typeof data.parentId === 'string')
1318
+ ids.add(data.parentId);
1319
+ }
1320
+ }
1321
+ for (const { node } of changes.nodeCreates)
1322
+ addNodeAndParent(node);
1323
+ for (const { node } of changes.nodeDeletes)
1324
+ addNodeAndParent(node);
1325
+ return ids;
1326
+ }
1327
+ function sceneOperationPatchHasLiveConflict(beforeState, changes) {
1328
+ const overrides = useLiveNodeOverrides.getState();
1329
+ const transforms = useLiveTransforms.getState();
1330
+ for (const id of sceneOperationPatchLiveConflictIds(beforeState, changes)) {
1331
+ if (overrides.get(id) || transforms.get(id))
1332
+ return true;
1333
+ }
1334
+ return false;
1335
+ }
1336
+ function areScenePatchValuesEqual(left, right) {
1337
+ if (Object.is(left, right))
1338
+ return true;
1339
+ if (typeof left !== typeof right || left === null || right === null)
1340
+ return false;
1341
+ if (Array.isArray(left) || Array.isArray(right)) {
1342
+ return (Array.isArray(left) &&
1343
+ Array.isArray(right) &&
1344
+ left.length === right.length &&
1345
+ left.every((value, index) => areScenePatchValuesEqual(value, right[index])));
1346
+ }
1347
+ if (typeof left !== 'object' || typeof right !== 'object')
1348
+ return false;
1349
+ const leftRecord = left;
1350
+ const rightRecord = right;
1351
+ const leftKeys = Object.keys(leftRecord);
1352
+ if (leftKeys.length !== Object.keys(rightRecord).length)
1353
+ return false;
1354
+ return leftKeys.every((key) => Object.hasOwn(rightRecord, key) &&
1355
+ areScenePatchValuesEqual(leftRecord[key], rightRecord[key]));
1356
+ }
1357
+ function parseSceneOperationPatchNode(value) {
1358
+ const builtin = AnyNodeSchema.safeParse(value);
1359
+ if (builtin.success)
1360
+ return builtin.data;
1361
+ if (!(value && typeof value === 'object' && !Array.isArray(value)))
1362
+ return null;
1363
+ const type = value.type;
1364
+ if (typeof type !== 'string')
1365
+ return null;
1366
+ const registered = nodeRegistry.get(type)?.schema.safeParse(value);
1367
+ return registered?.success ? registered.data : null;
1368
+ }
1369
+ function structuralSiblingIds(nodes, rootNodeIds, parentId) {
1370
+ if (!parentId)
1371
+ return rootNodeIds;
1372
+ const parent = nodes[parentId];
1373
+ if (!(parent && 'children' in parent && Array.isArray(parent.children)))
1374
+ return null;
1375
+ return parent.children.every((id) => typeof id === 'string')
1376
+ ? parent.children
1377
+ : null;
1378
+ }
1379
+ function insertSceneStructuralPlacements(base, placements) {
1380
+ if (placements.length === 0)
1381
+ return base;
1382
+ const result = new Array(base.length + placements.length);
1383
+ for (const change of placements) {
1384
+ if (!Number.isSafeInteger(change.position) ||
1385
+ change.position < 0 ||
1386
+ change.position >= result.length ||
1387
+ result[change.position] !== undefined) {
1388
+ return null;
1389
+ }
1390
+ result[change.position] = change.node.id;
1391
+ }
1392
+ let baseIndex = 0;
1393
+ for (let index = 0; index < result.length; index += 1) {
1394
+ if (result[index] !== undefined)
1395
+ continue;
1396
+ result[index] = base[baseIndex];
1397
+ baseIndex += 1;
1398
+ }
1399
+ return result;
1400
+ }
1401
+ function sceneOperationPatchNextState(beforeState, changes) {
1402
+ const createIds = new Set();
1403
+ const deleteIds = new Set();
1404
+ const updateIds = new Set();
1405
+ const materialIds = new Set();
1406
+ const parsedCreates = [];
1407
+ for (const change of changes.nodeCreates) {
1408
+ const parsed = parseSceneOperationPatchNode(change.node);
1409
+ if (!parsed ||
1410
+ parsed.id !== change.node.id ||
1411
+ createIds.has(parsed.id) ||
1412
+ Object.hasOwn(beforeState.nodes, parsed.id) ||
1413
+ !Number.isSafeInteger(change.position) ||
1414
+ change.position < 0) {
1415
+ return null;
1416
+ }
1417
+ createIds.add(parsed.id);
1418
+ parsedCreates.push({ node: change.node, position: change.position });
1419
+ }
1420
+ for (const change of changes.nodeDeletes) {
1421
+ const id = change.node.id;
1422
+ const current = beforeState.nodes[id];
1423
+ const parentId = change.node.parentId ?? null;
1424
+ const siblings = structuralSiblingIds(beforeState.nodes, beforeState.rootNodeIds, parentId);
1425
+ if (!current ||
1426
+ createIds.has(id) ||
1427
+ deleteIds.has(id) ||
1428
+ !Number.isSafeInteger(change.position) ||
1429
+ change.position < 0 ||
1430
+ siblings?.[change.position] !== id ||
1431
+ !areScenePatchValuesEqual(current, change.node)) {
1432
+ return null;
1433
+ }
1434
+ deleteIds.add(id);
1435
+ }
1436
+ for (const id of createIds) {
1437
+ if (deleteIds.has(id))
1438
+ return null;
1439
+ }
1440
+ for (const node of Object.values(beforeState.nodes)) {
1441
+ const parentId = node.parentId ?? null;
1442
+ if (parentId && deleteIds.has(parentId) && !deleteIds.has(node.id))
1443
+ return null;
1444
+ }
1445
+ const nextNodes = { ...beforeState.nodes };
1446
+ let nextRootNodeIds = deleteIds.size > 0
1447
+ ? beforeState.rootNodeIds.filter((id) => !deleteIds.has(id))
1448
+ : beforeState.rootNodeIds;
1449
+ const changedParentIds = new Set();
1450
+ for (const change of changes.nodeDeletes) {
1451
+ const parentId = change.node.parentId ?? null;
1452
+ if (parentId && !deleteIds.has(parentId))
1453
+ changedParentIds.add(parentId);
1454
+ delete nextNodes[change.node.id];
1455
+ }
1456
+ for (const parentId of changedParentIds) {
1457
+ const parent = nextNodes[parentId];
1458
+ if (!(parent && 'children' in parent && Array.isArray(parent.children)))
1459
+ return null;
1460
+ nextNodes[parentId] = {
1461
+ ...parent,
1462
+ children: parent.children.filter((id) => !deleteIds.has(id)),
1463
+ };
1464
+ }
1465
+ for (const change of parsedCreates)
1466
+ nextNodes[change.node.id] = change.node;
1467
+ const rootCreates = [];
1468
+ const existingParentCreates = new Map();
1469
+ for (const change of parsedCreates) {
1470
+ const parentId = change.node.parentId ?? null;
1471
+ if (!parentId) {
1472
+ rootCreates.push(change);
1473
+ continue;
1474
+ }
1475
+ const parent = nextNodes[parentId];
1476
+ if (!parent)
1477
+ return null;
1478
+ if (createIds.has(parentId)) {
1479
+ if (!('children' in parent) ||
1480
+ !Array.isArray(parent.children) ||
1481
+ parent.children[change.position] !== change.node.id) {
1482
+ return null;
1483
+ }
1484
+ continue;
1485
+ }
1486
+ const placements = existingParentCreates.get(parentId) ?? [];
1487
+ placements.push(change);
1488
+ existingParentCreates.set(parentId, placements);
1489
+ }
1490
+ const insertedRoots = insertSceneStructuralPlacements(nextRootNodeIds, rootCreates);
1491
+ if (!insertedRoots)
1492
+ return null;
1493
+ nextRootNodeIds = insertedRoots;
1494
+ for (const [parentId, placements] of existingParentCreates) {
1495
+ const parent = nextNodes[parentId];
1496
+ if (!(parent && 'children' in parent && Array.isArray(parent.children)))
1497
+ return null;
1498
+ const children = insertSceneStructuralPlacements(parent.children, placements);
1499
+ if (!children)
1500
+ return null;
1501
+ nextNodes[parentId] = { ...parent, children };
1502
+ }
1503
+ for (const change of parsedCreates) {
1504
+ const parentId = change.node.parentId ?? null;
1505
+ const siblings = structuralSiblingIds(nextNodes, nextRootNodeIds, parentId);
1506
+ if (siblings?.[change.position] !== change.node.id)
1507
+ return null;
1508
+ if (!('children' in change.node && Array.isArray(change.node.children)))
1509
+ continue;
1510
+ for (const childId of change.node.children) {
1511
+ if (nextNodes[childId]?.parentId !== change.node.id)
1512
+ return null;
1513
+ }
1514
+ }
1515
+ for (const { id, data, removeFields } of changes.nodeUpdates) {
1516
+ const node = nextNodes[id];
1517
+ if (!node ||
1518
+ createIds.has(id) ||
1519
+ deleteIds.has(id) ||
1520
+ updateIds.has(id) ||
1521
+ ('id' in data && data.id !== node.id) ||
1522
+ ('type' in data && data.type !== node.type) ||
1523
+ ('object' in data && data.object !== node.object) ||
1524
+ removeFields.some((field) => field === 'id' || field === 'object' || field === 'type' || Object.hasOwn(data, field))) {
1525
+ return null;
1526
+ }
1527
+ updateIds.add(id);
1528
+ const candidate = { ...node, ...data };
1529
+ for (const field of removeFields)
1530
+ delete candidate[field];
1531
+ const validated = parseSceneOperationPatchNode(candidate);
1532
+ if (!validated ||
1533
+ validated.id !== id ||
1534
+ validated.type !== node.type ||
1535
+ validated.object !== node.object) {
1536
+ return null;
1537
+ }
1538
+ nextNodes[id] = candidate;
1539
+ }
1540
+ const materials = changes.materialChanges.length > 0 ? { ...beforeState.materials } : beforeState.materials;
1541
+ for (const { id, material } of changes.materialChanges) {
1542
+ if (materialIds.has(id) ||
1543
+ (material !== null && (material.id !== id || !SceneMaterial.safeParse(material).success))) {
1544
+ return null;
1545
+ }
1546
+ materialIds.add(id);
1547
+ if (material === null)
1548
+ delete materials[id];
1549
+ else
1550
+ materials[id] = material;
1551
+ }
1552
+ return { materials, nodes: nextNodes, rootNodeIds: nextRootNodeIds };
1553
+ }
1554
+ export function applySceneOperationPatch(changes) {
1555
+ const beforeState = useScene.getState();
1556
+ if (changes.nodeUpdates.length === 0 &&
1557
+ changes.materialChanges.length === 0 &&
1558
+ changes.nodeCreates.length === 0 &&
1559
+ changes.nodeDeletes.length === 0) {
1560
+ return false;
1561
+ }
1562
+ if (sceneOperationPatchHasLiveConflict(beforeState, changes))
1563
+ return false;
1564
+ const next = sceneOperationPatchNextState(beforeState, changes);
1565
+ if (!next)
1566
+ return false;
1567
+ const before = sceneHistorySnapshotFromState(beforeState);
1568
+ const shouldScopeHistoryPause = useScene.temporal.getState().isTracking || getSceneHistoryPauseDepth() > 0;
1569
+ if (shouldScopeHistoryPause)
1570
+ pauseSceneHistory(useScene);
1571
+ try {
1572
+ useScene.setState(next);
1573
+ }
1574
+ finally {
1575
+ if (shouldScopeHistoryPause)
1576
+ resumeSceneHistory(useScene);
1577
+ }
1578
+ const currentState = useScene.getState();
1579
+ const current = sceneHistorySnapshotFromState(currentState);
1580
+ const touchedNodeIds = new Set([
1581
+ ...changes.nodeUpdates.map(({ id }) => id),
1582
+ ...changes.nodeCreates.map(({ node }) => node.id),
1583
+ ...changes.nodeDeletes.map(({ node }) => node.id),
1584
+ ]);
1585
+ for (const id of touchedNodeIds) {
1586
+ useLiveNodeOverrides.getState().clear(id);
1587
+ useLiveTransforms.getState().clear(id);
1588
+ }
1589
+ if (areSceneSnapshotsEqual(before, current))
1590
+ return false;
1591
+ for (const id of touchedNodeIds) {
1592
+ if (current.nodes[id])
1593
+ currentState.markDirty(id);
1594
+ else
1595
+ currentState.clearDirty(id);
1596
+ const beforeParentId = before.nodes[id]?.parentId;
1597
+ const currentParentId = current.nodes[id]?.parentId;
1598
+ if (beforeParentId)
1599
+ currentState.markDirty(beforeParentId);
1600
+ if (currentParentId)
1601
+ currentState.markDirty(currentParentId);
1602
+ }
1603
+ const structuralParentIds = new Set();
1604
+ for (const { node } of changes.nodeCreates) {
1605
+ if (node.parentId)
1606
+ structuralParentIds.add(node.parentId);
1607
+ }
1608
+ for (const { node } of changes.nodeDeletes) {
1609
+ if (node.parentId)
1610
+ structuralParentIds.add(node.parentId);
1611
+ }
1612
+ for (const parentId of structuralParentIds) {
1613
+ const parent = current.nodes[parentId];
1614
+ if (!(parent && 'children' in parent && Array.isArray(parent.children)))
1615
+ continue;
1616
+ for (const childId of parent.children)
1617
+ currentState.markDirty(childId);
1618
+ }
1619
+ if (changes.materialChanges.length > 0) {
1620
+ const materialRefs = new Set(changes.materialChanges.map(({ id }) => toSceneMaterialRef(id)));
1621
+ for (const node of Object.values(current.nodes)) {
1622
+ const slots = 'slots' in node ? node.slots : undefined;
1623
+ if (!(slots && Object.values(slots).some((ref) => materialRefs.has(ref))))
1624
+ continue;
1625
+ currentState.markDirty(node.id);
1626
+ if (node.parentId)
1627
+ currentState.markDirty(node.parentId);
1628
+ }
1629
+ }
1630
+ for (const { node } of changes.nodeDeletes)
1631
+ currentState.clearDirty(node.id);
1632
+ notifySceneCommit({
1633
+ origin: 'host',
1634
+ before,
1635
+ current,
1636
+ });
1637
+ return true;
1638
+ }
1639
+ export function applyScenePatch(changes) {
1640
+ return applySceneOperationPatch({
1641
+ ...changes,
1642
+ nodeCreates: [],
1643
+ nodeDeletes: [],
1644
+ });
1645
+ }
1646
+ export function applySceneSnapshot(snapshot, options) {
1647
+ const before = sceneHistorySnapshotFromState(useScene.getState());
1648
+ const temporalState = useScene.temporal.getState();
1649
+ if (!temporalState.isTracking || getSceneHistoryPauseDepth() > 0) {
1650
+ throw new Error('Cannot replace the scene snapshot during an active interaction');
1651
+ }
1652
+ pauseSceneHistory(useScene);
1653
+ try {
1654
+ useScene.getState().setScene(snapshot.nodes, snapshot.rootNodeIds, {
1655
+ collections: snapshot.collections,
1656
+ installedPlugins: snapshot.installedPlugins,
1657
+ materials: snapshot.materials,
1658
+ });
1659
+ useScene.temporal.getState().clear();
1660
+ }
1661
+ finally {
1662
+ resumeSceneHistory(useScene);
1663
+ }
1664
+ useLiveNodeOverrides.getState().clearAll();
1665
+ useLiveTransforms.getState().clearAll();
1666
+ const current = sceneHistorySnapshotFromState(useScene.getState());
1667
+ if (areSceneSnapshotsEqual(before, current))
1668
+ return false;
1669
+ notifySceneCommit({ origin: options.origin, before, current });
1670
+ return true;
1671
+ }
1042
1672
  // Track previous temporal state lengths and node snapshot for diffing
1043
1673
  let prevPastLength = 0;
1044
1674
  let prevFutureLength = 0;