@nowline/layout 0.8.6 → 0.9.0

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 (338) hide show
  1. package/dist/axis-columns.d.ts +70 -0
  2. package/dist/axis-columns.d.ts.map +1 -0
  3. package/dist/axis-columns.js +104 -0
  4. package/dist/axis-columns.js.map +1 -0
  5. package/dist/calendar-resolver.d.ts +27 -0
  6. package/dist/calendar-resolver.d.ts.map +1 -0
  7. package/dist/calendar-resolver.js +132 -0
  8. package/dist/calendar-resolver.js.map +1 -0
  9. package/dist/calendar.d.ts +0 -19
  10. package/dist/calendar.d.ts.map +1 -1
  11. package/dist/calendar.js +4 -44
  12. package/dist/calendar.js.map +1 -1
  13. package/dist/canvas-width.d.ts +23 -0
  14. package/dist/canvas-width.d.ts.map +1 -0
  15. package/dist/canvas-width.js +26 -0
  16. package/dist/canvas-width.js.map +1 -0
  17. package/dist/capacity.d.ts +28 -0
  18. package/dist/capacity.d.ts.map +1 -1
  19. package/dist/capacity.js +64 -29
  20. package/dist/capacity.js.map +1 -1
  21. package/dist/chart-translation.d.ts +22 -0
  22. package/dist/chart-translation.d.ts.map +1 -0
  23. package/dist/chart-translation.js +83 -0
  24. package/dist/chart-translation.js.map +1 -0
  25. package/dist/container-header-geometry.d.ts +64 -0
  26. package/dist/container-header-geometry.d.ts.map +1 -0
  27. package/dist/container-header-geometry.js +89 -0
  28. package/dist/container-header-geometry.js.map +1 -0
  29. package/dist/dsl-utils.d.ts +3 -4
  30. package/dist/dsl-utils.d.ts.map +1 -1
  31. package/dist/dsl-utils.js +8 -25
  32. package/dist/dsl-utils.js.map +1 -1
  33. package/dist/edge-routing.d.ts +69 -78
  34. package/dist/edge-routing.d.ts.map +1 -1
  35. package/dist/edge-routing.js +271 -401
  36. package/dist/edge-routing.js.map +1 -1
  37. package/dist/frame-tab-geometry.d.ts +24 -14
  38. package/dist/frame-tab-geometry.d.ts.map +1 -1
  39. package/dist/frame-tab-geometry.js +69 -23
  40. package/dist/frame-tab-geometry.js.map +1 -1
  41. package/dist/header-card-geometry.d.ts +12 -0
  42. package/dist/header-card-geometry.d.ts.map +1 -1
  43. package/dist/header-card-geometry.js +14 -2
  44. package/dist/header-card-geometry.js.map +1 -1
  45. package/dist/header-logo.d.ts +28 -0
  46. package/dist/header-logo.d.ts.map +1 -0
  47. package/dist/header-logo.js +51 -0
  48. package/dist/header-logo.js.map +1 -0
  49. package/dist/i18n.d.ts +11 -0
  50. package/dist/i18n.d.ts.map +1 -1
  51. package/dist/i18n.js +11 -1
  52. package/dist/i18n.js.map +1 -1
  53. package/dist/index.d.ts +20 -8
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +17 -7
  56. package/dist/index.js.map +1 -1
  57. package/dist/inline-date-pin-geometry.d.ts +19 -11
  58. package/dist/inline-date-pin-geometry.d.ts.map +1 -1
  59. package/dist/inline-date-pin-geometry.js +45 -46
  60. package/dist/inline-date-pin-geometry.js.map +1 -1
  61. package/dist/item-bar-geometry.d.ts +192 -4
  62. package/dist/item-bar-geometry.d.ts.map +1 -1
  63. package/dist/item-bar-geometry.js +258 -5
  64. package/dist/item-bar-geometry.js.map +1 -1
  65. package/dist/item-port-geometry.d.ts +13 -0
  66. package/dist/item-port-geometry.d.ts.map +1 -0
  67. package/dist/item-port-geometry.js +63 -0
  68. package/dist/item-port-geometry.js.map +1 -0
  69. package/dist/layout-context.d.ts +115 -51
  70. package/dist/layout-context.d.ts.map +1 -1
  71. package/dist/layout-context.js +96 -5
  72. package/dist/layout-context.js.map +1 -1
  73. package/dist/layout-insights.d.ts.map +1 -1
  74. package/dist/layout-insights.js +73 -1
  75. package/dist/layout-insights.js.map +1 -1
  76. package/dist/layout.d.ts +35 -0
  77. package/dist/layout.d.ts.map +1 -1
  78. package/dist/layout.js +18 -1315
  79. package/dist/layout.js.map +1 -1
  80. package/dist/nodes/anchor-node.d.ts.map +1 -1
  81. package/dist/nodes/anchor-node.js +11 -4
  82. package/dist/nodes/anchor-node.js.map +1 -1
  83. package/dist/nodes/content-extent.d.ts +24 -0
  84. package/dist/nodes/content-extent.d.ts.map +1 -0
  85. package/dist/nodes/content-extent.js +100 -0
  86. package/dist/nodes/content-extent.js.map +1 -0
  87. package/dist/nodes/dependency-edges.d.ts +6 -0
  88. package/dist/nodes/dependency-edges.d.ts.map +1 -0
  89. package/dist/nodes/dependency-edges.js +148 -0
  90. package/dist/nodes/dependency-edges.js.map +1 -0
  91. package/dist/nodes/footnote-node.d.ts +24 -3
  92. package/dist/nodes/footnote-node.d.ts.map +1 -1
  93. package/dist/nodes/footnote-node.js +129 -25
  94. package/dist/nodes/footnote-node.js.map +1 -1
  95. package/dist/nodes/group-node.d.ts +10 -21
  96. package/dist/nodes/group-node.d.ts.map +1 -1
  97. package/dist/nodes/group-node.js +79 -152
  98. package/dist/nodes/group-node.js.map +1 -1
  99. package/dist/nodes/header-node.d.ts +56 -0
  100. package/dist/nodes/header-node.d.ts.map +1 -0
  101. package/dist/nodes/header-node.js +163 -0
  102. package/dist/nodes/header-node.js.map +1 -0
  103. package/dist/nodes/include-node.d.ts +22 -12
  104. package/dist/nodes/include-node.d.ts.map +1 -1
  105. package/dist/nodes/include-node.js +95 -62
  106. package/dist/nodes/include-node.js.map +1 -1
  107. package/dist/nodes/item-measure.d.ts +135 -0
  108. package/dist/nodes/item-measure.d.ts.map +1 -0
  109. package/dist/nodes/item-measure.js +514 -0
  110. package/dist/nodes/item-measure.js.map +1 -0
  111. package/dist/nodes/item-node.d.ts +162 -9
  112. package/dist/nodes/item-node.d.ts.map +1 -1
  113. package/dist/nodes/item-node.js +256 -69
  114. package/dist/nodes/item-node.js.map +1 -1
  115. package/dist/nodes/marker-band.d.ts +118 -0
  116. package/dist/nodes/marker-band.d.ts.map +1 -0
  117. package/dist/nodes/marker-band.js +303 -0
  118. package/dist/nodes/marker-band.js.map +1 -0
  119. package/dist/nodes/marker-geometry.js +1 -1
  120. package/dist/nodes/marker-geometry.js.map +1 -1
  121. package/dist/nodes/milestone-node.d.ts +112 -15
  122. package/dist/nodes/milestone-node.d.ts.map +1 -1
  123. package/dist/nodes/milestone-node.js +246 -95
  124. package/dist/nodes/milestone-node.js.map +1 -1
  125. package/dist/nodes/nowline-node.d.ts +8 -0
  126. package/dist/nodes/nowline-node.d.ts.map +1 -0
  127. package/dist/nodes/nowline-node.js +74 -0
  128. package/dist/nodes/nowline-node.js.map +1 -0
  129. package/dist/nodes/parallel-node.d.ts +7 -12
  130. package/dist/nodes/parallel-node.d.ts.map +1 -1
  131. package/dist/nodes/parallel-node.js +66 -30
  132. package/dist/nodes/parallel-node.js.map +1 -1
  133. package/dist/nodes/region-markers.d.ts +41 -0
  134. package/dist/nodes/region-markers.d.ts.map +1 -0
  135. package/dist/nodes/region-markers.js +130 -0
  136. package/dist/nodes/region-markers.js.map +1 -0
  137. package/dist/nodes/roadmap-node.d.ts +9 -72
  138. package/dist/nodes/roadmap-node.d.ts.map +1 -1
  139. package/dist/nodes/roadmap-node.js +38 -788
  140. package/dist/nodes/roadmap-node.js.map +1 -1
  141. package/dist/nodes/row-track.d.ts +33 -0
  142. package/dist/nodes/row-track.d.ts.map +1 -0
  143. package/dist/nodes/row-track.js +170 -0
  144. package/dist/nodes/row-track.js.map +1 -0
  145. package/dist/nodes/sequence-item.d.ts +25 -0
  146. package/dist/nodes/sequence-item.d.ts.map +1 -0
  147. package/dist/nodes/sequence-item.js +396 -0
  148. package/dist/nodes/sequence-item.js.map +1 -0
  149. package/dist/nodes/sequence-one.d.ts +5 -0
  150. package/dist/nodes/sequence-one.d.ts.map +1 -0
  151. package/dist/nodes/sequence-one.js +26 -0
  152. package/dist/nodes/sequence-one.js.map +1 -0
  153. package/dist/nodes/swimlane-node.d.ts +5 -17
  154. package/dist/nodes/swimlane-node.d.ts.map +1 -1
  155. package/dist/nodes/swimlane-node.js +62 -145
  156. package/dist/nodes/swimlane-node.js.map +1 -1
  157. package/dist/nodes/wave-node.d.ts +52 -0
  158. package/dist/nodes/wave-node.d.ts.map +1 -0
  159. package/dist/nodes/wave-node.js +483 -0
  160. package/dist/nodes/wave-node.js.map +1 -0
  161. package/dist/non-working-display.d.ts +14 -0
  162. package/dist/non-working-display.d.ts.map +1 -0
  163. package/dist/non-working-display.js +28 -0
  164. package/dist/non-working-display.js.map +1 -0
  165. package/dist/phases/date-window.d.ts +48 -0
  166. package/dist/phases/date-window.d.ts.map +1 -0
  167. package/dist/phases/date-window.js +130 -0
  168. package/dist/phases/date-window.js.map +1 -0
  169. package/dist/phases/finalize.d.ts +198 -0
  170. package/dist/phases/finalize.d.ts.map +1 -0
  171. package/dist/phases/finalize.js +1058 -0
  172. package/dist/phases/finalize.js.map +1 -0
  173. package/dist/phases/measure.d.ts +77 -0
  174. package/dist/phases/measure.d.ts.map +1 -0
  175. package/dist/phases/measure.js +222 -0
  176. package/dist/phases/measure.js.map +1 -0
  177. package/dist/phases/place.d.ts +28 -0
  178. package/dist/phases/place.d.ts.map +1 -0
  179. package/dist/phases/place.js +225 -0
  180. package/dist/phases/place.js.map +1 -0
  181. package/dist/renderable.d.ts.map +1 -1
  182. package/dist/renderable.js +23 -3
  183. package/dist/renderable.js.map +1 -1
  184. package/dist/resolve-today.d.ts +11 -1
  185. package/dist/resolve-today.d.ts.map +1 -1
  186. package/dist/resolve-today.js +26 -12
  187. package/dist/resolve-today.js.map +1 -1
  188. package/dist/row-packer.d.ts +42 -16
  189. package/dist/row-packer.d.ts.map +1 -1
  190. package/dist/row-packer.js +51 -78
  191. package/dist/row-packer.js.map +1 -1
  192. package/dist/schedule.d.ts +58 -1
  193. package/dist/schedule.d.ts.map +1 -1
  194. package/dist/schedule.js +280 -106
  195. package/dist/schedule.js.map +1 -1
  196. package/dist/start-rules.d.ts +27 -0
  197. package/dist/start-rules.d.ts.map +1 -0
  198. package/dist/start-rules.js +91 -0
  199. package/dist/start-rules.js.map +1 -0
  200. package/dist/start-terms.d.ts +23 -0
  201. package/dist/start-terms.d.ts.map +1 -0
  202. package/dist/start-terms.js +27 -0
  203. package/dist/start-terms.js.map +1 -0
  204. package/dist/status-dot.d.ts +46 -0
  205. package/dist/status-dot.d.ts.map +1 -0
  206. package/dist/status-dot.js +78 -0
  207. package/dist/status-dot.js.map +1 -0
  208. package/dist/style-resolution.d.ts.map +1 -1
  209. package/dist/style-resolution.js +23 -43
  210. package/dist/style-resolution.js.map +1 -1
  211. package/dist/text-measure.d.ts +28 -0
  212. package/dist/text-measure.d.ts.map +1 -0
  213. package/dist/text-measure.js +70 -0
  214. package/dist/text-measure.js.map +1 -0
  215. package/dist/themes/contrast.d.ts +4 -0
  216. package/dist/themes/contrast.d.ts.map +1 -0
  217. package/dist/themes/contrast.js +32 -0
  218. package/dist/themes/contrast.js.map +1 -0
  219. package/dist/themes/dark.d.ts.map +1 -1
  220. package/dist/themes/dark.js +28 -1
  221. package/dist/themes/dark.js.map +1 -1
  222. package/dist/themes/grayscale.d.ts.map +1 -1
  223. package/dist/themes/grayscale.js +28 -1
  224. package/dist/themes/grayscale.js.map +1 -1
  225. package/dist/themes/index.d.ts +4 -11
  226. package/dist/themes/index.d.ts.map +1 -1
  227. package/dist/themes/index.js +6 -4
  228. package/dist/themes/index.js.map +1 -1
  229. package/dist/themes/light.d.ts.map +1 -1
  230. package/dist/themes/light.js +28 -1
  231. package/dist/themes/light.js.map +1 -1
  232. package/dist/themes/shape.d.ts +31 -0
  233. package/dist/themes/shape.d.ts.map +1 -1
  234. package/dist/themes/shared.d.ts +44 -50
  235. package/dist/themes/shared.d.ts.map +1 -1
  236. package/dist/themes/shared.js +79 -83
  237. package/dist/themes/shared.js.map +1 -1
  238. package/dist/time-scale.d.ts +42 -6
  239. package/dist/time-scale.d.ts.map +1 -1
  240. package/dist/time-scale.js +109 -17
  241. package/dist/time-scale.js.map +1 -1
  242. package/dist/types.d.ts +528 -11
  243. package/dist/types.d.ts.map +1 -1
  244. package/dist/view-preset.d.ts +61 -6
  245. package/dist/view-preset.d.ts.map +1 -1
  246. package/dist/view-preset.js +439 -24
  247. package/dist/view-preset.js.map +1 -1
  248. package/dist/wave-barrier.d.ts +107 -0
  249. package/dist/wave-barrier.d.ts.map +1 -0
  250. package/dist/wave-barrier.js +174 -0
  251. package/dist/wave-barrier.js.map +1 -0
  252. package/dist/wave-hatch.d.ts +4 -0
  253. package/dist/wave-hatch.d.ts.map +1 -0
  254. package/dist/wave-hatch.js +21 -0
  255. package/dist/wave-hatch.js.map +1 -0
  256. package/dist/wave-layout.d.ts +99 -0
  257. package/dist/wave-layout.d.ts.map +1 -0
  258. package/dist/wave-layout.js +130 -0
  259. package/dist/wave-layout.js.map +1 -0
  260. package/dist/working-calendar.d.ts +128 -4
  261. package/dist/working-calendar.d.ts.map +1 -1
  262. package/dist/working-calendar.js +725 -51
  263. package/dist/working-calendar.js.map +1 -1
  264. package/dist/working-span.d.ts +11 -0
  265. package/dist/working-span.d.ts.map +1 -0
  266. package/dist/working-span.js +18 -0
  267. package/dist/working-span.js.map +1 -0
  268. package/package.json +4 -4
  269. package/src/axis-columns.ts +140 -0
  270. package/src/calendar-resolver.ts +161 -0
  271. package/src/calendar.ts +4 -45
  272. package/src/canvas-width.ts +36 -0
  273. package/src/capacity.ts +71 -30
  274. package/src/chart-translation.ts +88 -0
  275. package/src/container-header-geometry.ts +116 -0
  276. package/src/dsl-utils.ts +8 -26
  277. package/src/edge-routing.ts +347 -491
  278. package/src/frame-tab-geometry.ts +78 -25
  279. package/src/header-card-geometry.ts +17 -2
  280. package/src/header-logo.ts +67 -0
  281. package/src/i18n.ts +22 -1
  282. package/src/index.ts +97 -12
  283. package/src/inline-date-pin-geometry.ts +55 -58
  284. package/src/item-bar-geometry.ts +341 -5
  285. package/src/item-port-geometry.ts +69 -0
  286. package/src/layout-context.ts +210 -72
  287. package/src/layout-insights.ts +109 -2
  288. package/src/layout.ts +57 -1530
  289. package/src/nodes/anchor-node.ts +11 -4
  290. package/src/nodes/content-extent.ts +107 -0
  291. package/src/nodes/dependency-edges.ts +171 -0
  292. package/src/nodes/footnote-node.ts +171 -31
  293. package/src/nodes/group-node.ts +92 -214
  294. package/src/nodes/header-node.ts +255 -0
  295. package/src/nodes/include-node.ts +124 -110
  296. package/src/nodes/item-measure.ts +723 -0
  297. package/src/nodes/item-node.ts +358 -80
  298. package/src/nodes/marker-band.ts +406 -0
  299. package/src/nodes/marker-geometry.ts +1 -1
  300. package/src/nodes/milestone-node.ts +321 -136
  301. package/src/nodes/nowline-node.ts +87 -0
  302. package/src/nodes/parallel-node.ts +69 -38
  303. package/src/nodes/region-markers.ts +194 -0
  304. package/src/nodes/roadmap-node.ts +46 -950
  305. package/src/nodes/row-track.ts +217 -0
  306. package/src/nodes/sequence-item.ts +433 -0
  307. package/src/nodes/sequence-one.ts +40 -0
  308. package/src/nodes/swimlane-node.ts +66 -205
  309. package/src/nodes/wave-node.ts +609 -0
  310. package/src/non-working-display.ts +32 -0
  311. package/src/phases/date-window.ts +191 -0
  312. package/src/phases/finalize.ts +1303 -0
  313. package/src/phases/measure.ts +329 -0
  314. package/src/phases/place.ts +318 -0
  315. package/src/renderable.ts +23 -3
  316. package/src/resolve-today.ts +28 -25
  317. package/src/row-packer.ts +85 -84
  318. package/src/schedule.ts +362 -114
  319. package/src/start-rules.ts +113 -0
  320. package/src/start-terms.ts +38 -0
  321. package/src/status-dot.ts +81 -0
  322. package/src/style-resolution.ts +27 -57
  323. package/src/text-measure.ts +71 -0
  324. package/src/themes/contrast.ts +34 -0
  325. package/src/themes/dark.ts +28 -1
  326. package/src/themes/grayscale.ts +28 -1
  327. package/src/themes/index.ts +10 -7
  328. package/src/themes/light.ts +28 -1
  329. package/src/themes/shape.ts +38 -0
  330. package/src/themes/shared.ts +85 -93
  331. package/src/time-scale.ts +117 -19
  332. package/src/types.ts +529 -20
  333. package/src/view-preset.ts +520 -27
  334. package/src/wave-barrier.ts +242 -0
  335. package/src/wave-hatch.ts +25 -0
  336. package/src/wave-layout.ts +203 -0
  337. package/src/working-calendar.ts +875 -57
  338. package/src/working-span.ts +19 -0
