@pascal-app/core 0.9.1 → 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 (321) hide show
  1. package/dist/events/bus.d.ts +30 -2
  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 +8 -1
  7. package/dist/hooks/scene-registry/scene-registry.d.ts.map +1 -1
  8. package/dist/hooks/scene-registry/scene-registry.js +27 -1
  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.test.js +93 -0
  13. package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts +69 -12
  14. package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts.map +1 -1
  15. package/dist/hooks/spatial-grid/spatial-grid-manager.js +476 -195
  16. package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts.map +1 -1
  17. package/dist/hooks/spatial-grid/spatial-grid-sync.js +19 -1
  18. package/dist/hooks/spatial-grid/wall-slab-overlap.test.d.ts +2 -0
  19. package/dist/hooks/spatial-grid/wall-slab-overlap.test.d.ts.map +1 -0
  20. package/dist/hooks/spatial-grid/wall-slab-overlap.test.js +363 -0
  21. package/dist/index.d.ts +16 -6
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +15 -5
  24. package/dist/lib/level-name.d.ts +4 -0
  25. package/dist/lib/level-name.d.ts.map +1 -0
  26. package/dist/lib/level-name.js +10 -0
  27. package/dist/lib/measurement-geometry.d.ts +19 -0
  28. package/dist/lib/measurement-geometry.d.ts.map +1 -0
  29. package/dist/lib/measurement-geometry.js +232 -0
  30. package/dist/lib/measurement-geometry.test.d.ts +2 -0
  31. package/dist/lib/measurement-geometry.test.d.ts.map +1 -0
  32. package/dist/lib/measurement-geometry.test.js +102 -0
  33. package/dist/lib/polygon-geometry.d.ts +0 -1
  34. package/dist/lib/polygon-geometry.d.ts.map +1 -1
  35. package/dist/lib/polygon-geometry.js +0 -18
  36. package/dist/lib/selection-proxy.d.ts +4 -0
  37. package/dist/lib/selection-proxy.d.ts.map +1 -0
  38. package/dist/lib/selection-proxy.js +12 -0
  39. package/dist/lib/slab-polygon.d.ts +31 -2
  40. package/dist/lib/slab-polygon.d.ts.map +1 -1
  41. package/dist/lib/slab-polygon.js +525 -31
  42. package/dist/lib/slab-polygon.test.d.ts +2 -0
  43. package/dist/lib/slab-polygon.test.d.ts.map +1 -0
  44. package/dist/lib/slab-polygon.test.js +635 -0
  45. package/dist/lib/slots.d.ts +14 -0
  46. package/dist/lib/slots.d.ts.map +1 -0
  47. package/dist/lib/slots.js +26 -0
  48. package/dist/lib/space-detection.d.ts +25 -2
  49. package/dist/lib/space-detection.d.ts.map +1 -1
  50. package/dist/lib/space-detection.js +414 -118
  51. package/dist/lib/space-detection.test.js +301 -2
  52. package/dist/lib/wall-distance.d.ts +60 -0
  53. package/dist/lib/wall-distance.d.ts.map +1 -0
  54. package/dist/lib/wall-distance.js +85 -0
  55. package/dist/lib/zone-quantities.d.ts +21 -0
  56. package/dist/lib/zone-quantities.d.ts.map +1 -0
  57. package/dist/lib/zone-quantities.js +183 -0
  58. package/dist/lib/zone-quantities.test.d.ts +2 -0
  59. package/dist/lib/zone-quantities.test.d.ts.map +1 -0
  60. package/dist/lib/zone-quantities.test.js +82 -0
  61. package/dist/material-library.d.ts +20 -1
  62. package/dist/material-library.d.ts.map +1 -1
  63. package/dist/material-library.js +2181 -441
  64. package/dist/registry/handles.d.ts +75 -2
  65. package/dist/registry/handles.d.ts.map +1 -1
  66. package/dist/registry/index.d.ts +3 -3
  67. package/dist/registry/index.d.ts.map +1 -1
  68. package/dist/registry/index.js +1 -1
  69. package/dist/registry/registry.d.ts +46 -3
  70. package/dist/registry/registry.d.ts.map +1 -1
  71. package/dist/registry/registry.js +82 -1
  72. package/dist/registry/registry.test.js +14 -1
  73. package/dist/registry/subtree.d.ts.map +1 -1
  74. package/dist/registry/subtree.js +4 -0
  75. package/dist/registry/subtree.test.js +30 -0
  76. package/dist/registry/types.d.ts +717 -38
  77. package/dist/registry/types.d.ts.map +1 -1
  78. package/dist/schema/index.d.ts +21 -5
  79. package/dist/schema/index.d.ts.map +1 -1
  80. package/dist/schema/index.js +17 -3
  81. package/dist/schema/material.d.ts +3 -2
  82. package/dist/schema/material.d.ts.map +1 -1
  83. package/dist/schema/material.js +7 -5
  84. package/dist/schema/material.test.d.ts +2 -0
  85. package/dist/schema/material.test.d.ts.map +1 -0
  86. package/dist/schema/material.test.js +43 -0
  87. package/dist/schema/nodes/box-vent.d.ts +2 -2
  88. package/dist/schema/nodes/cabinet.d.ts +534 -0
  89. package/dist/schema/nodes/cabinet.d.ts.map +1 -0
  90. package/dist/schema/nodes/cabinet.js +137 -0
  91. package/dist/schema/nodes/ceiling.d.ts +3 -2
  92. package/dist/schema/nodes/ceiling.d.ts.map +1 -1
  93. package/dist/schema/nodes/ceiling.js +4 -0
  94. package/dist/schema/nodes/chimney.d.ts +4 -4
  95. package/dist/schema/nodes/column.d.ts +3 -2
  96. package/dist/schema/nodes/column.d.ts.map +1 -1
  97. package/dist/schema/nodes/column.js +3 -0
  98. package/dist/schema/nodes/cupola.d.ts +2 -2
  99. package/dist/schema/nodes/door.d.ts +13 -5
  100. package/dist/schema/nodes/door.d.ts.map +1 -1
  101. package/dist/schema/nodes/door.js +12 -0
  102. package/dist/schema/nodes/dormer.d.ts +8 -8
  103. package/dist/schema/nodes/dormer.js +1 -1
  104. package/dist/schema/nodes/downspout.d.ts +2 -2
  105. package/dist/schema/nodes/duct-fitting.d.ts +91 -0
  106. package/dist/schema/nodes/duct-fitting.d.ts.map +1 -0
  107. package/dist/schema/nodes/duct-fitting.js +92 -0
  108. package/dist/schema/nodes/duct-segment.d.ts +61 -0
  109. package/dist/schema/nodes/duct-segment.d.ts.map +1 -0
  110. package/dist/schema/nodes/duct-segment.js +73 -0
  111. package/dist/schema/nodes/duct-terminal.d.ts +57 -0
  112. package/dist/schema/nodes/duct-terminal.d.ts.map +1 -0
  113. package/dist/schema/nodes/duct-terminal.js +51 -0
  114. package/dist/schema/nodes/elevator.d.ts +3 -2
  115. package/dist/schema/nodes/elevator.d.ts.map +1 -1
  116. package/dist/schema/nodes/elevator.js +3 -0
  117. package/dist/schema/nodes/eyebrow-vent.d.ts +2 -2
  118. package/dist/schema/nodes/fence.d.ts +18 -2
  119. package/dist/schema/nodes/fence.d.ts.map +1 -1
  120. package/dist/schema/nodes/fence.js +25 -2
  121. package/dist/schema/nodes/gutter.d.ts +2 -2
  122. package/dist/schema/nodes/hvac-equipment.d.ts +63 -0
  123. package/dist/schema/nodes/hvac-equipment.d.ts.map +1 -0
  124. package/dist/schema/nodes/hvac-equipment.js +55 -0
  125. package/dist/schema/nodes/item.d.ts +8 -0
  126. package/dist/schema/nodes/item.d.ts.map +1 -1
  127. package/dist/schema/nodes/item.js +11 -0
  128. package/dist/schema/nodes/level.d.ts +1 -1
  129. package/dist/schema/nodes/level.d.ts.map +1 -1
  130. package/dist/schema/nodes/level.js +2 -0
  131. package/dist/schema/nodes/lineset.d.ts +42 -0
  132. package/dist/schema/nodes/lineset.d.ts.map +1 -0
  133. package/dist/schema/nodes/lineset.js +38 -0
  134. package/dist/schema/nodes/liquid-line.d.ts +34 -0
  135. package/dist/schema/nodes/liquid-line.d.ts.map +1 -0
  136. package/dist/schema/nodes/liquid-line.js +24 -0
  137. package/dist/schema/nodes/measurement.d.ts +306 -0
  138. package/dist/schema/nodes/measurement.d.ts.map +1 -0
  139. package/dist/schema/nodes/measurement.js +87 -0
  140. package/dist/schema/nodes/measurement.test.d.ts +2 -0
  141. package/dist/schema/nodes/measurement.test.d.ts.map +1 -0
  142. package/dist/schema/nodes/measurement.test.js +139 -0
  143. package/dist/schema/nodes/pipe-fitting.d.ts +56 -0
  144. package/dist/schema/nodes/pipe-fitting.d.ts.map +1 -0
  145. package/dist/schema/nodes/pipe-fitting.js +45 -0
  146. package/dist/schema/nodes/pipe-segment.d.ts +48 -0
  147. package/dist/schema/nodes/pipe-segment.d.ts.map +1 -0
  148. package/dist/schema/nodes/pipe-segment.js +36 -0
  149. package/dist/schema/nodes/pipe-trap.d.ts +43 -0
  150. package/dist/schema/nodes/pipe-trap.d.ts.map +1 -0
  151. package/dist/schema/nodes/pipe-trap.js +36 -0
  152. package/dist/schema/nodes/ridge-vent-defaults.test.d.ts +2 -0
  153. package/dist/schema/nodes/ridge-vent-defaults.test.d.ts.map +1 -0
  154. package/dist/schema/nodes/ridge-vent-defaults.test.js +205 -0
  155. package/dist/schema/nodes/ridge-vent.d.ts +18 -3
  156. package/dist/schema/nodes/ridge-vent.d.ts.map +1 -1
  157. package/dist/schema/nodes/ridge-vent.js +221 -3
  158. package/dist/schema/nodes/roof-segment-shape.d.ts +90 -0
  159. package/dist/schema/nodes/roof-segment-shape.d.ts.map +1 -0
  160. package/dist/schema/nodes/roof-segment-shape.js +318 -0
  161. package/dist/schema/nodes/roof-segment-shape.test.d.ts +2 -0
  162. package/dist/schema/nodes/roof-segment-shape.test.d.ts.map +1 -0
  163. package/dist/schema/nodes/roof-segment-shape.test.js +175 -0
  164. package/dist/schema/nodes/roof-segment-surface.test.d.ts +2 -0
  165. package/dist/schema/nodes/roof-segment-surface.test.d.ts.map +1 -0
  166. package/dist/schema/nodes/roof-segment-surface.test.js +48 -0
  167. package/dist/schema/nodes/roof-segment-trim.test.d.ts +2 -0
  168. package/dist/schema/nodes/roof-segment-trim.test.d.ts.map +1 -0
  169. package/dist/schema/nodes/roof-segment-trim.test.js +107 -0
  170. package/dist/schema/nodes/roof-segment-walls.d.ts +101 -0
  171. package/dist/schema/nodes/roof-segment-walls.d.ts.map +1 -0
  172. package/dist/schema/nodes/roof-segment-walls.js +330 -0
  173. package/dist/schema/nodes/roof-segment-walls.test.d.ts +2 -0
  174. package/dist/schema/nodes/roof-segment-walls.test.d.ts.map +1 -0
  175. package/dist/schema/nodes/roof-segment-walls.test.js +92 -0
  176. package/dist/schema/nodes/roof-segment.d.ts +84 -8
  177. package/dist/schema/nodes/roof-segment.d.ts.map +1 -1
  178. package/dist/schema/nodes/roof-segment.js +290 -5
  179. package/dist/schema/nodes/roof.d.ts +8 -8
  180. package/dist/schema/nodes/shelf.d.ts +3 -2
  181. package/dist/schema/nodes/shelf.d.ts.map +1 -1
  182. package/dist/schema/nodes/shelf.js +6 -0
  183. package/dist/schema/nodes/skylight.d.ts +10 -10
  184. package/dist/schema/nodes/slab.d.ts +3 -2
  185. package/dist/schema/nodes/slab.d.ts.map +1 -1
  186. package/dist/schema/nodes/slab.js +4 -0
  187. package/dist/schema/nodes/solar-panel.d.ts +4 -4
  188. package/dist/schema/nodes/stair-segment.d.ts +4 -4
  189. package/dist/schema/nodes/stair.d.ts +13 -12
  190. package/dist/schema/nodes/stair.d.ts.map +1 -1
  191. package/dist/schema/nodes/stair.js +3 -0
  192. package/dist/schema/nodes/turbine-vent.d.ts +2 -2
  193. package/dist/schema/nodes/wall.d.ts +245 -6
  194. package/dist/schema/nodes/wall.d.ts.map +1 -1
  195. package/dist/schema/nodes/wall.js +257 -0
  196. package/dist/schema/nodes/wall.test.d.ts +2 -0
  197. package/dist/schema/nodes/wall.test.d.ts.map +1 -0
  198. package/dist/schema/nodes/wall.test.js +208 -0
  199. package/dist/schema/nodes/window.d.ts +11 -3
  200. package/dist/schema/nodes/window.d.ts.map +1 -1
  201. package/dist/schema/nodes/window.js +12 -0
  202. package/dist/schema/nodes/zone.d.ts +2 -0
  203. package/dist/schema/nodes/zone.d.ts.map +1 -1
  204. package/dist/schema/nodes/zone.js +6 -0
  205. package/dist/schema/scene-material.d.ts +41 -0
  206. package/dist/schema/scene-material.d.ts.map +1 -0
  207. package/dist/schema/scene-material.js +9 -0
  208. package/dist/schema/types.d.ts +1089 -94
  209. package/dist/schema/types.d.ts.map +1 -1
  210. package/dist/schema/types.js +23 -0
  211. package/dist/services/alignment-anchors.d.ts.map +1 -1
  212. package/dist/services/alignment-anchors.js +30 -1
  213. package/dist/services/alignment.d.ts +5 -0
  214. package/dist/services/alignment.d.ts.map +1 -1
  215. package/dist/services/alignment.js +2 -0
  216. package/dist/services/hosting.d.ts +8 -0
  217. package/dist/services/hosting.d.ts.map +1 -1
  218. package/dist/services/hosting.js +13 -4
  219. package/dist/services/hosting.test.js +32 -1
  220. package/dist/services/index.d.ts +8 -2
  221. package/dist/services/index.d.ts.map +1 -1
  222. package/dist/services/index.js +8 -2
  223. package/dist/services/level-height.d.ts +28 -0
  224. package/dist/services/level-height.d.ts.map +1 -0
  225. package/dist/services/level-height.js +62 -0
  226. package/dist/services/opening-guides.d.ts +141 -0
  227. package/dist/services/opening-guides.d.ts.map +1 -0
  228. package/dist/services/opening-guides.js +267 -0
  229. package/dist/services/opening-guides.test.d.ts +2 -0
  230. package/dist/services/opening-guides.test.d.ts.map +1 -0
  231. package/dist/services/opening-guides.test.js +208 -0
  232. package/dist/services/port-connectivity.d.ts +115 -0
  233. package/dist/services/port-connectivity.d.ts.map +1 -0
  234. package/dist/services/port-connectivity.js +300 -0
  235. package/dist/services/port-connectivity.test.d.ts +2 -0
  236. package/dist/services/port-connectivity.test.d.ts.map +1 -0
  237. package/dist/services/port-connectivity.test.js +297 -0
  238. package/dist/services/riser-diagram.d.ts +40 -0
  239. package/dist/services/riser-diagram.d.ts.map +1 -0
  240. package/dist/services/riser-diagram.js +110 -0
  241. package/dist/services/riser-diagram.test.d.ts +2 -0
  242. package/dist/services/riser-diagram.test.d.ts.map +1 -0
  243. package/dist/services/riser-diagram.test.js +66 -0
  244. package/dist/services/snap.d.ts +9 -1
  245. package/dist/services/snap.d.ts.map +1 -1
  246. package/dist/services/snap.js +21 -1
  247. package/dist/services/snap.test.js +43 -1
  248. package/dist/services/system-graph.d.ts +30 -0
  249. package/dist/services/system-graph.d.ts.map +1 -0
  250. package/dist/services/system-graph.js +164 -0
  251. package/dist/services/system-graph.test.d.ts +2 -0
  252. package/dist/services/system-graph.test.d.ts.map +1 -0
  253. package/dist/services/system-graph.test.js +138 -0
  254. package/dist/services/validate-dwv.d.ts +32 -0
  255. package/dist/services/validate-dwv.d.ts.map +1 -0
  256. package/dist/services/validate-dwv.js +118 -0
  257. package/dist/services/validate-dwv.test.d.ts +2 -0
  258. package/dist/services/validate-dwv.test.d.ts.map +1 -0
  259. package/dist/services/validate-dwv.test.js +109 -0
  260. package/dist/store/actions/node-actions.d.ts.map +1 -1
  261. package/dist/store/actions/node-actions.js +94 -1
  262. package/dist/store/actions/ridge-vent-update.test.d.ts +2 -0
  263. package/dist/store/actions/ridge-vent-update.test.d.ts.map +1 -0
  264. package/dist/store/actions/ridge-vent-update.test.js +264 -0
  265. package/dist/store/history-control.d.ts +11 -0
  266. package/dist/store/history-control.d.ts.map +1 -1
  267. package/dist/store/history-control.js +33 -0
  268. package/dist/store/use-scene-dirty-tracking.test.d.ts +2 -0
  269. package/dist/store/use-scene-dirty-tracking.test.d.ts.map +1 -0
  270. package/dist/store/use-scene-dirty-tracking.test.js +68 -0
  271. package/dist/store/use-scene-measurement-roundtrip.test.d.ts +2 -0
  272. package/dist/store/use-scene-measurement-roundtrip.test.d.ts.map +1 -0
  273. package/dist/store/use-scene-measurement-roundtrip.test.js +97 -0
  274. package/dist/store/use-scene-plugins.test.d.ts +2 -0
  275. package/dist/store/use-scene-plugins.test.d.ts.map +1 -0
  276. package/dist/store/use-scene-plugins.test.js +51 -0
  277. package/dist/store/use-scene-wall-slot-migration.test.d.ts +2 -0
  278. package/dist/store/use-scene-wall-slot-migration.test.d.ts.map +1 -0
  279. package/dist/store/use-scene-wall-slot-migration.test.js +210 -0
  280. package/dist/store/use-scene-window-migration.test.d.ts +2 -0
  281. package/dist/store/use-scene-window-migration.test.d.ts.map +1 -0
  282. package/dist/store/use-scene-window-migration.test.js +85 -0
  283. package/dist/store/use-scene.d.ts +17 -2
  284. package/dist/store/use-scene.d.ts.map +1 -1
  285. package/dist/store/use-scene.js +358 -51
  286. package/dist/systems/elevator/elevator-service.js +1 -1
  287. package/dist/systems/fence/fence-centerline.d.ts +12 -0
  288. package/dist/systems/fence/fence-centerline.d.ts.map +1 -0
  289. package/dist/systems/fence/fence-centerline.js +37 -0
  290. package/dist/systems/fence/fence-spline.d.ts +39 -0
  291. package/dist/systems/fence/fence-spline.d.ts.map +1 -0
  292. package/dist/systems/fence/fence-spline.js +196 -0
  293. package/dist/systems/fence/fence-spline.test.d.ts +2 -0
  294. package/dist/systems/fence/fence-spline.test.d.ts.map +1 -0
  295. package/dist/systems/fence/fence-spline.test.js +136 -0
  296. package/dist/systems/stair/stair-footprint.d.ts.map +1 -1
  297. package/dist/systems/stair/stair-footprint.js +21 -11
  298. package/dist/systems/stair/stair-opening-preview.d.ts +1139 -469
  299. package/dist/systems/stair/stair-opening-preview.d.ts.map +1 -1
  300. package/dist/systems/stair/stair-opening-sync.d.ts.map +1 -1
  301. package/dist/systems/stair/stair-opening-sync.js +1 -18
  302. package/dist/systems/stair/stair-opening-sync.test.js +39 -0
  303. package/dist/systems/wall/wall-mitering.d.ts.map +1 -1
  304. package/dist/systems/wall/wall-mitering.js +38 -2
  305. package/dist/systems/wall/wall-mitering.test.d.ts +2 -0
  306. package/dist/systems/wall/wall-mitering.test.d.ts.map +1 -0
  307. package/dist/systems/wall/wall-mitering.test.js +63 -0
  308. package/dist/utils/clone-scene-graph.d.ts +1 -0
  309. package/dist/utils/clone-scene-graph.d.ts.map +1 -1
  310. package/dist/utils/clone-scene-graph.js +23 -3
  311. package/dist/utils/clone-scene-graph.test.js +3 -0
  312. package/dist/utils/heal-scene-graph.d.ts +18 -0
  313. package/dist/utils/heal-scene-graph.d.ts.map +1 -0
  314. package/dist/utils/heal-scene-graph.js +90 -0
  315. package/dist/utils/heal-scene-graph.test.d.ts +2 -0
  316. package/dist/utils/heal-scene-graph.test.d.ts.map +1 -0
  317. package/dist/utils/heal-scene-graph.test.js +88 -0
  318. package/dist/validation/validate-build-json.d.ts +1 -0
  319. package/dist/validation/validate-build-json.d.ts.map +1 -1
  320. package/dist/validation/validate-build-json.js +31 -2
  321. package/package.json +3 -3
