@pascal-app/core 1.0.0-beta.5 → 1.0.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 (417) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +23 -0
  3. package/dist/capture/index.d.ts +3 -0
  4. package/dist/capture/index.d.ts.map +1 -0
  5. package/dist/capture/index.js +2 -0
  6. package/dist/capture/schema.d.ts +417 -0
  7. package/dist/capture/schema.d.ts.map +1 -0
  8. package/dist/capture/schema.js +350 -0
  9. package/dist/capture/source.d.ts +60 -0
  10. package/dist/capture/source.d.ts.map +1 -0
  11. package/dist/capture/source.js +254 -0
  12. package/dist/events/bus.d.ts +58 -7
  13. package/dist/events/bus.d.ts.map +1 -1
  14. package/dist/events/hidden-wall-pointer-hold.d.ts +28 -0
  15. package/dist/events/hidden-wall-pointer-hold.d.ts.map +1 -0
  16. package/dist/events/hidden-wall-pointer-hold.js +37 -0
  17. package/dist/hooks/spatial-grid/floor-placed-elevation.d.ts +4 -4
  18. package/dist/hooks/spatial-grid/floor-placed-elevation.d.ts.map +1 -1
  19. package/dist/hooks/spatial-grid/floor-placed-elevation.js +7 -12
  20. package/dist/hooks/spatial-grid/floor-placed-footprints.d.ts +4 -0
  21. package/dist/hooks/spatial-grid/floor-placed-footprints.d.ts.map +1 -0
  22. package/dist/hooks/spatial-grid/floor-placed-footprints.js +7 -0
  23. package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts +5 -8
  24. package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts.map +1 -1
  25. package/dist/hooks/spatial-grid/spatial-grid-manager.js +23 -106
  26. package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts +2931 -2
  27. package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts.map +1 -1
  28. package/dist/hooks/spatial-grid/spatial-grid-sync.js +207 -30
  29. package/dist/hooks/spatial-grid/support-host-patch.d.ts +45 -1
  30. package/dist/hooks/spatial-grid/support-host-patch.d.ts.map +1 -1
  31. package/dist/hooks/spatial-grid/support-host-patch.js +174 -9
  32. package/dist/hooks/spatial-grid/use-spatial-query.d.ts +1 -1
  33. package/dist/hooks/spatial-grid/use-spatial-query.js +1 -1
  34. package/dist/index.d.ts +74 -64
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +74 -61
  37. package/dist/lib/asset-storage.d.ts.map +1 -1
  38. package/dist/lib/asset-storage.js +4 -1
  39. package/dist/lib/door-operation.d.ts +1 -1
  40. package/dist/lib/item-polygon-overlap.d.ts +6 -0
  41. package/dist/lib/item-polygon-overlap.d.ts.map +1 -0
  42. package/dist/lib/item-polygon-overlap.js +67 -0
  43. package/dist/lib/level-name.d.ts +1 -1
  44. package/dist/lib/measurement-geometry.d.ts +4 -4
  45. package/dist/lib/plan-footprint.d.ts +41 -0
  46. package/dist/lib/plan-footprint.d.ts.map +1 -0
  47. package/dist/lib/plan-footprint.js +58 -0
  48. package/dist/lib/polygon-union.d.ts +4 -0
  49. package/dist/lib/polygon-union.d.ts.map +1 -0
  50. package/dist/lib/polygon-union.js +306 -0
  51. package/dist/lib/roof-overlap.d.ts +32 -0
  52. package/dist/lib/roof-overlap.d.ts.map +1 -0
  53. package/dist/lib/roof-overlap.js +72 -0
  54. package/dist/lib/room-topology-index.d.ts +1 -1
  55. package/dist/lib/room-topology-index.js +1 -1
  56. package/dist/lib/selection-proxy.d.ts +1 -1
  57. package/dist/lib/slab-polygon.d.ts +36 -3
  58. package/dist/lib/slab-polygon.d.ts.map +1 -1
  59. package/dist/lib/slab-polygon.js +135 -29
  60. package/dist/lib/space-detection.d.ts +12 -1
  61. package/dist/lib/space-detection.d.ts.map +1 -1
  62. package/dist/lib/space-detection.js +58 -25
  63. package/dist/lib/terrain-brush.d.ts +1 -1
  64. package/dist/lib/terrain-brush.js +1 -1
  65. package/dist/lib/terrain-codec.d.ts +2 -2
  66. package/dist/lib/terrain-codec.js +1 -1
  67. package/dist/lib/terrain-raycast.d.ts +1 -1
  68. package/dist/lib/terrain-raycast.js +1 -1
  69. package/dist/lib/terrain-source.d.ts +3 -3
  70. package/dist/lib/terrain-source.js +3 -3
  71. package/dist/lib/terrain-support.d.ts +1 -1
  72. package/dist/lib/terrain-support.js +3 -3
  73. package/dist/lib/unit-containment.d.ts +19 -0
  74. package/dist/lib/unit-containment.d.ts.map +1 -0
  75. package/dist/lib/unit-containment.js +158 -0
  76. package/dist/lib/unit-report.d.ts +19 -0
  77. package/dist/lib/unit-report.d.ts.map +1 -0
  78. package/dist/lib/unit-report.js +36 -0
  79. package/dist/lib/wall-distance.d.ts +2 -2
  80. package/dist/lib/wall-distance.js +1 -1
  81. package/dist/lib/zone-quantities.d.ts +1 -1
  82. package/dist/lib/zone-quantities.js +7 -7
  83. package/dist/material-library.d.ts +1 -1
  84. package/dist/material-library.js +1 -1
  85. package/dist/procedural-items/fixtures.d.ts +261 -0
  86. package/dist/procedural-items/fixtures.d.ts.map +1 -0
  87. package/dist/procedural-items/fixtures.js +349 -0
  88. package/dist/procedural-items/index.d.ts +10 -0
  89. package/dist/procedural-items/index.d.ts.map +1 -0
  90. package/dist/procedural-items/index.js +9 -0
  91. package/dist/procedural-items/integration.d.ts +92 -0
  92. package/dist/procedural-items/integration.d.ts.map +1 -0
  93. package/dist/procedural-items/integration.js +19 -0
  94. package/dist/procedural-items/library-colors.d.ts +56 -0
  95. package/dist/procedural-items/library-colors.d.ts.map +1 -0
  96. package/dist/procedural-items/library-colors.js +84 -0
  97. package/dist/procedural-items/materials.d.ts +5 -0
  98. package/dist/procedural-items/materials.d.ts.map +1 -0
  99. package/dist/procedural-items/materials.js +47 -0
  100. package/dist/procedural-items/node.d.ts +280 -0
  101. package/dist/procedural-items/node.d.ts.map +1 -0
  102. package/dist/procedural-items/node.js +71 -0
  103. package/dist/procedural-items/placement.d.ts +7 -0
  104. package/dist/procedural-items/placement.d.ts.map +1 -0
  105. package/dist/procedural-items/placement.js +48 -0
  106. package/dist/procedural-items/query.d.ts +50 -0
  107. package/dist/procedural-items/query.d.ts.map +1 -0
  108. package/dist/procedural-items/query.js +298 -0
  109. package/dist/procedural-items/recipe.d.ts +144 -0
  110. package/dist/procedural-items/recipe.d.ts.map +1 -0
  111. package/dist/procedural-items/recipe.js +372 -0
  112. package/dist/procedural-items/spatial.d.ts +19 -0
  113. package/dist/procedural-items/spatial.d.ts.map +1 -0
  114. package/dist/procedural-items/spatial.js +39 -0
  115. package/dist/registry/__bench__/relations-resolver.bench.js +2 -2
  116. package/dist/registry/handles.d.ts +36 -7
  117. package/dist/registry/handles.d.ts.map +1 -1
  118. package/dist/registry/index.d.ts +6 -7
  119. package/dist/registry/index.d.ts.map +1 -1
  120. package/dist/registry/index.js +4 -5
  121. package/dist/registry/registry.d.ts +13 -6
  122. package/dist/registry/registry.d.ts.map +1 -1
  123. package/dist/registry/registry.js +37 -4
  124. package/dist/registry/relations-resolver.d.ts +2 -2
  125. package/dist/registry/relations-resolver.js +1 -1
  126. package/dist/registry/scene-api.d.ts +18 -2
  127. package/dist/registry/scene-api.d.ts.map +1 -1
  128. package/dist/registry/scene-api.js +50 -2
  129. package/dist/registry/subtree.d.ts +1 -1
  130. package/dist/registry/subtree.js +2 -2
  131. package/dist/registry/types.d.ts +246 -21
  132. package/dist/registry/types.d.ts.map +1 -1
  133. package/dist/registry/use-registry-version.js +1 -1
  134. package/dist/registry/validate-relations.d.ts +4 -0
  135. package/dist/registry/validate-relations.d.ts.map +1 -0
  136. package/dist/registry/validate-relations.js +22 -0
  137. package/dist/schema/__bench__/node-parsers.bench.d.ts +24 -0
  138. package/dist/schema/__bench__/node-parsers.bench.d.ts.map +1 -0
  139. package/dist/schema/__bench__/node-parsers.bench.js +314 -0
  140. package/dist/schema/__fixtures__/node-fixtures.d.ts +28 -0
  141. package/dist/schema/__fixtures__/node-fixtures.d.ts.map +1 -0
  142. package/dist/schema/__fixtures__/node-fixtures.js +127 -0
  143. package/dist/schema/base.d.ts +1 -1
  144. package/dist/schema/base.d.ts.map +1 -1
  145. package/dist/schema/base.js +9 -2
  146. package/dist/schema/collections.d.ts +1 -1
  147. package/dist/schema/collections.js +1 -1
  148. package/dist/schema/compiled-node-parsers.d.ts +40 -0
  149. package/dist/schema/compiled-node-parsers.d.ts.map +1 -0
  150. package/dist/schema/compiled-node-parsers.js +95 -0
  151. package/dist/schema/index.d.ts +73 -66
  152. package/dist/schema/index.d.ts.map +1 -1
  153. package/dist/schema/index.js +65 -57
  154. package/dist/schema/material.d.ts +1 -0
  155. package/dist/schema/material.d.ts.map +1 -1
  156. package/dist/schema/material.js +2 -1
  157. package/dist/schema/nodes/automatic-downspout.d.ts +17 -0
  158. package/dist/schema/nodes/automatic-downspout.d.ts.map +1 -0
  159. package/dist/schema/nodes/automatic-downspout.js +237 -0
  160. package/dist/schema/nodes/block.d.ts +107 -0
  161. package/dist/schema/nodes/block.d.ts.map +1 -0
  162. package/dist/schema/nodes/block.js +206 -0
  163. package/dist/schema/nodes/box-vent.d.ts +8 -2
  164. package/dist/schema/nodes/box-vent.d.ts.map +1 -1
  165. package/dist/schema/nodes/box-vent.js +4 -2
  166. package/dist/schema/nodes/building.d.ts +2 -2
  167. package/dist/schema/nodes/building.d.ts.map +1 -1
  168. package/dist/schema/nodes/building.js +8 -5
  169. package/dist/schema/nodes/cabinet.d.ts +26 -4
  170. package/dist/schema/nodes/cabinet.d.ts.map +1 -1
  171. package/dist/schema/nodes/cabinet.js +29 -8
  172. package/dist/schema/nodes/ceiling.d.ts +2 -2
  173. package/dist/schema/nodes/ceiling.js +5 -5
  174. package/dist/schema/nodes/chimney.d.ts +2 -2
  175. package/dist/schema/nodes/chimney.js +2 -2
  176. package/dist/schema/nodes/column.d.ts +7 -6
  177. package/dist/schema/nodes/column.d.ts.map +1 -1
  178. package/dist/schema/nodes/column.js +3 -2
  179. package/dist/schema/nodes/construction-dimension.d.ts +1 -1
  180. package/dist/schema/nodes/construction-dimension.js +2 -2
  181. package/dist/schema/nodes/cupola.d.ts +9 -1
  182. package/dist/schema/nodes/cupola.d.ts.map +1 -1
  183. package/dist/schema/nodes/cupola.js +4 -2
  184. package/dist/schema/nodes/door.d.ts +1 -1
  185. package/dist/schema/nodes/door.js +2 -2
  186. package/dist/schema/nodes/dormer.d.ts +45 -2
  187. package/dist/schema/nodes/dormer.d.ts.map +1 -1
  188. package/dist/schema/nodes/dormer.js +133 -9
  189. package/dist/schema/nodes/downspout.d.ts +11 -1
  190. package/dist/schema/nodes/downspout.d.ts.map +1 -1
  191. package/dist/schema/nodes/downspout.js +26 -2
  192. package/dist/schema/nodes/duct-fitting.d.ts +18 -1
  193. package/dist/schema/nodes/duct-fitting.d.ts.map +1 -1
  194. package/dist/schema/nodes/duct-fitting.js +26 -5
  195. package/dist/schema/nodes/duct-segment.d.ts +23 -1
  196. package/dist/schema/nodes/duct-segment.d.ts.map +1 -1
  197. package/dist/schema/nodes/duct-segment.js +21 -1
  198. package/dist/schema/nodes/duct-terminal.d.ts +1 -1
  199. package/dist/schema/nodes/duct-terminal.js +1 -1
  200. package/dist/schema/nodes/elevator.d.ts +1 -1
  201. package/dist/schema/nodes/elevator.js +2 -2
  202. package/dist/schema/nodes/eyebrow-vent.d.ts +8 -2
  203. package/dist/schema/nodes/eyebrow-vent.d.ts.map +1 -1
  204. package/dist/schema/nodes/eyebrow-vent.js +4 -2
  205. package/dist/schema/nodes/fence.d.ts +1 -1
  206. package/dist/schema/nodes/fence.js +2 -2
  207. package/dist/schema/nodes/guide.d.ts +1 -1
  208. package/dist/schema/nodes/guide.js +2 -2
  209. package/dist/schema/nodes/gutter.d.ts +36 -2
  210. package/dist/schema/nodes/gutter.d.ts.map +1 -1
  211. package/dist/schema/nodes/gutter.js +310 -3
  212. package/dist/schema/nodes/hanger-overrides.d.ts +7 -0
  213. package/dist/schema/nodes/hanger-overrides.d.ts.map +1 -0
  214. package/dist/schema/nodes/hanger-overrides.js +6 -0
  215. package/dist/schema/nodes/hvac-equipment.d.ts +1 -1
  216. package/dist/schema/nodes/hvac-equipment.js +1 -1
  217. package/dist/schema/nodes/imported-mesh.d.ts +40 -0
  218. package/dist/schema/nodes/imported-mesh.d.ts.map +1 -0
  219. package/dist/schema/nodes/imported-mesh.js +27 -0
  220. package/dist/schema/nodes/item.d.ts +3 -2
  221. package/dist/schema/nodes/item.d.ts.map +1 -1
  222. package/dist/schema/nodes/item.js +8 -3
  223. package/dist/schema/nodes/lean-to-extension.d.ts +212 -0
  224. package/dist/schema/nodes/lean-to-extension.d.ts.map +1 -0
  225. package/dist/schema/nodes/lean-to-extension.js +114 -0
  226. package/dist/schema/nodes/level.d.ts +2 -2
  227. package/dist/schema/nodes/level.d.ts.map +1 -1
  228. package/dist/schema/nodes/level.js +1 -1
  229. package/dist/schema/nodes/lineset.d.ts +1 -1
  230. package/dist/schema/nodes/lineset.js +1 -1
  231. package/dist/schema/nodes/liquid-line.d.ts +1 -1
  232. package/dist/schema/nodes/liquid-line.js +1 -1
  233. package/dist/schema/nodes/measurement.d.ts +1 -1
  234. package/dist/schema/nodes/measurement.js +2 -2
  235. package/dist/schema/nodes/pipe-fitting.d.ts +10 -2
  236. package/dist/schema/nodes/pipe-fitting.d.ts.map +1 -1
  237. package/dist/schema/nodes/pipe-fitting.js +7 -3
  238. package/dist/schema/nodes/pipe-segment.d.ts +24 -2
  239. package/dist/schema/nodes/pipe-segment.d.ts.map +1 -1
  240. package/dist/schema/nodes/pipe-segment.js +21 -1
  241. package/dist/schema/nodes/pipe-trap.d.ts +2 -2
  242. package/dist/schema/nodes/pipe-trap.js +1 -1
  243. package/dist/schema/nodes/ridge-vent.d.ts +2 -2
  244. package/dist/schema/nodes/ridge-vent.d.ts.map +1 -1
  245. package/dist/schema/nodes/ridge-vent.js +7 -4
  246. package/dist/schema/nodes/roof-segment-shape.d.ts +5 -1
  247. package/dist/schema/nodes/roof-segment-shape.d.ts.map +1 -1
  248. package/dist/schema/nodes/roof-segment-shape.js +65 -2
  249. package/dist/schema/nodes/roof-segment-walls.d.ts +4 -2
  250. package/dist/schema/nodes/roof-segment-walls.d.ts.map +1 -1
  251. package/dist/schema/nodes/roof-segment-walls.js +2 -2
  252. package/dist/schema/nodes/roof-segment.d.ts +42 -2
  253. package/dist/schema/nodes/roof-segment.d.ts.map +1 -1
  254. package/dist/schema/nodes/roof-segment.js +81 -4
  255. package/dist/schema/nodes/roof.d.ts +23 -2
  256. package/dist/schema/nodes/roof.d.ts.map +1 -1
  257. package/dist/schema/nodes/roof.js +17 -3
  258. package/dist/schema/nodes/scan.d.ts +19 -2
  259. package/dist/schema/nodes/scan.d.ts.map +1 -1
  260. package/dist/schema/nodes/scan.js +13 -3
  261. package/dist/schema/nodes/shelf.d.ts +3 -3
  262. package/dist/schema/nodes/shelf.d.ts.map +1 -1
  263. package/dist/schema/nodes/shelf.js +3 -4
  264. package/dist/schema/nodes/site.d.ts +1 -1
  265. package/dist/schema/nodes/site.js +2 -2
  266. package/dist/schema/nodes/skylight.d.ts +5 -5
  267. package/dist/schema/nodes/skylight.js +2 -2
  268. package/dist/schema/nodes/slab.d.ts +1 -1
  269. package/dist/schema/nodes/slab.js +3 -3
  270. package/dist/schema/nodes/solar-panel.d.ts +1 -1
  271. package/dist/schema/nodes/solar-panel.js +3 -3
  272. package/dist/schema/nodes/spawn.d.ts +1 -1
  273. package/dist/schema/nodes/spawn.js +1 -1
  274. package/dist/schema/nodes/stair-segment.d.ts +1 -1
  275. package/dist/schema/nodes/stair-segment.js +2 -2
  276. package/dist/schema/nodes/stair.d.ts +2 -2
  277. package/dist/schema/nodes/stair.js +3 -3
  278. package/dist/schema/nodes/structural-grid.d.ts +1 -1
  279. package/dist/schema/nodes/structural-grid.js +1 -1
  280. package/dist/schema/nodes/turbine-vent.d.ts +8 -2
  281. package/dist/schema/nodes/turbine-vent.d.ts.map +1 -1
  282. package/dist/schema/nodes/turbine-vent.js +4 -2
  283. package/dist/schema/nodes/unit.d.ts +33 -0
  284. package/dist/schema/nodes/unit.d.ts.map +1 -0
  285. package/dist/schema/nodes/unit.js +13 -0
  286. package/dist/schema/nodes/wall.d.ts +5 -5
  287. package/dist/schema/nodes/wall.d.ts.map +1 -1
  288. package/dist/schema/nodes/wall.js +13 -6
  289. package/dist/schema/nodes/window.d.ts +10 -3
  290. package/dist/schema/nodes/window.d.ts.map +1 -1
  291. package/dist/schema/nodes/window.js +6 -2
  292. package/dist/schema/nodes/zone.d.ts +2 -2
  293. package/dist/schema/nodes/zone.js +2 -2
  294. package/dist/schema/scene-material.js +2 -2
  295. package/dist/schema/types.d.ts +815 -118
  296. package/dist/schema/types.d.ts.map +1 -1
  297. package/dist/schema/types.js +79 -46
  298. package/dist/services/alignment-anchors.d.ts +6 -11
  299. package/dist/services/alignment-anchors.d.ts.map +1 -1
  300. package/dist/services/alignment-anchors.js +7 -34
  301. package/dist/services/drag-session.d.ts +4 -4
  302. package/dist/services/drag-session.js +1 -1
  303. package/dist/services/hosting.d.ts +4 -10
  304. package/dist/services/hosting.d.ts.map +1 -1
  305. package/dist/services/hosting.js +6 -16
  306. package/dist/services/index.d.ts +16 -13
  307. package/dist/services/index.d.ts.map +1 -1
  308. package/dist/services/index.js +16 -13
  309. package/dist/services/level-height.d.ts +2 -2
  310. package/dist/services/level-height.js +3 -3
  311. package/dist/services/movement.d.ts +3 -3
  312. package/dist/services/movement.js +2 -2
  313. package/dist/services/port-connectivity.d.ts +1 -1
  314. package/dist/services/port-connectivity.js +1 -1
  315. package/dist/services/riser-diagram.d.ts +1 -1
  316. package/dist/services/shelf-board.d.ts +5 -0
  317. package/dist/services/shelf-board.d.ts.map +1 -0
  318. package/dist/services/shelf-board.js +15 -0
  319. package/dist/services/storey.d.ts +2 -2
  320. package/dist/services/storey.js +2 -2
  321. package/dist/services/surface-hosting-procedural-parity.fixtures.d.ts +13 -0
  322. package/dist/services/surface-hosting-procedural-parity.fixtures.d.ts.map +1 -0
  323. package/dist/services/surface-hosting-procedural-parity.fixtures.js +4691 -0
  324. package/dist/services/surface-hosting-shelf-parity.fixtures.d.ts +1060 -0
  325. package/dist/services/surface-hosting-shelf-parity.fixtures.d.ts.map +1 -0
  326. package/dist/services/surface-hosting-shelf-parity.fixtures.js +3127 -0
  327. package/dist/services/surface-hosting.d.ts +88 -0
  328. package/dist/services/surface-hosting.d.ts.map +1 -0
  329. package/dist/services/surface-hosting.js +268 -0
  330. package/dist/services/surface-region.d.ts +8 -0
  331. package/dist/services/surface-region.d.ts.map +1 -0
  332. package/dist/services/surface-region.js +44 -0
  333. package/dist/services/system-graph.d.ts +12 -1
  334. package/dist/services/system-graph.d.ts.map +1 -1
  335. package/dist/services/system-graph.js +36 -6
  336. package/dist/services/validate-dwv.d.ts +1 -1
  337. package/dist/services/validate-dwv.js +1 -1
  338. package/dist/store/actions/node-actions.d.ts +9 -2
  339. package/dist/store/actions/node-actions.d.ts.map +1 -1
  340. package/dist/store/actions/node-actions.js +475 -24
  341. package/dist/store/history-control.d.ts +3 -3
  342. package/dist/store/history-invalidation.d.ts +3 -0
  343. package/dist/store/history-invalidation.d.ts.map +1 -0
  344. package/dist/store/history-invalidation.js +72 -0
  345. package/dist/store/scene-hydration.d.ts +5 -0
  346. package/dist/store/scene-hydration.d.ts.map +1 -0
  347. package/dist/store/scene-hydration.js +57 -0
  348. package/dist/store/use-interactive.d.ts +2 -2
  349. package/dist/store/use-live-terrain.d.ts +1 -1
  350. package/dist/store/use-scene.d.ts +7 -4
  351. package/dist/store/use-scene.d.ts.map +1 -1
  352. package/dist/store/use-scene.js +361 -87
  353. package/dist/systems/elevator/elevator-dispatch.d.ts +2 -2
  354. package/dist/systems/elevator/elevator-dispatch.js +1 -1
  355. package/dist/systems/elevator/elevator-geometry.d.ts +1 -1
  356. package/dist/systems/elevator/elevator-opening-sync.d.ts +1 -1
  357. package/dist/systems/elevator/elevator-opening-sync.js +2 -2
  358. package/dist/systems/elevator/elevator-opening-system.js +3 -3
  359. package/dist/systems/elevator/elevator-runtime.d.ts +3 -3
  360. package/dist/systems/elevator/elevator-runtime.js +3 -3
  361. package/dist/systems/elevator/elevator-service.d.ts +1 -1
  362. package/dist/systems/elevator/elevator-service.js +1 -1
  363. package/dist/systems/fence/fence-centerline.d.ts +2 -2
  364. package/dist/systems/fence/fence-centerline.js +2 -2
  365. package/dist/systems/fence/fence-spline.d.ts +2 -2
  366. package/dist/systems/roof/roof-elevation-system.d.ts +3 -0
  367. package/dist/systems/roof/roof-elevation-system.d.ts.map +1 -0
  368. package/dist/systems/roof/roof-elevation-system.js +94 -0
  369. package/dist/systems/roof/roof-elevation.d.ts +4 -0
  370. package/dist/systems/roof/roof-elevation.d.ts.map +1 -0
  371. package/dist/systems/roof/roof-elevation.js +95 -0
  372. package/dist/systems/roof/roof-footprint.d.ts +17 -0
  373. package/dist/systems/roof/roof-footprint.d.ts.map +1 -0
  374. package/dist/systems/roof/roof-footprint.js +83 -0
  375. package/dist/systems/slab/slab-placement.d.ts +1 -1
  376. package/dist/systems/slab/slab-support.d.ts +1 -1
  377. package/dist/systems/slab/slab-support.js +4 -4
  378. package/dist/systems/stair/stair-flight.d.ts +17 -0
  379. package/dist/systems/stair/stair-flight.d.ts.map +1 -0
  380. package/dist/systems/stair/stair-flight.js +27 -0
  381. package/dist/systems/stair/stair-footprint.d.ts +1 -1
  382. package/dist/systems/stair/stair-opening-preview.d.ts +1621 -1267
  383. package/dist/systems/stair/stair-opening-preview.d.ts.map +1 -1
  384. package/dist/systems/stair/stair-opening-preview.js +2 -2
  385. package/dist/systems/stair/stair-opening-sync.d.ts +1 -1
  386. package/dist/systems/stair/stair-opening-sync.js +6 -6
  387. package/dist/systems/stair/stair-opening-system.d.ts +1 -0
  388. package/dist/systems/stair/stair-opening-system.d.ts.map +1 -1
  389. package/dist/systems/stair/stair-opening-system.js +100 -103
  390. package/dist/systems/stair/stair-rise.d.ts +1 -1
  391. package/dist/systems/stair/stair-rise.d.ts.map +1 -1
  392. package/dist/systems/stair/stair-rise.js +23 -24
  393. package/dist/systems/wall/wall-curve.d.ts +3 -2
  394. package/dist/systems/wall/wall-curve.d.ts.map +1 -1
  395. package/dist/systems/wall/wall-curve.js +73 -0
  396. package/dist/systems/wall/wall-footprint.d.ts +3 -2
  397. package/dist/systems/wall/wall-footprint.d.ts.map +1 -1
  398. package/dist/systems/wall/wall-footprint.js +3 -2
  399. package/dist/systems/wall/wall-mitering.d.ts +1 -1
  400. package/dist/systems/wall/wall-mitering.js +1 -1
  401. package/dist/systems/wall/wall-move.d.ts +1 -1
  402. package/dist/systems/wall/wall-top.d.ts +1 -1
  403. package/dist/systems/wall/wall-topology.d.ts +2 -2
  404. package/dist/systems/wall/wall-topology.js +4 -4
  405. package/dist/utils/clone-scene-graph.d.ts +3 -3
  406. package/dist/utils/clone-scene-graph.d.ts.map +1 -1
  407. package/dist/utils/clone-scene-graph.js +35 -3
  408. package/dist/utils/scene-migrations.d.ts +3 -3
  409. package/dist/utils/scene-migrations.js +3 -3
  410. package/dist/utils/vertical-scene-migration.js +5 -5
  411. package/dist/validation/validate-build-json.d.ts +7 -1
  412. package/dist/validation/validate-build-json.d.ts.map +1 -1
  413. package/dist/validation/validate-build-json.js +92 -4
  414. package/package.json +23 -5
  415. package/dist/systems/elevator/elevator-runtime-system.d.ts +0 -2
  416. package/dist/systems/elevator/elevator-runtime-system.d.ts.map +0 -1
  417. package/dist/systems/elevator/elevator-runtime-system.js +0 -8