@@ -1,435 +1,305 @@
1
- // Channel-based dependency-arrow router. Replaces the single-elbow
2
- // `routeEdge` shipped in m2g with one that:
1
+ // Date-port dependency router (specs/true-width-layout.md §3.4).
3
2
  //
4
- // - Drops the vertical leg in the cleanest inter-column gutter (item
5
- // bars are obstacles).
6
- // - Nudges the elbow X away from any visible parallel/group bracket
7
- // within `BRACKET_NUDGE_PX` so arrows breathe instead of hugging
8
- // the bracket line.
9
- // - Assigns distinct slots when multiple edges share a channel so
10
- // parallel arrows don't stack on top of one another.
11
- // - Falls back to under-bar routing (rendered behind the bars with a
12
- // thinner stroke) when no clean channel exists in the source-target
13
- // gap.
3
+ // An arrow runs on the dates. It leaves the middle of its source's last
4
+ // day through the source's bottom edge (top edge when the target is
5
+ // above), drops into a gap between rows, runs horizontally in that gap to
6
+ // the middle of the target's first day, and turns into the target's top
7
+ // edge (bottom edge when the source is below), arrowhead first. Day
8
+ // centres, never date lines: in a chain the successor starts exactly on
9
+ // its predecessor's end date, so a leg on the date line would run down the
10
+ // successor's edge.
14
11
  //
