@nowline/layout 0.8.6 → 0.10.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
@@ -0,0 +1,329 @@
1
+ // Phase 2, measure (specs/layout-phases.md §3.3): everything the chart is
2
+ // laid out around, sized before any lane is placed. The header (its card
3
+ // fixes the chart's left edge and top), pixels per day and the natural
4
+ // time scale, the header rows above the chart (now-pill row, tick panel,
5
+ // wave strip, marker band) and the timeline of the natural window.
6
+ //
7
+ // The marker band is an estimate: floating milestones get their x only
8
+ // from placement, and an extended axis can add calendar runs, so finalize
9
+ // packs every marker again (f5) and grows the band when it needs more
10
+ // rows (f7). Nothing here reads placement state.
11
+
12
+ import type { NowlineFile, ResolveResult } from '@nowline/core';
13
+ import { cappedPixelsPerDay } from '../canvas-width.js';
14
+ import { propValue } from '../dsl-utils.js';
15
+ import { resolveHeaderLogo } from '../header-logo.js';
16
+ import type { LayoutOptions } from '../layout.js';
17
+ import { HeaderNode, type MeasuredHeader } from '../nodes/header-node.js';
18
+ import {
19
+ calendarMarkerEntries,
20
+ type DatePinnedMarker,
21
+ datePinnedMarkerEntries,
22
+ markerLabelsRightX,
23
+ markerRowPlacements,
24
+ packMarkerRow,
25
+ } from '../nodes/marker-band.js';
26
+ import { MARKER_ROW_CENTER_OFFSET_PX, MARKER_ROW_PITCH_PX } from '../nodes/marker-geometry.js';
27
+ import { hasVisibleWaves } from '../schedule.js';
28
+ import { resolveStyle } from '../style-resolution.js';
29
+ import { estimateTextWidth } from '../text-measure.js';
30
+ import {
31
+ GUTTER_PX,
32
+ HEADER_HEIGHT_PX,
33
+ NOW_PILL_HEIGHT_PX,
34
+ WAVE_STRIP_HEIGHT_PX,
35
+ } from '../themes/shared.js';
36
+ import { TimeScale } from '../time-scale.js';
37
+ import type { MarkerRowPlacement, PositionedTimelineScale } from '../types.js';
38
+ import {
39
+ buildHeaderTicks,
40
+ buildNonWorkingRuns,
41
+ buildWorkingExceptions,
42
+ type ViewPreset,
43
+ } from '../view-preset.js';
44
+ import type { DateWindow, RoadmapScope } from './date-window.js';
45
+
46
+ /** Gap (px) between the header rows and the chart top. */
47
+ const HEADER_ROWS_TO_CHART_GAP_PX = 8;
48
+
49
+ export interface MeasuredRoadmap {
50
+ header: MeasuredHeader;
51
+ /** The node that measured the header; finalize places it (f15). */
52
+ headerNode: HeaderNode;
53
+ /** The natural axis. f2 keeps `originX` and `ppd` when it extends the scale. */
54
+ axis: { originX: number; ppd: number; naturalWidth: number; scale: TimeScale };
55
+ /** The canvas right edge before anything grows it: the natural axis plus a gutter. */
56
+ naturalChartRightX: number;
57
+ bands: HeaderBands;
58
+ /** Ticks and calendar runs for the natural window; f2 refreshes them. */
59
+ timeline: PositionedTimelineScale;
60
+ markers: MarkerEstimate;
61
+ }
62
+
63
+ /** The rows above the chart, top to bottom, and where the chart starts. */
64
+ export interface HeaderBands {
65
+ timelineY: number;
66
+ /**
67
+ * The now-pill row, reserved here when `today` falls inside the natural
68
+ * window. When the axis extension (f2) brings `today` in, f7 reserves it.
69
+ */
70
+ pillRowHeight: number;
71
+ tickPanelY: number;
72
+ tickPanelHeight: number;
73
+ waveStripY: number;
74
+ markerRowY: number;
75
+ /** Rows the pre-pack needed. f7 grows the band when the final pack needs more. */
76
+ estimatedMarkerRows: number;
77
+ /** Height of the rows above: pill row, tick panel, wave strip, estimated marker band. */
78
+ rowsHeight: number;
79
+ chartTopY: number;
80
+ /** Height of the mirrored bottom tick panel (f8); 0 when there is none. */
81
+ bottomTickPanelHeight: number;
82
+ }
83
+
84
+ export interface MarkerEstimate {
85
+ /** Date-pinned anchors and milestones with their x; they seed the entity edges. */
86
+ datePinned: ReadonlyArray<DatePinnedMarker>;
87
+ /** Pre-pack placements, already at the band's y. */
88
+ placements: ReadonlyMap<string, MarkerRowPlacement>;
89
+ /**
90
+ * Whether the band is drawn at all: the roadmap declares an anchor or a
91
+ * milestone, or the natural window holds a titled run or a working
92
+ * exception.
93
+ */
94
+ hasEntities: boolean;
95
+ }
96
+
97
+ export function measureRoadmap(
98
+ file: NowlineFile,
99
+ resolved: ResolveResult,
100
+ options: LayoutOptions,
101
+ scope: RoadmapScope,
102
+ window: DateWindow,
103
+ ): MeasuredRoadmap {
104
+ const { locale, styleCtx } = scope;
105
+ const { calendar, preset: scale, startDate, timelineDays, nonWorking } = window;
106
+ const endDate = window.naturalEndDate;
107
+ // Waves only draw when one has items (specs/waves.md §9.1).
108
+ const plan = hasVisibleWaves(window.schedule) ? window.schedule.plan : undefined;
109
+ const headerStyle = scope.roadmapStyle;
110
+
111
+ // `top` (default) keeps the legacy single-strip header; `bottom`
112
+ // suppresses the top tick panel (now-pill and marker row stay
113
+ // at the top because anchors / milestones live there); `both`
114
+ // mirrors the strip at the chart bottom too. See specs/dsl.md
115
+ // and specs/rendering.md § Timeline Scale.
116
+ // `header-height` sizes every tick panel (top and mirrored bottom);
117
+ // `none` drops them all, whatever `timeline-position` asks for.
118
+ const tickPanelBucketHeight =
119
+ HEADER_HEIGHT_PX[headerStyle.headerHeight as keyof typeof HEADER_HEIGHT_PX] ??
120
+ HEADER_HEIGHT_PX.md;
121
+ const showTopTickPanel = tickPanelBucketHeight > 0 && headerStyle.timelinePosition !== 'bottom';
122
+ const showBottomTickPanel =
123
+ tickPanelBucketHeight > 0 &&
124
+ (headerStyle.timelinePosition === 'bottom' || headerStyle.timelinePosition === 'both');
125
+
126
+ // Measure the header: the beside card's size or the above band's
127
+ // height, which fix the chart's left edge and top. The company logo
128
+ // (when the caller loaded one) widens the card by its slot and can
129
+ // make it taller; see header-logo.ts.
130
+ const titleStr = file.roadmapDecl?.title ?? file.roadmapDecl?.name ?? '';
131
+ const authorStr = propValue(file.roadmapDecl?.properties ?? [], 'author');
132
+ const headerNode = new HeaderNode({
133
+ title: titleStr,
134
+ author: authorStr,
135
+ style: headerStyle,
136
+ logo: resolveHeaderLogo(file, options.logo),
137
+ });
138
+ const measuredHeader = headerNode.measure();
139
+ const { chartLeftX } = measuredHeader;
140
+
141
+ // The chart sizes to natural content width (date window × ppd) plus
142
+ // chrome padding. `options.width`, when set, is a best-effort
143
+ // maximum canvas width: it shrinks ppd (never below
144
+ // `MIN_PIXELS_PER_DAY`) so header card + gutters + timeline fit,
145
+ // but caption spills and run labels can still grow the canvas
146
+ // past it in finalize. The two `GUTTER_PX` insets keep the header
147
+ // card and attribution wordmark from butting against the canvas edges.
148
+ const ppd = cappedPixelsPerDay({
149
+ naturalPixelsPerDay: scale.pixelsPerUnit / calendar.daysPerUnit(scale.unit),
150
+ dayCount: timelineDays,
151
+ chromeWidth: chartLeftX + 2 * GUTTER_PX,
152
+ maxWidth: options.width,
153
+ });
154
+ const naturalWidth = timelineDays * ppd;
155
+ const originX = chartLeftX + GUTTER_PX;
156
+ const totalChartWidth = naturalWidth;
157
+
158
+ // Header layout (top → bottom):
159
+ // 1. Now-pill row (16 px) — only when there's a now-line
160
+ // 2. Tick-label panel (`header-height`, 24 px at md) — unless
161
+ // `timeline-position:bottom` or `header-height:none`
162
+ // 3. Wave strip (20 px) — only when the roadmap declares waves
163
+ // 4. Marker row (≥26 px) — sized to the packed row count
164
+ // 5. 8 px gap, then the chart begins
165
+ const willHaveNowline =
166
+ options.today !== undefined && options.today >= startDate && options.today <= endDate;
167
+ const pillRowHeight = willHaveNowline ? NOW_PILL_HEIGHT_PX : 0;
168
+ // When `timeline-position:bottom` or `header-height:none` is set,
169
+ // the top tick panel collapses to height 0 — the now-pill and
170
+ // marker row stack directly without the date strip between them.
171
+ const tickPanelHeight = showTopTickPanel ? tickPanelBucketHeight : 0;
172
+ // The wave strip (specs/waves.md §8.8) has a fixed height, so the
173
+ // chart top is known before the barrier passes run; it sits above
174
+ // the marker rows, so the post-hoc marker re-pack never moves it.
175
+ const waveStripHeight = plan ? WAVE_STRIP_HEIGHT_PX : 0;
176
+
177
+ // Build the time scale up front — packMarkerRow needs it to
178
+ // resolve date-pinned entity x positions before we can size the
179
+ // marker row band.
180
+ const timeScale = new TimeScale({
181
+ domain: [startDate, endDate],
182
+ range: [originX, originX + naturalWidth],
183
+ calendar,
184
+ nonWorking,
185
+ });
186
+ // Titled non-working runs and working exceptions join the marker
187
+ // row (5b). These are provisional: they size the band before the
188
+ // timeline exists, and their ticks only decide `seam`, which the
189
+ // labels never read. Finalize re-packs with the final ones.
190
+ const provisionalRunEntries = calendarMarkerEntries(
191
+ buildNonWorkingRuns(timeScale, [], scale.unit, false),
192
+ buildWorkingExceptions(timeScale),
193
+ );
194
+ const hasMarkerEntities =
195
+ resolved.content.anchors.size + resolved.content.milestones.size > 0 ||
196
+ provisionalRunEntries.length > 0;
197
+ // Initial canvas extent = natural date window + canonical
198
+ // gutters. Item caption spills are unknown until the swimlane
199
+ // pass runs and grow the canvas in finalize. The now-pill doesn't
200
+ // contribute here — `buildNowline` flips it to flag mode whenever a
201
+ // centered pill would clip an edge.
202
+ const naturalChartRightX = originX + totalChartWidth + GUTTER_PX;
203
+
204
+ // Resolve each date-pinned anchor and milestone's x. Used both for
205
+ // the marker-row pack and for `after:` resolution downstream — an
206
+ // item with `after:kickoff` would otherwise see an empty
207
+ // entityRightEdges entry and silently fall through to `cursor.x`
208
+ // (= the chart origin). After-only milestones still resolve later
209
+ // once their predecessors are known.
210
+ const datePinnedEntries = datePinnedMarkerEntries(
211
+ resolved.content.anchors,
212
+ resolved.content.milestones,
213
+ timeScale,
214
+ );
215
+
216
+ // Pack the date-pinned markers. Each entity gets a row index
217
+ // (0 = in-row baseline, 1 = bumped down by one step, …) and a
218
+ // label box that may be flipped to the LEFT side when the natural
219
+ // right-side label would overflow the canvas. Earlier
220
+ // entries claim row 0; later ones drop to row 1+ when their
221
+ // bounding box (diamond + label) overlaps an already-placed
222
+ // entry. The renderer reads `labelBox.x/y` directly.
223
+ // Titled runs and working exceptions pack after the markers. Every
224
+ // entry packs against
225
+ // the right edge the post-layout canvas growth will give its label.
226
+ const preliminaryEntries = [...datePinnedEntries, ...provisionalRunEntries];
227
+ const packed = packMarkerRow(
228
+ preliminaryEntries,
229
+ chartLeftX,
230
+ markerLabelsRightX(preliminaryEntries, chartLeftX, naturalChartRightX, estimateTextWidth),
231
+ estimateTextWidth,
232
+ );
233
+ const markerRowsCount = hasMarkerEntities ? Math.max(1, packed.rowCount) : 0;
234
+ const markerRowHeight = markerRowsCount * MARKER_ROW_PITCH_PX;
235
+ const headerRowsHeight = pillRowHeight + tickPanelHeight + waveStripHeight + markerRowHeight;
236
+ const timelineHeightBudget = headerRowsHeight + HEADER_ROWS_TO_CHART_GAP_PX;
237
+ const timelineY = headerRowsTimelineY(measuredHeader, headerRowsHeight);
238
+ const tickPanelY = timelineY + pillRowHeight;
239
+ const waveStripY = tickPanelY + tickPanelHeight;
240
+ const markerRowY = waveStripY + waveStripHeight;
241
+
242
+ const ticks = buildHeaderTicks(timeScale, scale, calendar, locale);
243
+ const timeline: PositionedTimelineScale = {
244
+ box: { x: originX, y: timelineY, width: naturalWidth, height: 0 },
245
+ ticks,
246
+ pixelsPerDay: ppd,
247
+ originX,
248
+ startDate,
249
+ endDate,
250
+ labelStyle: resolveStyle('roadmap', [], styleCtx),
251
+ pillRowHeight,
252
+ tickPanelY,
253
+ tickPanelHeight,
254
+ markerRow: {
255
+ y: markerRowY + MARKER_ROW_CENTER_OFFSET_PX,
256
+ height: markerRowHeight,
257
+ collisionY: markerRowY - 8,
258
+ },
259
+ // `bottomTickPanelY` / `bottomTickPanelHeight` are filled in by
260
+ // finalize, AFTER swimlanes + includes are placed (and after the
261
+ // marker re-pack potentially grows the chart), so the panel sits
262
+ // directly above the footnote area.
263
+ minorGrid: headerStyle.minorGrid,
264
+ ...(plan ? { waveStrip: { y: waveStripY, height: waveStripHeight } } : {}),
265
+ };
266
+ setCalendarRuns(timeline, timeScale, scale.unit);
267
+
268
+ return {
269
+ header: measuredHeader,
270
+ headerNode,
271
+ axis: { originX, ppd, naturalWidth, scale: timeScale },
272
+ naturalChartRightX,
273
+ bands: {
274
+ timelineY,
275
+ pillRowHeight,
276
+ tickPanelY,
277
+ tickPanelHeight,
278
+ waveStripY,
279
+ markerRowY,
280
+ estimatedMarkerRows: markerRowsCount,
281
+ rowsHeight: headerRowsHeight,
282
+ chartTopY: timelineY + timelineHeightBudget,
283
+ bottomTickPanelHeight: showBottomTickPanel ? tickPanelBucketHeight : 0,
284
+ },
285
+ timeline,
286
+ markers: {
287
+ datePinned: datePinnedEntries,
288
+ // AnchorNode and MilestoneNode read these to recover both the y
289
+ // and the resolved label box; after-only milestones (not
290
+ // pre-positioned here) take the band's first row until the
291
+ // final pack.
292
+ placements: markerRowPlacements(packed.placements, markerRowY),
293
+ hasEntities: hasMarkerEntities,
294
+ },
295
+ };
296
+ }
297
+
298
+ /**
299
+ * Where header rows `rowsHeight` tall start (the timeline y). In beside
300
+ * mode the header card's BOTTOM aligns with the bottom of the header rows
301
+ * so it visually anchors to the chart's top. When the card is taller than
302
+ * the rows + a 4 px top inset, push the timeline down so the card has room
303
+ * without clipping above the canvas.
304
+ */
305
+ export function headerRowsTimelineY(header: MeasuredHeader, rowsHeight: number): number {
306
+ return Math.max(header.chartTopY, header.minRowsBottomY - rowsHeight);
307
+ }
308
+
309
+ /**
310
+ * Set (or refresh) the timeline's non-working runs from its current ticks,
311
+ * and its working exceptions. `nonWorkingDisplay` and `nonWorking` stay
312
+ * omitted when the window holds no non-working day, and `workingExceptions`
313
+ * when it holds no exception, so a calendar without either keeps its exact
314
+ * model shape. The extension pass only grows the window, so a refresh never
315
+ * loses an entry.
316
+ */
317
+ export function setCalendarRuns(
318
+ timeline: PositionedTimelineScale,
319
+ scale: TimeScale,
320
+ unit: ViewPreset['unit'],
321
+ ): void {
322
+ const runs = buildNonWorkingRuns(scale, timeline.ticks, unit, timeline.minorGrid);
323
+ if (runs) {
324
+ timeline.nonWorkingDisplay = scale.showsNonWorking ? 'show' : 'hide';
325
+ timeline.nonWorking = runs;
326
+ }
327
+ const exceptions = buildWorkingExceptions(scale);
328
+ if (exceptions) timeline.workingExceptions = exceptions;
329
+ }
@@ -0,0 +1,318 @@
1
+ // Phase 3, place (specs/layout-phases.md §3.4): the root context, the
2
+ // entity-edge seeds from the date-pinned markers, the footnote index, the
3
+ // lanes inside the wave barrier driver (P1), the corridor rerun (P2), the
4
+ // kept isolated regions (P4), the chart bottom (P5) and the milestones (P6).
5
+ //
6
+ // Placement never reads the canvas: lanes leave their width to finalize,
7
+ // regions are fitted only there, and milestones get their marker row and
8
+ // label box from the final pack. It places on the natural scale, which
9
+ // finalize may extend but never re-places on.
10
+ //
11
+ // Placement lays the chart out in local coordinates, with its top at y 0:
12
+ // lanes, regions, milestone cut lines and slack arrows. The chart's top on
13
+ // the canvas is known only once the header rows above it stop growing, so
14
+ // finalize translates the whole chart there, once (f7).
15
+
16
+ import type { ResolveResult, SwimlaneDeclaration } from '@nowline/core';
17
+ import { AxisColumns } from '../axis-columns.js';
18
+ import { defaultRowBand } from '../band-scale.js';
19
+ import { localeStrings } from '../i18n.js';
20
+ import { createLayoutContext, type LayoutContext, type LayoutHelpers } from '../layout-context.js';
21
+ import { indexFootnotes } from '../nodes/footnote-node.js';
22
+ import { buildIncludeRegions } from '../nodes/include-node.js';
23
+ import {
24
+ buildMilestones,
25
+ collectSlackCorridors,
26
+ type MilestonePlacement,
27
+ } from '../nodes/milestone-node.js';
28
+ import { SwimlaneNode } from '../nodes/swimlane-node.js';
29
+ import { hasVisibleWaves, type RoadmapSchedule } from '../schedule.js';
30
+ import { isScheduleWarmStart } from '../start-rules.js';
31
+ import { resolveStyle } from '../style-resolution.js';
32
+ import { SPACING_PX } from '../themes/shared.js';
33
+ import type { TimeScale } from '../time-scale.js';
34
+ import type { PositionedIncludeRegion, PositionedRoadmap, PositionedSwimlane } from '../types.js';
35
+ import { solveWaveBarriers, type WaveBarrierSeed } from '../wave-barrier.js';
36
+ import {
37
+ beginWavePass,
38
+ createWaveLayoutState,
39
+ freezeWaves,
40
+ seedWaveEdges,
41
+ WAVE_EDGE_TOLERANCE_PX,
42
+ } from '../wave-layout.js';
43
+ import type { DateWindow, RoadmapScope } from './date-window.js';
44
+ import type { MeasuredRoadmap } from './measure.js';
45
+
46
+ /** Gap (px) between the last lane and the first isolated region's tab. */
47
+ const LANES_TO_REGIONS_GAP_PX = 8;
48
+
49
+ /** A placed chart, every y in the chart's local frame (top at 0) until f7 translates it. */
50
+ export interface PlacedChart {
51
+ /** Entity maps and placed items of the kept pass; finalize reads them, never writes placements. */
52
+ ctx: LayoutContext;
53
+ swimlanes: PositionedSwimlane[];
54
+ includes: PositionedIncludeRegion[];
55
+ /**
56
+ * Each kept region's own context, in `includes` order: its placed
57
+ * items and marker edges, which its dependency edges read (f11).
58
+ */
59
+ regionContexts: LayoutContext[];
60
+ /** x, slack arrows and overrun; f6 adds the row and label box and builds `PositionedMilestone`. */
61
+ milestones: MilestonePlacement[];
62
+ laneEntries: SwimlaneDeclaration[];
63
+ /** Rightmost bar or spilled caption of the kept lane pass. */
64
+ maxRightX: number;
65
+ /** Bottom of the last lane or region (P5). */
66
+ bottomY: number;
67
+ waveSolve?: PositionedRoadmap['waveSolve'];
68
+ }
69
+
70
+ export function placeChart(
71
+ resolved: ResolveResult,
72
+ scope: RoadmapScope,
73
+ window: DateWindow,
74
+ measured: MeasuredRoadmap,
75
+ helpers: LayoutHelpers,
76
+ ): PlacedChart {
77
+ const { theme, styleCtx } = scope;
78
+ const { cal, calendar, schedule } = window;
79
+ const plan = schedule.plan;
80
+ const { originX, scale: timeScale } = measured.axis;
81
+ const { timeline } = measured;
82
+
83
+ // Footnotes index must exist before sequencing items reference
84
+ // them.
85
+ const ctx = createLayoutContext(
86
+ resolved,
87
+ {
88
+ theme,
89
+ cal,
90
+ schedule,
91
+ timeline,
92
+ scale: timeScale,
93
+ calendar,
94
+ axisColumns: new AxisColumns(window, timeScale),
95
+ bandScale: defaultRowBand(),
96
+ waves: plan
97
+ ? createWaveLayoutState(
98
+ plan,
99
+ timeScale,
100
+ originX,
101
+ localeStrings(scope.locale).waveBackgroundItem,
102
+ hasVisibleWaves(schedule),
103
+ )
104
+ : undefined,
105
+ },
106
+ {
107
+ chartTopY: 0,
108
+ chartBottomY: 0,
109
+ swimlaneBottomY: 0,
110
+ },
111
+ {
112
+ footnotes: indexFootnotes(resolved.content.footnotes),
113
+ // A copy: the final pack (P10) replaces the context's
114
+ // placements, and the estimate stays as measured.
115
+ markerRowPlacements: new Map(measured.markers.placements),
116
+ },
117
+ );
118
+
119
+ // Seed the entity-edge maps from the date-pinned pack so item
120
+ // `after:kickoff` references resolve before swimlanes run. Mid
121
+ // points use the row-aware centerY so dependency arrows from the
122
+ // anchor land on the diamond's actual row.
123
+ for (const e of measured.markers.datePinned) {
124
+ const placement = ctx.markerRowPlacements.get(e.id);
125
+ if (!placement) continue;
126
+ ctx.entityLeftEdges.set(e.id, e.centerX);
127
+ ctx.entityRightEdges.set(e.id, e.centerX);
128
+ ctx.entityMidpoints.set(e.id, { x: e.centerX, y: placement.centerY });
129
+ }
130
+
131
+ // Snapshot the entity maps after pre-positioning so we can reset
132
+ // them between the two swimlane passes. Date-pinned anchors and
133
+ // milestones live in this baseline; item-derived entries get
134
+ // re-added on each pass.
135
+ const baselineEntityLeft = new Map(ctx.entityLeftEdges);
136
+ const baselineEntityRight = new Map(ctx.entityRightEdges);
137
+ const baselineEntityMid = new Map(ctx.entityMidpoints);
138
+ // Item-only maps don't carry baseline entries (date-pinned
139
+ // markers are never placed items), so pass-2 reruns reset them
140
+ // to fresh empties. With waves, every reset re-seeds the wave
141
+ // edges (`entityRightEdges[w_k] = E_k`, `entityLeftEdges[w_k] =
142
+ // S_k`) on top of the baseline, so forward references stay
143
+ // dropped exactly as they are without waves.
144
+ const resetEntityMaps = (): void => {
145
+ ctx.entityLeftEdges = new Map(baselineEntityLeft);
146
+ ctx.entityRightEdges = new Map(baselineEntityRight);
147
+ ctx.entityMidpoints = new Map(baselineEntityMid);
148
+ // placedItems and itemFlowKey only ever hold item entries
149
+ // (markers never write to them), so a fresh map is the right
150
+ // reset: the next pass's items will repopulate.
151
+ ctx.placedItems = new Map();
152
+ ctx.itemFlowKey = new Map();
153
+ ctx.currentFlowKey = '';
154
+ if (ctx.waves) seedWaveEdges(ctx.waves, ctx.entityLeftEdges, ctx.entityRightEdges);
155
+ };
156
+
157
+ // Build swimlanes (declared order). Inter-band gap comes from
158
+ // the swimlane default style's `spacing` bucket. Default
159
+ // `spacing: none` keeps existing samples byte-stable; bumping to
160
+ // `md` introduces an 8 px gap.
161
+ const laneEntries = [...resolved.content.swimlanes.values()];
162
+ const swimlaneDefaultStyle = resolveStyle('swimlane', [], styleCtx);
163
+ const interBandGapPx = SPACING_PX[swimlaneDefaultStyle.spacing as keyof typeof SPACING_PX] ?? 0;
164
+
165
+ const runSwimlaneLoop = (): {
166
+ swimlanes: PositionedSwimlane[];
167
+ nextY: number;
168
+ maxRightX: number;
169
+ } => {
170
+ ctx.nextParallelId = 0;
171
+ ctx.nextGroupId = 0;
172
+ const out: PositionedSwimlane[] = [];
173
+ let cursorY = ctx.chartTopY;
174
+ let bIndex = 0;
175
+ let maxRightX = ctx.timeline.originX;
176
+ for (const lane of laneEntries) {
177
+ if (bIndex > 0) cursorY += interBandGapPx;
178
+ const { positioned, usedHeight, usedRightX } = new SwimlaneNode(
179
+ { lane, bandIndex: bIndex },
180
+ helpers,
181
+ ).place({ x: ctx.timeline.originX, y: cursorY }, ctx);
182
+ out.push(positioned);
183
+ cursorY += usedHeight;
184
+ if (usedRightX > maxRightX) maxRightX = usedRightX;
185
+ bIndex++;
186
+ }
187
+ return { swimlanes: out, nextY: cursorY, maxRightX };
188
+ };
189
+
190
+ const isolated = resolved.content.isolatedRegions;
191
+
192
+ // Pass 1 — place items without corridor knowledge. With waves
193
+ // (specs/waves.md §8.3-§8.4) it is the barrier driver's pass
194
+ // loop: each pass resets the maps, re-seeds the wave edges, places
195
+ // the lanes with the pass's floors, then places the isolated
196
+ // regions only to accumulate their members (those region
197
+ // placements are discarded). The driver iterates to the least
198
+ // fixpoint; its last pass ran with the solved S/E, so its lane
199
+ // placements are kept.
200
+ let pass: ReturnType<typeof runSwimlaneLoop>;
201
+ let waveSolve: PositionedRoadmap['waveSolve'];
202
+ const waves = ctx.waves;
203
+ if (waves) {
204
+ let last: ReturnType<typeof runSwimlaneLoop> | undefined;
205
+ // Warm start (specs/scheduler-unification.md §5, F4): engine C's
206
+ // `S_k` and `E_k`, projected, are a lower bound of engine A's, so
207
+ // the passes start from them and settle on the same fixpoint in
208
+ // fewer passes.
209
+ const seed = isScheduleWarmStart() ? waveSeed(waves.ids, schedule, timeScale) : undefined;
210
+ const solved = solveWaveBarriers(
211
+ waves.ids.length,
212
+ waves.origin,
213
+ waves.floors,
214
+ (S, E) => {
215
+ beginWavePass(waves, S, E);
216
+ resetEntityMaps();
217
+ last = runSwimlaneLoop();
218
+ if (isolated.length > 0) {
219
+ buildIncludeRegions(
220
+ isolated,
221
+ ctx,
222
+ last.nextY + LANES_TO_REGIONS_GAP_PX,
223
+ helpers,
224
+ measured.naturalChartRightX,
225
+ );
226
+ }
227
+ return waves.pass;
228
+ },
229
+ WAVE_EDGE_TOLERANCE_PX,
230
+ seed,
231
+ );
232
+ // From here on the barrier is frozen: later placements floor
233
+ // with the solved S/E and accumulate nothing.
234
+ freezeWaves(waves, solved);
235
+ waveSolve = { passes: solved.passes, capped: solved.capped };
236
+ pass = last as ReturnType<typeof runSwimlaneLoop>;
237
+ } else {
238
+ pass = runSwimlaneLoop();
239
+ }
240
+ let swimlanes = pass.swimlanes;
241
+ let y = pass.nextY;
242
+
243
+ // Collect slack-arrow corridors from the milestones (mirrors
244
+ // MilestoneNode.place's pred resolution). When the result is
245
+ // non-empty, an item sat inside an arrow's path on pass 1 — rerun
246
+ // the swimlane loop with corridors known so the row-packer can
247
+ // bump the offending items down to a clear row. Bumping never
248
+ // changes an item's x, so corridors stay valid across the rerun;
249
+ // no fixed-point iteration needed. With waves the rerun runs once,
250
+ // after the barrier has settled: it floors with the frozen S/E,
251
+ // and the reset re-seeds the wave edges.
252
+ const corridors = collectSlackCorridors(resolved.content.milestones, ctx);
253
+ if (corridors.length > 0) {
254
+ resetEntityMaps();
255
+ ctx.slackCorridors = corridors;
256
+ pass = runSwimlaneLoop();
257
+ swimlanes = pass.swimlanes;
258
+ y = pass.nextY;
259
+ }
260
+
261
+ // Include regions under the swimlanes. Reserve the 8 px gap +
262
+ // tab-reserve only when there's at least one isolated region —
263
+ // otherwise the now-line and chart bottom would extend past the
264
+ // last swimlane into empty space. With waves this is the one kept
265
+ // region placement, floored with the frozen S/E.
266
+ let includes: PositionedIncludeRegion[] = [];
267
+ let regionContexts: LayoutContext[] = [];
268
+ if (isolated.length > 0) {
269
+ const r = buildIncludeRegions(
270
+ isolated,
271
+ ctx,
272
+ y + LANES_TO_REGIONS_GAP_PX,
273
+ helpers,
274
+ measured.naturalChartRightX,
275
+ );
276
+ includes = r.regions;
277
+ regionContexts = r.contexts;
278
+ y = r.endY;
279
+ }
280
+
281
+ ctx.chartBottomY = y;
282
+ ctx.swimlaneBottomY = y;
283
+
284
+ // Milestones first so anchors know which xs are occupied (for
285
+ // collision bumps). Placement gives each its x, slack arrows and
286
+ // overrun; its marker row and label box come from the final pack in
287
+ // finalize, once every centerX is known and the canvas is final.
288
+ const milestones = buildMilestones(resolved.content.milestones, ctx);
289
+
290
+ return {
291
+ ctx,
292
+ swimlanes,
293
+ includes,
294
+ regionContexts,
295
+ milestones,
296
+ laneEntries,
297
+ maxRightX: pass.maxRightX,
298
+ bottomY: y,
299
+ ...(waveSolve ? { waveSolve } : {}),
300
+ };
301
+ }
302
+
303
+ /**
304
+ * The barrier driver's warm start: engine C's wave spans projected to px
305
+ * (`start` is `S_k`; the exclusive `end` projects to `E_k`). A wave C did
306
+ * not schedule seeds nothing.
307
+ */
308
+ function waveSeed(
309
+ ids: readonly string[],
310
+ schedule: RoadmapSchedule,
311
+ scale: TimeScale,
312
+ ): WaveBarrierSeed {
313
+ const spans = ids.map((id) => schedule.waves?.get(id));
314
+ return {
315
+ S: spans.map((w) => (w ? scale.forward(w.start) : null)),
316
+ E: spans.map((w) => (w ? scale.forward(w.end) : null)),
317
+ };
318
+ }
package/src/renderable.ts CHANGED
@@ -14,9 +14,29 @@
14
14
  // Recursively places children inside this call.