@@ -1,30 +1,37 @@
1
1
  'use client';
2
2
  import { temporal } from 'zundo';
3
3
  import { create } from 'zustand';
4
- import { parseMaterialRef, toSceneMaterialRef } from '../material-library';
5
- import { getNodePluginId, isNodeKindEnabled, nodeRegistry } from '../registry/registry';
6
- import { BuildingNode } from '../schema';
7
- import { generateCollectionId } from '../schema/collections';
8
- import { DoorNode as DoorNodeSchema } from '../schema/nodes/door';
9
- import { ElevatorNode as ElevatorNodeSchema } from '../schema/nodes/elevator';
10
- import { LevelNode, normalizeLevelBaseElevation } from '../schema/nodes/level';
11
- import { getPitchFromActiveRoofHeight, } from '../schema/nodes/roof-segment';
12
- import { segmentPointToRoofWallFace } from '../schema/nodes/roof-segment-walls';
13
- import { ShelfNode as ShelfNodeSchema } from '../schema/nodes/shelf';
14
- import { SiteNode } from '../schema/nodes/site';
15
- import { getEffectiveStairSurfaceMaterial, StairNode as StairNodeSchema, } from '../schema/nodes/stair';
16
- import { StairSegmentNode as StairSegmentNodeSchema } from '../schema/nodes/stair-segment';
17
- import { getEffectiveWallSurfaceMaterial } from '../schema/nodes/wall';
18
- import { WindowNode as WindowNodeSchema } from '../schema/nodes/window';
19
- import { generateSceneMaterialId, SceneMaterial, } from '../schema/scene-material';
20
- import { AnyNode as AnyNodeSchema } from '../schema/types';
21
- import { healSceneNodes } from '../utils/heal-scene-graph';
22
- import { removeRetiredDrawingSheetNodes } from '../utils/retired-scene-nodes';
23
- import { migrateVerticalSceneNodes } from '../utils/vertical-scene-migration';
24
- import * as nodeActions from './actions/node-actions';
25
- import { areSceneSnapshotsEqual, getSceneHistoryPauseDepth, notifySceneCommit, pauseSceneHistory, resetSceneHistoryPauseDepth, resumeSceneHistory, } from './history-control';
26
- import useLiveNodeOverrides from './use-live-node-overrides';
27
- import useLiveTransforms from './use-live-transforms';
4
+ import { parseMaterialRef, toSceneMaterialRef } from '../material-library.js';
5
+ import { getNodePluginId, isNodeKindEnabled, nodeRegistry } from '../registry/registry.js';
6
+ import { BuildingNode } from '../schema/index.js';
7
+ import { generateCollectionId } from '../schema/collections.js';
8
+ import { compiledNodeSchema } from '../schema/compiled-node-parsers.js';
9
+ import { DoorNode as DoorNodeSchema } from '../schema/nodes/door.js';
10
+ import { createDormerDefaultWindow, DormerNode as DormerNodeSchema, getDormerDefaultWindowFace, } from '../schema/nodes/dormer.js';
11
+ import { ElevatorNode as ElevatorNodeSchema } from '../schema/nodes/elevator.js';
12
+ import { LevelNode, normalizeLevelBaseElevation } from '../schema/nodes/level.js';
13
+ import { getPitchFromActiveRoofHeight, } from '../schema/nodes/roof-segment.js';
14
+ import { segmentPointToRoofWallFace } from '../schema/nodes/roof-segment-walls.js';
15
+ import { ShelfNode as ShelfNodeSchema } from '../schema/nodes/shelf.js';
16
+ import { SiteNode } from '../schema/nodes/site.js';
17
+ import { getEffectiveStairSurfaceMaterial, StairNode as StairNodeSchema, } from '../schema/nodes/stair.js';
18
+ import { StairSegmentNode as StairSegmentNodeSchema } from '../schema/nodes/stair-segment.js';
19
+ import { getEffectiveWallSurfaceMaterial } from '../schema/nodes/wall.js';
20
+ import { WindowNode as WindowNodeSchema } from '../schema/nodes/window.js';
21
+ import { generateSceneMaterialId, SceneMaterial, } from '../schema/scene-material.js';
22
+ import { AnyNode as AnyNodeSchema } from '../schema/types.js';
23
+ import { syncAutoElevatorOpenings } from '../systems/elevator/elevator-opening-sync.js';
24
+ import { syncAutoStairOpenings } from '../systems/stair/stair-opening-sync.js';
25
+ import { syncStairRises } from '../systems/stair/stair-rise.js';
26
+ import { healSceneNodes } from '../utils/heal-scene-graph.js';
27
+ import { removeRetiredDrawingSheetNodes } from '../utils/retired-scene-nodes.js';
28
+ import { migrateVerticalSceneNodes } from '../utils/vertical-scene-migration.js';
29
+ import * as nodeActions from './actions/node-actions.js';
30
+ import { areSceneSnapshotsEqual, getSceneHistoryPauseDepth, notifySceneCommit, pauseSceneHistory, resetSceneHistoryPauseDepth, resumeSceneHistory, runWithSceneCommitNodeIds, } from './history-control.js';
31
+ import { getHistoryDirtyNodeIds } from './history-invalidation.js';
32
+ import { invalidatePendingHydration, isHydrationNormalization, queueSceneNormalization, runSceneHydration, } from './scene-hydration.js';
33
+ import useLiveNodeOverrides from './use-live-node-overrides.js';
34
+ import useLiveTransforms from './use-live-transforms.js';
28
35
  function getFiniteNumber(value, fallback) {
29
36
  return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
30
37
  }
