@pascal-app/core 0.8.0 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (471) hide show
  1. package/dist/events/bus.d.ts +67 -13
  2. package/dist/events/bus.d.ts.map +1 -1
  3. package/dist/hooks/scene-registry/item-clip-registry.d.ts +18 -0
  4. package/dist/hooks/scene-registry/item-clip-registry.d.ts.map +1 -0
  5. package/dist/hooks/scene-registry/item-clip-registry.js +10 -0
  6. package/dist/hooks/scene-registry/scene-registry.d.ts +14 -23
  7. package/dist/hooks/scene-registry/scene-registry.d.ts.map +1 -1
  8. package/dist/hooks/scene-registry/scene-registry.js +59 -26
  9. package/dist/hooks/scene-registry/scene-registry.test.d.ts +2 -0
  10. package/dist/hooks/scene-registry/scene-registry.test.d.ts.map +1 -0
  11. package/dist/hooks/scene-registry/scene-registry.test.js +26 -0
  12. package/dist/hooks/spatial-grid/floor-placed-elevation.d.ts +14 -0
  13. package/dist/hooks/spatial-grid/floor-placed-elevation.d.ts.map +1 -0
  14. package/dist/hooks/spatial-grid/floor-placed-elevation.js +51 -0
  15. package/dist/hooks/spatial-grid/floor-placed-elevation.test.d.ts +2 -0
  16. package/dist/hooks/spatial-grid/floor-placed-elevation.test.d.ts.map +1 -0
  17. package/dist/hooks/spatial-grid/floor-placed-elevation.test.js +366 -0
  18. package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts +79 -11
  19. package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts.map +1 -1
  20. package/dist/hooks/spatial-grid/spatial-grid-manager.js +508 -141
  21. package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts +28 -1
  22. package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts.map +1 -1
  23. package/dist/hooks/spatial-grid/spatial-grid-sync.js +109 -18
  24. package/dist/hooks/spatial-grid/wall-slab-overlap.test.d.ts +2 -0
  25. package/dist/hooks/spatial-grid/wall-slab-overlap.test.d.ts.map +1 -0
  26. package/dist/hooks/spatial-grid/wall-slab-overlap.test.js +363 -0
  27. package/dist/index.d.ts +36 -8
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +33 -6
  30. package/dist/lib/level-name.d.ts +4 -0
  31. package/dist/lib/level-name.d.ts.map +1 -0
  32. package/dist/lib/level-name.js +10 -0
  33. package/dist/lib/measurement-geometry.d.ts +19 -0
  34. package/dist/lib/measurement-geometry.d.ts.map +1 -0
  35. package/dist/lib/measurement-geometry.js +232 -0
  36. package/dist/lib/measurement-geometry.test.d.ts +2 -0
  37. package/dist/lib/measurement-geometry.test.d.ts.map +1 -0
  38. package/dist/lib/measurement-geometry.test.js +102 -0
  39. package/dist/lib/polygon-geometry.d.ts +0 -1
  40. package/dist/lib/polygon-geometry.d.ts.map +1 -1
  41. package/dist/lib/polygon-geometry.js +0 -18
  42. package/dist/lib/polygon-relations.d.ts +10 -0
  43. package/dist/lib/polygon-relations.d.ts.map +1 -0
  44. package/dist/lib/polygon-relations.js +73 -0
  45. package/dist/lib/selection-proxy.d.ts +4 -0
  46. package/dist/lib/selection-proxy.d.ts.map +1 -0
  47. package/dist/lib/selection-proxy.js +12 -0
  48. package/dist/lib/slab-polygon.d.ts +31 -2
  49. package/dist/lib/slab-polygon.d.ts.map +1 -1
  50. package/dist/lib/slab-polygon.js +525 -31
  51. package/dist/lib/slab-polygon.test.d.ts +2 -0
  52. package/dist/lib/slab-polygon.test.d.ts.map +1 -0
  53. package/dist/lib/slab-polygon.test.js +635 -0
  54. package/dist/lib/slots.d.ts +14 -0
  55. package/dist/lib/slots.d.ts.map +1 -0
  56. package/dist/lib/slots.js +26 -0
  57. package/dist/lib/space-detection-pause.test.d.ts +2 -0
  58. package/dist/lib/space-detection-pause.test.d.ts.map +1 -0
  59. package/dist/lib/space-detection-pause.test.js +42 -0
  60. package/dist/lib/space-detection.d.ts +54 -2
  61. package/dist/lib/space-detection.d.ts.map +1 -1
  62. package/dist/lib/space-detection.js +599 -124
  63. package/dist/lib/space-detection.test.d.ts +2 -0
  64. package/dist/lib/space-detection.test.d.ts.map +1 -0
  65. package/dist/lib/space-detection.test.js +376 -0
  66. package/dist/lib/wall-distance.d.ts +60 -0
  67. package/dist/lib/wall-distance.d.ts.map +1 -0
  68. package/dist/lib/wall-distance.js +85 -0
  69. package/dist/lib/zone-quantities.d.ts +21 -0
  70. package/dist/lib/zone-quantities.d.ts.map +1 -0
  71. package/dist/lib/zone-quantities.js +183 -0
  72. package/dist/lib/zone-quantities.test.d.ts +2 -0
  73. package/dist/lib/zone-quantities.test.d.ts.map +1 -0
  74. package/dist/lib/zone-quantities.test.js +82 -0
  75. package/dist/material-library.d.ts +20 -1
  76. package/dist/material-library.d.ts.map +1 -1
  77. package/dist/material-library.js +3737 -200
  78. package/dist/registry/__bench__/relations-resolver.bench.d.ts +15 -0
  79. package/dist/registry/__bench__/relations-resolver.bench.d.ts.map +1 -0
  80. package/dist/registry/__bench__/relations-resolver.bench.js +170 -0
  81. package/dist/registry/handles.d.ts +394 -0
  82. package/dist/registry/handles.d.ts.map +1 -0
  83. package/dist/registry/handles.js +24 -0
  84. package/dist/registry/index.d.ts +7 -0
  85. package/dist/registry/index.d.ts.map +1 -0
  86. package/dist/registry/index.js +4 -0
  87. package/dist/registry/registry.d.ts +136 -0
  88. package/dist/registry/registry.d.ts.map +1 -0
  89. package/dist/registry/registry.js +282 -0
  90. package/dist/registry/registry.test.d.ts +2 -0
  91. package/dist/registry/registry.test.d.ts.map +1 -0
  92. package/dist/registry/registry.test.js +196 -0
  93. package/dist/registry/relations-resolver.d.ts +42 -0
  94. package/dist/registry/relations-resolver.d.ts.map +1 -0
  95. package/dist/registry/relations-resolver.js +77 -0
  96. package/dist/registry/relations-resolver.test.d.ts +2 -0
  97. package/dist/registry/relations-resolver.test.d.ts.map +1 -0
  98. package/dist/registry/relations-resolver.test.js +183 -0
  99. package/dist/registry/scene-api.d.ts +44 -0
  100. package/dist/registry/scene-api.d.ts.map +1 -0
  101. package/dist/registry/scene-api.js +111 -0
  102. package/dist/registry/scene-api.test.d.ts +2 -0
  103. package/dist/registry/scene-api.test.d.ts.map +1 -0
  104. package/dist/registry/scene-api.test.js +183 -0
  105. package/dist/registry/subtree.d.ts +67 -0
  106. package/dist/registry/subtree.d.ts.map +1 -0
  107. package/dist/registry/subtree.js +123 -0
  108. package/dist/registry/subtree.test.d.ts +2 -0
  109. package/dist/registry/subtree.test.d.ts.map +1 -0
  110. package/dist/registry/subtree.test.js +163 -0
  111. package/dist/registry/types.d.ts +2099 -0
  112. package/dist/registry/types.d.ts.map +1 -0
  113. package/dist/registry/types.js +1 -0
  114. package/dist/schema/index.d.ts +35 -4
  115. package/dist/schema/index.d.ts.map +1 -1
  116. package/dist/schema/index.js +32 -4
  117. package/dist/schema/material.d.ts +13 -2
  118. package/dist/schema/material.d.ts.map +1 -1
  119. package/dist/schema/material.js +17 -5
  120. package/dist/schema/material.test.d.ts +2 -0
  121. package/dist/schema/material.test.d.ts.map +1 -0
  122. package/dist/schema/material.test.js +43 -0
  123. package/dist/schema/nodes/box-vent.d.ts +74 -0
  124. package/dist/schema/nodes/box-vent.d.ts.map +1 -0
  125. package/dist/schema/nodes/box-vent.js +63 -0
  126. package/dist/schema/nodes/building.d.ts +1 -1
  127. package/dist/schema/nodes/building.d.ts.map +1 -1
  128. package/dist/schema/nodes/building.js +3 -2
  129. package/dist/schema/nodes/cabinet.d.ts +534 -0
  130. package/dist/schema/nodes/cabinet.d.ts.map +1 -0
  131. package/dist/schema/nodes/cabinet.js +137 -0
  132. package/dist/schema/nodes/ceiling.d.ts +5 -2
  133. package/dist/schema/nodes/ceiling.d.ts.map +1 -1
  134. package/dist/schema/nodes/ceiling.js +5 -1
  135. package/dist/schema/nodes/chimney.d.ts +158 -0
  136. package/dist/schema/nodes/chimney.d.ts.map +1 -0
  137. package/dist/schema/nodes/chimney.js +67 -0
  138. package/dist/schema/nodes/column.d.ts +600 -7
  139. package/dist/schema/nodes/column.d.ts.map +1 -1
  140. package/dist/schema/nodes/column.js +589 -6
  141. package/dist/schema/nodes/cupola.d.ts +66 -0
  142. package/dist/schema/nodes/cupola.d.ts.map +1 -0
  143. package/dist/schema/nodes/cupola.js +28 -0
  144. package/dist/schema/nodes/door.d.ts +13 -5
  145. package/dist/schema/nodes/door.d.ts.map +1 -1
  146. package/dist/schema/nodes/door.js +12 -0
  147. package/dist/schema/nodes/dormer.d.ts +228 -0
  148. package/dist/schema/nodes/dormer.d.ts.map +1 -0
  149. package/dist/schema/nodes/dormer.js +119 -0
  150. package/dist/schema/nodes/downspout.d.ts +75 -0
  151. package/dist/schema/nodes/downspout.d.ts.map +1 -0
  152. package/dist/schema/nodes/downspout.js +63 -0
  153. package/dist/schema/nodes/duct-fitting.d.ts +91 -0
  154. package/dist/schema/nodes/duct-fitting.d.ts.map +1 -0
  155. package/dist/schema/nodes/duct-fitting.js +92 -0
  156. package/dist/schema/nodes/duct-segment.d.ts +61 -0
  157. package/dist/schema/nodes/duct-segment.d.ts.map +1 -0
  158. package/dist/schema/nodes/duct-segment.js +73 -0
  159. package/dist/schema/nodes/duct-terminal.d.ts +57 -0
  160. package/dist/schema/nodes/duct-terminal.d.ts.map +1 -0
  161. package/dist/schema/nodes/duct-terminal.js +51 -0
  162. package/dist/schema/nodes/elevator.d.ts +106 -0
  163. package/dist/schema/nodes/elevator.d.ts.map +1 -0
  164. package/dist/schema/nodes/elevator.js +57 -0
  165. package/dist/schema/nodes/eyebrow-vent.d.ts +68 -0
  166. package/dist/schema/nodes/eyebrow-vent.d.ts.map +1 -0
  167. package/dist/schema/nodes/eyebrow-vent.js +40 -0
  168. package/dist/schema/nodes/fence.d.ts +19 -2
  169. package/dist/schema/nodes/fence.d.ts.map +1 -1
  170. package/dist/schema/nodes/fence.js +27 -2
  171. package/dist/schema/nodes/gutter.d.ts +84 -0
  172. package/dist/schema/nodes/gutter.d.ts.map +1 -0
  173. package/dist/schema/nodes/gutter.js +76 -0
  174. package/dist/schema/nodes/hvac-equipment.d.ts +63 -0
  175. package/dist/schema/nodes/hvac-equipment.d.ts.map +1 -0
  176. package/dist/schema/nodes/hvac-equipment.js +55 -0
  177. package/dist/schema/nodes/item.d.ts +12 -0
  178. package/dist/schema/nodes/item.d.ts.map +1 -1
  179. package/dist/schema/nodes/item.js +20 -0
  180. package/dist/schema/nodes/level.d.ts +1 -1
  181. package/dist/schema/nodes/level.d.ts.map +1 -1
  182. package/dist/schema/nodes/level.js +4 -0
  183. package/dist/schema/nodes/lineset.d.ts +42 -0
  184. package/dist/schema/nodes/lineset.d.ts.map +1 -0
  185. package/dist/schema/nodes/lineset.js +38 -0
  186. package/dist/schema/nodes/liquid-line.d.ts +34 -0
  187. package/dist/schema/nodes/liquid-line.d.ts.map +1 -0
  188. package/dist/schema/nodes/liquid-line.js +24 -0
  189. package/dist/schema/nodes/measurement.d.ts +306 -0
  190. package/dist/schema/nodes/measurement.d.ts.map +1 -0
  191. package/dist/schema/nodes/measurement.js +87 -0
  192. package/dist/schema/nodes/measurement.test.d.ts +2 -0
  193. package/dist/schema/nodes/measurement.test.d.ts.map +1 -0
  194. package/dist/schema/nodes/measurement.test.js +139 -0
  195. package/dist/schema/nodes/pipe-fitting.d.ts +56 -0
  196. package/dist/schema/nodes/pipe-fitting.d.ts.map +1 -0
  197. package/dist/schema/nodes/pipe-fitting.js +45 -0
  198. package/dist/schema/nodes/pipe-segment.d.ts +48 -0
  199. package/dist/schema/nodes/pipe-segment.d.ts.map +1 -0
  200. package/dist/schema/nodes/pipe-segment.js +36 -0
  201. package/dist/schema/nodes/pipe-trap.d.ts +43 -0
  202. package/dist/schema/nodes/pipe-trap.d.ts.map +1 -0
  203. package/dist/schema/nodes/pipe-trap.js +36 -0
  204. package/dist/schema/nodes/ridge-vent-defaults.test.d.ts +2 -0
  205. package/dist/schema/nodes/ridge-vent-defaults.test.d.ts.map +1 -0
  206. package/dist/schema/nodes/ridge-vent-defaults.test.js +205 -0
  207. package/dist/schema/nodes/ridge-vent.d.ts +82 -0
  208. package/dist/schema/nodes/ridge-vent.d.ts.map +1 -0
  209. package/dist/schema/nodes/ridge-vent.js +248 -0
  210. package/dist/schema/nodes/roof-segment-shape.d.ts +90 -0
  211. package/dist/schema/nodes/roof-segment-shape.d.ts.map +1 -0
  212. package/dist/schema/nodes/roof-segment-shape.js +318 -0
  213. package/dist/schema/nodes/roof-segment-shape.test.d.ts +2 -0
  214. package/dist/schema/nodes/roof-segment-shape.test.d.ts.map +1 -0
  215. package/dist/schema/nodes/roof-segment-shape.test.js +175 -0
  216. package/dist/schema/nodes/roof-segment-surface.test.d.ts +2 -0
  217. package/dist/schema/nodes/roof-segment-surface.test.d.ts.map +1 -0
  218. package/dist/schema/nodes/roof-segment-surface.test.js +48 -0
  219. package/dist/schema/nodes/roof-segment-trim.test.d.ts +2 -0
  220. package/dist/schema/nodes/roof-segment-trim.test.d.ts.map +1 -0
  221. package/dist/schema/nodes/roof-segment-trim.test.js +107 -0
  222. package/dist/schema/nodes/roof-segment-walls.d.ts +101 -0
  223. package/dist/schema/nodes/roof-segment-walls.d.ts.map +1 -0
  224. package/dist/schema/nodes/roof-segment-walls.js +330 -0
  225. package/dist/schema/nodes/roof-segment-walls.test.d.ts +2 -0
  226. package/dist/schema/nodes/roof-segment-walls.test.d.ts.map +1 -0
  227. package/dist/schema/nodes/roof-segment-walls.test.js +92 -0
  228. package/dist/schema/nodes/roof-segment.d.ts +273 -3
  229. package/dist/schema/nodes/roof-segment.d.ts.map +1 -1
  230. package/dist/schema/nodes/roof-segment.js +539 -3
  231. package/dist/schema/nodes/roof.d.ts +8 -8
  232. package/dist/schema/nodes/roof.d.ts.map +1 -1
  233. package/dist/schema/nodes/roof.js +4 -16
  234. package/dist/schema/nodes/shelf.d.ts +106 -0
  235. package/dist/schema/nodes/shelf.d.ts.map +1 -0
  236. package/dist/schema/nodes/shelf.js +90 -0
  237. package/dist/schema/nodes/site.d.ts +1 -122
  238. package/dist/schema/nodes/site.d.ts.map +1 -1
  239. package/dist/schema/nodes/site.js +2 -6
  240. package/dist/schema/nodes/skylight.d.ts +279 -0
  241. package/dist/schema/nodes/skylight.d.ts.map +1 -0
  242. package/dist/schema/nodes/skylight.js +152 -0
  243. package/dist/schema/nodes/slab.d.ts +5 -2
  244. package/dist/schema/nodes/slab.d.ts.map +1 -1
  245. package/dist/schema/nodes/slab.js +5 -1
  246. package/dist/schema/nodes/solar-panel.d.ts +117 -0
  247. package/dist/schema/nodes/solar-panel.d.ts.map +1 -0
  248. package/dist/schema/nodes/solar-panel.js +47 -0
  249. package/dist/schema/nodes/stair-segment.d.ts +4 -4
  250. package/dist/schema/nodes/stair.d.ts +13 -12
  251. package/dist/schema/nodes/stair.d.ts.map +1 -1
  252. package/dist/schema/nodes/stair.js +3 -0
  253. package/dist/schema/nodes/surface-hole-metadata.d.ts +2 -0
  254. package/dist/schema/nodes/surface-hole-metadata.d.ts.map +1 -1
  255. package/dist/schema/nodes/surface-hole-metadata.js +3 -1
  256. package/dist/schema/nodes/turbine-vent.d.ts +68 -0
  257. package/dist/schema/nodes/turbine-vent.d.ts.map +1 -0
  258. package/dist/schema/nodes/turbine-vent.js +41 -0
  259. package/dist/schema/nodes/wall.d.ts +245 -6
  260. package/dist/schema/nodes/wall.d.ts.map +1 -1
  261. package/dist/schema/nodes/wall.js +257 -0
  262. package/dist/schema/nodes/wall.test.d.ts +2 -0
  263. package/dist/schema/nodes/wall.test.d.ts.map +1 -0
  264. package/dist/schema/nodes/wall.test.js +208 -0
  265. package/dist/schema/nodes/window.d.ts +11 -3
  266. package/dist/schema/nodes/window.d.ts.map +1 -1
  267. package/dist/schema/nodes/window.js +12 -0
  268. package/dist/schema/nodes/zone.d.ts +2 -0
  269. package/dist/schema/nodes/zone.d.ts.map +1 -1
  270. package/dist/schema/nodes/zone.js +6 -0
  271. package/dist/schema/scene-material.d.ts +41 -0
  272. package/dist/schema/scene-material.d.ts.map +1 -0
  273. package/dist/schema/scene-material.js +9 -0
  274. package/dist/schema/types.d.ts +2850 -655
  275. package/dist/schema/types.d.ts.map +1 -1
  276. package/dist/schema/types.js +49 -0
  277. package/dist/services/alignment-anchors.d.ts +100 -0
  278. package/dist/services/alignment-anchors.d.ts.map +1 -0
  279. package/dist/services/alignment-anchors.js +312 -0
  280. package/dist/services/alignment-anchors.test.d.ts +2 -0
  281. package/dist/services/alignment-anchors.test.d.ts.map +1 -0
  282. package/dist/services/alignment-anchors.test.js +383 -0
  283. package/dist/services/alignment.d.ts +131 -0
  284. package/dist/services/alignment.d.ts.map +1 -0
  285. package/dist/services/alignment.js +171 -0
  286. package/dist/services/alignment.test.d.ts +2 -0
  287. package/dist/services/alignment.test.d.ts.map +1 -0
  288. package/dist/services/alignment.test.js +92 -0
  289. package/dist/services/drag-session.d.ts +47 -0
  290. package/dist/services/drag-session.d.ts.map +1 -0
  291. package/dist/services/drag-session.js +100 -0
  292. package/dist/services/drag-session.test.d.ts +2 -0
  293. package/dist/services/drag-session.test.d.ts.map +1 -0
  294. package/dist/services/drag-session.test.js +219 -0
  295. package/dist/services/hosting.d.ts +88 -0
  296. package/dist/services/hosting.d.ts.map +1 -0
  297. package/dist/services/hosting.js +131 -0
  298. package/dist/services/hosting.test.d.ts +2 -0
  299. package/dist/services/hosting.test.d.ts.map +1 -0
  300. package/dist/services/hosting.test.js +230 -0
  301. package/dist/services/index.d.ts +13 -0
  302. package/dist/services/index.d.ts.map +1 -0
  303. package/dist/services/index.js +12 -0
  304. package/dist/services/level-height.d.ts +28 -0
  305. package/dist/services/level-height.d.ts.map +1 -0
  306. package/dist/services/level-height.js +62 -0
  307. package/dist/services/movement.d.ts +48 -0
  308. package/dist/services/movement.d.ts.map +1 -0
  309. package/dist/services/movement.js +65 -0
  310. package/dist/services/movement.test.d.ts +2 -0
  311. package/dist/services/movement.test.d.ts.map +1 -0
  312. package/dist/services/movement.test.js +133 -0
  313. package/dist/services/opening-guides.d.ts +141 -0
  314. package/dist/services/opening-guides.d.ts.map +1 -0
  315. package/dist/services/opening-guides.js +267 -0
  316. package/dist/services/opening-guides.test.d.ts +2 -0
  317. package/dist/services/opening-guides.test.d.ts.map +1 -0
  318. package/dist/services/opening-guides.test.js +208 -0
  319. package/dist/services/port-connectivity.d.ts +115 -0
  320. package/dist/services/port-connectivity.d.ts.map +1 -0
  321. package/dist/services/port-connectivity.js +300 -0
  322. package/dist/services/port-connectivity.test.d.ts +2 -0
  323. package/dist/services/port-connectivity.test.d.ts.map +1 -0
  324. package/dist/services/port-connectivity.test.js +297 -0
  325. package/dist/services/riser-diagram.d.ts +40 -0
  326. package/dist/services/riser-diagram.d.ts.map +1 -0
  327. package/dist/services/riser-diagram.js +110 -0
  328. package/dist/services/riser-diagram.test.d.ts +2 -0
  329. package/dist/services/riser-diagram.test.d.ts.map +1 -0
  330. package/dist/services/riser-diagram.test.js +66 -0
  331. package/dist/services/single-undo-dance.test.d.ts +2 -0
  332. package/dist/services/single-undo-dance.test.d.ts.map +1 -0
  333. package/dist/services/single-undo-dance.test.js +210 -0
  334. package/dist/services/snap.d.ts +80 -0
  335. package/dist/services/snap.d.ts.map +1 -0
  336. package/dist/services/snap.js +140 -0
  337. package/dist/services/snap.test.d.ts +2 -0
  338. package/dist/services/snap.test.d.ts.map +1 -0
  339. package/dist/services/snap.test.js +137 -0
  340. package/dist/services/system-graph.d.ts +30 -0
  341. package/dist/services/system-graph.d.ts.map +1 -0
  342. package/dist/services/system-graph.js +164 -0
  343. package/dist/services/system-graph.test.d.ts +2 -0
  344. package/dist/services/system-graph.test.d.ts.map +1 -0
  345. package/dist/services/system-graph.test.js +138 -0
  346. package/dist/services/validate-dwv.d.ts +32 -0
  347. package/dist/services/validate-dwv.d.ts.map +1 -0
  348. package/dist/services/validate-dwv.js +118 -0
  349. package/dist/services/validate-dwv.test.d.ts +2 -0
  350. package/dist/services/validate-dwv.test.d.ts.map +1 -0
  351. package/dist/services/validate-dwv.test.js +109 -0
  352. package/dist/solar-panel-presets.d.ts +17 -0
  353. package/dist/solar-panel-presets.d.ts.map +1 -0
  354. package/dist/solar-panel-presets.js +43 -0
  355. package/dist/store/actions/node-actions.d.ts +14 -2
  356. package/dist/store/actions/node-actions.d.ts.map +1 -1
  357. package/dist/store/actions/node-actions.js +585 -15
  358. package/dist/store/actions/node-mutation-sanitize.test.d.ts +2 -0
  359. package/dist/store/actions/node-mutation-sanitize.test.d.ts.map +1 -0
  360. package/dist/store/actions/node-mutation-sanitize.test.js +147 -0
  361. package/dist/store/actions/reparent.test.d.ts +2 -0
  362. package/dist/store/actions/reparent.test.d.ts.map +1 -0
  363. package/dist/store/actions/reparent.test.js +212 -0
  364. package/dist/store/actions/ridge-vent-update.test.d.ts +2 -0
  365. package/dist/store/actions/ridge-vent-update.test.d.ts.map +1 -0
  366. package/dist/store/actions/ridge-vent-update.test.js +264 -0
  367. package/dist/store/history-control.d.ts +11 -0
  368. package/dist/store/history-control.d.ts.map +1 -1
  369. package/dist/store/history-control.js +33 -0
  370. package/dist/store/use-interactive.d.ts +39 -0
  371. package/dist/store/use-interactive.d.ts.map +1 -1
  372. package/dist/store/use-interactive.js +75 -0
  373. package/dist/store/use-live-node-overrides.d.ts +22 -0
  374. package/dist/store/use-live-node-overrides.d.ts.map +1 -0
  375. package/dist/store/use-live-node-overrides.js +59 -0
  376. package/dist/store/use-scene-dirty-tracking.test.d.ts +2 -0
  377. package/dist/store/use-scene-dirty-tracking.test.d.ts.map +1 -0
  378. package/dist/store/use-scene-dirty-tracking.test.js +68 -0
  379. package/dist/store/use-scene-elevator-migration.test.d.ts +2 -0
  380. package/dist/store/use-scene-elevator-migration.test.d.ts.map +1 -0
  381. package/dist/store/use-scene-elevator-migration.test.js +109 -0
  382. package/dist/store/use-scene-measurement-roundtrip.test.d.ts +2 -0
  383. package/dist/store/use-scene-measurement-roundtrip.test.d.ts.map +1 -0
  384. package/dist/store/use-scene-measurement-roundtrip.test.js +97 -0
  385. package/dist/store/use-scene-plugins.test.d.ts +2 -0
  386. package/dist/store/use-scene-plugins.test.d.ts.map +1 -0
  387. package/dist/store/use-scene-plugins.test.js +51 -0
  388. package/dist/store/use-scene-wall-slot-migration.test.d.ts +2 -0
  389. package/dist/store/use-scene-wall-slot-migration.test.d.ts.map +1 -0
  390. package/dist/store/use-scene-wall-slot-migration.test.js +210 -0
  391. package/dist/store/use-scene-window-migration.test.d.ts +2 -0
  392. package/dist/store/use-scene-window-migration.test.d.ts.map +1 -0
  393. package/dist/store/use-scene-window-migration.test.js +85 -0
  394. package/dist/store/use-scene.d.ts +28 -2
  395. package/dist/store/use-scene.d.ts.map +1 -1
  396. package/dist/store/use-scene.js +546 -49
  397. package/dist/systems/elevator/elevator-dispatch.d.ts +12 -0
  398. package/dist/systems/elevator/elevator-dispatch.d.ts.map +1 -0
  399. package/dist/systems/elevator/elevator-dispatch.js +57 -0
  400. package/dist/systems/elevator/elevator-geometry.d.ts +15 -0
  401. package/dist/systems/elevator/elevator-geometry.d.ts.map +1 -0
  402. package/dist/systems/elevator/elevator-geometry.js +50 -0
  403. package/dist/systems/elevator/elevator-opening-sync.d.ts +6 -0
  404. package/dist/systems/elevator/elevator-opening-sync.d.ts.map +1 -0
  405. package/dist/systems/elevator/elevator-opening-sync.js +218 -0
  406. package/dist/systems/elevator/elevator-opening-sync.test.d.ts +2 -0
  407. package/dist/systems/elevator/elevator-opening-sync.test.d.ts.map +1 -0
  408. package/dist/systems/elevator/elevator-opening-sync.test.js +146 -0
  409. package/dist/systems/elevator/elevator-opening-system.d.ts +2 -0
  410. package/dist/systems/elevator/elevator-opening-system.d.ts.map +1 -0
  411. package/dist/systems/elevator/elevator-opening-system.js +48 -0
  412. package/dist/systems/elevator/elevator-runtime-system.d.ts +2 -0
  413. package/dist/systems/elevator/elevator-runtime-system.d.ts.map +1 -0
  414. package/dist/systems/elevator/elevator-runtime-system.js +8 -0
  415. package/dist/systems/elevator/elevator-runtime.d.ts +18 -0
  416. package/dist/systems/elevator/elevator-runtime.d.ts.map +1 -0
  417. package/dist/systems/elevator/elevator-runtime.js +225 -0
  418. package/dist/systems/elevator/elevator-runtime.test.d.ts +2 -0
  419. package/dist/systems/elevator/elevator-runtime.test.d.ts.map +1 -0
  420. package/dist/systems/elevator/elevator-runtime.test.js +64 -0
  421. package/dist/systems/elevator/elevator-service.d.ts +19 -0
  422. package/dist/systems/elevator/elevator-service.d.ts.map +1 -0
  423. package/dist/systems/elevator/elevator-service.js +111 -0
  424. package/dist/systems/fence/fence-centerline.d.ts +12 -0
  425. package/dist/systems/fence/fence-centerline.d.ts.map +1 -0
  426. package/dist/systems/fence/fence-centerline.js +37 -0
  427. package/dist/systems/fence/fence-spline.d.ts +39 -0
  428. package/dist/systems/fence/fence-spline.d.ts.map +1 -0
  429. package/dist/systems/fence/fence-spline.js +196 -0
  430. package/dist/systems/fence/fence-spline.test.d.ts +2 -0
  431. package/dist/systems/fence/fence-spline.test.d.ts.map +1 -0
  432. package/dist/systems/fence/fence-spline.test.js +136 -0
  433. package/dist/systems/stair/stair-footprint.d.ts +44 -0
  434. package/dist/systems/stair/stair-footprint.d.ts.map +1 -0
  435. package/dist/systems/stair/stair-footprint.js +178 -0
  436. package/dist/systems/stair/stair-opening-preview.d.ts +2447 -0
  437. package/dist/systems/stair/stair-opening-preview.d.ts.map +1 -0
  438. package/dist/systems/stair/stair-opening-preview.js +93 -0
  439. package/dist/systems/stair/stair-opening-preview.test.d.ts +2 -0
  440. package/dist/systems/stair/stair-opening-preview.test.d.ts.map +1 -0
  441. package/dist/systems/stair/stair-opening-preview.test.js +80 -0
  442. package/dist/systems/stair/stair-opening-sync.d.ts.map +1 -1
  443. package/dist/systems/stair/stair-opening-sync.js +155 -147
  444. package/dist/systems/stair/stair-opening-sync.test.js +424 -2
  445. package/dist/systems/stair/stair-opening-system.d.ts +2 -0
  446. package/dist/systems/stair/stair-opening-system.d.ts.map +1 -0
  447. package/dist/systems/stair/stair-opening-system.js +104 -0
  448. package/dist/systems/wall/wall-mitering.d.ts.map +1 -1
  449. package/dist/systems/wall/wall-mitering.js +38 -2
  450. package/dist/systems/wall/wall-mitering.test.d.ts +2 -0
  451. package/dist/systems/wall/wall-mitering.test.d.ts.map +1 -0
  452. package/dist/systems/wall/wall-mitering.test.js +63 -0
  453. package/dist/systems/wall/wall-move.d.ts +48 -0
  454. package/dist/systems/wall/wall-move.d.ts.map +1 -0
  455. package/dist/systems/wall/wall-move.js +193 -0
  456. package/dist/utils/clone-scene-graph.d.ts +7 -4
  457. package/dist/utils/clone-scene-graph.d.ts.map +1 -1
  458. package/dist/utils/clone-scene-graph.js +29 -7
  459. package/dist/utils/clone-scene-graph.test.d.ts +2 -0
  460. package/dist/utils/clone-scene-graph.test.d.ts.map +1 -0
  461. package/dist/utils/clone-scene-graph.test.js +62 -0
  462. package/dist/utils/heal-scene-graph.d.ts +18 -0
  463. package/dist/utils/heal-scene-graph.d.ts.map +1 -0
  464. package/dist/utils/heal-scene-graph.js +90 -0
  465. package/dist/utils/heal-scene-graph.test.d.ts +2 -0
  466. package/dist/utils/heal-scene-graph.test.d.ts.map +1 -0
  467. package/dist/utils/heal-scene-graph.test.js +88 -0
  468. package/dist/validation/validate-build-json.d.ts +44 -0
  469. package/dist/validation/validate-build-json.d.ts.map +1 -0
  470. package/dist/validation/validate-build-json.js +256 -0
  471. package/package.json +11 -4