15
15
  //
16
16
  // `place` may invoke `measure` again — implementations should make
17
- // `measure` cheap. The production nodes (`ItemNode`, `SwimlaneNode`,
18
- // etc. under `nodes/`) follow this pattern: place re-measures, but
19
- // measure is O(content) without I/O.
17
+ // `measure` cheap: O(content), without I/O. Of the production nodes
18
+ // only `ItemNode` implements the interface (below).
19
+ //
20
+ // Which nodes measure (specs/layout-phases.md, §3.6 and §3.7):
21
+ //
22
+ // - `ItemNode` implements `Renderable`, and its measure is complete:
23
+ // `measureItem` (`nodes/item-measure.ts`) sizes the bar once, wrapped
24
+ // title and chip growth included, and the row track takes the row
25
+ // pitch from it before the item is placed. Nothing patches a placed
26
+ // item's height afterwards.
27
+ // - Lanes, groups and parallels do not measure. Their height is the
28
+ // result of row packing on x positions, so measuring them would be
29
+ // placing them twice; they stay single-pass (`sequenceRowTrack`).
30
+ // Anchors, milestones, include regions and the footnote panel place
31
+ // directly as well.
32
+ // - `HeaderNode` has `measure()` and `place()` of its own and does not
33
+ // take this signature: it has no time scale or band to measure
34
+ // against. Its logo slot and text inset live there; drawing the logo
35
+ // stays in the renderer.
36
+ // - The roadmap itself is split by phase, not by node: the measure
37
+ // phase (`phases/measure.ts`) sizes the header, the axis and the
38
+ // header rows before any lane is placed, and the finalize phase
39
+ // (`phases/finalize.ts`) runs everything that needs final geometry.
20
40
 
21
41
  import type { BandScale } from './band-scale.js';
22
42
  import type { TimeScale } from './time-scale.js';