@@ -82,7 +89,7 @@ function normalizeStairNode(node) {
82
89
  railingHeight: getFiniteNumber(node.railingHeight, 0.92),
83
90
  children: getStringArray(node.children),
84
91
  };
85
- const parsed = StairNodeSchema.safeParse(sanitized);
92
+ const parsed = compiledNodeSchema(StairNodeSchema).safeParse(sanitized);
86
93
  if (!parsed.success)
87
94
  return null;
88
95
  if (hasTotalRise)
@@ -107,18 +114,18 @@ function normalizeStairSegmentNode(node) {
107
114
  fillToFloor: getBoolean(node.fillToFloor, true),
108
115
  thickness: getFiniteNumber(node.thickness, 0.25),
109
116
  };
110
- const parsed = StairSegmentNodeSchema.safeParse(sanitized);
117
+ const parsed = compiledNodeSchema(StairSegmentNodeSchema).safeParse(sanitized);
111
118
  return parsed.success ? parsed.data : null;
112
119
  }
113
120
  function normalizeDoorNode(node) {
114
- const parsed = DoorNodeSchema.safeParse(node);
121
+ const parsed = compiledNodeSchema(DoorNodeSchema).safeParse(node);
115
122
  return parsed.success ? { ...node, ...parsed.data } : null;
116
123
  }