@@ -1,17 +1,32 @@
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';
4
6
  import { BuildingNode } from '../schema';
5
7
  import { generateCollectionId } from '../schema/collections';
8
+ import { DoorNode as DoorNodeSchema } from '../schema/nodes/door';
9
+ import { ElevatorNode as ElevatorNodeSchema } from '../schema/nodes/elevator';
6
10
  import { LevelNode } 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';
7
14
  import { SiteNode } from '../schema/nodes/site';
8
- import { StairNode as StairNodeSchema } from '../schema/nodes/stair';
15
+ import { getEffectiveStairSurfaceMaterial, StairNode as StairNodeSchema, } from '../schema/nodes/stair';
9
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, } from '../schema/scene-material';
20
+ import { healSceneNodes } from '../utils/heal-scene-graph';
10
21
  import * as nodeActions from './actions/node-actions';
11
22
  import { resetSceneHistoryPauseDepth } from './history-control';
12
23
  function getFiniteNumber(value, fallback) {
13
24
  return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
14
25
  }
26
+ function getFiniteNumberInRange(value, fallback, min, max) {
27
+ const finite = getFiniteNumber(value, fallback);
28
+ return Math.min(Math.max(finite, min), max);
29
+ }
15
30
  function getBoolean(value, fallback) {
16
31
  return typeof value === 'boolean' ? value : fallback;
17
32
  }
