@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,242 @@
1
+ // Wave barriers (specs/waves.md §5.1, §8.2-§8.5).
2
+ //
3
+ // One unit-agnostic driver serves both schedulers: engine A (pixel layout)
4
+ // and engine C (`scheduleRoadmap`, days). Each wraps its lane walk in a
5
+ // `runPass(S, E)` callback that floors starts by the barrier and reports the
6
+ // latest member end per wave; the driver iterates to the least fixpoint.
7
+ //
8
+ // Arrays are **0-based** throughout this module: wave k (1-based, as in
9
+ // `WavePlan` and the spec's `w_1 … w_n`) lives at index `k - 1` of `S`, `E`,
10
+ // `floors` and `memberEnd`.
11
+
12
+ import type { GroupBlock, ItemDeclaration, ParallelBlock, WavePlan } from '@nowline/core';
13
+ import { waveFloorDates } from './start-terms.js';
14
+ import type { WorkingCalendar } from './working-calendar.js';
15
+
16
+ /** A node the walkers floor: a leaf item or a container. */
17
+ export type WaveNode = ItemDeclaration | GroupBlock | ParallelBlock;
18
+
19
+ /** What one pass reports back to the driver. */
20
+ export interface WavePassResult {
21
+ /** Latest member end per wave (0-based); `-Infinity` when the wave has no member. */
22
+ memberEnd: readonly number[];
23
+ }
24
+
25
+ export interface WaveBarrierResult {
26
+ /** Wave starts `S_k` at index `k - 1`. */
27
+ S: number[];
28
+ /** Wave ends `E_k` at index `k - 1`. */
29
+ E: number[];
30
+ /** Passes the solve ran (at most `n + 1`). */
31
+ passes: number;
32
+ /** True when the solve hit the `n + 1` cap without settling (invalid input). */
33
+ capped: boolean;
34
+ }
35
+
36
+ /** A warm start for `solveWaveBarriers`: estimates of `S_k` / `E_k` from below (0-based, null: none). */
37
+ export interface WaveBarrierSeed {
38
+ S: ReadonlyArray<number | null>;
39
+ E: ReadonlyArray<number | null>;
40
+ }
41
+
42
+ /**
43
+ * Solve the barrier recurrence (§8.2). `floors[k - 1]` is `A_k` in the
44
+ * caller's units, or null for no floor; `origin` is the axis origin (0 days,
45
+ * or the timeline's origin x).
46
+ *
47
+ * `runPass` must be pure in `(S, E)`: the caller resets its entity maps every
48
+ * pass. On convergence the last `runPass` call ran with the returned `S`/`E`,
49
+ * so the caller can keep that pass's placements. When the cap is hit, the
50
+ * driver calls `runPass` once more with the returned `S`/`E` for the same
51
+ * reason; that extra call is not counted in `passes`.
52
+ *
53
+ * `tolerance` is how far a floor must pass the prior edge (`E_{k-1}`, or
54
+ * the origin for `S_1`) to set `S_k`; a floor within it is a tie and the
55
+ * prior edge wins. The day engines use 0 (exact `max`). Engine A passes its
56
+ * sub-pixel edge tolerance: its floors are `origin + days * ppd` while
57
+ * `E_{k-1}` is a sum of item widths, so a same-day floor can exceed the
58
+ * prior end by an ulp and would otherwise open a phantom gap.
59
+ *
60
+ * `seed` warm-starts the solve (specs/scheduler-unification.md §5, F4):
61
+ * `seed.S[k - 1]` and `seed.E[k - 1]` are an estimate of `S_k` and `E_k`
62
+ * from below (engine A passes engine C's, projected), or null for none. The
63
+ * first pass starts each `S_k` (k > 1) and `E_k` at no less than its seed
64
+ * instead of at the floor chain alone, so the passes settle sooner on the
65
+ * same fixpoint: Kleene iteration from any lower bound of the least
66
+ * fixpoint reaches it, and for valid input (WV10) the fixpoint is unique
67
+ * (`S_1` is fixed, and each `E_k` then follows from `S_1..S_k` and
68
+ * `E_1..E_{k-1}`), so a pass can only confirm the right one. `S_1` is never
69
+ * seeded: the driver keeps the first pass's `S_1` to the end.
70
+ */
71
+ export function solveWaveBarriers(
72
+ n: number,
73
+ origin: number,
74
+ floors: ReadonlyArray<number | null>,
75
+ runPass: (S: readonly number[], E: readonly number[]) => WavePassResult,
76
+ tolerance = 0,
77
+ seed?: WaveBarrierSeed,
78
+ ): WaveBarrierResult {
79
+ const A = floors.map((f) => f ?? -Infinity);
80
+ let S: number[] = [];
81
+ for (let i = 0; i < n; i++) {
82
+ const chained = floorPast(i === 0 ? origin : S[i - 1], A[i], tolerance);
83
+ S.push(i === 0 ? chained : Math.max(chained, seed?.S[i] ?? -Infinity));
84
+ }
85
+ let E = S.map((s, i) => Math.max(s, seed?.E[i] ?? -Infinity));
86
+ for (let pass = 1; pass <= n + 1; pass++) {
87
+ const m = runPass(S, E).memberEnd;
88
+ const S2: number[] = [];
89
+ const E2: number[] = [];
90
+ for (let i = 0; i < n; i++) {
91
+ S2.push(i === 0 ? S[0] : floorPast(E2[i - 1], A[i], tolerance));
92
+ E2.push(Math.max(S2[i], m[i] ?? -Infinity));
93
+ }
94
+ if (sameArray(S, S2) && sameArray(E, E2)) {
95
+ return { S, E, passes: pass, capped: false };
96
+ }
97
+ S = S2;
98
+ E = E2;
99
+ }
100
+ runPass(S, E);
101
+ return { S, E, passes: n + 1, capped: true };
102
+ }
103
+
104
+ /** `max(prior, floor)`, except a floor within `tolerance` of `prior` is a tie: `prior`. */
105
+ function floorPast(prior: number, floor: number, tolerance: number): number {
106
+ return floor - prior > tolerance ? floor : prior;
107
+ }
108
+
109
+ function sameArray(a: readonly number[], b: readonly number[]): boolean {
110
+ for (let i = 0; i < a.length; i++) {
111
+ if (a[i] !== b[i]) return false;
112
+ }
113
+ return true;
114
+ }
115
+
116
+ /**
117
+ * Floor offsets `A_k` in working days from `startDate`, from the plan's
118
+ * unclamped declaration dates (§5.1). A floor on a non-working day counts
119
+ * as the next working day. A floor before the start yields a negative
120
+ * offset; `S_1 = max(0, A_1)` absorbs it.
121
+ */
122
+ export function waveFloorDays(
123
+ plan: WavePlan,
124
+ startDate: Date,
125
+ calendar: WorkingCalendar,
126
+ ): Array<number | null> {
127
+ return waveFloorDates(plan).map((d) => (d ? calendar.workingIndexOf(startDate, d) : null));
128
+ }
129
+
130
+ /**
131
+ * Per-pass wave state, in the caller's units (days for engine C, px for engine
132
+ * A): the floor of a node and the member-end accumulator. One instance per pass.
133
+ */
134
+ export class WavePass {
135
+ /** Latest member end per wave (0-based); `-Infinity` when the wave has no member. */
136
+ readonly memberEnd: number[];
137
+ /** The member that set `memberEnd` (latest end; ties keep the first placed). */
138
+ readonly binding: Array<ItemDeclaration | undefined>;
139
+ /** Members placed per wave. */
140
+ readonly memberCount: number[];
141
+
142
+ constructor(
143
+ readonly plan: WavePlan,
144
+ readonly S: readonly number[],
145
+ ) {
146
+ const n = plan.waves.length;
147
+ this.memberEnd = new Array<number>(n).fill(-Infinity);
148
+ this.binding = new Array<ItemDeclaration | undefined>(n).fill(undefined);
149
+ this.memberCount = new Array<number>(n).fill(0);
150
+ }
151
+
152
+ /**
153
+ * The barrier floor of `node`: `S[ew]` for an item, and
154
+ * `max(S[ew(C)], S[lw(C)])` for a container (§5.1 start rule). Undefined
155
+ * for background work with no lead wave.
156
+ */
157
+ floor(node: WaveNode): number | undefined {
158
+ const ew = this.plan.waveOf.get(node);
159
+ const lw = node.$type === 'ItemDeclaration' ? undefined : this.plan.leadOf.get(node);
160
+ let f: number | undefined;
161
+ if (ew !== undefined) f = this.S[ew - 1];
162
+ if (lw !== undefined) f = Math.max(f ?? -Infinity, this.S[lw - 1]);
163
+ return f;
164
+ }
165
+
166
+ /** `max(start, floor(node))`, or `start` unchanged when there is no floor. */
167
+ apply(node: WaveNode, start: number): number {
168
+ const f = this.floor(node);
169
+ return f === undefined ? start : Math.max(start, f);
170
+ }
171
+
172
+ /** Record a placed item's end; a no-op for background work. */
173
+ accumulate(item: ItemDeclaration, end: number): void {
174
+ const k = this.plan.memberWave(item);
175
+ if (k === undefined) return;
176
+ this.memberCount[k - 1]++;
177
+ if (end > this.memberEnd[k - 1]) {
178
+ this.memberEnd[k - 1] = end;
179
+ this.binding[k - 1] = item;
180
+ }
181
+ }
182
+ }
183
+
184
+ /** One wave's solved span, in the caller's units. */
185
+ export interface WaveSpan {
186
+ id: string;
187
+ /** 1-based declaration order. */
188
+ index: number;
189
+ start: number;
190
+ end: number;
191
+ memberCount: number;
192
+ /** The binding member's id ?? title; omitted when no member sets `E_k > S_k`. */
193
+ heldBy?: string;
194
+ /** The `after:` element that set `S_k`, when it beat `E_{k-1}` (or the origin). */
195
+ floorRef?: string;
196
+ }
197
+
198
+ /**
199
+ * Summarise the solved waves from the driver's result and the final pass
200
+ * (the pass that ran with `result.S` / `result.E`). `tolerance` must be
201
+ * the one the solve ran with, so `floorRef` is set exactly when the floor
202
+ * set `S_k`.
203
+ */
204
+ export function summarizeWaves(
205
+ plan: WavePlan,
206
+ result: WaveBarrierResult,
207
+ finalPass: WavePass,
208
+ origin: number,
209
+ floors: ReadonlyArray<number | null>,
210
+ tolerance = 0,
211
+ ): WaveSpan[] {
212
+ return plan.waves.map((w, i) => {
213
+ const span: WaveSpan = {
214
+ id: w.name as string,
215
+ index: i + 1,
216
+ start: result.S[i],
217
+ end: result.E[i],
218
+ memberCount: finalPass.memberCount[i],
219
+ };
220
+ const binding = finalPass.binding[i];
221
+ if (
222
+ binding !== undefined &&
223
+ finalPass.memberEnd[i] === result.E[i] &&
224
+ result.E[i] > result.S[i]
225
+ ) {
226
+ const label = binding.name ?? binding.title;
227
+ if (label !== undefined) span.heldBy = label;
228
+ }
229
+ const floor = floors[i];
230
+ const floorRef = plan.floors[i]?.ref;
231
+ const prior = i === 0 ? origin : result.E[i - 1];
232
+ if (
233
+ floor !== null &&
234
+ floor !== undefined &&
235
+ floorRef !== undefined &&
236
+ floor - prior > tolerance
237
+ ) {
238
+ span.floorRef = floorRef;
239
+ }
240
+ return span;
241
+ });
242
+ }
@@ -0,0 +1,25 @@
1
+ // Wave hatch kind: which of the theme's two hatch strokes marks background
2
+ // work on a bar (specs/waves.md §9.3), picked by the bar fill's luminance.
3
+ // Moved from the SVG renderer (R5) so each background item and the legend's
4
+ // hatch swatch carry their kind and the renderer only draws the pattern.
5
+
6
+ import { fillLuminance } from './status-dot.js';
7
+ import type { ResolvedStyle, WaveHatchKind } from './types.js';
8
+
9
+ /**
10
+ * Bar-fill luminance at or above which background work gets the dark hatch
11
+ * (`wave.hatch`); below it, the light one (`wave.hatchOnDark`). The same
12
+ * crossover the status dot picks its palette by (`pickStatusDotPalette`), so
13
+ * the hatch and the dot agree on which bars read as dark.
14
+ */
15
+ const WAVE_HATCH_LIGHT_FILL_MIN_LUMINANCE = 0.24;
16
+
17
+ /** The painted fill of a bar, as the renderer's `rectFrame` paints it. */
18
+ function barFill(bg: ResolvedStyle['bg']): string {
19
+ return bg === 'none' ? 'transparent' : bg;
20
+ }
21
+
22
+ /** The hatch kind for a bar (or legend swatch) whose resolved `bg` is `bg`. */
23
+ export function waveHatchKind(bg: ResolvedStyle['bg']): WaveHatchKind {
24
+ return fillLuminance(barFill(bg)) >= WAVE_HATCH_LIGHT_FILL_MIN_LUMINANCE ? 'dark' : 'light';
25
+ }
@@ -0,0 +1,203 @@
1
+ // Engine A (pixel layout) wave state: specs/waves.md §8.4.
2
+ //
3
+ // The place phase (`phases/place.ts`) runs its lane loop (plus a region pass) inside the
4
+ // unit-agnostic barrier driver (`solveWaveBarriers`). Every pass floors
5
+ // starts with the pass's `S` (in px) and accumulates member ends; this
6
+ // module holds that state and the small helpers the sequencers and
7
+ // container nodes call. Without waves `LayoutContext.waves` is undefined
8
+ // and every helper here is a no-op, so wave-free output is unchanged.
9
+
10
+ import type { ItemDeclaration, WavePlan } from '@nowline/core';
11
+ import { addDays } from './calendar.js';
12
+ import type { LayoutContext } from './layout-context.js';
13
+ import { waveFloorDates } from './start-terms.js';
14
+ import type { TimeScale } from './time-scale.js';
15
+ import type { PositionedItem } from './types.js';
16
+ import { type WaveBarrierResult, type WaveNode, WavePass } from './wave-barrier.js';
17
+ import { spanEndDate } from './working-calendar.js';
18
+
19
+ /**
20
+ * Sub-pixel tolerance for wave edges: a milestone whose center is closer
21
+ * than this to a boundary sits on it (specs/waves.md §9.2), a wave must
22
+ * move past a date by at least this much to count as an overrun, and an
23
+ * item must start this close to its `after:` date for that date to have
24
+ * set its start (NL.I1008). Pixel ends are float sums, so exact
25
+ * comparisons would flip on rounding noise.
26
+ */
27
+ export const WAVE_EDGE_TOLERANCE_PX = 0.5;
28
+
29
+ /** Engine A's barrier state, shared by the main lanes and the regions. */
30
+ export interface WaveLayoutState {
31
+ readonly plan: WavePlan;
32
+ /** Wave ids in declaration order (wave k at index `k - 1`). */
33
+ readonly ids: readonly string[];
34
+ /** The axis origin in px (`S_1 >= origin`). */
35
+ readonly origin: number;
36
+ /**
37
+ * `A_k` in px from the unclamped `scale.forward` of each floor date
38
+ * (never `forwardWithinDomain` or the edge maps), or null: no floor.
39
+ */
40
+ readonly floors: ReadonlyArray<number | null>;
41
+ /** `S_k` / `E_k` in px (0-based) that the current pass floors with and seeds. */
42
+ S: readonly number[];
43
+ E: readonly number[];
44
+ /** The current pass: floors from `S`, plus the member-end accumulator. */
45
+ pass: WavePass;
46
+ /**
47
+ * Set once the driver settles: `S`/`E` are the solved values, `pass` is
48
+ * the pass that ran with them, and later placements (the slack rerun,
49
+ * the final region placement) floor without accumulating. The spans
50
+ * then come from `summarizeWaves(plan, solve, pass, origin, floors)`.
51
+ */
52
+ frozen: boolean;
53
+ /** The driver's result, set with `frozen`. */
54
+ solve?: WaveBarrierResult;
55
+ /**
56
+ * The localized hover line of a background item
57
+ * (`PositionedItem.waveTooltip`), set where the item is placed: the
58
+ * renderer has no locale table (specs/waves.md §9.3).
59
+ */
60
+ readonly backgroundTooltip: string;
61
+ /**
62
+ * Some wave has a member (`hasVisibleWaves`). When none does, the
63
+ * barrier still runs but nothing is drawn: no item is background work.
64
+ */
65
+ readonly visible: boolean;
66
+ }
67
+
68
+ /**
69
+ * Fresh state for a roadmap with waves; `scale` maps the floor dates,
70
+ * `backgroundTooltip` is the locale's background-item line, and `visible`
71
+ * says whether any wave has a member.
72
+ */
73
+ export function createWaveLayoutState(
74
+ plan: WavePlan,
75
+ scale: TimeScale,
76
+ origin: number,
77
+ backgroundTooltip: string,
78
+ visible: boolean,
79
+ ): WaveLayoutState {
80
+ const S = plan.waves.map(() => origin);
81
+ return {
82
+ plan,
83
+ ids: plan.waves.map((w) => w.name as string),
84
+ origin,
85
+ floors: waveFloorDates(plan).map((d) => (d ? scale.forward(d) : null)),
86
+ S,
87
+ E: S,
88
+ pass: new WavePass(plan, S),
89
+ frozen: false,
90
+ backgroundTooltip,
91
+ visible,
92
+ };
93
+ }
94
+
95
+ /** Start a barrier pass with the driver's `S`/`E`. */
96
+ export function beginWavePass(
97
+ state: WaveLayoutState,
98
+ S: readonly number[],
99
+ E: readonly number[],
100
+ ): void {
101
+ state.S = S;
102
+ state.E = E;
103
+ state.pass = new WavePass(state.plan, S);
104
+ }
105
+
106
+ /**
107
+ * Freeze the state on the driver's result. The last pass the driver ran
108
+ * used exactly `result.S` / `result.E` (on convergence and when capped),
109
+ * so `state.pass` already floors with the solved starts.
110
+ */
111
+ export function freezeWaves(state: WaveLayoutState, result: WaveBarrierResult): void {
112
+ state.S = result.S;
113
+ state.E = result.E;
114
+ state.frozen = true;
115
+ state.solve = result;
116
+ }
117
+
118
+ /**
119
+ * Seed the wave edges into a pair of entity-edge maps (specs/waves.md
120
+ * §8.4): `entityRightEdges[w_k] = E_k`, `entityLeftEdges[w_k] = S_k`, so
121
+ * `after:<wave>`, `before:<wave>` and a floating `milestone after:<wave>`
122
+ * resolve through the existing lookups. Wave ids get no midpoint, so a
123
+ * wave reference draws no dependency arrow.
124
+ */
125
+ export function seedWaveEdges(
126
+ state: WaveLayoutState,
127
+ left: Map<string, number>,
128
+ right: Map<string, number>,
129
+ ): void {
130
+ state.ids.forEach((id, i) => {
131
+ left.set(id, state.S[i]);
132
+ right.set(id, state.E[i]);
133
+ });
134
+ }
135
+
136
+ /**
137
+ * The barrier floor applied to a start: `max(x, S[ew(node)], S[lw(node)])`
138
+ * (specs/waves.md §5.1 start rule). `x` unchanged without waves and for
139
+ * background work with no lead wave.
140
+ */
141
+ export function waveFloorX(node: WaveNode, x: number, ctx: LayoutContext): number {
142
+ return ctx.waves ? ctx.waves.pass.apply(node, x) : x;
143
+ }
144
+
145
+ /** Record a placed item's logical end (members only; never while frozen). */
146
+ export function accumulateWaveMember(
147
+ node: ItemDeclaration,
148
+ logicalEnd: number,
149
+ ctx: LayoutContext,
150
+ ): void {
151
+ if (ctx.waves && !ctx.waves.frozen) ctx.waves.pass.accumulate(node, logicalEnd);
152
+ }
153
+
154
+ /** `PositionedItem.waveRole`, or undefined when the roadmap draws no waves. */
155
+ export function waveRoleOf(node: ItemDeclaration, ctx: LayoutContext): PositionedItem['waveRole'] {
156
+ if (!ctx.waves?.visible) return undefined;
157
+ return ctx.waves.plan.memberWave(node) !== undefined ? 'member' : 'background';
158
+ }
159
+
160
+ /** True when `x` sits on a wave boundary: any `S_k` or `E_k` except the origin. */
161
+ export function isOnWaveBoundary(state: WaveLayoutState, x: number): boolean {
162
+ for (const b of [...state.S, ...state.E]) {
163
+ if (Math.abs(b - state.origin) < WAVE_EDGE_TOLERANCE_PX) continue;
164
+ if (Math.abs(x - b) < WAVE_EDGE_TOLERANCE_PX) return true;
165
+ }
166
+ return false;
167
+ }
168
+
169
+ /**
170
+ * The whole working-day index nearest to `x` on the timeline. Under `show`
171
+ * x is linear in calendar days, so the nearest calendar day is read first
172
+ * and then counted in working days (a non-working day counts as the next
173
+ * working day), which keeps the dates engine A reports equal to engine C's.
174
+ */
175
+ function dayAtX(x: number, ctx: LayoutContext): number {
176
+ const days = Math.round((x - ctx.timeline.originX) / ctx.timeline.pixelsPerDay);
177
+ if (!ctx.scale.showsNonWorking) return days;
178
+ const start = ctx.timeline.startDate;
179
+ return ctx.calendar.workingIndexOf(start, addDays(start, days));
180
+ }
181
+
182
+ /**
183
+ * The working day whose start is nearest `x` on the timeline: a point,
184
+ * such as a start (UTC midnight). Extent growth never moves the origin or
185
+ * the pixels-per-day, so this is stable across it.
186
+ */
187
+ export function dateAtX(x: number, ctx: LayoutContext): Date {
188
+ return ctx.calendar.dateAtWorkingIndex(ctx.timeline.startDate, dayAtX(x, ctx));
189
+ }
190
+
191
+ /**
192
+ * The exclusive end date of the span [startX, endX] on the timeline
193
+ * (`spanEndDate` over the nearest whole working days): a Mon–Fri span
194
+ * ends on Saturday.
195
+ */
196
+ export function spanEndDateAtX(startX: number, endX: number, ctx: LayoutContext): Date {
197
+ return spanEndDate(
198
+ ctx.calendar,
199
+ ctx.timeline.startDate,
200
+ dayAtX(startX, ctx),
201
+ dayAtX(endX, ctx),
202
+ );
203
+ }