@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
@@ -0,0 +1,208 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { computeEdgeGaps, computeOpeningGuides, detectAlongWallAlignment, detectEqualSpacing, detectVerticalAlignment, } from './opening-guides';
3
+ function span(id, centerS, width, centerY = 1, height = 1) {
4
+ return { id, centerS, width, centerY, height };
5
+ }
6
+ const WALL = { length: 10, height: 2.5 };
7
+ describe('detectEqualSpacing', () => {
8
+ test('returns null for fewer than three openings', () => {
9
+ const a = span('a', 0.5, 1);
10
+ const b = span('b', 2.5, 1);
11
+ expect(detectEqualSpacing([a, b], 'b', 0.03, 0.02)).toBeNull();
12
+ });
13
+ test('detects a run of equal gaps across three openings', () => {
14
+ // width 1 each: a[0,1] b[2,3] c[4,5] → two gaps of 1m.
15
+ const a = span('a', 0.5, 1);
16
+ const b = span('b', 2.5, 1);
17
+ const c = span('c', 4.5, 1);
18
+ const run = detectEqualSpacing([a, b, c], 'b', 0.03, 0.02);
19
+ expect(run).not.toBeNull();
20
+ expect(run?.gap).toBeCloseTo(1);
21
+ expect(run?.segments).toHaveLength(2);
22
+ expect(run?.openingIds).toEqual(['a', 'b', 'c']);
23
+ expect(run?.segments[0]).toEqual({ fromS: 1, toS: 2 });
24
+ expect(run?.segments[1]).toEqual({ fromS: 3, toS: 4 });
25
+ });
26
+ test('extends a run across four openings (three gaps)', () => {
27
+ const openings = [span('a', 0.5, 1), span('b', 2.5, 1), span('c', 4.5, 1), span('d', 6.5, 1)];
28
+ const run = detectEqualSpacing(openings, 'c', 0.03, 0.02);
29
+ expect(run?.segments).toHaveLength(3);
30
+ expect(run?.openingIds).toEqual(['a', 'b', 'c', 'd']);
31
+ });
32
+ test('returns null when gaps differ beyond tolerance', () => {
33
+ const a = span('a', 0.5, 1); // [0,1]
34
+ const b = span('b', 2.5, 1); // [2,3] → gap 1
35
+ const c = span('c', 5, 1); // [4.5,5.5] → gap 1.5
36
+ expect(detectEqualSpacing([a, b, c], 'b', 0.03, 0.02)).toBeNull();
37
+ });
38
+ test('returns null when the moving opening is not part of the equal run', () => {
39
+ const a = span('a', 0.5, 1);
40
+ const b = span('b', 2.5, 1);
41
+ const c = span('c', 4.5, 1); // a,b,c form equal gaps of 1
42
+ const d = span('d', 10, 1); // far right, breaks the run
43
+ expect(detectEqualSpacing([a, b, c, d], 'd', 0.03, 0.02)).toBeNull();
44
+ });
45
+ test('a near-zero (touching) gap breaks a run', () => {
46
+ const a = span('a', 0.5, 1); // [0,1]
47
+ const b = span('b', 1.505, 1); // [1.005,2.005] → gap 0.005 < minGap
48
+ const c = span('c', 3.005, 1); // [2.505,3.505] → gap 0.5
49
+ expect(detectEqualSpacing([a, b, c], 'b', 0.03, 0.02)).toBeNull();
50
+ });
51
+ test('honours the equal-spacing tolerance', () => {
52
+ const a = span('a', 0.5, 1); // [0,1]
53
+ const b = span('b', 2.5, 1); // [2,3] → gap 1.0
54
+ const c = span('c', 4.52, 1); // [4.02,5.02] → gap 1.02
55
+ expect(detectEqualSpacing([a, b, c], 'b', 0.03, 0.02)?.segments).toHaveLength(2);
56
+ expect(detectEqualSpacing([a, b, c], 'b', 0.01, 0.02)).toBeNull();
57
+ });
58
+ });
59
+ describe('computeEdgeGaps', () => {
60
+ test('measures clearance to the nearest neighbour on each side', () => {
61
+ const moving = span('m', 5, 1); // [4.5,5.5]
62
+ const left = span('l', 2, 1); // [1.5,2.5]
63
+ const right = span('r', 8, 1); // [7.5,8.5]
64
+ const gaps = computeEdgeGaps(moving, [left, right], WALL, 0.02);
65
+ const byside = Object.fromEntries(gaps.map((g) => [g.side, g]));
66
+ expect(byside.left?.distance).toBeCloseTo(2);
67
+ expect(byside.left?.target).toBe('opening');
68
+ expect(byside.left?.targetId).toBe('l');
69
+ expect(byside.right?.distance).toBeCloseTo(2);
70
+ expect(byside.right?.targetId).toBe('r');
71
+ });
72
+ test('falls back to wall ends with no neighbour', () => {
73
+ const moving = span('m', 5, 1); // [4.5,5.5]
74
+ const gaps = computeEdgeGaps(moving, [], WALL, 0.02);
75
+ const byside = Object.fromEntries(gaps.map((g) => [g.side, g]));
76
+ expect(byside.left?.target).toBe('wall-start');
77
+ expect(byside.left?.distance).toBeCloseTo(4.5);
78
+ expect(byside.right?.target).toBe('wall-end');
79
+ expect(byside.right?.distance).toBeCloseTo(4.5);
80
+ });
81
+ test('omits a side that is flush / overlapping (below minGap)', () => {
82
+ const moving = span('m', 5, 1); // [4.5,5.5]
83
+ const flush = span('l', 4, 1); // [3.5,4.5] right edge touches moving left
84
+ const gaps = computeEdgeGaps(moving, [flush], WALL, 0.02);
85
+ expect(gaps.find((g) => g.side === 'left')).toBeUndefined();
86
+ expect(gaps.find((g) => g.side === 'right')?.target).toBe('wall-end');
87
+ });
88
+ });
89
+ describe('detectAlongWallAlignment', () => {
90
+ test('detects edge-to-edge alignment within tolerance', () => {
91
+ const moving = span('m', 5, 2); // [4,6]
92
+ const sib = span('s', 7.05, 2); // left edge 6.05
93
+ const a = detectAlongWallAlignment(moving, [sib], 0.08);
94
+ expect(a?.movingFeature).toBe('right');
95
+ expect(a?.targetFeature).toBe('left');
96
+ expect(a?.snap).toBeCloseTo(0.05);
97
+ expect(a?.s).toBeCloseTo(6.05);
98
+ });
99
+ test('detects centre alignment', () => {
100
+ const moving = span('m', 5, 2);
101
+ const sib = span('s', 5.03, 0.5); // centre 5.03, edges far from moving edges
102
+ const a = detectAlongWallAlignment(moving, [sib], 0.08);
103
+ expect(a?.movingFeature).toBe('center');
104
+ expect(a?.targetFeature).toBe('center');
105
+ expect(a?.snap).toBeCloseTo(0.03);
106
+ });
107
+ test('returns null when nothing is within tolerance', () => {
108
+ const moving = span('m', 5, 2);
109
+ const sib = span('s', 9, 2);
110
+ expect(detectAlongWallAlignment(moving, [sib], 0.08)).toBeNull();
111
+ });
112
+ });
113
+ describe('detectVerticalAlignment', () => {
114
+ test('detects a shared sill within tolerance', () => {
115
+ const moving = span('m', 5, 1, 1.5, 1); // sill 1.0
116
+ const sib = span('s', 8, 1, 2.04, 2); // sill 1.04
117
+ const a = detectVerticalAlignment(moving, [sib], 0.08);
118
+ expect(a?.movingFeature).toBe('sill');
119
+ expect(a?.targetFeature).toBe('sill');
120
+ expect(a?.snap).toBeCloseTo(0.04);
121
+ expect(a?.y).toBeCloseTo(1.04);
122
+ });
123
+ test('returns null when sills/tops differ beyond tolerance', () => {
124
+ const moving = span('m', 5, 1, 1.5, 1); // sill 1, top 2, centre 1.5
125
+ const sib = span('s', 8, 1, 0.4, 0.4); // sill 0.2, top 0.6, centre 0.4
126
+ expect(detectVerticalAlignment(moving, [sib], 0.08)).toBeNull();
127
+ });
128
+ });
129
+ describe('computeOpeningGuides', () => {
130
+ test('includes sill/head for windows', () => {
131
+ const moving = span('m', 5, 1, 1.5, 1); // bottom 1, top 2
132
+ const guides = computeOpeningGuides({
133
+ moving,
134
+ siblings: [],
135
+ wall: WALL,
136
+ includeVertical: true,
137
+ });
138
+ expect(guides.sillHead?.sill).toBeCloseTo(1);
139
+ expect(guides.sillHead?.head).toBeCloseTo(0.5); // 2.5 - 2
140
+ expect(guides.sillHead?.bottomY).toBeCloseTo(1);
141
+ expect(guides.sillHead?.topY).toBeCloseTo(2);
142
+ });
143
+ test('omits vertical guides for doors (sit on the floor)', () => {
144
+ const moving = span('m', 5, 1, 1, 2);
145
+ const sib = span('s', 8, 1, 1, 2);
146
+ const guides = computeOpeningGuides({
147
+ moving,
148
+ siblings: [sib],
149
+ wall: WALL,
150
+ includeVertical: false,
151
+ });
152
+ expect(guides.sillHead).toBeNull();
153
+ expect(guides.vertical).toBeNull();
154
+ // along-wall + proximity still computed for doors
155
+ expect(guides.gaps.length).toBeGreaterThan(0);
156
+ });
157
+ test('combines proximity and equal-spacing in one pass', () => {
158
+ const moving = span('b', 2.5, 1);
159
+ const guides = computeOpeningGuides({
160
+ moving,
161
+ siblings: [span('a', 0.5, 1), span('c', 4.5, 1)],
162
+ wall: WALL,
163
+ includeVertical: true,
164
+ });
165
+ expect(guides.gaps).toHaveLength(2);
166
+ expect(guides.equalSpacing?.gap).toBeCloseTo(1);
167
+ expect(guides.equalSpacing?.openingIds).toEqual(['a', 'b', 'c']);
168
+ });
169
+ });
170
+ describe('opening-guides — review regressions', () => {
171
+ test('detectEqualSpacing finds a run that starts partway through a drifting sequence', () => {
172
+ // gaps 1.00, 1.02, 1.04 — only [b,c,d] is equal within 0.03 and includes the
173
+ // moving opening; a first-gap-anchored greedy scan used to drop it.
174
+ const openings = [span('a', 0.5, 1), span('b', 2.5, 1), span('c', 4.52, 1), span('d', 6.56, 1)];
175
+ const run = detectEqualSpacing(openings, 'd', 0.03, 0.02);
176
+ expect(run?.openingIds).toEqual(['b', 'c', 'd']);
177
+ expect(run?.gap).toBeCloseTo(1.03);
178
+ expect(run?.segments).toHaveLength(2);
179
+ });
180
+ test('detectEqualSpacing prefers the leftmost run on a length tie', () => {
181
+ // gaps 1,1,2,2 with the moving opening in the middle — two equal-length runs.
182
+ const openings = [
183
+ span('a', 0.5, 1),
184
+ span('b', 2.5, 1),
185
+ span('c', 4.5, 1),
186
+ span('d', 7.5, 1),
187
+ span('e', 10.5, 1),
188
+ ];
189
+ expect(detectEqualSpacing(openings, 'c', 0.03, 0.02)?.openingIds).toEqual(['a', 'b', 'c']);
190
+ });
191
+ test('computeEdgeGaps suppresses both sides when a sibling overlaps', () => {
192
+ const moving = span('m', 5, 1); // [4.5,5.5]
193
+ const containing = span('s', 5, 2); // [4,6] straddles both edges
194
+ expect(computeEdgeGaps(moving, [containing], WALL, 0.02)).toEqual([]);
195
+ });
196
+ test('alignment detectors ignore the moving opening if present in siblings', () => {
197
+ const moving = span('m', 5, 2, 1.5, 1);
198
+ expect(detectAlongWallAlignment(moving, [moving], 0.08)).toBeNull();
199
+ expect(detectVerticalAlignment(moving, [moving], 0.08)).toBeNull();
200
+ });
201
+ test('detectAlongWallAlignment reports a negative snap when the feature is past the target', () => {
202
+ const moving = span('m', 5, 2); // centre 5
203
+ const sib = span('s', 4.96, 0.5); // centre 4.96
204
+ const a = detectAlongWallAlignment(moving, [sib], 0.08);
205
+ expect(a?.movingFeature).toBe('center');
206
+ expect(a?.snap).toBeCloseTo(-0.04);
207
+ });
208
+ });
@@ -0,0 +1,115 @@
1
+ import type { AnyNode, AnyNodeId } from '../schema';
2
+ /**
3
+ * Connectivity-aware editing for port-bearing distribution kinds
4
+ * (HVAC ductwork AND DWV plumbing).
5
+ *
6
+ * Two nodes are "connected" when a port of one coincides in space with a
7
+ * port of the other — exactly how the placement tools mate a fitting onto
8
+ * a duct end (they snap the fitting's collar onto the run's open port).
9
+ * This service reads that relationship back out so an edit to one node can
10
+ * carry its neighbours along.
11
+ *
12
+ * Pure logic: it asks each node for its ports via `def.ports` (level-local
13
+ * meters) and does arithmetic. No Three.js, no rendering — it lives in
14
+ * core and is consumed by the editor's move tool and the duct/pipe
15
+ * selection affordances alike.
16
+ *
17
+ * ## Propagation model
18
+ *
19
+ * The joint graph is snapshotted once at drag start (`analyzePortConnectivity`)
20
+ * and walked every frame (`resolveConnectivityUpdates`) given the moved node's
21
+ * live transform. Deltas flow outward from the moved node through coincident
22
+ * ports:
23
+ *
24
+ * - **Fitting** (rigid): a collar pushed by delta `d` translates the whole
25
+ * fitting by `d`; every other collar carries that same `d` onward.
26
+ * - **Run** (stretch + slide, never skew): an endpoint pushed by delta `d` is
27
+ * split against the run's own axis. The *parallel* part slides only that
28
+ * endpoint (the run lengthens / shortens); the *perpendicular* part
29
+ * translates the entire run (so its direction is preserved). The far
30
+ * endpoint therefore moves by just the perpendicular part, and that part
31
+ * propagates onward to whatever is mated to the far endpoint.
32
+ *
33
+ * Propagation walks the whole connected component so a joint stays welded all
34
+ * the way down the chain, with a visited guard so cycles (looped runs) and
35
+ * shared joints terminate. First-reached (shortest path) wins on a node
36
+ * reachable two ways.
37
+ */
38
+ type Point = readonly [number, number, number];
39
+ /** A node carried by the edit, plus the snapshot needed to revert it. Kept
40
+ * deliberately small: the move tools read only `kind` + `nodeId` and the
41
+ * matching start snapshot to revert before the single tracked commit. */
42
+ export type PortConnection = {
43
+ /** A fitting mated collar-to-collar: it translates rigidly. */
44
+ kind: 'rigid-node';
45
+ nodeId: AnyNodeId;
46
+ /** Node's `position` at edit-start. */
47
+ startPosition: Point;
48
+ } | {
49
+ /** A run whose endpoint(s) ride the edit: it stretches and/or
50
+ * translates, never skews. */
51
+ kind: 'run';
52
+ nodeId: AnyNodeId;
53
+ /** The run's full `path` at edit-start. */
54
+ startPath: Point[];
55
+ };
56
+ /** One node in the snapshotted joint graph (everything reachable from the
57
+ * moved node, excluding the moved node itself). */
58
+ type GraphNode = {
59
+ id: AnyNodeId;
60
+ role: 'run' | 'fitting';
61
+ ports: ReadonlyArray<{
62
+ id: string;
63
+ position: Point;
64
+ system?: string;
65
+ }>;
66
+ startPath?: Point[];
67
+ startPosition?: Point;
68
+ };
69
+ /** Who else sits on a given node's port, keyed `nodeId` → `portId` → mates. */
70
+ type Adjacency = Record<string, Record<string, Array<{
71
+ nodeId: AnyNodeId;
72
+ portId: string;
73
+ }>>>;
74
+ export type PortConnectivity = {
75
+ movedNodeId: AnyNodeId;
76
+ /** The moved node's port world positions at edit-start, keyed by port id —
77
+ * the reference each frame's delta is measured from. */
78
+ startMovedPorts: Record<string, Point>;
79
+ /** Reachable run/fitting nodes (excludes the moved node), keyed by id. */
80
+ graph: Record<string, GraphNode>;
81
+ /** Port coincidence edges across the moved node + every graph node. */
82
+ adjacency: Adjacency;
83
+ /** Flat list of carried nodes for the move tools' revert + "anything to
84
+ * follow?" check. Derived from `graph`. */
85
+ connections: PortConnection[];
86
+ };
87
+ /**
88
+ * Snapshot the joint graph reachable from `movedNode`'s ports, taken at the
89
+ * start of a move/resize. Call once before the drag; feed the result to
90
+ * `resolveConnectivityUpdates` on every frame.
91
+ *
92
+ * Only `run`-role partners (segments) and `fitting`-role partners are walked —
93
+ * terminals and equipment usually mount to a surface and shouldn't be yanked
94
+ * off it when an adjacent fitting nudges. Fittings that declare
95
+ * `portConnectivityFollow: false` are anchored fixtures (e.g. pipe-trap) and
96
+ * are skipped, so a connected run stretches against them instead.
97
+ */
98
+ export declare function analyzePortConnectivity(movedNode: AnyNode, nodes: Record<string, AnyNode>): PortConnectivity;
99
+ /**
100
+ * Given the moved node in its live (in-drag) transform, produce the patches
101
+ * that keep every connected node attached. `previewNode` is the moved node
102
+ * with its current drag position/rotation applied so its ports recompute.
103
+ *
104
+ * Walks the snapshotted graph, propagating each port delta outward: fittings
105
+ * translate rigidly, runs stretch along their axis and translate across it
106
+ * (never skew when driven from one end), and effective port movement carries on
107
+ * to neighbouring joints. Port-level output guards bound cycles while still
108
+ * allowing a looped/shared run to accept constraints at both endpoints.
109
+ */
110
+ export declare function resolveConnectivityUpdates(connectivity: PortConnectivity, previewNode: AnyNode): {
111
+ id: AnyNodeId;
112
+ data: Partial<AnyNode>;
113
+ }[];
114
+ export {};
115
+ //# sourceMappingURL=port-connectivity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"port-connectivity.d.ts","sourceRoot":"","sources":["../../src/services/port-connectivity.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AAEnD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAEH,KAAK,KAAK,GAAG,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;AAY9C;;0EAE0E;AAC1E,MAAM,MAAM,cAAc,GACtB;IACE,+DAA+D;IAC/D,IAAI,EAAE,YAAY,CAAA;IAClB,MAAM,EAAE,SAAS,CAAA;IACjB,uCAAuC;IACvC,aAAa,EAAE,KAAK,CAAA;CACrB,GACD;IACE;mCAC+B;IAC/B,IAAI,EAAE,KAAK,CAAA;IACX,MAAM,EAAE,SAAS,CAAA;IACjB,2CAA2C;IAC3C,SAAS,EAAE,KAAK,EAAE,CAAA;CACnB,CAAA;AAEL;oDACoD;AACpD,KAAK,SAAS,GAAG;IACf,EAAE,EAAE,SAAS,CAAA;IACb,IAAI,EAAE,KAAK,GAAG,SAAS,CAAA;IACvB,KAAK,EAAE,aAAa,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,KAAK,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IACtE,SAAS,CAAC,EAAE,KAAK,EAAE,CAAA;IACnB,aAAa,CAAC,EAAE,KAAK,CAAA;CACtB,CAAA;AAED,+EAA+E;AAC/E,KAAK,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC,CAAC,CAAA;AAE7F,MAAM,MAAM,gBAAgB,GAAG;IAC7B,WAAW,EAAE,SAAS,CAAA;IACtB;6DACyD;IACzD,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAA;IACtC,0EAA0E;IAC1E,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAA;IAChC,uEAAuE;IACvE,SAAS,EAAE,SAAS,CAAA;IACpB;gDAC4C;IAC5C,WAAW,EAAE,cAAc,EAAE,CAAA;CAC9B,CAAA;AAkCD;;;;;;;;;;GAUG;AACH,wBAAgB,uBAAuB,CACrC,SAAS,EAAE,OAAO,EAClB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,gBAAgB,CAuFlB;AA4GD;;;;;;;;;;GAUG;AACH,wBAAgB,0BAA0B,CACxC,YAAY,EAAE,gBAAgB,EAC9B,WAAW,EAAE,OAAO,GACnB;IAAE,EAAE,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,CAAA;CAAE,EAAE,CA0F7C"}
@@ -0,0 +1,300 @@
1
+ import { nodeRegistry } from '../registry';
2
+ /** Distance (meters) under which two ports count as the same joint. Joints
3
+ * formed by placement snapping coincide to sub-millimeter; 5 cm leaves
4
+ * generous slack for grid-snapped hand placement without false matches. */
5
+ const COINCIDENT_EPS_M = 0.05;
6
+ /** Below this (meters) a propagated delta is treated as zero — stops the
7
+ * walk from chasing sub-millimeter perpendicular residue. */
8
+ const DELTA_EPS_M = 1e-4;
9
+ const PROPAGATION_EPS_M = 1e-9;
10
+ function portsOf(node) {
11
+ return nodeRegistry.get(node.type)?.ports?.(node);
12
+ }
13
+ /** A node's distribution role from the registry (run / fitting / …). */
14
+ function roleOf(node) {
15
+ return nodeRegistry.get(node.type)?.distributionRole;
16
+ }
17
+ function distSq(a, b) {
18
+ const dx = a[0] - b[0];
19
+ const dy = a[1] - b[1];
20
+ const dz = a[2] - b[2];
21
+ return dx * dx + dy * dy + dz * dz;
22
+ }
23
+ /** Two ports mate when they coincide AND don't cross incompatible systems
24
+ * (a supply duct and a waste pipe that merely touch must not fuse). */
25
+ function portsMate(a, b, epsSq) {
26
+ if (distSq(a.position, b.position) > epsSq)
27
+ return false;
28
+ if (a.system && b.system && a.system !== b.system)
29
+ return false;
30
+ return true;
31
+ }
32
+ /**
33
+ * Snapshot the joint graph reachable from `movedNode`'s ports, taken at the
34
+ * start of a move/resize. Call once before the drag; feed the result to
35
+ * `resolveConnectivityUpdates` on every frame.
36
+ *
37
+ * Only `run`-role partners (segments) and `fitting`-role partners are walked —
38
+ * terminals and equipment usually mount to a surface and shouldn't be yanked
39
+ * off it when an adjacent fitting nudges. Fittings that declare
40
+ * `portConnectivityFollow: false` are anchored fixtures (e.g. pipe-trap) and
41
+ * are skipped, so a connected run stretches against them instead.
42
+ */
43
+ export function analyzePortConnectivity(movedNode, nodes) {
44
+ const epsSq = COINCIDENT_EPS_M * COINCIDENT_EPS_M;
45
+ const movedPorts = portsOf(movedNode) ?? [];
46
+ const startMovedPorts = {};
47
+ for (const p of movedPorts)
48
+ startMovedPorts[p.id] = p.position;
49
+ // Candidate partners: every run + every following fitting in the scene.
50
+ const candidates = [];
51
+ for (const other of Object.values(nodes)) {
52
+ if (!other || other.id === movedNode.id)
53
+ continue;
54
+ const role = roleOf(other);
55
+ if (role !== 'run' && role !== 'fitting')
56
+ continue;
57
+ if (role === 'fitting' && nodeRegistry.get(other.type)?.portConnectivityFollow === false) {
58
+ continue;
59
+ }
60
+ const ports = portsOf(other);
61
+ if (!ports)
62
+ continue;
63
+ const startPath = role === 'run'
64
+ ? other.path?.map((p) => [...p])
65
+ : undefined;
66
+ if (role === 'run' && (!startPath || startPath.length < 2))
67
+ continue;
68
+ const startPosition = role === 'fitting'
69
+ ? (() => {
70
+ const pos = other.position;
71
+ return pos ? [pos[0], pos[1], pos[2]] : undefined;
72
+ })()
73
+ : undefined;
74
+ if (role === 'fitting' && !startPosition)
75
+ continue;
76
+ candidates.push({ id: other.id, role, ports, startPath, startPosition });
77
+ }
78
+ // Walk outward from the moved node, collecting every node reachable through
79
+ // coincident ports. The adjacency records each port's mates so the resolver
80
+ // can replay the same edges with live deltas.
81
+ const adjacency = {};
82
+ const addEdge = (nodeId, portId, mate) => {
83
+ const byPort = adjacency[nodeId] ?? {};
84
+ adjacency[nodeId] = byPort;
85
+ const mates = byPort[portId] ?? [];
86
+ byPort[portId] = mates;
87
+ mates.push(mate);
88
+ };
89
+ const graph = {};
90
+ const visited = new Set([movedNode.id]);
91
+ // Seed: the moved node's own ports.
92
+ const queue = [{ id: movedNode.id, ports: movedPorts }];
93
+ while (queue.length > 0) {
94
+ const { id, ports } = queue.shift();
95
+ for (const port of ports) {
96
+ for (const cand of candidates) {
97
+ if (cand.id === id)
98
+ continue;
99
+ for (const cp of cand.ports) {
100
+ if (!portsMate(port, cp, epsSq))
101
+ continue;
102
+ addEdge(id, port.id, { nodeId: cand.id, portId: cp.id });
103
+ addEdge(cand.id, cp.id, { nodeId: id, portId: port.id });
104
+ if (!visited.has(cand.id)) {
105
+ visited.add(cand.id);
106
+ graph[cand.id] = cand;
107
+ queue.push({ id: cand.id, ports: cand.ports });
108
+ }
109
+ }
110
+ }
111
+ }
112
+ }
113
+ const connections = Object.values(graph).map((g) => g.role === 'fitting'
114
+ ? { kind: 'rigid-node', nodeId: g.id, startPosition: g.startPosition }
115
+ : { kind: 'run', nodeId: g.id, startPath: g.startPath });
116
+ return {
117
+ movedNodeId: movedNode.id,
118
+ startMovedPorts,
119
+ graph,
120
+ adjacency,
121
+ connections,
122
+ };
123
+ }
124
+ function add(a, b) {
125
+ return [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
126
+ }
127
+ function sub(a, b) {
128
+ return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
129
+ }
130
+ function lenSq(v) {
131
+ return v[0] * v[0] + v[1] * v[1] + v[2] * v[2];
132
+ }
133
+ /** Split `delta` into the component along unit `axis` and the remainder. */
134
+ function decompose(delta, axis) {
135
+ const dot = delta[0] * axis[0] + delta[1] * axis[1] + delta[2] * axis[2];
136
+ const parallel = [axis[0] * dot, axis[1] * dot, axis[2] * dot];
137
+ return { parallel, perp: sub(delta, parallel) };
138
+ }
139
+ function scale(v, scalar) {
140
+ return [v[0] * scalar, v[1] * scalar, v[2] * scalar];
141
+ }
142
+ function average(deltas) {
143
+ const sum = deltas.reduce((acc, delta) => add(acc, delta), [0, 0, 0]);
144
+ return scale(sum, 1 / deltas.length);
145
+ }
146
+ function nearlyEqual(a, b) {
147
+ return lenSq(sub(a, b)) <= DELTA_EPS_M * DELTA_EPS_M;
148
+ }
149
+ function propagationEqual(a, b) {
150
+ return lenSq(sub(a, b)) <= PROPAGATION_EPS_M * PROPAGATION_EPS_M;
151
+ }
152
+ function effectivePortDeltas(constraints) {
153
+ return Object.fromEntries(Object.entries(constraints).map(([portId, bySource]) => [
154
+ portId,
155
+ average(Object.values(bySource)),
156
+ ]));
157
+ }
158
+ /** Unit direction of the run's segment adjacent to its `start` / `end` tip. */
159
+ function endpointAxis(path, portId) {
160
+ const n = path.length;
161
+ const [a, b] = portId === 'start' ? [path[1], path[0]] : [path[n - 2], path[n - 1]];
162
+ const dir = sub(b, a);
163
+ const l2 = lenSq(dir);
164
+ if (l2 < 1e-12)
165
+ return [0, 0, 0];
166
+ const l = Math.sqrt(l2);
167
+ return [dir[0] / l, dir[1] / l, dir[2] / l];
168
+ }
169
+ function runPathFromSinglePortDelta(startPath, portId, delta) {
170
+ const nearIdx = portId === 'start' ? 0 : startPath.length - 1;
171
+ const axis = endpointAxis(startPath, portId);
172
+ const { parallel, perp } = decompose(delta, axis);
173
+ const path = startPath.map((p) => add(p, perp));
174
+ path[nearIdx] = add(path[nearIdx], parallel);
175
+ return path;
176
+ }
177
+ function runEndpointDeltas(startPath, path) {
178
+ return {
179
+ start: sub(path[0], startPath[0]),
180
+ end: sub(path[path.length - 1], startPath[startPath.length - 1]),
181
+ };
182
+ }
183
+ function runPathFromPortDeltas(startPath, portDeltas) {
184
+ const startDelta = portDeltas.start;
185
+ const endDelta = portDeltas.end;
186
+ if (startDelta && endDelta) {
187
+ if (startPath.length === 2) {
188
+ return [add(startPath[0], startDelta), add(startPath[1], endDelta)];
189
+ }
190
+ if (nearlyEqual(startDelta, endDelta)) {
191
+ return startPath.map((p) => add(p, startDelta));
192
+ }
193
+ const startParts = decompose(startDelta, endpointAxis(startPath, 'start'));
194
+ const endParts = decompose(endDelta, endpointAxis(startPath, 'end'));
195
+ const commonPerp = average([startParts.perp, endParts.perp]);
196
+ const path = startPath.map((p) => add(p, commonPerp));
197
+ path[0] = add(path[0], startParts.parallel);
198
+ path[path.length - 1] = add(path[path.length - 1], endParts.parallel);
199
+ return path;
200
+ }
201
+ return runPathFromSinglePortDelta(startPath, startDelta ? 'start' : 'end', (startDelta ?? endDelta));
202
+ }
203
+ /**
204
+ * Given the moved node in its live (in-drag) transform, produce the patches
205
+ * that keep every connected node attached. `previewNode` is the moved node
206
+ * with its current drag position/rotation applied so its ports recompute.
207
+ *
208
+ * Walks the snapshotted graph, propagating each port delta outward: fittings
209
+ * translate rigidly, runs stretch along their axis and translate across it
210
+ * (never skew when driven from one end), and effective port movement carries on
211
+ * to neighbouring joints. Port-level output guards bound cycles while still
212
+ * allowing a looped/shared run to accept constraints at both endpoints.
213
+ */
214
+ export function resolveConnectivityUpdates(connectivity, previewNode) {
215
+ const { graph, adjacency, startMovedPorts, movedNodeId } = connectivity;
216
+ if (Object.keys(graph).length === 0)
217
+ return [];
218
+ const newPorts = portsOf(previewNode) ?? [];
219
+ const newMovedPos = {};
220
+ for (const p of newPorts)
221
+ newMovedPos[p.id] = p.position;
222
+ // Each queue item drives a node's port by a delta ("this collar / endpoint
223
+ // must move by this much").
224
+ const queue = [];
225
+ const results = {};
226
+ const constrainedPorts = {};
227
+ const propagatedPorts = {};
228
+ const enqueueMates = (nodeId, portId, delta) => {
229
+ const byPort = propagatedPorts[nodeId] ?? {};
230
+ propagatedPorts[nodeId] = byPort;
231
+ const previous = byPort[portId];
232
+ if (previous && propagationEqual(previous, delta))
233
+ return;
234
+ byPort[portId] = delta;
235
+ for (const mate of adjacency[nodeId]?.[portId] ?? []) {
236
+ if (mate.nodeId === movedNodeId)
237
+ continue;
238
+ queue.push({
239
+ nodeId: mate.nodeId,
240
+ portId: mate.portId,
241
+ delta,
242
+ sourceKey: `${nodeId}:${portId}`,
243
+ });
244
+ }
245
+ };
246
+ const acceptPortDelta = (nodeId, portId, sourceKey, delta) => {
247
+ const byPort = constrainedPorts[nodeId] ?? {};
248
+ constrainedPorts[nodeId] = byPort;
249
+ const bySource = byPort[portId] ?? {};
250
+ byPort[portId] = bySource;
251
+ const existing = bySource[sourceKey];
252
+ if (existing && propagationEqual(existing, delta)) {
253
+ return false;
254
+ }
255
+ bySource[sourceKey] = delta;
256
+ return true;
257
+ };
258
+ // Seed from the moved node's live port deltas.
259
+ for (const [portId, start] of Object.entries(startMovedPorts)) {
260
+ const now = newMovedPos[portId];
261
+ if (!now)
262
+ continue;
263
+ const delta = sub(now, start);
264
+ if (lenSq(delta) <= DELTA_EPS_M * DELTA_EPS_M)
265
+ continue;
266
+ enqueueMates(movedNodeId, portId, delta);
267
+ }
268
+ while (queue.length > 0) {
269
+ const { nodeId, portId, delta, sourceKey } = queue.shift();
270
+ const node = graph[nodeId];
271
+ if (!node)
272
+ continue;
273
+ if (!acceptPortDelta(nodeId, portId, sourceKey, delta))
274
+ continue;
275
+ const portDeltas = effectivePortDeltas(constrainedPorts[nodeId]);
276
+ if (node.role === 'fitting') {
277
+ const start = node.startPosition;
278
+ const effectiveDelta = average(Object.values(portDeltas));
279
+ results[nodeId] = {
280
+ id: nodeId,
281
+ data: { position: add(start, effectiveDelta) },
282
+ };
283
+ // Rigid: every collar carries the effective body translation onward.
284
+ for (const p of node.ports) {
285
+ enqueueMates(nodeId, p.id, effectiveDelta);
286
+ }
287
+ }
288
+ else {
289
+ const startPath = node.startPath;
290
+ const path = runPathFromPortDeltas(startPath, portDeltas);
291
+ results[nodeId] = { id: nodeId, data: { path } };
292
+ for (const [nextPortId, nextDelta] of Object.entries(runEndpointDeltas(startPath, path))) {
293
+ if (lenSq(nextDelta) <= DELTA_EPS_M * DELTA_EPS_M)
294
+ continue;
295
+ enqueueMates(nodeId, nextPortId, nextDelta);
296
+ }
297
+ }
298
+ }
299
+ return Object.values(results);
300
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=port-connectivity.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"port-connectivity.test.d.ts","sourceRoot":"","sources":["../../src/services/port-connectivity.test.ts"],"names":[],"mappings":""}