@@ -19,7 +34,7 @@ function getEnumValue(value, allowed, fallback) {
19
34
  return typeof value === 'string' && allowed.includes(value) ? value : fallback;
20
35
  }
21
36
  function getNullableString(value) {
22
- return typeof value === 'string' ? value : null;
37
+ return typeof value === 'string' && value.length > 0 ? value : null;
23
38
  }
24
39
  function getStringArray(value) {
25
40
  return Array.isArray(value)
@@ -81,40 +96,227 @@ function normalizeStairSegmentNode(node) {
81
96
  const parsed = StairSegmentNodeSchema.safeParse(sanitized);
82
97
  return parsed.success ? parsed.data : null;
83
98
  }
84
- function migrateWallSurfaceMaterials(node) {
85
- const hasInterior = node.interiorMaterial !== undefined || typeof node.interiorMaterialPreset === 'string';
86
- const hasExterior = node.exteriorMaterial !== undefined || typeof node.exteriorMaterialPreset === 'string';
87
- const legacyFinish = {
88
- material: node.material,
89
- materialPreset: typeof node.materialPreset === 'string' ? node.materialPreset : undefined,
99
+ function normalizeDoorNode(node) {
100
+ const parsed = DoorNodeSchema.safeParse(node);
101
+ return parsed.success ? { ...node, ...parsed.data } : null;
102
+ }
103
+ // Windows saved before a schema field existed (e.g. `columnRatios`/`rowRatios`/
104
+ // `frameThickness`) load without it; the mesh builder then reads undefined and
105
+ // throws every frame. Zod-parse on load so schema defaults land, like doors.
106
+ function normalizeWindowNode(node) {
107
+ const parsed = WindowNodeSchema.safeParse(node);
108
+ return parsed.success ? { ...node, ...parsed.data } : null;
109
+ }
110
+ function normalizeShelfNode(node) {
111
+ const sanitized = {
112
+ ...node,
113
+ children: getStringArray(node.children),
114
+ position: getVector3(node.position, [0, 0, 0]),
115
+ rotation: getVector3(node.rotation, [0, 0, 0]),
116
+ width: getFiniteNumberInRange(node.width, 1.2, 0.3, 3.0),
117
+ depth: getFiniteNumberInRange(node.depth, 0.3, 0.1, 1.0),
118
+ thickness: getFiniteNumberInRange(node.thickness, 0.04, 0.01, 0.1),
119
+ height: getFiniteNumberInRange(node.height, 0.9, 0.05, 2.5),
120
+ rows: Math.round(getFiniteNumberInRange(node.rows, 1, 1, 8)),
121
+ columns: Math.round(getFiniteNumberInRange(node.columns, 1, 1, 6)),
122
+ style: getEnumValue(node.style, ['wall-shelf', 'bookshelf', 'open-rack', 'cubby'], 'wall-shelf'),
123
+ withBack: getBoolean(node.withBack, false),
124
+ withSides: getBoolean(node.withSides, true),
125
+ withBottom: getBoolean(node.withBottom, false),
126
+ bracketStyle: getEnumValue(node.bracketStyle, ['minimal', 'industrial', 'hidden'], 'minimal'),
90
127
  };
91
- if (!(hasInterior || hasExterior)) {
92
- if (legacyFinish.material === undefined && legacyFinish.materialPreset === undefined) {
93
- return node;
128
+ const parsed = ShelfNodeSchema.safeParse(sanitized);
129
+ return parsed.success ? parsed.data : null;
130
+ }
131
+ function normalizeElevatorNode(node) {
132
+ const sanitized = {
133
+ ...node,
134
+ position: getVector3(node.position, [0, 0, 0]),
135
+ rotation: getFiniteNumber(node.rotation, 0),
136
+ width: getFiniteNumber(node.width, 1.84),
137
+ depth: getFiniteNumber(node.depth, 1.84),
138
+ shaftWidth: node.shaftWidth === undefined ? undefined : getFiniteNumber(node.shaftWidth, 1.84),
139
+ shaftDepth: node.shaftDepth === undefined ? undefined : getFiniteNumber(node.shaftDepth, 1.84),
140
+ shaftWallThickness: getFiniteNumber(node.shaftWallThickness, 0.09),
141
+ cabHeight: getFiniteNumber(node.cabHeight, 2.35),
142
+ doorWidth: getFiniteNumber(node.doorWidth, 0.95),
143
+ doorHeight: getFiniteNumber(node.doorHeight, 2.1),
144
+ fromLevelId: getNullableString(node.fromLevelId),
145
+ toLevelId: getNullableString(node.toLevelId),
146
+ servedLevelIds: node.servedLevelIds === undefined ? undefined : getStringArray(node.servedLevelIds),
147
+ disabledLevelIds: getStringArray(node.disabledLevelIds),
148
+ serviceOnlyLevelIds: getStringArray(node.serviceOnlyLevelIds),
149
+ defaultLevelId: getNullableString(node.defaultLevelId),
150
+ speed: getFiniteNumber(node.speed, 2.2),
151
+ doorDurationMs: getFiniteNumber(node.doorDurationMs, 900),
152
+ dwellMs: getFiniteNumber(node.dwellMs, 1400),
153
+ };
154
+ const parsed = ElevatorNodeSchema.safeParse(sanitized);
155
+ return parsed.success ? parsed.data : null;
156
+ }
157
+ function findBuildingIdForLevel(levelId, nodes) {
158
+ const level = nodes[levelId];
159
+ const directBuildingId = typeof level?.parentId === 'string' ? level.parentId : null;
160
+ if (directBuildingId && nodes[directBuildingId]?.type === 'building') {
161
+ return directBuildingId;
162
+ }
163
+ for (const [candidateId, candidate] of Object.entries(nodes)) {
164
+ if (candidate?.type !== 'building')
165
+ continue;
166
+ if (getStringArray(candidate.children).includes(levelId)) {
167
+ return candidateId;
94
168
  }
95
- return {
96
- ...node,
97
- interiorMaterial: legacyFinish.material,
98
- interiorMaterialPreset: legacyFinish.materialPreset,
99
- exteriorMaterial: legacyFinish.material,
100
- exteriorMaterialPreset: legacyFinish.materialPreset,
101
- };
102
169
  }
103
- if (!hasInterior) {
104
- return {
105
- ...node,
106
- interiorMaterial: node.exteriorMaterial,
107
- interiorMaterialPreset: node.exteriorMaterialPreset,
108
- };
170
+ return null;
171
+ }
172
+ function migrateElevatorParent(id, node, nodes) {
173
+ const parentId = typeof node.parentId === 'string' ? node.parentId : null;
174
+ if (!parentId)
175
+ return node;
176
+ const parent = parentId ? nodes[parentId] : null;
177
+ if (parent?.type !== 'level')
178
+ return node;
179
+ const buildingId = findBuildingIdForLevel(parentId, nodes);
180
+ if (!buildingId)
181
+ return node;
182
+ const building = buildingId ? nodes[buildingId] : null;
183
+ if (building?.type !== 'building')
184
+ return node;
185
+ nodes[parentId] = {
186
+ ...parent,
187
+ children: getStringArray(parent.children).filter((childId) => childId !== id),
188
+ };
189
+ const buildingChildren = getStringArray(building.children);
190
+ nodes[buildingId] = {
191
+ ...building,
192
+ children: buildingChildren.includes(id) ? buildingChildren : [...buildingChildren, id],
193
+ };
194
+ return {
195
+ ...node,
196
+ parentId: buildingId,
197
+ };
198
+ }
199
+ // Reuse an already-minted scene material for an identical inline legacy
200
+ // material so a whole building painted one custom colour collapses to one
201
+ // shared datablock (mirrors `commitSlotPaint`'s dedupe-on-match).
202
+ function findMintedSceneMaterialRef(material, mintedMaterials) {
203
+ const target = JSON.stringify(material);
204
+ for (const sceneMaterial of Object.values(mintedMaterials)) {
205
+ if (JSON.stringify(sceneMaterial.material) === target) {
206
+ return toSceneMaterialRef(sceneMaterial.id);
207
+ }
109
208
  }
110
- if (!hasExterior) {
111
- return {
112
- ...node,
113
- exteriorMaterial: node.interiorMaterial,
114
- exteriorMaterialPreset: node.interiorMaterialPreset,
209
+ return undefined;
210
+ }
211
+ // Turn a legacy surface spec (`{ material, materialPreset }`) into a
212
+ // `MaterialRef`: a preset that's already a `library:`/`scene:` ref is used
213
+ // as-is; an inline material mints (or reuses) a scene material. Returns
214
+ // undefined when the spec carries no material. Shared by every legacy→slots
215
+ // migration below.
216
+ function legacySpecToMaterialRef(spec, mintedMaterials) {
217
+ if (typeof spec.materialPreset === 'string' && parseMaterialRef(spec.materialPreset)) {
218
+ return spec.materialPreset;
219
+ }
220
+ if (spec.material !== undefined) {
221
+ const existing = findMintedSceneMaterialRef(spec.material, mintedMaterials);
222
+ if (existing)
223
+ return existing;
224
+ const id = generateSceneMaterialId();
225
+ mintedMaterials[id] = {
226
+ id,
227
+ name: `Material ${Object.keys(mintedMaterials).length + 1}`,
228
+ material: spec.material,
115
229
  };
230
+ return toSceneMaterialRef(id);
231
+ }
232
+ return undefined;
233
+ }
234
+ // Move the retired inline `material*` / `interiorMaterial*` / `exteriorMaterial*`
235
+ // fields onto the unified `node.slots` model (interior / exterior → a
236
+ // `library:`/`scene:` ref), minting scene materials for inline customs into
237
+ // `mintedMaterials` (merged into the scene material map by the caller). Already
238
+ // slot-modelled walls and walls with no legacy material are left untouched.
239
+ function migrateWallSurfaceMaterials(node, mintedMaterials) {
240
+ if (node.slots && (node.slots.interior !== undefined || node.slots.exterior !== undefined)) {
241
+ return node;
242
+ }
243
+ const slots = { ...(node.slots ?? {}) };
244
+ for (const side of ['interior', 'exterior']) {
245
+ const spec = getEffectiveWallSurfaceMaterial(node, side);
246
+ const ref = legacySpecToMaterialRef(spec, mintedMaterials);
247
+ if (ref)
248
+ slots[side] = ref;
249
+ }
250
+ if (Object.keys(slots).length === 0) {
251
+ return node;
252
+ }
253
+ return {
254
+ ...node,
255
+ slots,
256
+ material: undefined,
257
+ materialPreset: undefined,
258
+ interiorMaterial: undefined,
259
+ interiorMaterialPreset: undefined,
260
+ exteriorMaterial: undefined,
261
+ exteriorMaterialPreset: undefined,
262
+ };
263
+ }
264
+ // Move a kind's single legacy `material` / `materialPreset` onto its declared
265
+ // slots. A pre-slot-model node painted one material rendered that material on
266
+ // every part (each slot resolves `node.slots[slot]` → legacy → default), so the
267
+ // migration writes the same ref to every slot id the kind can expose — unused
268
+ // conditional slots are harmless. Already slot-modelled or unpainted nodes are
269
+ // left untouched. Mirrors `migrateWallSurfaceMaterials` for single-surface and
270
+ // whole-object kinds (slab, ceiling, fence, column, shelf).
271
+ function migrateSingleMaterialSlots(node, slotIds, mintedMaterials) {
272
+ if (node.slots && Object.keys(node.slots).length > 0) {
273
+ return node;
274
+ }
275
+ const ref = legacySpecToMaterialRef({ material: node.material, materialPreset: node.materialPreset }, mintedMaterials);
276
+ if (!ref) {
277
+ return node;
278
+ }
279
+ const slots = {};
280
+ for (const slotId of slotIds)
281
+ slots[slotId] = ref;
282
+ return { ...node, slots, material: undefined, materialPreset: undefined };
283
+ }
284
+ // Stair carries per-role legacy fields (`treadMaterial*` / `sideMaterial*` /
285
+ // `railingMaterial*`) plus a catch-all. Map each to its slot via the same
286
+ // fallback chain the renderer uses (`getEffectiveStairSurfaceMaterial`):
287
+ // tread→treads, side→body, railing→railing. Runs after
288
+ // `migrateStairSurfaceMaterials` has normalised the legacy fields.
289
+ function migrateStairSurfaceSlots(node, mintedMaterials) {
290
+ if (node.slots && Object.keys(node.slots).length > 0) {
291
+ return node;
292
+ }
293
+ const roleToSlot = [
294
+ ['tread', 'treads'],
295
+ ['side', 'body'],
296
+ ['railing', 'railing'],
297
+ ];
298
+ const slots = {};
299
+ for (const [role, slotId] of roleToSlot) {
300
+ const spec = getEffectiveStairSurfaceMaterial(node, role);
301
+ const ref = legacySpecToMaterialRef(spec, mintedMaterials);
302
+ if (ref)
303
+ slots[slotId] = ref;
304
+ }
305
+ if (Object.keys(slots).length === 0) {
306
+ return node;
116
307
  }
117
- return node;
308
+ return {
309
+ ...node,
310
+ slots,
311
+ material: undefined,
312
+ materialPreset: undefined,
313
+ treadMaterial: undefined,
314
+ treadMaterialPreset: undefined,
315
+ sideMaterial: undefined,
316
+ sideMaterialPreset: undefined,
317
+ railingMaterial: undefined,
318
+ railingMaterialPreset: undefined,
319
+ };
118
320
  }
119
321
  function migrateStairSurfaceMaterials(node) {
120
322
  const hasRailing = node.railingMaterial !== undefined || typeof node.railingMaterialPreset === 'string';
@@ -233,7 +435,20 @@ function migrateRoofSurfaceMaterials(node) {
233
435
  return next;
234
436
  }
235
437
  function migrateNodes(nodes) {
236
- const patchedNodes = { ...nodes };
438
+ // Repair pre-existing corruption (null children, zero-length walls) before
439
+ // any per-type migration runs, so already-saved scenes load cleanly.
440
+ const { nodes: healed } = healSceneNodes(nodes);
441
+ const patchedNodes = { ...healed };
442
+ // Scene materials minted while moving legacy wall fields onto `node.slots`;
443
+ // merged into the scene material map by the caller (`setScene`).
444
+ const mintedMaterials = {};
445
+ // Pass 1: all node types except elevator.
446
+ // Elevator migration (migrateElevatorParent) mutates level.children to remove
447
+ // the elevator ID. If the elevator is processed before its parent level in
448
+ // Object.entries order, the level migration in this same pass would then see
449
+ // a children array that still contains the elevator ID and filter it out as
450
+ // "missing" — corrupting the level. Running elevators in a second pass after
451
+ // all levels are stable avoids the race entirely.
237
452
  for (const [id, node] of Object.entries(patchedNodes)) {
238
453
  // 1. Item scale migration
239
454
  if (node.type === 'item' && !('scale' in node)) {
@@ -244,6 +459,9 @@ function migrateNodes(nodes) {
244
459
  const oldRoof = node;
245
460
  const suffix = id.includes('_') ? id.split('_')[1] : Math.random().toString(36).slice(2);
246
461
  const segmentId = `rseg_${suffix}`;
462
+ const segWidth = oldRoof.length ?? 8;
463
+ const segDepth = (oldRoof.leftWidth ?? 2.2) + (oldRoof.rightWidth ?? 2.2);
464
+ const legacyRoofHeight = oldRoof.height ?? 2.5;
247
465
  const segment = {
248
466
  object: 'node',
249
467
  id: segmentId,
@@ -254,10 +472,18 @@ function migrateNodes(nodes) {
254
472
  position: [0, 0, 0],
255
473
  rotation: 0,
256
474
  roofType: 'gable',
257
- width: oldRoof.length ?? 8,
258
- depth: (oldRoof.leftWidth ?? 2.2) + (oldRoof.rightWidth ?? 2.2),
259
- wallHeight: 0,
260
- roofHeight: oldRoof.height ?? 2.5,
475
+ width: segWidth,
476
+ depth: segDepth,
477
+ // Schema default (0.5), NOT 0: a zero-height wall builds a flat,
478
+ // degenerate CSG brush → "Coplanar clip not handled" + NaN geometry, so
479
+ // the migrated legacy roof never renders. New roofs use 0.5 too.
480
+ wallHeight: 0.5,
481
+ pitch: getPitchFromActiveRoofHeight({
482
+ roofType: 'gable',
483
+ width: segWidth,
484
+ depth: segDepth,
485
+ roofHeight: legacyRoofHeight,
486
+ }),
261
487
  wallThickness: 0.1,
262
488
  deckThickness: 0.1,
263
489
  overhang: 0.3,
@@ -269,11 +495,47 @@ function migrateNodes(nodes) {
269
495
  children: [segmentId],
270
496
  };
271
497
  }
498
+ // 2b. roof-segment: guarantee a valid positive pitch (degrees).
499
+ // Saved scenes wrote `roofHeight` in metres; the schema now stores `pitch`
500
+ // in degrees. Convert the legacy field when present, and — crucially — fall
501
+ // back to the schema default for any segment that carries no usable pitch or
502
+ // roofHeight (older/partial saves). Without this, the slope-frame guard
503
+ // resolves a missing pitch to a FLAT frame, so the roof renders as a slab.
504
+ // The migration result is cast, not zod-parsed, so the schema default never
505
+ // applies on its own — this branch is the only place it lands.
506
+ if (node.type === 'roof-segment') {
507
+ const currentPitch = node.pitch;
508
+ const hasValidPitch = typeof currentPitch === 'number' && currentPitch > 0;
509
+ if (!hasValidPitch) {
510
+ const { roofHeight, ...rest } = node;
511
+ const width = typeof node.width === 'number' ? node.width : 8;
512
+ const depth = typeof node.depth === 'number' ? node.depth : 6;
513
+ const roofType = (typeof node.roofType === 'string' ? node.roofType : 'gable');
514
+ const derived = typeof roofHeight === 'number' && roofHeight > 0
515
+ ? getPitchFromActiveRoofHeight({ roofType, width, depth, roofHeight })
516
+ : 0;
517
+ // 40° matches the RoofSegmentNode schema default.
518
+ patchedNodes[id] = { ...rest, pitch: derived > 0 ? derived : 40 };
519
+ }
520
+ }
521
+ if (node.type === 'door') {
522
+ const normalized = normalizeDoorNode(node);
523
+ if (normalized) {
524
+ patchedNodes[id] = normalized;
525
+ }
526
+ }
527
+ if (node.type === 'window') {
528
+ const normalized = normalizeWindowNode(node);
529
+ if (normalized) {
530
+ patchedNodes[id] = normalized;
531
+ }
532
+ }
272
533
  if (node.type === 'stair') {
273
534
  const normalized = normalizeStairNode(migrateStairSurfaceMaterials(node));
274
535
  if (normalized) {
275
536
  patchedNodes[id] = normalized;
276
537
  }
538
+ patchedNodes[id] = migrateStairSurfaceSlots(patchedNodes[id], mintedMaterials);
277
539
  }
278
540
  if (node.type === 'stair-segment') {
279
541
  const normalized = normalizeStairSegmentNode(node);
@@ -282,13 +544,172 @@ function migrateNodes(nodes) {
282
544
  }
283
545
  }
284
546
  if (node.type === 'wall') {
285
- patchedNodes[id] = migrateWallSurfaceMaterials(patchedNodes[id]);
547
+ patchedNodes[id] = migrateWallSurfaceMaterials(patchedNodes[id], mintedMaterials);
548
+ }
549
+ // Cabinet v2→v3: node-level `doorStyle` was dead (geometry reads only the
550
+ // per-compartment `doorType`) and was removed from both cabinet schemas;
551
+ // `handlePosition: 'edge'` behaved identically to 'auto' and was dropped
552
+ // from the enum; the compartment stack became a discriminated union that
553
+ // rejects a `cooktopLayout` mismatched to its gas/induction type (the old
554
+ // loose schema ignored it).
555
+ if (node.type === 'cabinet' || node.type === 'cabinet-module') {
556
+ const { doorStyle: _doorStyle, ...rest } = node;
557
+ const next = rest;
558
+ if (next.handlePosition === 'edge')
559
+ next.handlePosition = 'auto';
560
+ if (Array.isArray(next.stack)) {
561
+ next.stack = next.stack.map((compartment) => {
562
+ if (!compartment || typeof compartment !== 'object')
563
+ return compartment;
564
+ const layout = compartment.cooktopLayout;
565
+ if (typeof layout !== 'string')
566
+ return compartment;
567
+ if (compartment.type === 'cooktop-gas' && !layout.startsWith('gas-')) {
568
+ return { ...compartment, cooktopLayout: 'gas-4burner' };
569
+ }
570
+ if (compartment.type === 'cooktop-induction' && !layout.startsWith('induction-')) {
571
+ return { ...compartment, cooktopLayout: 'induction-4zone' };
572
+ }
573
+ return compartment;
574
+ });
575
+ }
576
+ patchedNodes[id] = next;
577
+ }
578
+ if (node.type === 'slab' || node.type === 'ceiling') {
579
+ patchedNodes[id] = migrateSingleMaterialSlots(patchedNodes[id], ['surface'], mintedMaterials);
580
+ }
581
+ if (node.type === 'fence') {
582
+ patchedNodes[id] = migrateSingleMaterialSlots(patchedNodes[id], ['posts', 'infill', 'base', 'rail'], mintedMaterials);
583
+ }
584
+ if (node.type === 'column') {
585
+ patchedNodes[id] = migrateSingleMaterialSlots(patchedNodes[id], ['shaft', 'base', 'capital', 'frame'], mintedMaterials);
586
+ }
587
+ if (node.type === 'shelf') {
588
+ const normalized = normalizeShelfNode(node);
589
+ if (normalized) {
590
+ patchedNodes[id] = normalized;
591
+ }
592
+ patchedNodes[id] = migrateSingleMaterialSlots(patchedNodes[id], ['shelves', 'frame', 'back'], mintedMaterials);
593
+ }
594
+ // Roof-segment hosting was added in this migration cycle (the same
595
+ // pattern as shelf above). Older segments saved before the schema
596
+ // gained `children` need the field initialised so
597
+ // `createNode(chimney, segmentId)` finds an array to append to —
598
+ // without this every "Add Element" click on the roof panel results
599
+ // in an orphaned accessory (parented in scene state but never
600
+ // appended to `seg.children`, so the renderer's recursive
601
+ // `<NodeRenderer>` mount never sees it).
602
+ if (node.type === 'roof-segment' && !Array.isArray(node.children)) {
603
+ patchedNodes[id] = { ...node, children: [] };
604
+ }
605
+ // Roof-hosted wall children (door / window / item) originally stored
606
+ // SEGMENT-LOCAL positions with the face yaw in rotation[1]; the
607
+ // format moved to explicit `roofFace` + FACE-LOCAL coords so the
608
+ // renderer's face frame can track segment edits live. Convert in
609
+ // place: face from the old cardinal yaw, u/v from the outer-plane
610
+ // projection, z re-based from the outer plane to the wall mid-plane.
611
+ if ((node.type === 'door' || node.type === 'window' || node.type === 'item') &&
612
+ typeof node.roofSegmentId === 'string' &&
613
+ node.roofFace === undefined) {
614
+ const current = patchedNodes[id];
615
+ const segment = patchedNodes[current.roofSegmentId];
616
+ if (segment?.type === 'roof-segment') {
617
+ const tau = Math.PI * 2;
618
+ const yaw = (((current.rotation?.[1] ?? 0) % tau) + tau) % tau;
619
+ const eps = 1e-3;
620
+ const face = yaw < eps || tau - yaw < eps
621
+ ? 'front'
622
+ : Math.abs(yaw - Math.PI) < eps
623
+ ? 'back'
624
+ : Math.abs(yaw - Math.PI / 2) < eps
625
+ ? 'right'
626
+ : Math.abs(yaw - (3 * Math.PI) / 2) < eps
627
+ ? 'left'
628
+ : null;
629
+ if (face) {
630
+ const { u, v, dist } = segmentPointToRoofWallFace(segment, face, current.position);
631
+ patchedNodes[id] = {
632
+ ...current,
633
+ roofFace: face,
634
+ position: [u, v, dist + (segment.wallThickness ?? 0.1) / 2],
635
+ rotation: [0, 0, 0],
636
+ };
637
+ }
638
+ }
286
639
  }
287
640
  if (node.type === 'roof') {
288
641
  patchedNodes[id] = migrateRoofSurfaceMaterials(patchedNodes[id]);
289
642
  }
643
+ // Legacy: site.children used to hold nested BuildingNode / ItemNode
644
+ // objects (see the SiteNode schema before the children-as-ids fix).
645
+ // Flatten any leftover nested children into ids, and absorb the
646
+ // embedded nodes into the flat map so the rest of the loader can
647
+ // treat the site like every other parent.
648
+ if (node.type === 'site' && Array.isArray(node.children)) {
649
+ let needsFlatten = false;
650
+ const flattened = [];
651
+ for (const child of node.children) {
652
+ if (typeof child === 'string') {
653
+ flattened.push(child);
654
+ }
655
+ else if (child && typeof child === 'object' && typeof child.id === 'string') {
656
+ needsFlatten = true;
657
+ flattened.push(child.id);
658
+ if (!patchedNodes[child.id]) {
659
+ patchedNodes[child.id] = { ...child, parentId: id };
660
+ }
661
+ }
662
+ }
663
+ if (needsFlatten) {
664
+ patchedNodes[id] = { ...node, children: flattened };
665
+ }
666
+ }
667
+ // Level children normalization.
668
+ // Pre-0.9.1 JSONs may carry child IDs that no longer exist in the node
669
+ // map (e.g. elevator IDs that lived under a level before the elevator
670
+ // parent migration moved them up to building). If those dangling IDs are
671
+ // left in place, collectReachableNodeIds marks the level as having
672
+ // reachable children that don't exist, which corrupts the scene graph
673
+ // traversal and leaves the LevelNode in a broken state — making floors
674
+ // impossible to drag or delete after import.
675
+ // We intentionally do NOT filter by type prefix here; being permissive
676
+ // about which types are allowed as children prevents data loss when new
677
+ // child types are added to the schema in the future.
678
+ if (node.type === 'level') {
679
+ const rawChildren = getStringArray(node.children);
680
+ const validChildren = rawChildren.filter((childId) => {
681
+ const exists = Boolean(patchedNodes[childId]);
682
+ if (!exists) {
683
+ console.warn('[migrateNodes] level', id, 'references missing child', childId, '— dropping');
684
+ }
685
+ return exists;
686
+ });
687
+ const levelNumber = getFiniteNumber(node.level, 0);
688
+ patchedNodes[id] = {
689
+ ...node,
690
+ level: levelNumber,
691
+ children: validChildren,
692
+ };
693
+ }
694
+ }
695
+ // Pass 2: elevator migration.
696
+ // migrateElevatorParent mutates the parent level's children array (removes
697
+ // the elevator ID from it). Running this after Pass 1 guarantees that the
698
+ // level normalization above has already seen a clean children list — if we
699
+ // ran elevator migration inside Pass 1, the order of Object.entries
700
+ // iteration would be non-deterministic: processing an elevator before its
701
+ // parent level would mutate the level's children mid-iteration, potentially
702
+ // causing the level branch above to see a stale node reference.
703
+ for (const [id, node] of Object.entries(patchedNodes)) {
704
+ if (node.type !== 'elevator')
705
+ continue;
706
+ const parentMigrated = migrateElevatorParent(id, node, patchedNodes);
707
+ const normalized = normalizeElevatorNode(parentMigrated);
708
+ if (normalized) {
709
+ patchedNodes[id] = normalized;
710
+ }
290
711
  }
291
- return patchedNodes;
712
+ return { nodes: patchedNodes, mintedMaterials };
292
713
  }
293
714
  function getNodeChildIds(node) {
294
715
  if (!('children' in node && Array.isArray(node.children))) {
@@ -347,6 +768,9 @@ const useScene = create()(temporal((set, get) => ({
347
768
  dirtyNodes: new Set(),
348
769
  // 4. Collections
349
770
  collections: {},
771
+ materials: {},
772
+ installedPlugins: [],
773
+ hasExplicitPluginInstallState: false,
350
774
  // 5. Read-only lock
351
775
  readOnly: false,
352
776
  setReadOnly: (readOnly) => set({ readOnly }),
@@ -356,15 +780,25 @@ const useScene = create()(temporal((set, get) => ({
356
780
  rootNodeIds: [],
357
781
  dirtyNodes: new Set(),
358
782
  collections: {},
783
+ materials: {},
784
+ installedPlugins: [],
785
+ hasExplicitPluginInstallState: false,
359
786
  });
360
787
  },
361
788
  clearScene: () => {
789
+ const installedPlugins = get().installedPlugins;
790
+ const hasExplicitPluginInstallState = get().hasExplicitPluginInstallState;
362
791
  get().unloadScene();
363
792
  get().loadScene(); // Default scene
793
+ set({ installedPlugins, hasExplicitPluginInstallState });
364
794
  },
365
- setScene: (nodes, rootNodeIds) => {
795
+ setScene: (nodes, rootNodeIds, extra) => {
366
796
  // Apply backward compatibility migrations
367
- const patchedNodes = migrateNodes(nodes);
797
+ const { nodes: patchedNodes, mintedMaterials } = migrateNodes(nodes);
798
+ // Scene materials minted by the wall legacy→slots migration join the
799
+ // loaded palette (existing refs win on id collision — there are none,
800
+ // ids are freshly generated).
801
+ const materials = { ...mintedMaterials, ...(extra?.materials ?? {}) };
368
802
  // Remove orphans: nodes whose parentId points to a non-existent node
369
803
  const cleanedNodes = { ...patchedNodes };
370
804
  for (const node of Object.values(cleanedNodes)) {
@@ -373,12 +807,6 @@ const useScene = create()(temporal((set, get) => ({
373
807
  delete cleanedNodes[node.id];
374
808
  }
375
809
  }
376
- set({
377
- nodes: cleanedNodes,
378
- rootNodeIds,
379
- dirtyNodes: new Set(),
380
- collections: {},
381
- });
382
810
  const normalizedRootNodeIds = normalizeRootNodeIds(cleanedNodes, rootNodeIds);
383
811
  const reachableNodeIds = collectReachableNodeIds(cleanedNodes, normalizedRootNodeIds);
384
812
  if (normalizedRootNodeIds.length > 0) {
@@ -389,17 +817,47 @@ const useScene = create()(temporal((set, get) => ({
389
817
  delete cleanedNodes[node.id];
390
818
  }
391
819
  }
820
+ // Single tracked `set`: with zundo, every tracked write pushes the
821
+ // pre-write state onto `pastStates`. Writing the scene in two steps
822
+ // (as this used to) exposed a half-normalized intermediate state —
823
+ // and the pre-load (possibly empty) state — as undo targets.
392
824
  set({
393
825
  nodes: cleanedNodes,
394
826
  rootNodeIds: normalizedRootNodeIds,
395
827
  dirtyNodes: new Set(),
396
- collections: {},
828
+ collections: extra?.collections ?? {},
829
+ materials,
830
+ installedPlugins: Array.from(new Set(extra?.installedPlugins ?? [])),
831
+ hasExplicitPluginInstallState: extra?.hasExplicitPluginInstallState ?? false,
397
832
  });
398
833
  // Mark all nodes as dirty to trigger re-validation
399
834
  Object.values(cleanedNodes).forEach((node) => {
400
835
  get().markDirty(node.id);
401
836
  });
402
837
  },
838
+ setInstalledPlugins: (pluginIds, options) => {
839
+ if (get().readOnly)
840
+ return;
841
+ const nextInstalledPlugins = Array.from(new Set(pluginIds));
842
+ const previousInstalledPlugins = get().installedPlugins;
843
+ const dirtyNodes = new Set(get().dirtyNodes);
844
+ for (const node of Object.values(get().nodes)) {
845
+ if (!getNodePluginId(node.type))
846
+ continue;
847
+ if (!isNodeKindEnabled(node.type, nextInstalledPlugins)) {
848
+ dirtyNodes.delete(node.id);
849
+ }
850
+ else if (!isNodeKindEnabled(node.type, previousInstalledPlugins)) {
851
+ if (nodeRegistry.get(node.type)?.dirtyTracking !== false)
852
+ dirtyNodes.add(node.id);
853
+ }
854
+ }
855
+ set({
856
+ installedPlugins: nextInstalledPlugins,
857
+ hasExplicitPluginInstallState: options?.explicit ?? get().hasExplicitPluginInstallState,
858
+ dirtyNodes,
859
+ });
860
+ },
403
861
  loadScene: () => {
404
862
  if (get().rootNodeIds.length > 0) {
405
863
  // Assign all nodes as dirty to force re-validation
@@ -417,7 +875,7 @@ const useScene = create()(temporal((set, get) => ({
417
875
  children: [level0.id],
418
876
  });
419
877
  const site = SiteNode.parse({
420
- children: [building],
878
+ children: [building.id],
421
879
  });
422
880
  // Define all nodes flat
423
881
  const nodes = {
@@ -430,6 +888,11 @@ const useScene = create()(temporal((set, get) => ({
430
888
  set({ nodes, rootNodeIds });
431
889
  },
432
890
  markDirty: (id) => {
891
+ const node = get().nodes[id];
892
+ if (node && !isNodeKindEnabled(node.type, get().installedPlugins))
893
+ return;
894
+ if (node && nodeRegistry.get(node.type)?.dirtyTracking === false)
895
+ return;
433
896
  get().dirtyNodes.add(id);
434
897
  },
435
898
  clearDirty: (id) => {
@@ -437,6 +900,7 @@ const useScene = create()(temporal((set, get) => ({
437
900
  },
438
901
  createNodes: (ops) => nodeActions.createNodesAction(set, get, ops),
439
902
  createNode: (node, parentId) => nodeActions.createNodesAction(set, get, [{ node, parentId }]),
903
+ applyNodeChanges: (changes) => nodeActions.applyNodeChangesAction(set, get, changes),
440
904
  updateNodes: (updates) => nodeActions.updateNodesAction(set, get, updates),
441
905
  updateNode: (id, data) => nodeActions.updateNodesAction(set, get, [{ id, data }]),
442
906
  // --- DELETE ---
@@ -540,10 +1004,37 @@ const useScene = create()(temporal((set, get) => ({
540
1004
  return { collections: nextCollections, nodes: nextNodes };
541
1005
  });
542
1006
  },
1007
+ // --- SCENE MATERIALS ---
1008
+ addSceneMaterial: (material) => {
1009
+ if (get().readOnly)
1010
+ return;
1011
+ set((state) => ({
1012
+ materials: { ...state.materials, [material.id]: material },
1013
+ }));
1014
+ },
1015
+ updateSceneMaterial: (id, data) => {
1016
+ if (get().readOnly)
1017
+ return;
1018
+ set((state) => {
1019
+ const material = state.materials[id];
1020
+ if (!material)
1021
+ return state;
1022
+ return { materials: { ...state.materials, [id]: { ...material, ...data } } };
1023
+ });
1024
+ },
1025
+ removeSceneMaterial: (id) => {
1026
+ if (get().readOnly)
1027
+ return;
1028
+ set((state) => {
1029
+ const materials = { ...state.materials };
1030
+ delete materials[id];
1031
+ return { materials };
1032
+ });
1033
+ },
543
1034
  }), {
544
1035
  partialize: (state) => {
545
- const { nodes, rootNodeIds, collections } = state;
546
- return { nodes, rootNodeIds, collections };
1036
+ const { nodes, rootNodeIds, collections, materials, installedPlugins } = state;
1037
+ return { nodes, rootNodeIds, collections, materials, installedPlugins };
547
1038
  },
548
1039
  limit: 50, // Limit to last 50 actions
549
1040
  }));
@@ -554,6 +1045,12 @@ let prevFutureLength = 0;
554
1045
  let prevNodesSnapshot = null;
555
1046
  export function clearSceneHistory() {
556
1047
  resetSceneHistoryPauseDepth();
1048
+ // Resetting the pause-depth counter without resuming would strand the
1049
+ // temporal store in `isTracking: false` if a pause window was active when
1050
+ // the scene was (re)loaded — every edit after the load would then be
1051
+ // invisible to undo. Resume unconditionally so the cleared history starts
1052
+ // tracking from the loaded baseline.
1053
+ useScene.temporal.getState().resume();
557
1054
  useScene.temporal.getState().clear();
558
1055
  prevPastLength = 0;
559
1056
  prevFutureLength = 0;