@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.
- package/dist/axis-columns.d.ts +70 -0
- package/dist/axis-columns.d.ts.map +1 -0
- package/dist/axis-columns.js +104 -0
- package/dist/axis-columns.js.map +1 -0
- package/dist/calendar-resolver.d.ts +27 -0
- package/dist/calendar-resolver.d.ts.map +1 -0
- package/dist/calendar-resolver.js +132 -0
- package/dist/calendar-resolver.js.map +1 -0
- package/dist/calendar.d.ts +0 -19
- package/dist/calendar.d.ts.map +1 -1
- package/dist/calendar.js +4 -44
- package/dist/calendar.js.map +1 -1
- package/dist/canvas-width.d.ts +23 -0
- package/dist/canvas-width.d.ts.map +1 -0
- package/dist/canvas-width.js +26 -0
- package/dist/canvas-width.js.map +1 -0
- package/dist/capacity.d.ts +28 -0
- package/dist/capacity.d.ts.map +1 -1
- package/dist/capacity.js +64 -29
- package/dist/capacity.js.map +1 -1
- package/dist/chart-translation.d.ts +22 -0
- package/dist/chart-translation.d.ts.map +1 -0
- package/dist/chart-translation.js +83 -0
- package/dist/chart-translation.js.map +1 -0
- package/dist/container-header-geometry.d.ts +64 -0
- package/dist/container-header-geometry.d.ts.map +1 -0
- package/dist/container-header-geometry.js +89 -0
- package/dist/container-header-geometry.js.map +1 -0
- package/dist/dsl-utils.d.ts +3 -4
- package/dist/dsl-utils.d.ts.map +1 -1
- package/dist/dsl-utils.js +8 -25
- package/dist/dsl-utils.js.map +1 -1
- package/dist/edge-routing.d.ts +69 -78
- package/dist/edge-routing.d.ts.map +1 -1
- package/dist/edge-routing.js +271 -401
- package/dist/edge-routing.js.map +1 -1
- package/dist/frame-tab-geometry.d.ts +24 -14
- package/dist/frame-tab-geometry.d.ts.map +1 -1
- package/dist/frame-tab-geometry.js +69 -23
- package/dist/frame-tab-geometry.js.map +1 -1
- package/dist/header-card-geometry.d.ts +12 -0
- package/dist/header-card-geometry.d.ts.map +1 -1
- package/dist/header-card-geometry.js +14 -2
- package/dist/header-card-geometry.js.map +1 -1
- package/dist/header-logo.d.ts +28 -0
- package/dist/header-logo.d.ts.map +1 -0
- package/dist/header-logo.js +51 -0
- package/dist/header-logo.js.map +1 -0
- package/dist/i18n.d.ts +11 -0
- package/dist/i18n.d.ts.map +1 -1
- package/dist/i18n.js +11 -1
- package/dist/i18n.js.map +1 -1
- package/dist/index.d.ts +20 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -7
- package/dist/index.js.map +1 -1
- package/dist/inline-date-pin-geometry.d.ts +19 -11
- package/dist/inline-date-pin-geometry.d.ts.map +1 -1
- package/dist/inline-date-pin-geometry.js +45 -46
- package/dist/inline-date-pin-geometry.js.map +1 -1
- package/dist/item-bar-geometry.d.ts +192 -4
- package/dist/item-bar-geometry.d.ts.map +1 -1
- package/dist/item-bar-geometry.js +258 -5
- package/dist/item-bar-geometry.js.map +1 -1
- package/dist/item-port-geometry.d.ts +13 -0
- package/dist/item-port-geometry.d.ts.map +1 -0
- package/dist/item-port-geometry.js +63 -0
- package/dist/item-port-geometry.js.map +1 -0
- package/dist/layout-context.d.ts +115 -51
- package/dist/layout-context.d.ts.map +1 -1
- package/dist/layout-context.js +96 -5
- package/dist/layout-context.js.map +1 -1
- package/dist/layout-insights.d.ts.map +1 -1
- package/dist/layout-insights.js +73 -1
- package/dist/layout-insights.js.map +1 -1
- package/dist/layout.d.ts +35 -0
- package/dist/layout.d.ts.map +1 -1
- package/dist/layout.js +18 -1315
- package/dist/layout.js.map +1 -1
- package/dist/nodes/anchor-node.d.ts.map +1 -1
- package/dist/nodes/anchor-node.js +11 -4
- package/dist/nodes/anchor-node.js.map +1 -1
- package/dist/nodes/content-extent.d.ts +24 -0
- package/dist/nodes/content-extent.d.ts.map +1 -0
- package/dist/nodes/content-extent.js +100 -0
- package/dist/nodes/content-extent.js.map +1 -0
- package/dist/nodes/dependency-edges.d.ts +6 -0
- package/dist/nodes/dependency-edges.d.ts.map +1 -0
- package/dist/nodes/dependency-edges.js +148 -0
- package/dist/nodes/dependency-edges.js.map +1 -0
- package/dist/nodes/footnote-node.d.ts +24 -3
- package/dist/nodes/footnote-node.d.ts.map +1 -1
- package/dist/nodes/footnote-node.js +129 -25
- package/dist/nodes/footnote-node.js.map +1 -1
- package/dist/nodes/group-node.d.ts +10 -21
- package/dist/nodes/group-node.d.ts.map +1 -1
- package/dist/nodes/group-node.js +79 -152
- package/dist/nodes/group-node.js.map +1 -1
- package/dist/nodes/header-node.d.ts +56 -0
- package/dist/nodes/header-node.d.ts.map +1 -0
- package/dist/nodes/header-node.js +163 -0
- package/dist/nodes/header-node.js.map +1 -0
- package/dist/nodes/include-node.d.ts +22 -12
- package/dist/nodes/include-node.d.ts.map +1 -1
- package/dist/nodes/include-node.js +95 -62
- package/dist/nodes/include-node.js.map +1 -1
- package/dist/nodes/item-measure.d.ts +135 -0
- package/dist/nodes/item-measure.d.ts.map +1 -0
- package/dist/nodes/item-measure.js +514 -0
- package/dist/nodes/item-measure.js.map +1 -0
- package/dist/nodes/item-node.d.ts +162 -9
- package/dist/nodes/item-node.d.ts.map +1 -1
- package/dist/nodes/item-node.js +256 -69
- package/dist/nodes/item-node.js.map +1 -1
- package/dist/nodes/marker-band.d.ts +118 -0
- package/dist/nodes/marker-band.d.ts.map +1 -0
- package/dist/nodes/marker-band.js +303 -0
- package/dist/nodes/marker-band.js.map +1 -0
- package/dist/nodes/marker-geometry.js +1 -1
- package/dist/nodes/marker-geometry.js.map +1 -1
- package/dist/nodes/milestone-node.d.ts +112 -15
- package/dist/nodes/milestone-node.d.ts.map +1 -1
- package/dist/nodes/milestone-node.js +246 -95
- package/dist/nodes/milestone-node.js.map +1 -1
- package/dist/nodes/nowline-node.d.ts +8 -0
- package/dist/nodes/nowline-node.d.ts.map +1 -0
- package/dist/nodes/nowline-node.js +74 -0
- package/dist/nodes/nowline-node.js.map +1 -0
- package/dist/nodes/parallel-node.d.ts +7 -12
- package/dist/nodes/parallel-node.d.ts.map +1 -1
- package/dist/nodes/parallel-node.js +66 -30
- package/dist/nodes/parallel-node.js.map +1 -1
- package/dist/nodes/region-markers.d.ts +41 -0
- package/dist/nodes/region-markers.d.ts.map +1 -0
- package/dist/nodes/region-markers.js +130 -0
- package/dist/nodes/region-markers.js.map +1 -0
- package/dist/nodes/roadmap-node.d.ts +9 -72
- package/dist/nodes/roadmap-node.d.ts.map +1 -1
- package/dist/nodes/roadmap-node.js +38 -788
- package/dist/nodes/roadmap-node.js.map +1 -1
- package/dist/nodes/row-track.d.ts +33 -0
- package/dist/nodes/row-track.d.ts.map +1 -0
- package/dist/nodes/row-track.js +170 -0
- package/dist/nodes/row-track.js.map +1 -0
- package/dist/nodes/sequence-item.d.ts +25 -0
- package/dist/nodes/sequence-item.d.ts.map +1 -0
- package/dist/nodes/sequence-item.js +396 -0
- package/dist/nodes/sequence-item.js.map +1 -0
- package/dist/nodes/sequence-one.d.ts +5 -0
- package/dist/nodes/sequence-one.d.ts.map +1 -0
- package/dist/nodes/sequence-one.js +26 -0
- package/dist/nodes/sequence-one.js.map +1 -0
- package/dist/nodes/swimlane-node.d.ts +5 -17
- package/dist/nodes/swimlane-node.d.ts.map +1 -1
- package/dist/nodes/swimlane-node.js +62 -145
- package/dist/nodes/swimlane-node.js.map +1 -1
- package/dist/nodes/wave-node.d.ts +52 -0
- package/dist/nodes/wave-node.d.ts.map +1 -0
- package/dist/nodes/wave-node.js +483 -0
- package/dist/nodes/wave-node.js.map +1 -0
- package/dist/non-working-display.d.ts +14 -0
- package/dist/non-working-display.d.ts.map +1 -0
- package/dist/non-working-display.js +28 -0
- package/dist/non-working-display.js.map +1 -0
- package/dist/phases/date-window.d.ts +48 -0
- package/dist/phases/date-window.d.ts.map +1 -0
- package/dist/phases/date-window.js +130 -0
- package/dist/phases/date-window.js.map +1 -0
- package/dist/phases/finalize.d.ts +198 -0
- package/dist/phases/finalize.d.ts.map +1 -0
- package/dist/phases/finalize.js +1058 -0
- package/dist/phases/finalize.js.map +1 -0
- package/dist/phases/measure.d.ts +77 -0
- package/dist/phases/measure.d.ts.map +1 -0
- package/dist/phases/measure.js +222 -0
- package/dist/phases/measure.js.map +1 -0
- package/dist/phases/place.d.ts +28 -0
- package/dist/phases/place.d.ts.map +1 -0
- package/dist/phases/place.js +225 -0
- package/dist/phases/place.js.map +1 -0
- package/dist/renderable.d.ts.map +1 -1
- package/dist/renderable.js +23 -3
- package/dist/renderable.js.map +1 -1
- package/dist/resolve-today.d.ts +11 -1
- package/dist/resolve-today.d.ts.map +1 -1
- package/dist/resolve-today.js +26 -12
- package/dist/resolve-today.js.map +1 -1
- package/dist/row-packer.d.ts +42 -16
- package/dist/row-packer.d.ts.map +1 -1
- package/dist/row-packer.js +51 -78
- package/dist/row-packer.js.map +1 -1
- package/dist/schedule.d.ts +58 -1
- package/dist/schedule.d.ts.map +1 -1
- package/dist/schedule.js +280 -106
- package/dist/schedule.js.map +1 -1
- package/dist/start-rules.d.ts +27 -0
- package/dist/start-rules.d.ts.map +1 -0
- package/dist/start-rules.js +91 -0
- package/dist/start-rules.js.map +1 -0
- package/dist/start-terms.d.ts +23 -0
- package/dist/start-terms.d.ts.map +1 -0
- package/dist/start-terms.js +27 -0
- package/dist/start-terms.js.map +1 -0
- package/dist/status-dot.d.ts +46 -0
- package/dist/status-dot.d.ts.map +1 -0
- package/dist/status-dot.js +78 -0
- package/dist/status-dot.js.map +1 -0
- package/dist/style-resolution.d.ts.map +1 -1
- package/dist/style-resolution.js +23 -43
- package/dist/style-resolution.js.map +1 -1
- package/dist/text-measure.d.ts +28 -0
- package/dist/text-measure.d.ts.map +1 -0
- package/dist/text-measure.js +70 -0
- package/dist/text-measure.js.map +1 -0
- package/dist/themes/contrast.d.ts +4 -0
- package/dist/themes/contrast.d.ts.map +1 -0
- package/dist/themes/contrast.js +32 -0
- package/dist/themes/contrast.js.map +1 -0
- package/dist/themes/dark.d.ts.map +1 -1
- package/dist/themes/dark.js +28 -1
- package/dist/themes/dark.js.map +1 -1
- package/dist/themes/grayscale.d.ts.map +1 -1
- package/dist/themes/grayscale.js +28 -1
- package/dist/themes/grayscale.js.map +1 -1
- package/dist/themes/index.d.ts +4 -11
- package/dist/themes/index.d.ts.map +1 -1
- package/dist/themes/index.js +6 -4
- package/dist/themes/index.js.map +1 -1
- package/dist/themes/light.d.ts.map +1 -1
- package/dist/themes/light.js +28 -1
- package/dist/themes/light.js.map +1 -1
- package/dist/themes/shape.d.ts +31 -0
- package/dist/themes/shape.d.ts.map +1 -1
- package/dist/themes/shared.d.ts +44 -50
- package/dist/themes/shared.d.ts.map +1 -1
- package/dist/themes/shared.js +79 -83
- package/dist/themes/shared.js.map +1 -1
- package/dist/time-scale.d.ts +42 -6
- package/dist/time-scale.d.ts.map +1 -1
- package/dist/time-scale.js +109 -17
- package/dist/time-scale.js.map +1 -1
- package/dist/types.d.ts +528 -11
- package/dist/types.d.ts.map +1 -1
- package/dist/view-preset.d.ts +61 -6
- package/dist/view-preset.d.ts.map +1 -1
- package/dist/view-preset.js +439 -24
- package/dist/view-preset.js.map +1 -1
- package/dist/wave-barrier.d.ts +107 -0
- package/dist/wave-barrier.d.ts.map +1 -0
- package/dist/wave-barrier.js +174 -0
- package/dist/wave-barrier.js.map +1 -0
- package/dist/wave-hatch.d.ts +4 -0
- package/dist/wave-hatch.d.ts.map +1 -0
- package/dist/wave-hatch.js +21 -0
- package/dist/wave-hatch.js.map +1 -0
- package/dist/wave-layout.d.ts +99 -0
- package/dist/wave-layout.d.ts.map +1 -0
- package/dist/wave-layout.js +130 -0
- package/dist/wave-layout.js.map +1 -0
- package/dist/working-calendar.d.ts +128 -4
- package/dist/working-calendar.d.ts.map +1 -1
- package/dist/working-calendar.js +725 -51
- package/dist/working-calendar.js.map +1 -1
- package/dist/working-span.d.ts +11 -0
- package/dist/working-span.d.ts.map +1 -0
- package/dist/working-span.js +18 -0
- package/dist/working-span.js.map +1 -0
- package/package.json +4 -4
- package/src/axis-columns.ts +140 -0
- package/src/calendar-resolver.ts +161 -0
- package/src/calendar.ts +4 -45
- package/src/canvas-width.ts +36 -0
- package/src/capacity.ts +71 -30
- package/src/chart-translation.ts +88 -0
- package/src/container-header-geometry.ts +116 -0
- package/src/dsl-utils.ts +8 -26
- package/src/edge-routing.ts +347 -491
- package/src/frame-tab-geometry.ts +78 -25
- package/src/header-card-geometry.ts +17 -2
- package/src/header-logo.ts +67 -0
- package/src/i18n.ts +22 -1
- package/src/index.ts +97 -12
- package/src/inline-date-pin-geometry.ts +55 -58
- package/src/item-bar-geometry.ts +341 -5
- package/src/item-port-geometry.ts +69 -0
- package/src/layout-context.ts +210 -72
- package/src/layout-insights.ts +109 -2
- package/src/layout.ts +57 -1530
- package/src/nodes/anchor-node.ts +11 -4
- package/src/nodes/content-extent.ts +107 -0
- package/src/nodes/dependency-edges.ts +171 -0
- package/src/nodes/footnote-node.ts +171 -31
- package/src/nodes/group-node.ts +92 -214
- package/src/nodes/header-node.ts +255 -0
- package/src/nodes/include-node.ts +124 -110
- package/src/nodes/item-measure.ts +723 -0
- package/src/nodes/item-node.ts +358 -80
- package/src/nodes/marker-band.ts +406 -0
- package/src/nodes/marker-geometry.ts +1 -1
- package/src/nodes/milestone-node.ts +321 -136
- package/src/nodes/nowline-node.ts +87 -0
- package/src/nodes/parallel-node.ts +69 -38
- package/src/nodes/region-markers.ts +194 -0
- package/src/nodes/roadmap-node.ts +46 -950
- package/src/nodes/row-track.ts +217 -0
- package/src/nodes/sequence-item.ts +433 -0
- package/src/nodes/sequence-one.ts +40 -0
- package/src/nodes/swimlane-node.ts +66 -205
- package/src/nodes/wave-node.ts +609 -0
- package/src/non-working-display.ts +32 -0
- package/src/phases/date-window.ts +191 -0
- package/src/phases/finalize.ts +1303 -0
- package/src/phases/measure.ts +329 -0
- package/src/phases/place.ts +318 -0
- package/src/renderable.ts +23 -3
- package/src/resolve-today.ts +28 -25
- package/src/row-packer.ts +85 -84
- package/src/schedule.ts +362 -114
- package/src/start-rules.ts +113 -0
- package/src/start-terms.ts +38 -0
- package/src/status-dot.ts +81 -0
- package/src/style-resolution.ts +27 -57
- package/src/text-measure.ts +71 -0
- package/src/themes/contrast.ts +34 -0
- package/src/themes/dark.ts +28 -1
- package/src/themes/grayscale.ts +28 -1
- package/src/themes/index.ts +10 -7
- package/src/themes/light.ts +28 -1
- package/src/themes/shape.ts +38 -0
- package/src/themes/shared.ts +85 -93
- package/src/time-scale.ts +117 -19
- package/src/types.ts +529 -20
- package/src/view-preset.ts +520 -27
- package/src/wave-barrier.ts +242 -0
- package/src/wave-hatch.ts +25 -0
- package/src/wave-layout.ts +203 -0
- package/src/working-calendar.ts +875 -57
- 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.
|
|
18
|
-
//
|
|
19
|
-
//
|
|
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';
|