117
124
  // Windows saved before a schema field existed (e.g. `columnRatios`/`rowRatios`/
118
125
  // `frameThickness`) load without it; the mesh builder then reads undefined and
119
126
  // throws every frame. Zod-parse on load so schema defaults land, like doors.
120
127
  function normalizeWindowNode(node) {
121
- const parsed = WindowNodeSchema.safeParse(node);
128
+ const parsed = compiledNodeSchema(WindowNodeSchema).safeParse(node);
122
129
  return parsed.success ? { ...node, ...parsed.data } : null;
123
130
  }
124
131
  function normalizeShelfNode(node) {
@@ -139,7 +146,7 @@ function normalizeShelfNode(node) {
139
146
  withBottom: getBoolean(node.withBottom, false),
140
147
  bracketStyle: getEnumValue(node.bracketStyle, ['minimal', 'industrial', 'hidden'], 'minimal'),
141
148
  };
142
- const parsed = ShelfNodeSchema.safeParse(sanitized);
149
+ const parsed = compiledNodeSchema(ShelfNodeSchema).safeParse(sanitized);
143
150
  return parsed.success ? parsed.data : null;
144
151
  }
145
152
  function normalizeElevatorNode(node) {
@@ -165,7 +172,7 @@ function normalizeElevatorNode(node) {
165
172
  doorDurationMs: getFiniteNumber(node.doorDurationMs, 900),
166
173
  dwellMs: getFiniteNumber(node.dwellMs, 1400),
167
174
  };
168
- const parsed = ElevatorNodeSchema.safeParse(sanitized);
175
+ const parsed = compiledNodeSchema(ElevatorNodeSchema).safeParse(sanitized);
169
176
  return parsed.success ? parsed.data : null;
170
177
  }