15
- // Containers are NOT treated as obstacles. Endpoints inside a parallel
16
- // or group route directly through the items-only obstacle map and use
17
- // the under-bar fallback when needed. Looping around container edges
18
- // to avoid a single intersecting bar produced unsatisfying detours
19
- // (see `specs/handoffs/handoff-channel-routing-design.md`).
20
- /** Distance (px) the elbow X must keep from any visible bracket. Picked
21
- * to leave a small visible gap between the arrow's vertical leg and
22
- * the bracket stroke without forcing the arrow far from its natural
23
- * mid-gutter line. */
24
- export const BRACKET_NUDGE_PX = 4;
25
- /** Minimum horizontal source stub (px) — distance from the source's
26
- * exit point to the vertical-leg elbow. Below this, the source-side
27
- * arrow body collapses into the bar's right edge with no visible
28
- * horizontal segment. Treated as a hard constraint when picking
29
- * the channel X for left-to-right edges. */
30
- export const MIN_SOURCE_STUB_PX = 6;
31
- /** Minimum horizontal target stub (px) — distance from the vertical-leg
32
- * elbow to the target's left edge. Below this, the arrowhead has no
33
- * horizontal lead-in: the leg appears to plunge directly into the bar
34
- * without a visible target-side stub. Treated as a hard constraint
35
- * when picking the channel X for left-to-right edges; conflicts with
36
- * obstacle / bracket clearance trigger the under-bar fallback. */
37
- export const MIN_TARGET_STUB_PX = 6;
38
- /** Spacing between slots inside one channel. With a 12 px gutter and
39
- * this spacing, three slots (-1, 0, +1) fit comfortably; more than
40
- * three sharing a channel collapses to centerline. */
41
- const SLOT_SPACING_PX = 3;
42
- /** Tolerance (px) when grouping edges by channel X for slot assignment.
43
- * Edges within this distance share a channel. */
44
- const SLOT_GROUP_TOLERANCE_PX = 1;
45
- /** Stub length leaving each endpoint before the elbow turn for
46
- * right-to-left edges. The router also inserts a quarter-arc at the
47
- * elbow corner; the renderer's `roundedOrthogonalPath` rounds the
48
- * corners. Left-to-right edges use the per-side `MIN_*_STUB_PX`
49
- * constants instead — they provide tighter, asymmetric control over
50
- * the source vs target sides. */
51
- const STUB_OUT_PX = 10;
52
- export class ChannelGrid {
53
- items;
54
- brackets;
55
- constructor(input) {
56
- this.items = input.itemBars;
57
- this.brackets = input.brackets;
58
- }
59
- /** True when a vertical line at `x` intersects any item bar within
60
- * the Y span `[yMin, yMax]`. */
61
- hasObstacle(x, yMin, yMax) {
62
- const lo = Math.min(yMin, yMax);
63
- const hi = Math.max(yMin, yMax);
64
- for (const bar of this.items) {
65
- if (x <= bar.x || x >= bar.x + bar.width)
66
- continue;
67
- if (bar.y + bar.height <= lo || bar.y >= hi)
68
- continue;
69
- return true;
70
- }
71
- return false;
72
- }
73
- /** Visible brackets within `radius` px of `x` whose Y span overlaps
74
- * `[yMin, yMax]`. The clearance nudge uses these to shift the
75
- * elbow X away from the bracket. */
76
- bracketsNear(x, yMin, yMax, radius) {
77
- const lo = Math.min(yMin, yMax);
78
- const hi = Math.max(yMin, yMax);
79
- const out = [];
80
- for (const b of this.brackets) {
81
- if (Math.abs(b.x - x) > radius)
82
- continue;
83
- if (b.yBottom <= lo || b.yTop >= hi)
84
- continue;
85
- out.push(b);
86
- }
87
- return out;
88
- }
12
+ // Legs pass behind bars and cross them at right angles. The horizontal leg
13
+ // sits in a gap no bar, spilled caption or container header reaches into
14
+ // (between the source and the target, over the leg's x-range); among the
15
+ // gaps, the router takes the one whose vertical legs keep clear of every
16
+ // bar edge, then the one whose vertical legs cross the fewest bars, then
17
+ // the one nearest the source. Arrows sharing a gap take offset lanes
18
+ // `CHANNEL_LANE_PX` apart; arrows landing in the same day column share one
19
+ // lane, so they merge into one trunk over their target (§8 O1).
20
+ //
21
+ // Two shapes need no gap: a target on its source's row (a dotted leg at
22
+ // the row's midline, or no arrow when it starts where the source ends),
23
+ // and a marker source (`dependency-edges.ts`). Everything here is pure
24
+ // geometry on numbers the caller reads off the final model.
25
+ /** Spacing between the lanes of one gap (§3.4 "Channels"). */
26
+ export const CHANNEL_LANE_PX = 2.5;
27
+ /** A vertical leg keeps more than this from a bar's left or right edge over its height (§6). */
28
+ export const EDGE_CLEARANCE_PX = 1;
29
+ /** Full arrowhead width (and the length along the leg is `ARROW_HEAD_LENGTH_PX`). */
30
+ export const ARROW_HEAD_WIDTH_PX = 7;
31
+ /** Length of a full arrowhead along its leg. */
32
+ export const ARROW_HEAD_LENGTH_PX = 5;
33
+ /** The narrowest arrowhead; a target narrower than this gets a dot of this diameter (§8 O6). */
34
+ export const ARROW_HEAD_MIN_PX = 3;
35
+ /** Two x's (or y's) closer than this are the same date line. */
36
+ const SAME_PX = 0.01;
37
+ /** A gap must be at least this tall to carry a leg. */
38
+ const MIN_GAP_PX = 2;
39
+ /** Clearance kept between a horizontal leg and anything it must not touch. */
40
+ const BLOCK_MARGIN_PX = 1;
41
+ /** Where an arrow leaves `end`: the middle of its last day, or its middle when shorter than a day. */
42
+ export function sourcePortX(end, pixelsPerDay) {
43
+ return end.endX - Math.min(pixelsPerDay / 2, (end.endX - end.startX) / 2);
89
44
  }
90
- /**
91
- * Walk a positioned swimlane tree and collect every painted item bar
92
- * (visual edges) AND every visible bracket stroke. Output feeds
93
- * `ChannelGrid` so the router knows what to avoid.
94
- *
95
- * Swimlanes contained in include regions count too — items inside an
96
- * isolated region share the parent timeline so cross-region arrows
97
- * still need the obstacle/bracket data to route cleanly.
98
- */
99
- export function collectRoutingObstacles(swimlanes, includes) {
100
- const itemBars = [];
101
- const brackets = [];
102
- const visitChild = (child) => {
103
- if (child.kind === 'item') {
104
- itemBars.push(child.box);
105
- return;
106
- }
107
- if (child.kind === 'parallel') {
108
- // Parallel `bracket:solid|dashed` paints `[ ]` strokes at
109
- // the box's left/right edges with 12 px vertical padding
110
- // (matches `renderParallel`). The bracket has THREE
111
- // strokes per side — a vertical bar at the box edge, plus
112
- // top/bottom horizontal "feet" extending 4 px inward
113
- // toward the parallel's center. We model the verticals as
114
- // full-height bracket lines AND emit thin-Y-band entries
115
- // at the foot tips so the clearance nudge sees the foot
116
- // extent (otherwise an elbow nudged just past the
117
- // vertical at +4 px would land squarely on the inward
118
- // foot tip — see issue #2 in the channel-routing handoff).
119
- if (child.style.bracket === 'solid' || child.style.bracket === 'dashed') {
120
- const padding = 12;
121
- const stub = 4;
122
- const yTop = child.box.y - padding;
123
- const yBottom = child.box.y + child.box.height + padding;
124
- const lx = child.box.x;
125
- const rx = child.box.x + child.box.width;
126
- brackets.push({ x: lx, yTop, yBottom });
127
- brackets.push({ x: rx, yTop, yBottom });
128
- // Foot tips: tiny Y bands centered on each foot row
129
- // so the entries only fire when the elbow's Y span
130
- // actually crosses the foot line.
131
- brackets.push({ x: lx + stub, yTop: yTop - 1, yBottom: yTop + 1 });
132
- brackets.push({ x: lx + stub, yTop: yBottom - 1, yBottom: yBottom + 1 });
133
- brackets.push({ x: rx - stub, yTop: yTop - 1, yBottom: yTop + 1 });
134
- brackets.push({ x: rx - stub, yTop: yBottom - 1, yBottom: yBottom + 1 });
135
- }
136
- for (const sub of child.children)
137
- visitChild(sub);
138
- return;
139
- }
140
- if (child.kind === 'group') {
141
- // Bracket-style groups (no fill) paint a left-side `[`
142
- // glyph along `box.x`. Filled-style groups have no bracket
143
- // — the chiclet is the visual instead.
144
- const isFilled = child.style.bg !== 'none' && child.style.bg !== '#ffffff';
145
- if (!isFilled && child.style.bracket && child.style.bracket !== 'none') {
146
- brackets.push({
147
- x: child.box.x,
148
- yTop: child.box.y,
149
- yBottom: child.box.y + child.box.height,
150
- });
151
- }
152
- for (const sub of child.children)
153
- visitChild(sub);
154
- return;
155
- }
156
- };
157
- const visitSwimlane = (lane) => {
158
- for (const child of lane.children)
159
- visitChild(child);
160
- for (const nested of lane.nested)
161
- visitSwimlane(nested);
162
- };
163
- for (const lane of swimlanes)
164
- visitSwimlane(lane);
165
- for (const region of includes) {
166
- for (const lane of region.nestedSwimlanes)
167
- visitSwimlane(lane);
168
- }
169
- return { itemBars, brackets };
45
+ /** Where an arrow lands on `end`: the middle of its first day, or its middle when shorter than a day. */
46
+ export function targetPortX(end, pixelsPerDay) {
47
+ return end.startX + Math.min(pixelsPerDay / 2, (end.endX - end.startX) / 2);
48
+ }
49
+ /** Arrowhead for a leg landing across a target `width` wide (§8 O6). */
50
+ export function arrowHead(width) {
51
+ if (width < ARROW_HEAD_MIN_PX)
52
+ return { headWidth: ARROW_HEAD_MIN_PX, head: 'dot' };
53
+ return { headWidth: Math.min(ARROW_HEAD_WIDTH_PX, width), head: 'arrow' };
170
54
  }
55
+ const within = (y, lo, hi) => y >= lo - SAME_PX && y <= hi + SAME_PX;
171
56
  /**
172
- * Pick a channel X for one edge (no slot offset yet). Returns the
173
- * provisional X, an `underBar` flag, and the satisfiable stub range
174
- * so the bracket-clearance nudge can stay inside it. Same-row edges
175
- * (`from.y ≈ to.y`) get a degenerate "channel" at midX so the
176
- * renderer emits a straight line.
57
+ * True when the target sits on its source's row: each one's row midline
58
+ * crosses the other (a container source, which has no row, only needs the
59
+ * target's midline to cross it).
177
60
  */
178
- function pickChannelX(req, grid) {
179
- const { from, to } = req;
180
- if (Math.abs(from.y - to.y) < 0.5) {
181
- return { x: (from.x + to.x) / 2, underBar: false, range: null };
61
+ function onSameRow(s, t) {
62
+ if (t.rowY === undefined || !within(t.rowY, s.top, s.bottom))
63
+ return false;
64
+ return s.rowY === undefined || within(s.rowY, t.top, t.bottom);
65
+ }
66
+ /** The free stretches of `(lo, hi)` no block over `[xLo, xHi]` reaches into. */
67
+ function freeGaps(blocks, xLo, xHi, lo, hi) {
68
+ const covered = [];
69
+ for (const b of blocks) {
70
+ if (b.x >= xHi + BLOCK_MARGIN_PX || b.x + b.width <= xLo - BLOCK_MARGIN_PX)
71
+ continue;
72
+ const top = b.y - BLOCK_MARGIN_PX;
73
+ const bottom = b.y + b.height + BLOCK_MARGIN_PX;
74
+ if (bottom <= lo || top >= hi)
75
+ continue;
76
+ covered.push([Math.max(lo, top), Math.min(hi, bottom)]);
182
77
  }
183
- const yMin = Math.min(from.y, to.y);
184
- const yMax = Math.max(from.y, to.y);
185
- if (from.x <= to.x) {
186
- // Left-to-right edge. The elbow X must satisfy both the
187
- // min-source-stub and min-target-stub constraints — when the
188
- // gutter is wider than the sum of the stubs, that's a band;
189
- // when it's tighter the band collapses or inverts and the
190
- // arrow drops to under-bar at the target-stub line.
191
- const minX = from.x + MIN_SOURCE_STUB_PX;
192
- const maxX = to.x - MIN_TARGET_STUB_PX;
193
- if (minX > maxX) {
194
- // Gutter narrower than the combined min stubs — give up
195
- // and honor the target-stub line so the arrowhead still
196
- // has its visible lead-in. Source stub absorbs the loss.
197
- return {
198
- x: to.x - MIN_TARGET_STUB_PX,
199
- underBar: true,
200
- range: null,
201
- };
202
- }
203
- const range = { minX, maxX };
204
- // Try the natural gutter midpoint, clamped into the range.
205
- const naturalMid = (from.x + to.x) / 2;
206
- const mid = Math.max(minX, Math.min(maxX, naturalMid));
207
- if (!grid.hasObstacle(mid, yMin, yMax)) {
208
- return { x: mid, underBar: false, range };
209
- }
210
- // Walk inside the satisfiable range looking for a clear strip.
211
- const span = maxX - minX;
212
- for (let step = 1; step <= span; step++) {
213
- const left = mid - step;
214
- if (left >= minX && !grid.hasObstacle(left, yMin, yMax)) {
215
- return { x: left, underBar: false, range };
216
- }
217
- const right = mid + step;
218
- if (right <= maxX && !grid.hasObstacle(right, yMin, yMax)) {
219
- return { x: right, underBar: false, range };
220
- }
221
- }
222
- return { x: mid, underBar: true, range };
78
+ covered.sort((a, b) => a[0] - b[0]);
79
+ const gaps = [];
80
+ let y = lo;
81
+ for (const [top, bottom] of covered) {
82
+ if (top - y >= MIN_GAP_PX)
83
+ gaps.push([y, top]);
84
+ y = Math.max(y, bottom);
223
85
  }
224
- // Right-to-left edge: source ends past target's left edge. Try a
225
- // channel just right of source first; then walk leftward across
226
- // the source/target span looking for any clear vertical strip.
227
- const probes = [from.x + STUB_OUT_PX, to.x - STUB_OUT_PX];
228
- for (const probe of probes) {
229
- if (probe > 0 && !grid.hasObstacle(probe, yMin, yMax)) {
230
- return { x: probe, underBar: false, range: null };
231
- }
86
+ if (hi - y >= MIN_GAP_PX)
87
+ gaps.push([y, hi]);
88
+ return gaps;
89
+ }
90
+ /** Bar or container edges a vertical leg at `x` over `(y0, y1)` runs within the clearance of. */
91
+ function grazes(edges, x, y0, y1) {
92
+ const lo = Math.min(y0, y1);
93
+ const hi = Math.max(y0, y1);
94
+ let n = 0;
95
+ for (const e of edges) {
96
+ if (Math.abs(e.x - x) > EDGE_CLEARANCE_PX)
97
+ continue;
98
+ if (e.bottom <= lo || e.top >= hi)
99
+ continue;
100
+ n++;
101
+ }
102
+ return n;
103
+ }
104
+ /** Blocks a vertical leg at `x` over `(y0, y1)` passes behind. */
105
+ function crossings(blocks, x, y0, y1) {
106
+ const lo = Math.min(y0, y1);
107
+ const hi = Math.max(y0, y1);
108
+ let n = 0;
109
+ for (const b of blocks) {
110
+ if (b.height <= 0 || x <= b.x || x >= b.x + b.width)
111
+ continue;
112
+ if (b.y + b.height <= lo || b.y >= hi)
113
+ continue;
114
+ n++;
232
115
  }
233
- // Fallback to under-bar at the source-side stub.
234
- return { x: from.x + STUB_OUT_PX, underBar: true, range: null };
116
+ return n;
235
117
  }
236
118
  /**
237
- * Apply the bracket-clearance nudge to a chosen channel X. Shifts `x`
238
- * away from the nearest visible bracket within `BRACKET_NUDGE_PX`,
239
- * preferring the direction that stays inside the satisfiable stub
240
- * range. When neither direction fits — or the candidate position is
241
- * still within nudge distance of another bracket — returns the input
242
- * X clamped to the range and signals `forceUnderBar` so the leg paints
243
- * BEHIND the bars and bracket strokes (z-order: bracket renders after
244
- * under-bar edges, so the bracket cleanly covers the colliding
245
- * portion).
119
+ * Pick the gap for an arrow from `from` (on the source's edge) to `to` (on
120
+ * the target's edge): clear of every bar edge first, then fewest bars
121
+ * crossed, then nearest the source. With no free gap at all, the leg runs
122
+ * just past the source and passes behind whatever is there.
246
123
  */
247
- function nudgeAwayFromBrackets(x, range, req, grid) {
248
- const yMin = Math.min(req.from.y, req.to.y);
249
- const yMax = Math.max(req.from.y, req.to.y);
250
- const near = grid.bracketsNear(x, yMin, yMax, BRACKET_NUDGE_PX);
251
- if (near.length === 0)
252
- return { x, forceUnderBar: false };
253
- // Find the closest bracket — that's the one driving the nudge.
254
- let closest = near[0];
255
- let closestDist = Math.abs(near[0].x - x);
256
- for (let i = 1; i < near.length; i++) {
257
- const d = Math.abs(near[i].x - x);
258
- if (d < closestDist) {
259
- closest = near[i];
260
- closestDist = d;
261
- }
124
+ function pickGap(from, to, dir, obstacles) {
125
+ const xLo = Math.min(from.x, to.x);
126
+ const xHi = Math.max(from.x, to.x);
127
+ const lo = Math.min(from.y, to.y);
128
+ const hi = Math.max(from.y, to.y);
129
+ let best = null;
130
+ for (const gap of freeGaps(obstacles.blocks, xLo, xHi, lo, hi)) {
131
+ const centre = (gap[0] + gap[1]) / 2;
132
+ const key = [
133
+ grazes(obstacles.edges, from.x, from.y, centre) +
134
+ grazes(obstacles.edges, to.x, centre, to.y),
135
+ crossings(obstacles.blocks, from.x, from.y, centre) +
136
+ crossings(obstacles.blocks, to.x, centre, to.y),
137
+ Math.abs(centre - from.y),
138
+ ];
139
+ if (!best || lessThan(key, best.key))
140
+ best = { gap, centre, key };
262
141
  }
263
- // Try both sides of the closest bracket. Accept the first
264
- // candidate that stays within the satisfiable range AND clears
265
- // every other nearby bracket. The recheck radius is one epsilon
266
- // tighter than `BRACKET_NUDGE_PX` so the closest bracket itself
267
- // (now sitting at exactly the nudge distance) doesn't falsely
268
- // trip the rejection — and so a SECOND bracket exactly at the
269
- // nudge distance is also accepted as "just clear enough".
270
- const candidates = [closest.x + BRACKET_NUDGE_PX, closest.x - BRACKET_NUDGE_PX];
271
- const recheckRadius = BRACKET_NUDGE_PX - 0.01;
272
- for (const candidate of candidates) {
273
- if (range && (candidate < range.minX || candidate > range.maxX))
274
- continue;
275
- // Clamp right-to-left candidates to the natural gap so they
276
- // don't escape past either endpoint (range is null on that path).
277
- const clamped = range
278
- ? candidate
279
- : Math.max(Math.min(req.from.x, req.to.x) + 1, Math.min(Math.max(req.from.x, req.to.x) - 1, candidate));
280
- const stillNear = grid.bracketsNear(clamped, yMin, yMax, recheckRadius);
281
- if (stillNear.length === 0) {
282
- return { x: clamped, forceUnderBar: false };
283
- }
142
+ if (best)
143
+ return { gap: best.gap, centre: best.centre };
144
+ const reach = Math.min(4, (hi - lo) / 2);
145
+ const centre = from.y + dir * reach;
146
+ return { gap: [lo, hi], centre };
147
+ }
148
+ function lessThan(a, b) {
149
+ for (let i = 0; i < a.length; i++) {
150
+ if (a[i] !== b[i])
151
+ return a[i] < b[i];
152
+ }
153
+ return false;
154
+ }
155
+ /** Lane offsets to try, the first one toward the source (so the arrowhead keeps its leg). */
156
+ function laneOffsets(dir, halfHeight) {
157
+ const out = [0];
158
+ for (let k = 1; k * CHANNEL_LANE_PX <= halfHeight + SAME_PX; k++) {
159
+ out.push(-dir * k * CHANNEL_LANE_PX, dir * k * CHANNEL_LANE_PX);
284
160
  }
285
- // No nudge fits. Keep the channel at its current X (clamped into
286
- // the range) and force under-bar so item bars + bracket strokes
287
- // mask the colliding portion of the leg.
288
- const fallback = range ? Math.min(Math.max(x, range.minX), range.maxX) : x;
289
- return { x: fallback, forceUnderBar: true };
161
+ return out;
290
162
  }
291
163
  /**
292
- * Greedy interval coloring per channel. Edges sharing a channel
293
- * (within `SLOT_GROUP_TOLERANCE_PX`) get distinct slot indices;
294
- * overlapping Y spans land on different slots. Slot 0 is the
295
- * centerline; ±1 sit `SLOT_SPACING_PX` to either side; further slots
296
- * collapse back to the centerline (rare; visual stacking accepted).
164
+ * Give each gap-routed arrow its lane: an arrow whose day column already
165
+ * has a lane in this gap joins it; otherwise the first offset no other
166
+ * horizontal leg over the same x's sits on, or, when the gap is full, the
167
+ * least crowded one.
297
168
  */
298
- function assignSlots(edges) {
299
- // Group by channel X (sort first so close-X edges collapse).
300
- const byX = [...edges].sort((a, b) => {
301
- if (Math.abs(a.channelX - b.channelX) >= SLOT_GROUP_TOLERANCE_PX) {
302
- return a.channelX - b.channelX;
303
- }
304
- // Within the same channel, deterministic by orderIndex.
305
- return a.orderIndex - b.orderIndex;
306
- });
307
- const slotsByOrderIndex = new Map();
308
- let groupStart = 0;
309
- while (groupStart < byX.length) {
310
- let groupEnd = groupStart + 1;
311
- while (groupEnd < byX.length &&
312
- Math.abs(byX[groupEnd].channelX - byX[groupStart].channelX) < SLOT_GROUP_TOLERANCE_PX) {
313
- groupEnd++;
169
+ function assignLanes(pending) {
170
+ const placed = [];
171
+ const out = new Map();
172
+ const order = [...pending].sort((a, b) => a.centre - b.centre || a.xLo - b.xLo || a.index - b.index);
173
+ for (const p of order) {
174
+ const [lo, hi] = p.gap;
175
+ const mate = placed.find((q) => q.bundle === p.bundle && q.y >= lo - SAME_PX && q.y <= hi + SAME_PX);
176
+ let y = p.centre;
177
+ if (mate) {
178
+ y = mate.y;
314
179
  }
315
- const group = byX.slice(groupStart, groupEnd);
316
- // Within the group, greedy color by Y span. Sort by min Y then
317
- // (deterministically) by orderIndex.
318
- const sortedGroup = [...group].sort((a, b) => {
319
- const aMin = Math.min(a.req.from.y, a.req.to.y);
320
- const bMin = Math.min(b.req.from.y, b.req.to.y);
321
- if (Math.abs(aMin - bMin) > 0.5)
322
- return aMin - bMin;
323
- return a.orderIndex - b.orderIndex;
324
- });
325
- // Track per-slot maxY assigned so far.
326
- const slotMaxY = [];
327
- for (const entry of sortedGroup) {
328
- const eMin = Math.min(entry.req.from.y, entry.req.to.y);
329
- const eMax = Math.max(entry.req.from.y, entry.req.to.y);
330
- let assigned = -1;
331
- for (let s = 0; s < slotMaxY.length; s++) {
332
- if (slotMaxY[s] <= eMin) {
333
- assigned = s;
334
- slotMaxY[s] = eMax;
335
- break;
180
+ else {
181
+ // The gap already keeps `BLOCK_MARGIN_PX` from its neighbours.
182
+ const offsets = laneOffsets(p.dir, (hi - lo) / 2);
183
+ let bestConflicts = Number.POSITIVE_INFINITY;
184
+ for (const off of offsets) {
185
+ const cy = p.centre + off;
186
+ const conflicts = placed.filter((q) => q.bundle !== p.bundle &&
187
+ Math.abs(q.y - cy) < CHANNEL_LANE_PX - SAME_PX &&
188
+ q.xLo < p.xHi + BLOCK_MARGIN_PX &&
189
+ q.xHi > p.xLo - BLOCK_MARGIN_PX).length;
190
+ if (conflicts < bestConflicts) {
191
+ bestConflicts = conflicts;
192
+ y = cy;
336
193
  }
194
+ if (conflicts === 0)
195
+ break;
337
196
  }
338
- if (assigned === -1) {
339
- assigned = slotMaxY.length;
340
- slotMaxY.push(eMax);
341
- }
342
- slotsByOrderIndex.set(entry.orderIndex, assigned);
343
197
  }
344
- groupStart = groupEnd;
198
+ placed.push({ y, xLo: p.xLo, xHi: p.xHi, bundle: p.bundle });
199
+ out.set(p.index, y);
345
200
  }
346
- return slotsByOrderIndex;
347
- }
348
- /** Convert a slot index to its signed offset around the channel
349
- * centerline. Slots 0/1/2/... become 0, +SLOT, -SLOT, +2*SLOT,
350
- * -2*SLOT... so the FIRST edge sits on the centerline (no shift)
351
- * and additional edges fan out alternately. Past 2 ± slots, slots
352
- * collapse back to the centerline (visual stacking accepted). */
353
- function slotOffset(slot) {
354
- if (slot === 0)
355
- return 0;
356
- if (slot > 4)
357
- return 0; // collapse beyond ±2 slots
358
- const half = Math.ceil(slot / 2);
359
- const sign = slot % 2 === 1 ? 1 : -1;
360
- return sign * half * SLOT_SPACING_PX;
201
+ return out;
361
202
  }
362
203
  /**
363
- * Marker → item stub. Source X sits on the marker's vertical cut
364
- * line; the stub is a short horizontal segment to the target's left
365
- * visual edge at the target's row mid-Y. No channel selection, no
366
- * obstacle check — the cut line is the stem.
204
+ * Route every item dependency of one chart (the host, or one isolated
205
+ * region) in a batch, so arrows sharing a gap can take lanes. Returns one
206
+ * entry per request; `null` where position already says it (a target on
207
+ * its source's row that starts where the source ends).
367
208
  */
368
- function routeMarkerStub(req) {
369
- // When the cut line sits AT or PAST the target's left edge (item
370
- // hugs the anchor's date column), nudge the source 1 px back so
371
- // routeEdge produces a non-degenerate path with a visible
372
- // arrowhead.
373
- const from = req.from.x >= req.to.x ? { x: req.to.x - 1, y: req.from.y } : req.from;
374
- return {
375
- fromId: req.fromId,
376
- toId: req.toId,
377
- waypoints: [from, req.to],
378
- underBar: false,
379
- };
380
- }
381
- /**
382
- * Route every dependency edge in one batch so slot assignment can
383
- * coordinate across edges that share a channel. Marker → item edges
384
- * route as direct stubs and bypass the channel router.
385
- */
386
- export function routeChannelEdges(requests, grid) {
387
- const results = new Array(requests.length);
388
- const channelEdges = [];
389
- for (let i = 0; i < requests.length; i++) {
390
- const req = requests[i];
391
- if (req.isMarkerSource) {
392
- results[i] = routeMarkerStub(req);
393
- continue;
209
+ export function routeDependencies(requests, obstacles, pixelsPerDay) {
210
+ const out = new Array(requests.length).fill(null);
211
+ const pending = [];
212
+ requests.forEach((req, index) => {
213
+ const { source: s, target: t } = req;
214
+ const slack = t.startX - s.endX > SAME_PX;
215
+ if (onSameRow(s, t) && t.startX >= s.endX - SAME_PX) {
216
+ if (!slack)
217
+ return;
218
+ // A dotted leg along the row, from the source's end date to the
219
+ // target's painted start.
220
+ const y = s.exitY !== undefined && within(s.exitY, t.top, t.bottom) ? s.exitY : (t.rowY ?? 0);
221
+ out[index] = {
222
+ fromId: req.fromId,
223
+ toId: req.toId,
224
+ waypoints: [
225
+ { x: s.endX, y },
226
+ { x: t.paintedX, y },
227
+ ],
228
+ slack: true,
229
+ ...arrowHead(t.bottom - t.top),
230
+ };
231
+ return;
232
+ }
233
+ const px = sourcePortX(s, pixelsPerDay);
234
+ const tx = targetPortX(t, pixelsPerDay);
235
+ let from;
236
+ let to;
237
+ let dir;
238
+ if (t.top >= s.bottom - SAME_PX) {
239
+ dir = 1;
240
+ from = { x: px, y: s.bottom };
241
+ to = { x: tx, y: t.top };
394
242
  }
395
- // Same-row contiguous chains drop entirely. The caller skips
396
- // them before passing in, but defend against direct calls.
397
- if (Math.abs(req.from.y - req.to.y) < 0.5 && req.to.x - req.from.x < 20) {
398
- results[i] = {
243
+ else if (t.bottom <= s.top + SAME_PX) {
244
+ dir = -1;
245
+ from = { x: px, y: s.top };
246
+ to = { x: tx, y: t.bottom };
247
+ }
248
+ else {
249
+ // Overlapping but not on one row (a target that starts before
250
+ // its source ends): leave the source's bottom, run below both,
251
+ // come up into the target's bottom.
252
+ const below = Math.max(s.bottom, t.bottom) + 4;
253
+ out[index] = {
399
254
  fromId: req.fromId,
400
255
  toId: req.toId,
401
- waypoints: [req.from, req.to],
402
- underBar: false,
256
+ waypoints: [
257
+ { x: px, y: s.bottom },
258
+ { x: px, y: below },
259
+ { x: tx, y: below },
260
+ { x: tx, y: t.bottom },
261
+ ],
262
+ slack,
263
+ ...arrowHead(t.paintedWidth),
403
264
  };
404
- continue;
265
+ return;
405
266
  }
406
- const { x: provisionalX, underBar: provisionalUnderBar, range } = pickChannelX(req, grid);
407
- const { x: channelX, forceUnderBar } = nudgeAwayFromBrackets(provisionalX, range, req, grid);
408
- const underBar = provisionalUnderBar || forceUnderBar;
409
- channelEdges.push({ req, channelX, underBar, orderIndex: i });
410
- }
411
- const slots = assignSlots(channelEdges);
412
- for (const entry of channelEdges) {
413
- const offset = slotOffset(slots.get(entry.orderIndex) ?? 0);
414
- const slotX = entry.channelX + offset;
415
- results[entry.orderIndex] = {
416
- fromId: entry.req.fromId,
417
- toId: entry.req.toId,
418
- waypoints: buildOrthogonalPath(entry.req.from, entry.req.to, slotX),
419
- underBar: entry.underBar,
267
+ if (Math.abs(px - tx) < SAME_PX) {
268
+ out[index] = {
269
+ fromId: req.fromId,
270
+ toId: req.toId,
271
+ waypoints: [from, to],
272
+ slack,
273
+ ...arrowHead(t.paintedWidth),
274
+ };
275
+ return;
276
+ }
277
+ const { gap, centre } = pickGap(from, to, dir, obstacles);
278
+ pending.push({
279
+ index,
280
+ req,
281
+ from,
282
+ to,
283
+ dir,
284
+ gap,
285
+ centre,
286
+ xLo: Math.min(px, tx),
287
+ xHi: Math.max(px, tx),
288
+ bundle: `${Math.round(tx * 100)}:${dir}`,
289
+ });
290
+ });
291
+ const lanes = assignLanes(pending);
292
+ for (const p of pending) {
293
+ const y = lanes.get(p.index) ?? p.centre;
294
+ const slack = p.req.target.startX - p.req.source.endX > SAME_PX;
295
+ out[p.index] = {
296
+ fromId: p.req.fromId,
297
+ toId: p.req.toId,
298
+ waypoints: [p.from, { x: p.from.x, y }, { x: p.to.x, y }, p.to],
299
+ slack,
300
+ ...arrowHead(p.req.target.paintedWidth),
420
301
  };
421
302
  }
422
- return results;
423
- }
424
- /**
425
- * Build the orthogonal polyline through a chosen vertical channel.
426
- * Source-out-stub → vertical leg → target-in-stub. Same-row edges
427
- * collapse to two points (handled by the caller's same-row skip).
428
- */
429
- function buildOrthogonalPath(from, to, slotX) {
430
- if (Math.abs(from.y - to.y) < 0.5) {
431
- return [from, to];
432
- }
433
- return [from, { x: slotX, y: from.y }, { x: slotX, y: to.y }, to];
303
+ return out;
434
304
  }
435
305
  //# sourceMappingURL=edge-routing.js.map