@@ -0,0 +1,267 @@
1
+ // Proximity / alignment guides for wall-hosted openings (doors, windows).
2
+ //
3
+ // Pure geometry over a single host wall's LOCAL frame — no Three.js, no scene
4
+ // store, no React — so it runs identically for the 3D viewport and the 2D
5
+ // floor plan and is unit-testable in isolation. Callers extract the spans from
6
+ // the scene graph (an opening's `position[0]` is its along-wall centre, its
7
+ // `position[1]` its vertical centre with the wall base at y=0) and feed them in;
8
+ // the renderers transform the returned wall-local coordinates back to world
9
+ // (3D) or plan (2D).
10
+ //
11
+ // What it produces, mirroring the affordances architects expect (and Figma's
12
+ // smart guides):
13
+ // - sill/head : a window's bottom edge → floor and top edge → wall top.
14
+ // - edge gaps : along-wall clearance to the nearest neighbour opening (or
15
+ // the wall end) on each side.
16
+ // - alongWall : the moving opening's edge/centre lining up with a
17
+ // neighbour's edge/centre along the wall.
18
+ // - vertical : two openings sharing a sill / head / vertical centre.
19
+ // - equalSpacing : a run of 3+ openings with (near-)equal gaps between them.
20
+ //
21
+ // Detection is passive — it reports what currently coincides within tolerance
22
+ // and the snap delta that would make it exact, leaving the snap decision to the
23
+ // caller's manipulation policy (grid vs. alignment vs. Shift bypass).
24
+ export const DEFAULT_OPENING_GUIDE_TOLERANCES = {
25
+ // Parity with the along-wall snap threshold (`ALONG_WALL_ALIGN_THRESHOLD_M`).
26
+ align: 0.08,
27
+ equalSpacing: 0.03,
28
+ minGap: 0.02,
29
+ };
30
+ const leftEdge = (s) => s.centerS - s.width / 2;
31
+ const rightEdge = (s) => s.centerS + s.width / 2;
32
+ const bottomEdge = (s) => s.centerY - s.height / 2;
33
+ const topEdge = (s) => s.centerY + s.height / 2;
34
+ function alongWallFeatureCoord(s, feature) {
35
+ if (feature === 'left')
36
+ return leftEdge(s);
37
+ if (feature === 'right')
38
+ return rightEdge(s);
39
+ return s.centerS;
40
+ }
41
+ function verticalFeatureCoord(s, feature) {
42
+ if (feature === 'sill')
43
+ return bottomEdge(s);
44
+ if (feature === 'top')
45
+ return topEdge(s);
46
+ return s.centerY;
47
+ }
48
+ const ALONG_WALL_FEATURES = ['left', 'center', 'right'];
49
+ const VERTICAL_FEATURES = ['sill', 'center', 'top'];
50
+ /**
51
+ * Edge-to-edge clearance from the moving opening to the nearest neighbour on
52
+ * each side, falling back to the wall ends when there is no neighbour — the
53
+ * "how much wall is left here" reading. Returns 0–2 gaps (one per side); a side
54
+ * is omitted when its clearance is below `minGap` (the opening is flush against
55
+ * or overlapping that neighbour).
56
+ */
57
+ export function computeEdgeGaps(moving, siblings, wall, minGap) {
58
+ const movingLeft = leftEdge(moving);
59
+ const movingRight = rightEdge(moving);
60
+ // A sibling that straddles one of the moving opening's edges is an OVERLAP,
61
+ // not a neighbour: there is no clearance on that side, and we must not fall
62
+ // back to the wall end (which would report a misleading distance measured
63
+ // "through" the overlapping opening).
64
+ const leftCrossed = siblings.some((s) => leftEdge(s) < movingLeft && rightEdge(s) > movingLeft);
65
+ const rightCrossed = siblings.some((s) => leftEdge(s) < movingRight && rightEdge(s) > movingRight);
66
+ let leftNeighbour = null;
67
+ let rightNeighbour = null;
68
+ for (const sib of siblings) {
69
+ const sibRight = rightEdge(sib);
70
+ const sibLeft = leftEdge(sib);
71
+ // Entirely to the left of the moving opening → candidate left neighbour.
72
+ if (sibRight <= movingLeft && (leftNeighbour === null || sibRight > leftNeighbour.s)) {
73
+ leftNeighbour = { s: sibRight, id: sib.id };
74
+ }
75
+ // Entirely to the right → candidate right neighbour.
76
+ if (sibLeft >= movingRight && (rightNeighbour === null || sibLeft < rightNeighbour.s)) {
77
+ rightNeighbour = { s: sibLeft, id: sib.id };
78
+ }
79
+ }
80
+ const gaps = [];
81
+ if (!leftCrossed) {
82
+ const leftToS = leftNeighbour ? leftNeighbour.s : 0;
83
+ const leftDistance = movingLeft - leftToS;
84
+ if (leftDistance >= minGap) {
85
+ gaps.push({
86
+ side: 'left',
87
+ distance: leftDistance,
88
+ fromS: movingLeft,
89
+ toS: leftToS,
90
+ target: leftNeighbour ? 'opening' : 'wall-start',
91
+ targetId: leftNeighbour?.id,
92
+ });
93
+ }
94
+ }
95
+ if (!rightCrossed) {
96
+ const rightToS = rightNeighbour ? rightNeighbour.s : wall.length;
97
+ const rightDistance = rightToS - movingRight;
98
+ if (rightDistance >= minGap) {
99
+ gaps.push({
100
+ side: 'right',
101
+ distance: rightDistance,
102
+ fromS: movingRight,
103
+ toS: rightToS,
104
+ target: rightNeighbour ? 'opening' : 'wall-end',
105
+ targetId: rightNeighbour?.id,
106
+ });
107
+ }
108
+ }
109
+ return gaps;
110
+ }
111
+ /**
112
+ * The closest coincidence between any of the moving opening's edges/centre and
113
+ * any sibling's edges/centre along the wall, within `tolerance`. Edge-to-edge
114
+ * and centre-to-centre are weighed equally; the single closest pair wins
115
+ * (matching the one-guide-per-axis behaviour of the floor-plane resolver).
116
+ */
117
+ export function detectAlongWallAlignment(moving, siblings, tolerance) {
118
+ let best = null;
119
+ let bestAbs = tolerance;
120
+ for (const movingFeature of ALONG_WALL_FEATURES) {
121
+ const movingCoord = alongWallFeatureCoord(moving, movingFeature);
122
+ for (const sib of siblings) {
123
+ if (sib.id === moving.id)
124
+ continue;
125
+ for (const targetFeature of ALONG_WALL_FEATURES) {
126
+ const targetCoord = alongWallFeatureCoord(sib, targetFeature);
127
+ const diff = targetCoord - movingCoord;
128
+ const abs = Math.abs(diff);
129
+ if (abs <= bestAbs && (best === null || abs < bestAbs)) {
130
+ bestAbs = abs;
131
+ best = {
132
+ s: targetCoord,
133
+ movingFeature,
134
+ targetId: sib.id,
135
+ targetFeature,
136
+ snap: diff,
137
+ };
138
+ }
139
+ }
140
+ }
141
+ }
142
+ return best;
143
+ }
144
+ /**
145
+ * The closest coincidence between the moving opening's sill/centre/top and any
146
+ * sibling's sill/centre/top, within `tolerance` — the "these two windows share
147
+ * a sill height" detector. Same single-best-match policy as the along-wall
148
+ * variant.
149
+ */
150
+ export function detectVerticalAlignment(moving, siblings, tolerance) {
151
+ let best = null;
152
+ let bestAbs = tolerance;
153
+ for (const movingFeature of VERTICAL_FEATURES) {
154
+ const movingCoord = verticalFeatureCoord(moving, movingFeature);
155
+ for (const sib of siblings) {
156
+ if (sib.id === moving.id)
157
+ continue;
158
+ for (const targetFeature of VERTICAL_FEATURES) {
159
+ const targetCoord = verticalFeatureCoord(sib, targetFeature);
160
+ const diff = targetCoord - movingCoord;
161
+ const abs = Math.abs(diff);
162
+ if (abs <= bestAbs && (best === null || abs < bestAbs)) {
163
+ bestAbs = abs;
164
+ best = {
165
+ y: targetCoord,
166
+ movingFeature,
167
+ targetId: sib.id,
168
+ targetFeature,
169
+ snap: diff,
170
+ };
171
+ }
172
+ }
173
+ }
174
+ }
175
+ return best;
176
+ }
177
+ /**
178
+ * Figma-style equal-spacing detection: order all openings along the wall, look
179
+ * at the clearances BETWEEN consecutive openings, and return the longest run of
180
+ * ≥2 consecutive gaps that are equal within `tolerance` and that the moving
181
+ * opening participates in (so the badges only appear while the drag is actually
182
+ * forming or extending a series). Returns null when no such run exists.
183
+ *
184
+ * Gaps below `minGap` (touching/overlapping openings) break a run — a row of
185
+ * flush openings is not "equally spaced".
186
+ */
187
+ export function detectEqualSpacing(allOpenings, movingId, tolerance, minGap) {
188
+ if (allOpenings.length < 3)
189
+ return null;
190
+ const sorted = [...allOpenings].sort((a, b) => a.centerS - b.centerS);
191
+ const movingIndex = sorted.findIndex((s) => s.id === movingId);
192
+ if (movingIndex < 0)
193
+ return null;
194
+ // Clearance between opening i and i+1.
195
+ const gaps = [];
196
+ for (let i = 0; i < sorted.length - 1; i++) {
197
+ const a = sorted[i];
198
+ const b = sorted[i + 1];
199
+ if (!a || !b)
200
+ continue;
201
+ const fromS = rightEdge(a);
202
+ const toS = leftEdge(b);
203
+ gaps.push({ value: toS - fromS, fromS, toS });
204
+ }
205
+ // Longest contiguous window of gaps that are (a) each ≥ minGap and (b)
206
+ // mutually equal within tolerance (window max − min ≤ tolerance), spanning at
207
+ // least 2 gaps and including the moving opening. Brute force over windows
208
+ // (openings per wall are few). A first-gap-anchored greedy scan is NOT
209
+ // equivalent: it drops a valid run that begins partway through a drifting
210
+ // sequence — e.g. gaps 1.00, 1.02, 1.04 with the moving opening at the end,
211
+ // where [1.02, 1.04] is a real run. On a length tie the leftmost window wins,
212
+ // for determinism.
213
+ let best = null;
214
+ for (let lo = 0; lo < gaps.length; lo++) {
215
+ let min = Number.POSITIVE_INFINITY;
216
+ let max = Number.NEGATIVE_INFINITY;
217
+ for (let hi = lo; hi < gaps.length; hi++) {
218
+ const gap = gaps[hi];
219
+ if (!gap || gap.value < minGap)
220
+ break; // a sub-minGap gap can't join a run
221
+ min = Math.min(min, gap.value);
222
+ max = Math.max(max, gap.value);
223
+ if (max - min > tolerance)
224
+ break; // extending only widens the spread
225
+ const gapCount = hi - lo + 1;
226
+ if (gapCount < 2)
227
+ continue;
228
+ const firstOpening = lo; // gap i sits between openings i and i+1
229
+ const lastOpening = hi + 1;
230
+ if (movingIndex < firstOpening || movingIndex > lastOpening)
231
+ continue;
232
+ if (best !== null && gapCount <= best.segments.length)
233
+ continue;
234
+ const windowGaps = gaps.slice(lo, hi + 1);
235
+ best = {
236
+ gap: windowGaps.reduce((sum, g) => sum + g.value, 0) / windowGaps.length,
237
+ segments: windowGaps.map((g) => ({ fromS: g.fromS, toS: g.toS })),
238
+ openingIds: sorted.slice(firstOpening, lastOpening + 1).map((s) => s.id),
239
+ };
240
+ }
241
+ }
242
+ return best;
243
+ }
244
+ /**
245
+ * Compute every proximity/alignment guide for the moving opening in one pass.
246
+ * Pure: feed it the moving opening's wall-local span, its same-wall siblings,
247
+ * and the wall extent; render the result in whichever view.
248
+ */
249
+ export function computeOpeningGuides(input) {
250
+ const tol = { ...DEFAULT_OPENING_GUIDE_TOLERANCES, ...input.tolerances };
251
+ const { moving, siblings, wall, includeVertical } = input;
252
+ const sillHead = includeVertical
253
+ ? {
254
+ sill: bottomEdge(moving),
255
+ bottomY: bottomEdge(moving),
256
+ head: wall.height - topEdge(moving),
257
+ topY: topEdge(moving),
258
+ }
259
+ : null;
260
+ return {
261
+ sillHead,
262
+ gaps: computeEdgeGaps(moving, siblings, wall, tol.minGap),
263
+ alongWall: detectAlongWallAlignment(moving, siblings, tol.align),
264
+ vertical: includeVertical ? detectVerticalAlignment(moving, siblings, tol.align) : null,
265
+ equalSpacing: detectEqualSpacing([moving, ...siblings], moving.id, tol.equalSpacing, tol.minGap),
266
+ };
267
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=opening-guides.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"opening-guides.test.d.ts","sourceRoot":"","sources":["../../src/services/opening-guides.test.ts"],"names":[],"mappings":""}
@@ -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"}