171
178
  function findBuildingIdForLevel(levelId, nodes) {
@@ -295,6 +302,42 @@ function migrateSingleMaterialSlots(node, slotIds, mintedMaterials) {
295
302
  slots[slotId] = ref;
296
303
  return { ...node, slots, material: undefined, materialPreset: undefined };
297
304
  }
305
+ function migrateRoleMaterialSlots(node, roles, mintedMaterials) {
306
+ const slots = { ...(node.slots ?? {}) };
307
+ const next = { ...node };
308
+ let changed = false;
309
+ for (const role of roles) {
310
+ if (slots[role] === undefined) {
311
+ const ref = legacySpecToMaterialRef({
312
+ material: node[`${role}Material`] ?? node.material,
313
+ materialPreset: node[`${role}MaterialPreset`] ?? node.materialPreset,
314
+ }, mintedMaterials);
315
+ if (ref) {
316
+ slots[role] = ref;
317
+ changed = true;
318
+ }
319
+ }
320
+ if (`${role}Material` in next || `${role}MaterialPreset` in next)
321
+ changed = true;
322
+ delete next[`${role}Material`];
323
+ delete next[`${role}MaterialPreset`];
324
+ }
325
+ return changed ? { ...next, slots } : node;
326
+ }
327
+ function migrateRenamedSlot(node, previousId, nextId) {
328
+ if (!node.slots || node.slots[previousId] === undefined)
329
+ return node;
330
+ const slots = { ...node.slots };
331
+ if (slots[nextId] === undefined)
332
+ slots[nextId] = slots[previousId];
333
+ delete slots[previousId];
334
+ return { ...node, slots };
335
+ }
336
+ function migrateCupolaLouverSlot(node) {
337
+ if (!node.slots || node.slots.louvers !== undefined || node.slots.body === undefined)
338
+ return node;
339
+ return { ...node, slots: { ...node.slots, louvers: node.slots.body } };
340
+ }
298
341
  // Stair carries per-role legacy fields (`treadMaterial*` / `sideMaterial*` /
299
342
  // `railingMaterial*`) plus a catch-all. Map each to its slot via the same
300
343
  // fallback chain the renderer uses (`getEffectiveStairSurfaceMaterial`):
@@ -482,6 +525,39 @@ function migrateWallAssembly(node) {
482
525
  const { assemblyLayers: _assemblyLayers, ...wall } = node;
483
526
  return assemblyThickness > 0 ? { ...wall, thickness: assemblyThickness } : wall;
484
527
  }
528
+ function migrateBlockRename(id, node, nodes) {
529
+ if (node.type !== 'custom-mesh')
530
+ return [id, node];
531
+ const desiredId = id.startsWith('custom-mesh_') ? `block_${id.slice('custom-mesh_'.length)}` : id;
532
+ let nextId = desiredId;
533
+ let suffix = 1;
534
+ while (nextId !== id && nodes[nextId]) {
535
+ nextId = `${desiredId}_${suffix}`;
536
+ suffix += 1;
537
+ }
538
+ const nextNode = { ...node, id: nextId, type: 'block' };
539
+ if (nextId !== id) {
540
+ for (const candidate of Object.values(nodes)) {
541
+ if (!candidate || typeof candidate !== 'object')
542
+ continue;
543
+ if (candidate.parentId === id)
544
+ candidate.parentId = nextId;
545
+ if (Array.isArray(candidate.children)) {
546
+ candidate.children = candidate.children.map((childId) => childId === id ? nextId : childId);
547
+ }
548
+ }
549
+ }
550
+ return [nextId, nextNode];
551
+ }
552
+ function migrateBlockHostedItem(node) {
553
+ if (node.type !== 'item' ||
554
+ node.blockFaceId !== undefined ||
555
+ node.customMeshFaceId === undefined) {
556
+ return node;
557
+ }
558
+ const { customMeshFaceId, ...item } = node;
559
+ return { ...item, blockFaceId: customMeshFaceId };
560
+ }
485
561
  function migrateNodes(nodes) {
486
562
  // Repair pre-existing corruption (null children, zero-length walls) before
487
563
  // any per-type migration runs, so already-saved scenes load cleanly.
@@ -490,6 +566,13 @@ function migrateNodes(nodes) {
490
566
  // Scene materials minted while moving legacy wall fields onto `node.slots`;
491
567
  // merged into the scene material map by the caller (`setScene`).
492
568
  const mintedMaterials = {};
569
+ for (const [id, node] of Object.entries(patchedNodes)) {
570
+ const [nextId, nextNode] = migrateBlockRename(id, node, patchedNodes);
571
+ if (nextId !== id) {
572
+ delete patchedNodes[id];
573
+ }
574
+ patchedNodes[nextId] = migrateBlockHostedItem(nextNode);
575
+ }
493
576
  // Pass 1: all node types except elevator.
494
577
  // Elevator migration (migrateElevatorParent) mutates level.children to remove
495
578
  // the elevator ID. If the elevator is processed before its parent level in
@@ -578,6 +661,34 @@ function migrateNodes(nodes) {
578
661
  patchedNodes[id] = normalized;
579
662
  }
580
663
  }
664
+ // Dormers originally rendered one inline parametric window. Promote that
665
+ // default to a real hosted WindowNode so additional windows can use the
666
+ // regular window tool and inspector without changing the old appearance.
667
+ if (node.type === 'dormer') {
668
+ const hasLegacyInlineWindow = !Array.isArray(patchedNodes[id].children);
669
+ if (!hasLegacyInlineWindow)
670
+ continue;
671
+ const dormer = DormerNodeSchema.parse({
672
+ ...patchedNodes[id],
673
+ children: getStringArray(patchedNodes[id].children),
674
+ });
675
+ const children = getStringArray(dormer.children);
676
+ const hasHostedWindow = children.some((childId) => patchedNodes[childId]?.type === 'window');
677
+ if (!hasHostedWindow) {
678
+ const baseWindowId = `window_${id.replace(/^dormer_/, '')}_default`;
679
+ let windowId = baseWindowId;
680
+ let suffix = 1;
681
+ while (patchedNodes[windowId]) {
682
+ windowId = `${baseWindowId}_${suffix}`;
683
+ suffix += 1;
684
+ }
685
+ const host = dormer.roofSegmentId ? patchedNodes[dormer.roofSegmentId] : undefined;
686
+ const hostSegment = host?.type === 'roof-segment' ? host : undefined;
687
+ const window = createDormerDefaultWindow(dormer, windowId, getDormerDefaultWindowFace(dormer, hostSegment));
688
+ patchedNodes[windowId] = window;
689
+ patchedNodes[id] = { ...dormer, children: [...children, window.id] };
690
+ }
691
+ }
581
692
  if (node.type === 'construction-dimension') {
582
693
  patchedNodes[id] = migrateConstructionDimension(node);
583
694
  }
@@ -635,6 +746,26 @@ function migrateNodes(nodes) {
635
746
  if (node.type === 'column') {
636
747
  patchedNodes[id] = migrateSingleMaterialSlots(patchedNodes[id], ['shaft', 'base', 'capital', 'frame'], mintedMaterials);
637
748
  }
749
+ if (node.type === 'gutter') {
750
+ patchedNodes[id] = migrateRenamedSlot(patchedNodes[id], 'surface', 'gutter');
751
+ patchedNodes[id] = migrateSingleMaterialSlots(patchedNodes[id], ['gutter'], mintedMaterials);
752
+ }
753
+ if (node.type === 'downspout') {
754
+ patchedNodes[id] = migrateSingleMaterialSlots(patchedNodes[id], ['surface'], mintedMaterials);
755
+ }
756
+ if (node.type === 'box-vent') {
757
+ patchedNodes[id] = migrateRoleMaterialSlots(patchedNodes[id], ['base', 'top'], mintedMaterials);
758
+ }
759
+ if (node.type === 'cupola') {
760
+ patchedNodes[id] = migrateCupolaLouverSlot(patchedNodes[id]);
761
+ patchedNodes[id] = migrateRoleMaterialSlots(patchedNodes[id], ['base', 'body', 'roof', 'louvers'], mintedMaterials);
762
+ }
763
+ if (node.type === 'eyebrow-vent') {
764
+ patchedNodes[id] = migrateRoleMaterialSlots(patchedNodes[id], ['hood', 'front'], mintedMaterials);
765
+ }
766
+ if (node.type === 'turbine-vent') {
767
+ patchedNodes[id] = migrateRoleMaterialSlots(patchedNodes[id], ['base', 'head'], mintedMaterials);
768
+ }
638
769
  if (node.type === 'shelf') {
639
770
  const normalized = normalizeShelfNode(node);
640
771
  if (normalized) {
@@ -743,6 +874,12 @@ function migrateNodes(nodes) {
743
874
  children: validChildren,
744
875
  };
745
876
  }
877
+ // These kinds are not all schema-parsed on load, so defaults must also
878
+ // reach saved hosts that predate their children field.
879
+ if (['shelf', 'cabinet', 'cabinet-module', 'block', 'item', 'column'].includes(node.type) &&
880
+ patchedNodes[id].children === undefined) {
881
+ patchedNodes[id] = { ...patchedNodes[id], children: [] };
882
+ }
746
883
  }
747
884
  // Pass 2: elevator migration.
748
885
  // migrateElevatorParent mutates the parent level's children array (removes
@@ -885,13 +1022,99 @@ function sceneHistorySnapshotFromState(state) {
885
1022
  installedPlugins,
886
1023
  };
887
1024
  }
888
- const useScene = create()(temporal((set, get) => ({
1025
+ /**
1026
+ * A dirty mark is a promise that some system will rebuild the node and clear
1027
+ * the mark, so marks are only accepted for kinds with a dirty consumer: kinds
1028
+ * with `dirtyTracking: false` (and kinds of disabled plugins) have none, and
1029
+ * a mark for them would sit in the set for the whole session and defeat every
1030
+ * consumer's empty-set early exit. Ids without a node pass: tools mark nodes
1031
+ * they are about to create.
1032
+ */
1033
+ function isDirtyTrackable(id, scene) {
1034
+ const node = scene.nodes[id];
1035
+ if (!node)
1036
+ return true;
1037
+ if (!isNodeKindEnabled(node.type, scene.installedPlugins))
1038
+ return false;
1039
+ return nodeRegistry.get(node.type)?.dirtyTracking !== false;
1040
+ }
1041
+ /**
1042
+ * `markDirty` always applied the consumer-kind guard, but many call sites add
1043
+ * to the raw set directly (that is how stuck `level` marks got in) — enforcing
1044
+ * it in `add` itself keeps them all honest.
1045
+ */
1046
+ class GuardedDirtySet extends Set {
1047
+ getScene;
1048
+ constructor(getScene, from) {
1049
+ super();
1050
+ this.getScene = getScene;
1051
+ if (from)
1052
+ for (const id of from)
1053
+ this.add(id);
1054
+ }
1055
+ add(id) {
1056
+ if (!isDirtyTrackable(id, this.getScene()))
1057
+ return this;
1058
+ return super.add(id);
1059
+ }
1060
+ }
1061
+ function createSceneStore(config) {
1062
+ const hydratedConfig = (set, get, store) => {
1063
+ const setWithHydration = (partial, replace) => {
1064
+ const state = get();
1065
+ let next = typeof partial === 'function' ? partial(state) : partial;
1066
+ const documentChanged = ['nodes', 'rootNodeIds', 'materials', 'collections', 'installedPlugins'].some((key) => (replace || key in next) && next[key] !== state[key]);
1067
+ if (documentChanged && !isHydrationNormalization()) {
1068
+ invalidatePendingHydration();
1069
+ if (state.hydrationToken)
1070
+ next = { ...next, hydrationToken: null };
1071
+ }
1072
+ if (replace)
1073
+ set(next, true);
1074
+ else
1075
+ set(next);
1076
+ };
1077
+ store.setState = setWithHydration;
1078
+ return config(setWithHydration, get, store);
1079
+ };
1080
+ return create()(hydratedConfig);
1081
+ }
1082
+ function runTemporalJump(target, jump) {
1083
+ if (!target?.nodes) {
1084
+ jump();
1085
+ return;
1086
+ }
1087
+ const before = useScene.getState().nodes;
1088
+ const changed = new Set();
1089
+ for (const id of new Set([...Object.keys(before), ...Object.keys(target.nodes)])) {
1090
+ const nodeId = id;
1091
+ const previous = before[nodeId];
1092
+ const next = target.nodes[nodeId];
1093
+ if (previous === next)
1094
+ continue;
1095
+ // Structural hierarchy changes keep the full-level reconciliation fallback.
1096
+ if ([previous, next].some((node) => node && ['site', 'building', 'level'].includes(node.type))) {
1097
+ jump();
1098
+ return;
1099
+ }
1100
+ changed.add(nodeId);
1101
+ }
1102
+ runWithSceneCommitNodeIds(changed, jump);
1103
+ }
1104
+ const useScene = createSceneStore(temporal((set, get) => ({
889
1105
  // 1. Flat dictionary of all nodes
890
1106
  nodes: {},
891
1107
  // 2. Root node IDs
892
1108
  rootNodeIds: [],
893
1109
  // 3. Dirty set
894
- dirtyNodes: new Set(),
1110
+ dirtyNodes: new GuardedDirtySet(get),
1111
+ hydrationToken: null,
1112
+ hydrationId: null,
1113
+ invalidateHydration: () => {
1114
+ invalidatePendingHydration();
1115
+ if (get().hydrationToken)
1116
+ set({ hydrationToken: null });
1117
+ },
895
1118
  // 4. Collections
896
1119
  collections: {},
897
1120
  materials: {},
@@ -901,10 +1124,13 @@ const useScene = create()(temporal((set, get) => ({
901
1124
  readOnly: false,
902
1125
  setReadOnly: (readOnly) => set({ readOnly }),
903
1126
  unloadScene: () => {
1127
+ invalidatePendingHydration();
904
1128
  set({
1129
+ hydrationToken: null,
1130
+ hydrationId: null,
905
1131
  nodes: {},
906
1132
  rootNodeIds: [],
907
- dirtyNodes: new Set(),
1133
+ dirtyNodes: new GuardedDirtySet(get),
908
1134
  collections: {},
909
1135
  materials: {},
910
1136
  installedPlugins: [],
@@ -947,26 +1173,65 @@ const useScene = create()(temporal((set, get) => ({
947
1173
  // pre-write state onto `pastStates`. Writing the scene in two steps
948
1174
  // (as this used to) exposed a half-normalized intermediate state —
949
1175
  // and the pre-load (possibly empty) state — as undo targets.
950
- set({
951
- nodes: cleanedNodes,
952
- rootNodeIds: normalizedRootNodeIds,
953
- dirtyNodes: new Set(),
954
- collections: extra?.collections ?? {},
955
- materials,
956
- installedPlugins: Array.from(new Set(extra?.installedPlugins ?? [])),
957
- hasExplicitPluginInstallState: extra?.hasExplicitPluginInstallState ?? false,
958
- });
959
- // Mark all nodes as dirty to trigger re-validation
960
- Object.values(cleanedNodes).forEach((node) => {
961
- get().markDirty(node.id);
962
- });
1176
+ const hydrationId = {};
1177
+ runSceneHydration(() => {
1178
+ set({
1179
+ hydrationToken: null,
1180
+ hydrationId,
1181
+ nodes: cleanedNodes,
1182
+ rootNodeIds: normalizedRootNodeIds,
1183
+ dirtyNodes: new GuardedDirtySet(get),
1184
+ collections: extra?.collections ?? {},
1185
+ materials,
1186
+ installedPlugins: Array.from(new Set(extra?.installedPlugins ?? [])),
1187
+ hasExplicitPluginInstallState: extra?.hasExplicitPluginInstallState ?? false,
1188
+ });
1189
+ const applyNormalization = (updates) => {
1190
+ if (updates.length > 0)
1191
+ get().updateNodes(updates);
1192
+ };
1193
+ const hydratedNodes = Object.values(get().nodes);
1194
+ if (!get().readOnly) {
1195
+ pauseSceneHistory(useScene);
1196
+ try {
1197
+ if (hydratedNodes.some((node) => node.type === 'elevator')) {
1198
+ applyNormalization(syncAutoElevatorOpenings(get().nodes));
1199
+ }
1200
+ }
1201
+ finally {
1202
+ resumeSceneHistory(useScene);
1203
+ }
1204
+ if (hydratedNodes.some((node) => node.type === 'stair')) {
1205
+ // Spatial-grid subscribers must settle first. Owning this pass
1206
+ // here also covers opening systems that mount after the load.
1207
+ queueSceneNormalization(() => {
1208
+ if (get().hydrationId !== hydrationId)
1209
+ return;
1210
+ pauseSceneHistory(useScene);
1211
+ try {
1212
+ applyNormalization(syncStairRises(get().nodes));
1213
+ applyNormalization(syncAutoStairOpenings(get().nodes));
1214
+ }
1215
+ finally {
1216
+ resumeSceneHistory(useScene);
1217
+ }
1218
+ });
1219
+ }
1220
+ }
1221
+ // Mark all nodes as dirty to trigger re-validation
1222
+ Object.values(get().nodes).forEach((node) => {
1223
+ get().markDirty(node.id);
1224
+ });
1225
+ }, () => set({ hydrationToken: hydrationId }));
963
1226
  },
964
1227
  setInstalledPlugins: (pluginIds, options) => {
965
1228
  if (get().readOnly)
966
1229
  return;
967
1230
  const nextInstalledPlugins = Array.from(new Set(pluginIds));
968
1231
  const previousInstalledPlugins = get().installedPlugins;
969
- const dirtyNodes = new Set(get().dirtyNodes);
1232
+ // Guard against the *next* plugin list: the store still holds the old
1233
+ // one, and re-marks for newly enabled kinds must pass the guard.
1234
+ const dirtyNodes = new GuardedDirtySet(() => ({ nodes: get().nodes, installedPlugins: nextInstalledPlugins }), get().dirtyNodes);
970
1235
  for (const node of Object.values(get().nodes)) {
971
1236
  if (!getNodePluginId(node.type))
972
1237
  continue;
@@ -1017,10 +1282,9 @@ const useScene = create()(temporal((set, get) => ({
1017
1282
  set({ nodes, rootNodeIds });
1018
1283
  },
1019
1284
  markDirty: (id) => {
1020
- const node = get().nodes[id];
1021
- if (node && !isNodeKindEnabled(node.type, get().installedPlugins))
1022
- return;
1023
- if (node && nodeRegistry.get(node.type)?.dirtyTracking === false)
1285
+ // Guarded here too, not just in GuardedDirtySet.add — tests (and any
1286
+ // setState caller) can swap in a plain Set.
1287
+ if (!isDirtyTrackable(id, get()))
1024
1288
  return;
1025
1289
  get().dirtyNodes.add(id);
1026
1290
  },
@@ -1170,8 +1434,27 @@ const useScene = create()(temporal((set, get) => ({
1170
1434
  current: sceneHistorySnapshotFromState(currentState),
1171
1435
  });
1172
1436
  },
1437
+ wrapTemporal: (config) => (set, get, store) => {
1438
+ const state = config(set, get, store);
1439
+ return {
1440
+ ...state,
1441
+ undo: (steps = 1) => runTemporalJump(get().pastStates.slice().splice(-steps, steps)[0], () => state.undo(steps)),
1442
+ redo: (steps = 1) => runTemporalJump(get().futureStates.slice().splice(-steps, steps)[0], () => state.redo(steps)),
1443
+ };
1444
+ },
1173
1445
  limit: 50, // Limit to last 50 actions
1174
1446
  }));
1447
+ // Live state belongs to the hydration owner so even a lazy consumer cannot
1448
+ // miss an override that was set and cleared before its first frame.
1449
+ const invalidateForLiveState = () => {
1450
+ if (useLiveNodeOverrides.getState().overrides.size ||
1451
+ useLiveTransforms.getState().transforms.size) {
1452
+ useScene.getState().invalidateHydration();
1453
+ }
1454
+ };
1455
+ useLiveNodeOverrides.subscribe(invalidateForLiveState);
1456
+ useLiveTransforms.subscribe(invalidateForLiveState);
1457
+ useScene.subscribe(invalidateForLiveState);
1175
1458
  export default useScene;
1176
1459
  let sceneReadOnlyLeaseCount = 0;
1177
1460
  let sceneReadOnlyLeaseBaseline = false;
@@ -1543,6 +1826,8 @@ export function applySceneSnapshot(snapshot, options) {
1543
1826
  if (!temporalState.isTracking || getSceneHistoryPauseDepth() > 0) {
1544
1827
  throw new Error('Cannot replace the scene snapshot during an active interaction');
1545
1828
  }
1829
+ useLiveNodeOverrides.getState().clearAll();
1830
+ useLiveTransforms.getState().clearAll();
1546
1831
  pauseSceneHistory(useScene);
1547
1832
  try {
1548
1833
  useScene.getState().setScene(snapshot.nodes, snapshot.rootNodeIds, {
@@ -1555,18 +1840,15 @@ export function applySceneSnapshot(snapshot, options) {
1555
1840
  finally {
1556
1841
  resumeSceneHistory(useScene);
1557
1842
  }
1558
- useLiveNodeOverrides.getState().clearAll();
1559
- useLiveTransforms.getState().clearAll();
1560
1843
  const current = sceneHistorySnapshotFromState(useScene.getState());
1561
1844
  if (areSceneSnapshotsEqual(before, current))
1562
1845
  return false;
1563
1846
  notifySceneCommit({ origin: options.origin, before, current });
1564
1847
  return true;
1565
1848
  }
1566
- // Track previous temporal state lengths and node snapshot for diffing
1849
+ // Track previous temporal state lengths for identifying history jumps
1567
1850
  let prevPastLength = 0;
1568
1851
  let prevFutureLength = 0;
1569
- let prevNodesSnapshot = null;
1570
1852
  export function clearSceneHistory() {
1571
1853
  resetSceneHistoryPauseDepth();
1572
1854
  // Resetting the pause-depth counter without resuming would strand the
@@ -1578,10 +1860,14 @@ export function clearSceneHistory() {
1578
1860
  useScene.temporal.getState().clear();
1579
1861
  prevPastLength = 0;
1580
1862
  prevFutureLength = 0;
1581
- prevNodesSnapshot = null;
1582
1863
  }
1583
1864
  // Subscribe to the temporal store (Undo/Redo events)
1584
- useScene.temporal.subscribe((state) => {
1865
+ useScene.temporal.subscribe((state, previousState) => {
1866
+ // Zundo mutates its source stack before writing the scene. Reconciliation's
1867
+ // pause/resume notifications must not advance our pre-jump stack lengths.
1868
+ if (state.pastStates === previousState.pastStates &&
1869
+ state.futureStates === previousState.futureStates)
1870
+ return;
1585
1871
  const currentPastLength = state.pastStates.length;
1586
1872
  const currentFutureLength = state.futureStates.length;
1587
1873
  // Undo: futureStates increases (state moved from past to future)
@@ -1589,40 +1875,21 @@ useScene.temporal.subscribe((state) => {
1589
1875
  const didUndo = currentFutureLength > prevFutureLength;
1590
1876
  const didRedo = currentPastLength > prevPastLength && currentFutureLength < prevFutureLength;
1591
1877
  if (didUndo || didRedo) {
1592
- // Capture the previous snapshot before RAF fires
1593
- const snapshotBefore = prevNodesSnapshot;
1878
+ // Capture both layouts before another synchronous jump can replace them.
1879
+ // The state pushed onto the opposite stack includes history-paused derived
1880
+ // writes (such as stair rise), unlike a snapshot saved at the last edit.
1881
+ const snapshotBefore = didUndo
1882
+ ? state.futureStates[prevFutureLength]?.nodes
1883
+ : state.pastStates[prevPastLength]?.nodes;
1884
+ const snapshotAfter = useScene.getState().nodes;
1594
1885
  // Defer to a microtask so the scene store has settled before we diff,
1595
1886
  // but still mark walls/items dirty before the next paint.
1596
1887
  queueMicrotask(() => {
1597
1888
  const currentNodes = useScene.getState().nodes;
1598
1889
  const { markDirty } = useScene.getState();
1599
1890
  if (snapshotBefore) {
1600
- // Diff: only mark nodes that actually changed
1601
- for (const [id, node] of Object.entries(currentNodes)) {
1602
- if (snapshotBefore[id] !== node) {
1603
- markDirty(id);
1604
- // Also mark parent so merged geometries update
1605
- if (node.parentId)
1606
- markDirty(node.parentId);
1607
- }
1608
- }
1609
- // Nodes that were deleted (exist in prev but not current)
1610
- for (const [id, node] of Object.entries(snapshotBefore)) {
1611
- if (!currentNodes[id]) {
1612
- const parentId = node.parentId;
1613
- if (parentId) {
1614
- markDirty(parentId);
1615
- // Mark sibling nodes dirty so they can update their geometry
1616
- // (e.g. adjacent walls need to recalculate miter/junction geometry)
1617
- const parent = currentNodes[parentId];
1618
- if (parent && 'children' in parent && Array.isArray(parent.children)) {
1619
- for (const childId of parent.children) {
1620
- markDirty(childId);
1621
- }
1622
- }
1623
- }
1624
- }
1625
- }
1891
+ for (const id of getHistoryDirtyNodeIds(snapshotBefore, snapshotAfter))
1892
+ markDirty(id);
1626
1893
  }
1627
1894
  else {
1628
1895
  // No snapshot to diff against — fall back to marking all
@@ -1630,10 +1897,17 @@ useScene.temporal.subscribe((state) => {
1630
1897
  markDirty(node.id);
1631
1898
  }
1632
1899
  }
1900
+ // Undo/redo rewrites `nodes` without going through the delete actions,
1901
+ // so marks for nodes that no longer exist would sit in the set for the
1902
+ // rest of the session — no system clears a mark whose node is gone.
1903
+ const { dirtyNodes, clearDirty } = useScene.getState();
1904
+ for (const id of [...dirtyNodes]) {
1905
+ if (!currentNodes[id])
1906
+ clearDirty(id);
1907
+ }
1633
1908
  });
1634
1909
  }
1635
- // Update tracked lengths and snapshot
1910
+ // Update tracked lengths
1636
1911
  prevPastLength = currentPastLength;
1637
1912
  prevFutureLength = currentFutureLength;
1638
- prevNodesSnapshot = useScene.getState().nodes;
1639
1913
  });
@@ -1,5 +1,5 @@
1
- import type { AnyNode, AnyNodeId } from '../../schema';
2
- import type { ElevatorInteractiveState } from '../../store/use-interactive';
1
+ import type { AnyNode, AnyNodeId } from '../../schema/index.js';
2
+ import type { ElevatorInteractiveState } from '../../store/use-interactive.js';
3
3
  type ElevatorRuntimeMap = Record<AnyNodeId, ElevatorInteractiveState>;
4
4
  type ResolveElevatorDispatchTargetArgs = {
5
5
  elevators: ElevatorRuntimeMap;
@@ -1,4 +1,4 @@
1
- import { resolveElevatorServiceLevels } from './elevator-service';
1
+ import { resolveElevatorServiceLevels } from './elevator-service.js';
2
2
  function getRuntimeLevelId(runtime, elevator) {
3
3
  return runtime?.currentLevelId ?? elevator.defaultLevelId ?? elevator.fromLevelId ?? null;
4
4
  }
@@ -1,4 +1,4 @@
1
- import type { ElevatorDoorPanelStyle, ElevatorDoorStyle, ElevatorNode, ElevatorShaftStyle } from '../../schema';
1
+ import type { ElevatorDoorPanelStyle, ElevatorDoorStyle, ElevatorNode, ElevatorShaftStyle } from '../../schema/index.js';
2
2
  export type ElevatorDoorSide = 'left' | 'right';
3
3
  export declare function getResolvedElevatorDoorStyle(doorStyle: ElevatorNode['doorStyle'] | undefined): ElevatorDoorStyle;
4
4
  export declare function getResolvedElevatorDoorPanelStyle(doorPanelStyle: ElevatorNode['doorPanelStyle'] | undefined): ElevatorDoorPanelStyle;
@@ -1,4 +1,4 @@
1
- import type { AnyNode, AnyNodeId, CeilingNode, SlabNode } from '../../schema';
1
+ import type { AnyNode, AnyNodeId, CeilingNode, SlabNode } from '../../schema/index.js';
2
2
  export declare function syncAutoElevatorOpenings(nodes: Record<string, AnyNode>): {
3
3
  id: AnyNodeId;
4
4
  data: Partial<SlabNode | CeilingNode>;