@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,6 +1,8 @@
1
- import { nodeRegistry } from '../../registry/registry';
2
- import { AnyNode as AnyNodeSchema, createDefaultRidgeVentsForSegment, getEffectiveWallSurfaceMaterial, getWallSurfaceMaterialSignature, isAutoRidgeVentEnabled, isDefaultRidgeVentNode, } from '../../schema';
3
- import { addActiveSceneCommitNodeIds, runWithSceneCommitNodeIds } from '../history-control';
1
+ import { nodeRegistry } from '../../registry/registry.js';
2
+ import { validateNodeRelations } from '../../registry/validate-relations.js';
3
+ import { AnyNode as AnyNodeSchema, createDefaultGuttersForSegment, createDefaultRidgeVentsForSegment, DownspoutNode as DownspoutNodeSchema, defaultDownspoutMetadata, generateId, getDefaultGutterSide, getEffectiveWallSurfaceMaterial, getWallSurfaceMaterialSignature, isAutoGutterEnabled, isAutoRidgeVentEnabled, isDefaultDownspoutNode, isDefaultGutterNode, isDefaultRidgeVentNode, parseNode, planAutomaticDownspouts, resolveAutomaticDownspoutLength, } from '../../schema/index.js';
4
+ import { constrainWallCurveOffsetToAvoidIntersections } from '../../systems/wall/wall-curve.js';
5
+ import { activeSceneCommitNodeIds, addActiveSceneCommitNodeIds, runWithSceneCommitNodeIds, } from '../history-control.js';
4
6
  const DEFAULT_RIDGE_VENT_REFRESH_FIELDS = new Set([
5
7
  'roofType',
6
8
  'width',
@@ -15,6 +17,18 @@ const DEFAULT_RIDGE_VENT_REFRESH_FIELDS = new Set([
15
17
  'dutchWaistLengthRatio',
16
18
  'dutchGabletRake',
17
19
  ]);
20
+ const DEFAULT_GUTTER_REFRESH_FIELDS = new Set([
21
+ 'metadata',
22
+ 'position',
23
+ 'rotation',
24
+ 'roofType',
25
+ 'width',
26
+ 'depth',
27
+ 'wallHeight',
28
+ 'pitch',
29
+ 'overhang',
30
+ 'trim',
31
+ ]);
18
32
  const NUMBER_FORMAT_BOUNDS = {
19
33
  safeint: [Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER],
20
34
  int32: [-2147483648, 2147483647],
@@ -319,21 +333,46 @@ function formatNumericValue(value) {
319
333
  return '-Infinity';
320
334
  return String(value);
321
335
  }
322
- function numericSanitizeIssuesToMessage(issues) {
323
- return issues
324
- .map((issue) => {
325
- const path = issue.path.map(String).join('.') || '<root>';
326
- const to = issue.to === undefined ? '' : ` -> ${formatNumericValue(issue.to)}`;
327
- return `${path}: ${formatNumericValue(issue.from)} ${issue.action}${to}`;
328
- })
329
- .join('; ');
336
+ export function numericSanitizeIssuesToMessage(issues) {
337
+ if (!Array.isArray(issues))
338
+ return '';
339
+ try {
340
+ return issues
341
+ .map((issue) => {
342
+ const path = Array.isArray(issue?.path)
343
+ ? issue.path.map(String).join('.') || '<root>'
344
+ : '<unknown>';
345
+ const to = issue?.to === undefined ? '' : ` -> ${formatNumericValue(issue.to)}`;
346
+ return `${path}: ${formatNumericValue(issue?.from)} ${issue?.action ?? 'sanitized'}${to}`;
347
+ })
348
+ .join('; ');
349
+ }
350
+ catch {
351
+ return '<diagnostic unavailable>';
352
+ }
330
353
  }
331
354
  function warnSanitizedNodeMutation(mutation, nodeId, issues) {
332
- console.warn(`[Scene] Sanitized invalid numeric node ${mutation}`, nodeId, numericSanitizeIssuesToMessage(issues));
355
+ let message = '<diagnostic unavailable>';
356
+ try {
357
+ message = numericSanitizeIssuesToMessage(issues);
358
+ }
359
+ catch {
360
+ // Reporting must never interrupt a node mutation.
361
+ }
362
+ try {
363
+ console.warn(`[Scene] Sanitized invalid numeric node ${mutation}`, nodeId, message);
364
+ }
365
+ catch {
366
+ // A broken diagnostic sink must not interrupt a node mutation either.
367
+ }
333
368
  }
334
369
  function parseCreatedNode(node, parentId) {
335
370
  const candidate = { ...node, parentId };
336
- const parsed = AnyNodeSchema.safeParse(candidate);
371
+ const registered = nodeRegistry.get(candidate.type)?.schema;
372
+ // Generated definitions must reject invalid geometry instead of retaining a failed parse.
373
+ if (registered?.meta?.()?.strictMutations === true)
374
+ return registered.parse(candidate);
375
+ const parsed = parseNode(candidate);
337
376
  if (parsed.success)
338
377
  return parsed.data;
339
378
  const schema = getNodeSchemaForType(candidate.type);
@@ -359,9 +398,19 @@ function mergeNodeUpdate(currentNode, patch) {
359
398
  }
360
399
  function parseUpdatedNode(currentNode, data) {
361
400
  const candidate = mergeNodeUpdate(currentNode, data);
362
- const parsed = AnyNodeSchema.safeParse(candidate);
401
+ // Graph links survive schemas that omit children; only an explicit patch may change them.
402
+ const preserveChildren = (updated) => !Object.hasOwn(data, 'children') &&
403
+ 'children' in currentNode &&
404
+ Array.isArray(currentNode.children)
405
+ ? { ...updated, children: currentNode.children }
406
+ : updated;
407
+ const registered = nodeRegistry.get(currentNode.type)?.schema;
408
+ // Generated definitions must reject invalid geometry instead of retaining a failed parse.
409
+ if (registered?.meta?.()?.strictMutations === true)
410
+ return preserveChildren(registered.parse(candidate));
411
+ const parsed = parseNode(candidate);
363
412
  if (parsed.success)
364
- return parsed.data;
413
+ return preserveChildren(parsed.data);
365
414
  const schema = getNodeSchemaForType(candidate.type);
366
415
  const sanitized = sanitizeNumericValue(schema, data, currentNode, []);
367
416
  if (sanitized.issues.length === 0) {
@@ -398,6 +447,306 @@ function refreshDefaultRidgeVentsForSegment(nextNodes, segment) {
398
447
  };
399
448
  return nextVents.map((vent) => vent.id);
400
449
  }
450
+ function shouldRefreshDefaultGutters(data) {
451
+ return Object.keys(data).some((key) => DEFAULT_GUTTER_REFRESH_FIELDS.has(key));
452
+ }
453
+ function getLeanToGutterExclusions(nodes, segmentId) {
454
+ return Object.values(nodes).flatMap((node) => {
455
+ if (node.type !== 'lean-to-extension' ||
456
+ node.connectionMode !== 'auto' ||
457
+ node.hostRoofSegmentId !== segmentId ||
458
+ !node.hostRoofEdge) {
459
+ return [];
460
+ }
461
+ const range = node.hostRoofEdgeRange ?? [0, 1];
462
+ return [{ side: node.hostRoofEdge, from: range[0], to: range[1] }];
463
+ });
464
+ }
465
+ function addLeanToHostRoofId(node, nodes, roofIds) {
466
+ if (node?.type !== 'lean-to-extension' || !node.hostRoofSegmentId)
467
+ return;
468
+ const segment = nodes[node.hostRoofSegmentId];
469
+ const roofId = segment?.type === 'roof-segment'
470
+ ? segment.parentId
471
+ : node.hostRoofId;
472
+ if (roofId && nodes[roofId]?.type === 'roof')
473
+ roofIds.add(roofId);
474
+ }
475
+ // When an eave carries several default gutter runs on the same side (e.g. a run
476
+ // split by a lean-to exclusion), reuse the existing node whose plan position is
477
+ // closest to the desired run rather than an arbitrary queue order — otherwise
478
+ // the runs swap positions and their downspouts follow the wrong segment.
479
+ function takeNearestGutterId(candidateIds, desired, nodes) {
480
+ if (candidateIds.length === 0)
481
+ return undefined;
482
+ let bestIndex = 0;
483
+ let bestDistance = Number.POSITIVE_INFINITY;
484
+ for (let i = 0; i < candidateIds.length; i++) {
485
+ const node = nodes[candidateIds[i]];
486
+ const position = node && 'position' in node ? node.position : undefined;
487
+ const dx = (position?.[0] ?? 0) - desired.position[0];
488
+ const dz = (position?.[2] ?? 0) - desired.position[2];
489
+ const distance = dx * dx + dz * dz;
490
+ if (distance < bestDistance) {
491
+ bestDistance = distance;
492
+ bestIndex = i;
493
+ }
494
+ }
495
+ return candidateIds.splice(bestIndex, 1)[0];
496
+ }
497
+ function refreshDefaultGuttersForSegment(nextNodes, segment, roofSegments) {
498
+ const childIds = Array.isArray(segment.children) ? segment.children : [];
499
+ const existingIds = childIds.filter((childId) => isDefaultGutterNode(nextNodes[childId], segment.id));
500
+ if (!isAutoGutterEnabled(segment, nextNodes) && existingIds.length === 0) {
501
+ return { dirtyIds: [], deletedIds: [] };
502
+ }
503
+ const existingBySide = new Map();
504
+ for (const id of existingIds) {
505
+ const side = getDefaultGutterSide(nextNodes[id], segment.id);
506
+ if (!side)
507
+ continue;
508
+ const ids = existingBySide.get(side) ?? [];
509
+ ids.push(id);
510
+ existingBySide.set(side, ids);
511
+ }
512
+ const desiredGutters = isAutoGutterEnabled(segment, nextNodes)
513
+ ? createDefaultGuttersForSegment(segment, roofSegments, getLeanToGutterExclusions(nextNodes, segment.id))
514
+ : [];
515
+ const desiredChildIds = [];
516
+ const dirtyIds = [];
517
+ for (const desired of desiredGutters) {
518
+ const side = getDefaultGutterSide(desired, segment.id);
519
+ if (!side)
520
+ continue;
521
+ const matchingIds = existingBySide.get(side);
522
+ const existingId = matchingIds
523
+ ? takeNearestGutterId(matchingIds, desired, nextNodes)
524
+ : undefined;
525
+ if (existingId) {
526
+ const existing = nextNodes[existingId];
527
+ nextNodes[existingId] = {
528
+ ...existing,
529
+ parentId: segment.id,
530
+ roofSegmentId: segment.id,
531
+ position: desired.position,
532
+ rotation: desired.rotation,
533
+ length: desired.length,
534
+ };
535
+ desiredChildIds.push(existingId);
536
+ dirtyIds.push(existingId);
537
+ continue;
538
+ }
539
+ const desiredId = desired.id;
540
+ nextNodes[desiredId] = { ...desired, parentId: segment.id };
541
+ desiredChildIds.push(desiredId);
542
+ dirtyIds.push(desiredId);
543
+ }
544
+ const retainedIdSet = new Set(desiredChildIds);
545
+ const deletedIds = existingIds.filter((id) => !retainedIdSet.has(id));
546
+ const deletedGutterIds = new Set(deletedIds);
547
+ for (const [nodeId, node] of Object.entries(nextNodes)) {
548
+ if (node.type === 'downspout' &&
549
+ node.gutterId &&
550
+ deletedGutterIds.has(node.gutterId)) {
551
+ deletedIds.push(nodeId);
552
+ }
553
+ }
554
+ const deletedIdSet = new Set(deletedIds);
555
+ for (const id of deletedIds)
556
+ delete nextNodes[id];
557
+ nextNodes[segment.id] = {
558
+ ...segment,
559
+ children: [
560
+ ...childIds.filter((childId) => !deletedIdSet.has(childId) && !existingIds.includes(childId)),
561
+ ...desiredChildIds,
562
+ ],
563
+ };
564
+ return { dirtyIds, deletedIds };
565
+ }
566
+ function getRoofSegments(nextNodes, segment) {
567
+ const roof = segment.parentId ? nextNodes[segment.parentId] : undefined;
568
+ if (!(roof && roof.type === 'roof'))
569
+ return [segment];
570
+ return (roof.children ?? [])
571
+ .map((childId) => nextNodes[childId])
572
+ .filter((node) => node?.type === 'roof-segment');
573
+ }
574
+ function refreshDefaultDownspoutsForRoof(nextNodes, roofSegments) {
575
+ const gutters = roofSegments.flatMap((segment) => {
576
+ const current = nextNodes[segment.id];
577
+ if (current?.type !== 'roof-segment')
578
+ return [];
579
+ return (current.children ?? [])
580
+ .map((childId) => nextNodes[childId])
581
+ .filter((node) => node?.type === 'gutter' && isDefaultGutterNode(node, current.id));
582
+ });
583
+ const gutterById = new Map(gutters.map((gutter) => [gutter.id, gutter]));
584
+ const downspouts = Object.values(nextNodes).filter((node) => node?.type === 'downspout' && Boolean(node.gutterId && gutterById.has(node.gutterId)));
585
+ const generated = downspouts.filter((downspout) => {
586
+ if (!isDefaultDownspoutNode(downspout))
587
+ return false;
588
+ const gutter = downspout.gutterId ? gutterById.get(downspout.gutterId) : undefined;
589
+ return gutter?.outlets.some((outlet) => outlet.id === downspout.outletId && outlet.generatedBy === 'default-downspout');
590
+ });
591
+ const generatedIds = new Set(generated.map((downspout) => downspout.id));
592
+ const manual = downspouts.filter((downspout) => !generatedIds.has(downspout.id));
593
+ const placements = planAutomaticDownspouts({
594
+ segments: roofSegments,
595
+ gutters,
596
+ downspouts: manual,
597
+ });
598
+ const segmentById = new Map(roofSegments.map((segment) => [segment.id, segment]));
599
+ const availableByGutter = new Map();
600
+ for (const downspout of generated) {
601
+ if (!downspout.gutterId)
602
+ continue;
603
+ const available = availableByGutter.get(downspout.gutterId) ?? [];
604
+ available.push(downspout);
605
+ availableByGutter.set(downspout.gutterId, available);
606
+ }
607
+ const retainedIds = new Set();
608
+ const retainedOutletIds = new Set();
609
+ const dirtyIds = new Set();
610
+ const deletedIds = [];
611
+ const outletsByGutter = new Map(gutters.map((gutter) => [gutter.id, [...(gutter.outlets ?? [])]]));
612
+ for (const placement of placements) {
613
+ const gutter = gutterById.get(placement.gutterId);
614
+ if (!gutter?.roofSegmentId)
615
+ continue;
616
+ const segment = segmentById.get(gutter.roofSegmentId);
617
+ if (!segment)
618
+ continue;
619
+ const outlets = outletsByGutter.get(gutter.id) ?? [];
620
+ const available = availableByGutter.get(gutter.id) ?? [];
621
+ let bestIndex = -1;
622
+ let bestDistance = Number.POSITIVE_INFINITY;
623
+ for (let index = 0; index < available.length; index++) {
624
+ const candidate = available[index];
625
+ const outlet = outlets.find((entry) => entry.id === candidate.outletId);
626
+ const distance = outlet ? Math.abs(outlet.offset - placement.offset) : 0;
627
+ if (distance < bestDistance) {
628
+ bestDistance = distance;
629
+ bestIndex = index;
630
+ }
631
+ }
632
+ const existing = bestIndex >= 0 ? available.splice(bestIndex, 1)[0] : undefined;
633
+ const outletId = existing?.outletId ?? generateId('outlet');
634
+ const outletIndex = outlets.findIndex((outlet) => outlet.id === outletId);
635
+ const outlet = {
636
+ id: outletId,
637
+ offset: placement.offset,
638
+ diameter: 0.07,
639
+ generatedBy: 'default-downspout',
640
+ };
641
+ if (outletIndex >= 0)
642
+ outlets[outletIndex] = { ...outlets[outletIndex], ...outlet };
643
+ else
644
+ outlets.push(outlet);
645
+ outletsByGutter.set(gutter.id, outlets);
646
+ retainedOutletIds.add(outletId);
647
+ const length = resolveAutomaticDownspoutLength(nextNodes, segment, gutter, placement.offset);
648
+ const downspout = existing
649
+ ? {
650
+ ...existing,
651
+ parentId: segment.id,
652
+ gutterId: gutter.id,
653
+ outletId,
654
+ length: existing.lengthMode === 'manual' ? existing.length : length,
655
+ lengthMode: existing.lengthMode === 'manual' ? 'manual' : 'to-ground',
656
+ }
657
+ : DownspoutNodeSchema.parse({
658
+ name: 'Downspout',
659
+ parentId: segment.id,
660
+ gutterId: gutter.id,
661
+ outletId,
662
+ length,
663
+ lengthMode: 'to-ground',
664
+ diameter: outlet.diameter,
665
+ metadata: defaultDownspoutMetadata(),
666
+ });
667
+ nextNodes[downspout.id] = downspout;
668
+ retainedIds.add(downspout.id);
669
+ dirtyIds.add(downspout.id);
670
+ const currentSegment = nextNodes[segment.id];
671
+ if (currentSegment?.type === 'roof-segment') {
672
+ nextNodes[segment.id] = {
673
+ ...currentSegment,
674
+ children: Array.from(new Set([...(currentSegment.children ?? []), downspout.id])),
675
+ };
676
+ dirtyIds.add(segment.id);
677
+ }
678
+ }
679
+ for (const [gutterId, outlets] of outletsByGutter) {
680
+ const gutter = nextNodes[gutterId];
681
+ if (gutter?.type !== 'gutter')
682
+ continue;
683
+ nextNodes[gutterId] = {
684
+ ...gutter,
685
+ outlets: outlets.filter((outlet) => outlet.generatedBy !== 'default-downspout' || retainedOutletIds.has(outlet.id)),
686
+ };
687
+ dirtyIds.add(gutterId);
688
+ }
689
+ for (const downspout of generated) {
690
+ const downspoutId = downspout.id;
691
+ if (retainedIds.has(downspoutId))
692
+ continue;
693
+ const gutter = downspout.gutterId ? nextNodes[downspout.gutterId] : undefined;
694
+ if (gutter?.type === 'gutter' && downspout.outletId) {
695
+ nextNodes[gutter.id] = {
696
+ ...gutter,
697
+ outlets: (gutter.outlets ?? []).filter((outlet) => outlet.id !== downspout.outletId),
698
+ };
699
+ dirtyIds.add(gutter.id);
700
+ }
701
+ const parent = downspout.parentId ? nextNodes[downspout.parentId] : undefined;
702
+ if (parent?.type === 'roof-segment') {
703
+ nextNodes[parent.id] = {
704
+ ...parent,
705
+ children: (parent.children ?? []).filter((childId) => childId !== downspout.id),
706
+ };
707
+ dirtyIds.add(parent.id);
708
+ }
709
+ delete nextNodes[downspoutId];
710
+ deletedIds.push(downspoutId);
711
+ }
712
+ return { dirtyIds: [...dirtyIds], deletedIds };
713
+ }
714
+ function refreshDefaultGuttersForRoof(nextNodes, segment) {
715
+ const roofSegments = getRoofSegments(nextNodes, segment);
716
+ const dirtyIds = [];
717
+ const deletedIds = [];
718
+ for (const roofSegment of roofSegments) {
719
+ const current = nextNodes[roofSegment.id];
720
+ if (current?.type !== 'roof-segment')
721
+ continue;
722
+ const result = refreshDefaultGuttersForSegment(nextNodes, current, roofSegments);
723
+ dirtyIds.push(...result.dirtyIds);
724
+ deletedIds.push(...result.deletedIds);
725
+ }
726
+ const downspoutResult = refreshDefaultDownspoutsForRoof(nextNodes, roofSegments);
727
+ dirtyIds.push(...downspoutResult.dirtyIds);
728
+ deletedIds.push(...downspoutResult.deletedIds);
729
+ return { dirtyIds, deletedIds };
730
+ }
731
+ function collectDefaultGutterRefresh(result, dirtyIds, deletedIds) {
732
+ for (const id of result.dirtyIds)
733
+ dirtyIds.add(id);
734
+ for (const id of result.deletedIds)
735
+ deletedIds.add(id);
736
+ }
737
+ function refreshDefaultGuttersForRoofIds(nextNodes, roofIds, dirtyIds, deletedIds) {
738
+ for (const roofId of new Set(roofIds)) {
739
+ const roof = nextNodes[roofId];
740
+ if (roof?.type !== 'roof')
741
+ continue;
742
+ const segment = (roof.children ?? [])
743
+ .map((childId) => nextNodes[childId])
744
+ .find((child) => child?.type === 'roof-segment');
745
+ if (!segment)
746
+ continue;
747
+ collectDefaultGutterRefresh(refreshDefaultGuttersForRoof(nextNodes, segment), dirtyIds, deletedIds);
748
+ }
749
+ }
401
750
  // Track pending RAF for updateNodesAction to prevent multiple queued callbacks
402
751
  let pendingRafId = null;
403
752
  let pendingUpdates = new Set();
@@ -551,6 +900,8 @@ function buildWallMergePlans(nodes, idsToDelete) {
551
900
  const createNodesActionImpl = (set, get, ops) => {
552
901
  if (get().readOnly)
553
902
  return;
903
+ const extraNodesToMarkDirty = new Set();
904
+ const extraNodesToClearDirty = new Set();
554
905
  set((state) => {
555
906
  const nextNodes = { ...state.nodes };
556
907
  const nextRootIds = [...state.rootNodeIds];
@@ -585,6 +936,16 @@ const createNodesActionImpl = (set, get, ops) => {
585
936
  }
586
937
  }
587
938
  }
939
+ const refreshedRoofIds = new Set();
940
+ for (const { node } of ops) {
941
+ const created = nextNodes[node.id];
942
+ if (created?.type === 'roof-segment' && created.parentId) {
943
+ refreshedRoofIds.add(created.parentId);
944
+ }
945
+ addLeanToHostRoofId(created, nextNodes, refreshedRoofIds);
946
+ }
947
+ refreshDefaultGuttersForRoofIds(nextNodes, refreshedRoofIds, extraNodesToMarkDirty, extraNodesToClearDirty);
948
+ addActiveSceneCommitNodeIds([...extraNodesToMarkDirty, ...extraNodesToClearDirty]);
588
949
  return { nodes: nextNodes, rootNodeIds: nextRootIds };
589
950
  });
590
951
  // 4. System Sync
@@ -595,6 +956,10 @@ const createNodesActionImpl = (set, get, ops) => {
595
956
  else if (node.parentId)
596
957
  get().markDirty(node.parentId);
597
958
  });
959
+ for (const id of extraNodesToMarkDirty)
960
+ get().markDirty(id);
961
+ for (const id of extraNodesToClearDirty)
962
+ get().clearDirty(id);
598
963
  };
599
964
  const applyNodeChangesActionImpl = (set, get, changes) => {
600
965
  if (get().readOnly)
@@ -603,17 +968,21 @@ const applyNodeChangesActionImpl = (set, get, changes) => {
603
968
  const updateOps = changes.update ?? [];
604
969
  const deleteOps = changes.delete ?? [];
605
970
  const nodesToMarkDirty = new Set();
971
+ const nodesToClearDirty = new Set();
606
972
  const parentsToMarkDirty = new Set();
607
973
  set((state) => {
608
974
  const nextNodes = { ...state.nodes };
609
975
  const nextCollections = { ...state.collections };
610
976
  const nextRootIds = [...state.rootNodeIds];
611
977
  let resolvedRootIds = nextRootIds;
978
+ const roofsToRefresh = new Set();
612
979
  for (const { id, data } of updateOps) {
613
980
  const currentNode = nextNodes[id];
614
981
  if (!currentNode)
615
982
  continue;
983
+ addLeanToHostRoofId(currentNode, nextNodes, roofsToRefresh);
616
984
  const updatedNode = parseUpdatedNode(currentNode, data);
985
+ addLeanToHostRoofId(updatedNode, nextNodes, roofsToRefresh);
617
986
  if (data.parentId !== undefined && data.parentId !== currentNode.parentId) {
618
987
  const oldParentId = currentNode.parentId;
619
988
  if (oldParentId && nextNodes[oldParentId]) {
@@ -640,6 +1009,11 @@ const applyNodeChangesActionImpl = (set, get, changes) => {
640
1009
  nodesToMarkDirty.add(ventId);
641
1010
  }
642
1011
  }
1012
+ const currentSegment = nextNodes[id];
1013
+ if (currentSegment?.type === 'roof-segment' && shouldRefreshDefaultGutters(data)) {
1014
+ if (currentSegment.parentId)
1015
+ roofsToRefresh.add(currentSegment.parentId);
1016
+ }
643
1017
  nodesToMarkDirty.add(id);
644
1018
  }
645
1019
  for (const { node, parentId } of createOps) {
@@ -647,6 +1021,10 @@ const applyNodeChangesActionImpl = (set, get, changes) => {
647
1021
  const newNode = parseCreatedNode(node, effectiveParentId);
648
1022
  nextNodes[newNode.id] = newNode;
649
1023
  nodesToMarkDirty.add(newNode.id);
1024
+ if (newNode.type === 'roof-segment' && effectiveParentId) {
1025
+ roofsToRefresh.add(effectiveParentId);
1026
+ }
1027
+ addLeanToHostRoofId(newNode, nextNodes, roofsToRefresh);
650
1028
  if (effectiveParentId && nextNodes[effectiveParentId]) {
651
1029
  const parent = nextNodes[effectiveParentId];
652
1030
  if ('children' in parent && Array.isArray(parent.children)) {
@@ -676,6 +1054,13 @@ const applyNodeChangesActionImpl = (set, get, changes) => {
676
1054
  for (const id of deleteOps) {
677
1055
  collectDelete(id);
678
1056
  }
1057
+ for (const id of allIdsToDelete) {
1058
+ const node = nextNodes[id];
1059
+ addLeanToHostRoofId(node, nextNodes, roofsToRefresh);
1060
+ if (node?.type === 'roof-segment' && node.parentId) {
1061
+ roofsToRefresh.add(node.parentId);
1062
+ }
1063
+ }
679
1064
  for (const id of allIdsToDelete) {
680
1065
  const node = nextNodes[id];
681
1066
  if (!node)
@@ -705,12 +1090,21 @@ const applyNodeChangesActionImpl = (set, get, changes) => {
705
1090
  }
706
1091
  delete nextNodes[id];
707
1092
  }
708
- addActiveSceneCommitNodeIds([...allIdsToDelete, ...nodesToMarkDirty, ...parentsToMarkDirty]);
1093
+ refreshDefaultGuttersForRoofIds(nextNodes, roofsToRefresh, nodesToMarkDirty, nodesToClearDirty);
1094
+ addActiveSceneCommitNodeIds([
1095
+ ...allIdsToDelete,
1096
+ ...nodesToMarkDirty,
1097
+ ...nodesToClearDirty,
1098
+ ...parentsToMarkDirty,
1099
+ ]);
709
1100
  return { nodes: nextNodes, rootNodeIds: resolvedRootIds, collections: nextCollections };
710
1101
  });
711
1102
  for (const id of nodesToMarkDirty) {
712
1103
  get().markDirty(id);
713
1104
  }
1105
+ for (const id of nodesToClearDirty) {
1106
+ get().clearDirty(id);
1107
+ }
714
1108
  for (const id of parentsToMarkDirty) {
715
1109
  get().markDirty(id);
716
1110
  const parent = get().nodes[id];
@@ -726,13 +1120,24 @@ const updateNodesActionImpl = (set, get, updates) => {
726
1120
  return;
727
1121
  const parentsToUpdate = new Set();
728
1122
  const extraNodesToUpdate = new Set();
1123
+ const extraNodesToDelete = new Set();
1124
+ const roofsToRefresh = new Set();
729
1125
  set((state) => {
730
1126
  const nextNodes = { ...state.nodes };
731
1127
  for (const { id, data } of updates) {
732
1128
  const currentNode = nextNodes[id];
733
1129
  if (!currentNode)
734
1130
  continue;
735
- const updatedNode = parseUpdatedNode(currentNode, data);
1131
+ addLeanToHostRoofId(currentNode, nextNodes, roofsToRefresh);
1132
+ const curveOffset = currentNode.type === 'wall' ? data.curveOffset : undefined;
1133
+ const constrainedData = currentNode.type === 'wall' && typeof curveOffset === 'number'
1134
+ ? {
1135
+ ...data,
1136
+ curveOffset: constrainWallCurveOffsetToAvoidIntersections(currentNode, curveOffset, Object.values(nextNodes).filter((node) => node.type === 'wall' && node.parentId === currentNode.parentId)),
1137
+ }
1138
+ : data;
1139
+ const updatedNode = parseUpdatedNode(currentNode, constrainedData);
1140
+ addLeanToHostRoofId(updatedNode, nextNodes, roofsToRefresh);
736
1141
  // Handle Reparenting Logic
737
1142
  if (data.parentId !== undefined && data.parentId !== currentNode.parentId) {
738
1143
  // 1. Remove from old parent
@@ -774,11 +1179,18 @@ const updateNodesActionImpl = (set, get, updates) => {
774
1179
  extraNodesToUpdate.add(ventId);
775
1180
  }
776
1181
  }
1182
+ const currentSegment = nextNodes[id];
1183
+ if (currentSegment?.type === 'roof-segment' && shouldRefreshDefaultGutters(data)) {
1184
+ if (currentSegment.parentId)
1185
+ roofsToRefresh.add(currentSegment.parentId);
1186
+ }
777
1187
  }
1188
+ refreshDefaultGuttersForRoofIds(nextNodes, roofsToRefresh, extraNodesToUpdate, extraNodesToDelete);
778
1189
  addActiveSceneCommitNodeIds([
779
1190
  ...updates.map(({ id }) => id),
780
1191
  ...parentsToUpdate,
781
1192
  ...extraNodesToUpdate,
1193
+ ...extraNodesToDelete,
782
1194
  ]);
783
1195
  return { nodes: nextNodes };
784
1196
  });
@@ -797,6 +1209,9 @@ const updateNodesActionImpl = (set, get, updates) => {
797
1209
  for (const id of extraNodesToUpdate) {
798
1210
  pendingUpdates.add(id);
799
1211
  }
1212
+ for (const id of extraNodesToDelete) {
1213
+ get().clearDirty(id);
1214
+ }
800
1215
  if (pendingRafId !== null) {
801
1216
  cancelAnimationFrame(pendingRafId);
802
1217
  }
@@ -815,6 +1230,7 @@ const deleteNodesActionImpl = (set, get, ids) => {
815
1230
  const nodesToMarkDirty = new Set();
816
1231
  const deletedIds = new Set();
817
1232
  const mergePlans = buildWallMergePlans(get().nodes, ids);
1233
+ const requestedDeleteIds = new Set(ids);
818
1234
  set((state) => {
819
1235
  const nextNodes = { ...state.nodes };
820
1236
  const nextCollections = { ...state.collections };
@@ -828,7 +1244,9 @@ const deleteNodesActionImpl = (set, get, ids) => {
828
1244
  allIds.add(id);
829
1245
  const node = nextNodes[id];
830
1246
  const cascadeDeletes = node
831
- ? nodeRegistry.get(node.type)?.parametrics?.onDeleteCascade?.(node, nextNodes, allIds)
1247
+ ? nodeRegistry
1248
+ .get(node.type)
1249
+ ?.parametrics?.onDeleteCascade?.(node, nextNodes, allIds, requestedDeleteIds)
832
1250
  : null;
833
1251
  if (cascadeDeletes) {
834
1252
  for (const companionId of cascadeDeletes)
@@ -844,6 +1262,14 @@ const deleteNodesActionImpl = (set, get, ids) => {
844
1262
  for (const plan of mergePlans) {
845
1263
  allIds.add(plan.secondaryWallId);
846
1264
  }
1265
+ const affectedRoofIds = new Set();
1266
+ for (const id of allIds) {
1267
+ const node = nextNodes[id];
1268
+ addLeanToHostRoofId(node, nextNodes, affectedRoofIds);
1269
+ if (node?.type === 'roof-segment' && node.parentId) {
1270
+ affectedRoofIds.add(node.parentId);
1271
+ }
1272
+ }
847
1273
  for (const id of allIds)
848
1274
  deletedIds.add(id);
849
1275
  // Let each deleted kind undo what it imposed on its neighbours (e.g. an
@@ -857,7 +1283,7 @@ const deleteNodesActionImpl = (set, get, ids) => {
857
1283
  const onDelete = nodeRegistry.get(node.type)?.parametrics?.onDelete;
858
1284
  if (!onDelete)
859
1285
  continue;
860
- for (const { id: targetId, data } of onDelete(node, nextNodes)) {
1286
+ for (const { id: targetId, data } of onDelete(node, nextNodes, allIds, requestedDeleteIds)) {
861
1287
  if (allIds.has(targetId))
862
1288
  continue;
863
1289
  const target = nextNodes[targetId];
@@ -917,6 +1343,22 @@ const deleteNodesActionImpl = (set, get, ids) => {
917
1343
  }
918
1344
  }
919
1345
  }
1346
+ const deletedZoneIds = new Set();
1347
+ for (const id of allIds) {
1348
+ if (nextNodes[id]?.type === 'zone')
1349
+ deletedZoneIds.add(id);
1350
+ }
1351
+ if (deletedZoneIds.size > 0) {
1352
+ for (const node of Object.values(nextNodes)) {
1353
+ if (node.type !== 'unit' || allIds.has(node.id))
1354
+ continue;
1355
+ const members = node.members.filter((id) => !deletedZoneIds.has(id));
1356
+ if (members.length === node.members.length)
1357
+ continue;
1358
+ nextNodes[node.id] = { ...node, members };
1359
+ nodesToMarkDirty.add(node.id);
1360
+ }
1361
+ }
920
1362
  for (const id of allIds) {
921
1363
  const node = nextNodes[id];
922
1364
  if (!node)
@@ -947,7 +1389,8 @@ const deleteNodesActionImpl = (set, get, ids) => {
947
1389
  // 4. Delete the node itself
948
1390
  delete nextNodes[id];
949
1391
  }
950
- addActiveSceneCommitNodeIds([...allIds, ...parentsToMarkDirty, ...nodesToMarkDirty]);
1392
+ refreshDefaultGuttersForRoofIds(nextNodes, affectedRoofIds, nodesToMarkDirty, deletedIds);
1393
+ addActiveSceneCommitNodeIds([...deletedIds, ...parentsToMarkDirty, ...nodesToMarkDirty]);
951
1394
  return { nodes: nextNodes, rootNodeIds: nextRootIds, collections: nextCollections };
952
1395
  });
953
1396
  // Deleted ids must leave the dirty set: every consumer skips missing
@@ -970,10 +1413,18 @@ const deleteNodesActionImpl = (set, get, ids) => {
970
1413
  get().markDirty(id);
971
1414
  });
972
1415
  };
1416
+ function validatedSet(set) {
1417
+ return (change) => set((state) => {
1418
+ const patch = change(state);
1419
+ if (patch.nodes)
1420
+ validateNodeRelations(state.nodes, patch.nodes, activeSceneCommitNodeIds() ?? []);
1421
+ return patch;
1422
+ });
1423
+ }
973
1424
  export const createNodesAction = (set, get, ops) => runWithSceneCommitNodeIds(ops.flatMap(({ node, parentId }) => {
974
1425
  const effectiveParentId = parentId ?? node.parentId;
975
1426
  return effectiveParentId ? [node.id, effectiveParentId] : [node.id];
976
- }), () => createNodesActionImpl(set, get, ops));
1427
+ }), () => createNodesActionImpl(validatedSet(set), get, ops));
977
1428
  export const applyNodeChangesAction = (set, get, changes) => runWithSceneCommitNodeIds([
978
1429
  ...(changes.create ?? []).flatMap(({ node, parentId }) => {
979
1430
  const effectiveParentId = parentId ?? node.parentId;
@@ -981,6 +1432,6 @@ export const applyNodeChangesAction = (set, get, changes) => runWithSceneCommitN
981
1432
  }),
982
1433
  ...(changes.update ?? []).map(({ id }) => id),
983
1434
  ...(changes.delete ?? []),
984
- ], () => applyNodeChangesActionImpl(set, get, changes));
985
- export const updateNodesAction = (set, get, updates) => runWithSceneCommitNodeIds(updates.map(({ id }) => id), () => updateNodesActionImpl(set, get, updates));
986
- export const deleteNodesAction = (set, get, ids) => runWithSceneCommitNodeIds(ids, () => deleteNodesActionImpl(set, get, ids));
1435
+ ], () => applyNodeChangesActionImpl(validatedSet(set), get, changes));
1436
+ export const updateNodesAction = (set, get, updates) => runWithSceneCommitNodeIds(updates.map(({ id }) => id), () => updateNodesActionImpl(validatedSet(set), get, updates));
1437
+ export const deleteNodesAction = (set, get, ids) => runWithSceneCommitNodeIds(ids, () => deleteNodesActionImpl(validatedSet(set), get, ids));