@nowline/layout 0.8.5 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/band-scale.d.ts.map +1 -1
- 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 -154
- 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 -32
- 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 -147
- 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 +7 -7
- 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 -215
- 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 -39
- 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 -206
- 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
|
@@ -3,47 +3,74 @@
|
|
|
3
3
|
// and either pins to a fixed `date:` or floats to the rightmost `after:`
|
|
4
4
|
// predecessor. A non-binding (second-latest) predecessor drives the
|
|
5
5
|
// "slack" arrow when present. Date-pinned milestones whose predecessors
|
|
6
|
-
// would push past the date are flagged `isOverrun
|
|
6
|
+
// would push past the date are flagged `isOverrun` and slip: the diamond
|
|
7
|
+
// stays on the date, and a second one sits where the latest predecessor
|
|
8
|
+
// ends (`slipX`).
|
|
7
9
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
10
|
+
// Placement decides x, slip, slack arrows and overrun only (`MilestonePlacement`).
|
|
11
|
+
// The marker row and label box come from the final marker pack in
|
|
12
|
+
// finalize (`packMarkers`, f5, through `marker-band.ts::packMarkerRow`),
|
|
13
|
+
// which runs once every centerX is known and the canvas is final;
|
|
14
|
+
// `positionMilestones` then builds each `PositionedMilestone` from its
|
|
15
|
+
// placement and its row (`placeMarkers`, f6). An arrow that starts on a
|
|
16
|
+
// marker row (from an anchor or milestone predecessor) waits for those
|
|
17
|
+
// rows too: placement records the marker, `positionMilestones` reads its
|
|
18
|
+
// row.
|
|
14
19
|
|
|
15
20
|
import type { MilestoneDeclaration } from '@nowline/core';
|
|
16
|
-
import {
|
|
21
|
+
import { formatIsoDate, parseIsoDate, propValue, propValues } from '../dsl-utils.js';
|
|
22
|
+
import { itemSlackAttachY } from '../item-port-geometry.js';
|
|
17
23
|
import type { LayoutContext } from '../layout-context.js';
|
|
18
24
|
import { resolveStyle } from '../style-resolution.js';
|
|
19
|
-
import type {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
} from '
|
|
25
|
+
import type {
|
|
26
|
+
MarkerRowPlacement,
|
|
27
|
+
Point,
|
|
28
|
+
PositionedMilestone,
|
|
29
|
+
ResolvedStyle,
|
|
30
|
+
SlackCorridor,
|
|
31
|
+
} from '../types.js';
|
|
32
|
+
import { isOnWaveBoundary, WAVE_EDGE_TOLERANCE_PX } from '../wave-layout.js';
|
|
33
|
+
import { MARKER_DIAMOND_RADIUS_PX } from './marker-geometry.js';
|
|
26
34
|
|
|
27
35
|
/**
|
|
28
|
-
* One predecessor of a milestone with everything the
|
|
29
|
-
* pipeline
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
36
|
+
* One predecessor of a milestone with everything placement and the
|
|
37
|
+
* slack-arrow pipeline need: its end in time (`endX`, also where its slack
|
|
38
|
+
* or overrun line starts, specs/true-width-layout.md §3.5), an item's
|
|
39
|
+
* attach y in the chart's local frame (bar bottom strip when text spills,
|
|
40
|
+
* nominal row midline otherwise), and its flow key (used to dedupe so a
|
|
41
|
+
* chained-flow's siblings collapse to the last entry).
|
|
33
42
|
*/
|
|
34
43
|
export interface MilestonePredecessor {
|
|
35
44
|
ref: string;
|
|
36
|
-
|
|
37
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Where the predecessor ends in time: an item's duration end, a
|
|
47
|
+
* marker's x, a wave's `E_k`. The milestone sits on the latest one and
|
|
48
|
+
* overruns a date by it (specs/scheduler-unification.md §8 decision 6);
|
|
49
|
+
* its slack or overrun line starts here.
|
|
50
|
+
*/
|
|
51
|
+
endX: number;
|
|
52
|
+
/**
|
|
53
|
+
* An item's attach y. Undefined for every other reference: a marker
|
|
54
|
+
* (its arrow starts on its marker row, known only after the final
|
|
55
|
+
* pack), a wave, a group or a parallel.
|
|
56
|
+
*/
|
|
57
|
+
y: number | undefined;
|
|
58
|
+
/** Set when `ref` names an anchor or milestone of this file. */
|
|
59
|
+
marker?: true;
|
|
38
60
|
flowKey: string;
|
|
61
|
+
/**
|
|
62
|
+
* Set when `ref` names a wave: `endX` is the seeded wave end `E_k`.
|
|
63
|
+
* Wave references draw no slack arrows (specs/waves.md §5.3).
|
|
64
|
+
*/
|
|
65
|
+
wave?: string;
|
|
39
66
|
}
|
|
40
67
|
|
|
41
68
|
/**
|
|
42
|
-
* Resolve each `after:` reference into a `MilestonePredecessor`.
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
69
|
+
* Resolve each `after:` reference into a `MilestonePredecessor`. Its
|
|
70
|
+
* `endX` is the reference's right edge in time (`entityRightEdges`, an
|
|
71
|
+
* item's duration end, a marker's cut-line x), where its slack line
|
|
72
|
+
* starts: a bar spans exactly its dates, so its end date is its edge
|
|
73
|
+
* (specs/true-width-layout.md §3.5). Refs whose target is unknown drop
|
|
47
74
|
* silently — matches the legacy continue path.
|
|
48
75
|
*/
|
|
49
76
|
export function collectMilestonePredecessors(
|
|
@@ -52,14 +79,26 @@ export function collectMilestonePredecessors(
|
|
|
52
79
|
): MilestonePredecessor[] {
|
|
53
80
|
const out: MilestonePredecessor[] = [];
|
|
54
81
|
for (const ref of refs) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const
|
|
82
|
+
// Items attach off their FINAL box (see `item-port-geometry.ts`),
|
|
83
|
+
// so a bar that a row growth moved down still sources its arrow.
|
|
84
|
+
const item = ctx.placedItems.get(ref);
|
|
85
|
+
const endX = ctx.entityRightEdges.get(ref);
|
|
86
|
+
if (endX === undefined) continue;
|
|
87
|
+
const y = item ? itemSlackAttachY(item, ctx.bandScale.bandwidth()) : undefined;
|
|
88
|
+
// A wave reads its seeded end `E_k` (logical, not inset), so a
|
|
89
|
+
// milestone bound by it sits exactly on the boundary.
|
|
90
|
+
if (!item && ctx.waves?.plan.index.has(ref)) {
|
|
91
|
+
out.push({ ref, endX, y, flowKey: `wave:${ref}`, wave: ref });
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
59
94
|
// Markers don't share a flow with anything, so use their id
|
|
60
95
|
// as a unique flow key — every marker stands on its own.
|
|
61
96
|
const flowKey = ctx.itemFlowKey.get(ref) ?? `marker:${ref}`;
|
|
62
|
-
|
|
97
|
+
if (!item && ctx.markerIds.has(ref)) {
|
|
98
|
+
out.push({ ref, endX, y, marker: true, flowKey });
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
out.push({ ref, endX, y, flowKey });
|
|
63
102
|
}
|
|
64
103
|
return out;
|
|
65
104
|
}
|
|
@@ -77,32 +116,101 @@ export function lastPredecessorPerFlow(preds: MilestonePredecessor[]): Milestone
|
|
|
77
116
|
const m = new Map<string, MilestonePredecessor>();
|
|
78
117
|
for (const p of preds) {
|
|
79
118
|
const existing = m.get(p.flowKey);
|
|
80
|
-
if (!existing || p.
|
|
119
|
+
if (!existing || p.endX > existing.endX) m.set(p.flowKey, p);
|
|
81
120
|
}
|
|
82
121
|
return Array.from(m.values());
|
|
83
122
|
}
|
|
84
123
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
124
|
+
/**
|
|
125
|
+
* Where a milestone's slack or overrun arrow starts: at `x` on a chart y
|
|
126
|
+
* (an item predecessor's attach y, in the chart's local frame), or at `x`
|
|
127
|
+
* on the row of `marker` in the marker band, which `positionMilestones`
|
|
128
|
+
* reads from the final pack.
|
|
129
|
+
*/
|
|
130
|
+
export type MilestoneArrowStart = { x: number; y: number } | { x: number; marker: string };
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Whether a marker predecessor sits on top of the milestone at `x`: their
|
|
134
|
+
* diamonds touch or overlap. It draws no arrow then, since the arrow would
|
|
135
|
+
* run between two diamonds that already meet.
|
|
136
|
+
*/
|
|
137
|
+
function isOnTopOf(p: MilestonePredecessor, x: number): boolean {
|
|
138
|
+
return Math.abs(x - p.endX) <= 2 * MARKER_DIAMOND_RADIUS_PX;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* The slack arrow a non-binding predecessor of an after-only milestone at
|
|
143
|
+
* `maxEnd` draws, or null: it must end before the milestone and be an
|
|
144
|
+
* item or a marker clear of the milestone's diamond. A wave, a group and
|
|
145
|
+
* a parallel draw none.
|
|
146
|
+
*/
|
|
147
|
+
function slackArrowStart(p: MilestonePredecessor, maxEnd: number): MilestoneArrowStart | null {
|
|
148
|
+
if (p.endX >= maxEnd || p.wave !== undefined) return null;
|
|
149
|
+
if (p.y !== undefined) return { x: p.endX, y: p.y };
|
|
150
|
+
if (p.marker && !isOnTopOf(p, maxEnd)) return { x: p.endX, marker: p.ref };
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* The predecessor a dated milestone slips to: the one that ends latest in
|
|
156
|
+
* time and is not a wave (an overrunning wave is taken separately, by
|
|
157
|
+
* `overrunningWave`).
|
|
158
|
+
*/
|
|
159
|
+
export function latestArrowPredecessor(preds: MilestonePredecessor[]): MilestonePredecessor | null {
|
|
160
|
+
let maxPred: MilestonePredecessor | null = null;
|
|
161
|
+
for (const p of preds) {
|
|
162
|
+
if (p.wave !== undefined) continue;
|
|
163
|
+
if (!maxPred || p.endX > maxPred.endX) maxPred = p;
|
|
164
|
+
}
|
|
165
|
+
return maxPred;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* The wave predecessor that overruns a dated milestone at `x`: the
|
|
170
|
+
* latest-ending one whose end lies past `x` (by more than the boundary
|
|
171
|
+
* tolerance, so a wave ending on the date does not count). Ties keep the
|
|
172
|
+
* first listed.
|
|
173
|
+
*/
|
|
174
|
+
function overrunningWave(
|
|
175
|
+
preds: MilestonePredecessor[],
|
|
176
|
+
x: number,
|
|
177
|
+
): MilestonePredecessor | undefined {
|
|
178
|
+
let latest: MilestonePredecessor | undefined;
|
|
179
|
+
for (const p of preds) {
|
|
180
|
+
if (p.wave === undefined || p.endX - x < WAVE_EDGE_TOLERANCE_PX) continue;
|
|
181
|
+
if (!latest || p.endX > latest.endX) latest = p;
|
|
182
|
+
}
|
|
183
|
+
return latest;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* A milestone as placement leaves it: its x, slack arrows, overrun and cut
|
|
188
|
+
* lines, but no marker row or label box. Those come from the unified
|
|
189
|
+
* marker re-pack, which needs every milestone's x and the final canvas;
|
|
190
|
+
* `positionMilestones` combines the two.
|
|
191
|
+
*/
|
|
192
|
+
export interface MilestonePlacement {
|
|
193
|
+
id: string;
|
|
194
|
+
title: string;
|
|
195
|
+
x: number;
|
|
196
|
+
radius: number;
|
|
197
|
+
/** True for a `date:` milestone, false for an after-only one. */
|
|
198
|
+
fixed: boolean;
|
|
199
|
+
slackArrows: MilestoneArrowStart[] | undefined;
|
|
200
|
+
isOverrun: boolean;
|
|
201
|
+
/**
|
|
202
|
+
* Where an overrun dated milestone actually lands: the end of its
|
|
203
|
+
* latest predecessor (or overrunning wave), past `x`. Omitted when it
|
|
204
|
+
* does not overrun, or when the two diamonds would meet.
|
|
205
|
+
*/
|
|
206
|
+
slipX?: number;
|
|
207
|
+
style: ResolvedStyle;
|
|
208
|
+
cutTopY: number;
|
|
209
|
+
cutBottomY: number;
|
|
210
|
+
onWaveBoundary?: true;
|
|
211
|
+
overrunByWave?: string;
|
|
212
|
+
overrunDate?: string;
|
|
213
|
+
hiddenDate?: string;
|
|
106
214
|
}
|
|
107
215
|
|
|
108
216
|
export class MilestoneNode {
|
|
@@ -115,56 +223,59 @@ export class MilestoneNode {
|
|
|
115
223
|
* Returns null when the milestone is neither date-pinned nor has a
|
|
116
224
|
* resolvable `after:` predecessor (skipped silently — matches the
|
|
117
225
|
* legacy `buildMilestones` continue-paths).
|
|
226
|
+
*
|
|
227
|
+
* Writes the milestone's entity edges and midpoint; later milestones'
|
|
228
|
+
* `after:` references read the edges. The midpoint's y is an
|
|
229
|
+
* estimate: the pre-pack row of a dated milestone, the band's first
|
|
230
|
+
* row for an after-only one. Nothing reads it before f6 replaces it
|
|
231
|
+
* with the final pack's row, except f6's coverage trace, which
|
|
232
|
+
* reports a milestone whose row differs from it.
|
|
118
233
|
*/
|
|
119
|
-
place(ctx: LayoutContext):
|
|
234
|
+
place(ctx: LayoutContext): MilestonePlacement | null {
|
|
120
235
|
const m = this.milestone;
|
|
121
236
|
const style = resolveStyle('milestone', m.properties, ctx.styleCtx);
|
|
122
237
|
const dateRaw = propValue(m.properties, 'date');
|
|
123
238
|
const afterRaw = propValues(m.properties, 'after');
|
|
124
|
-
const date =
|
|
239
|
+
const date = parseIsoDate(dateRaw);
|
|
125
240
|
const inRowY = ctx.timeline.markerRow.y;
|
|
126
241
|
const radius = MARKER_DIAMOND_RADIUS_PX;
|
|
127
242
|
const title = m.title ?? this.id;
|
|
128
243
|
|
|
129
244
|
let centerX: number | null = null;
|
|
130
|
-
let
|
|
131
|
-
let labelBox: BoundingBox | null = null;
|
|
132
|
-
let labelSide: 'left' | 'right' = 'right';
|
|
245
|
+
let estimatedCenterY: number = inRowY;
|
|
133
246
|
let fixed = false;
|
|
134
|
-
let slackArrows:
|
|
247
|
+
let slackArrows: MilestoneArrowStart[] | undefined;
|
|
135
248
|
let isOverrun = false;
|
|
249
|
+
let slipX: number | undefined;
|
|
250
|
+
let overrunByWave: string | undefined;
|
|
136
251
|
|
|
137
252
|
if (date) {
|
|
138
253
|
const x = ctx.scale.forwardWithinDomain(date);
|
|
139
254
|
if (x === null) return null;
|
|
140
255
|
centerX = x;
|
|
141
256
|
fixed = true;
|
|
142
|
-
const
|
|
143
|
-
if (
|
|
144
|
-
centerY = placement.centerY;
|
|
145
|
-
labelBox = placement.labelBox;
|
|
146
|
-
labelSide = placement.labelSide;
|
|
147
|
-
}
|
|
257
|
+
const prePacked = ctx.markerRowPlacements.get(this.id);
|
|
258
|
+
if (prePacked) estimatedCenterY = prePacked.centerY;
|
|
148
259
|
// Date-pinned milestones whose `after:` predecessors finish
|
|
149
|
-
// past the pinned date are flagged `isOverrun
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
//
|
|
260
|
+
// past the pinned date are flagged `isOverrun` and slip to the
|
|
261
|
+
// latest one's end: the diamond stays on the date, a second
|
|
262
|
+
// one and the cut line move to `slipX` (specs/rendering.md,
|
|
263
|
+
// Milestones). The wave-deadline idiom (specs/waves.md §5.3,
|
|
264
|
+
// Example 11) counts too: a wave listed in `after:` that ends
|
|
265
|
+
// past the date.
|
|
154
266
|
const preds = collectMilestonePredecessors(afterRaw, ctx);
|
|
155
267
|
const dedupedPreds = lastPredecessorPerFlow(preds);
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
268
|
+
const maxPred = latestArrowPredecessor(dedupedPreds);
|
|
269
|
+
const wave = overrunningWave(dedupedPreds, x);
|
|
270
|
+
overrunByWave = wave?.wave;
|
|
271
|
+
const landsAt = Math.max(
|
|
272
|
+
maxPred && maxPred.endX > x ? maxPred.endX : x,
|
|
273
|
+
wave?.endX ?? x,
|
|
274
|
+
);
|
|
275
|
+
if (landsAt > x) {
|
|
161
276
|
isOverrun = true;
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
x: maxPred.x,
|
|
165
|
-
y: maxPred.y > 0 ? maxPred.y : centerY,
|
|
166
|
-
},
|
|
167
|
-
];
|
|
277
|
+
// Two diamonds that would meet draw as one, on the date.
|
|
278
|
+
if (landsAt - x > 2 * MARKER_DIAMOND_RADIUS_PX) slipX = landsAt;
|
|
168
279
|
}
|
|
169
280
|
} else if (afterRaw.length > 0) {
|
|
170
281
|
// Float to the rightmost (binding) predecessor; every
|
|
@@ -178,65 +289,27 @@ export class MilestoneNode {
|
|
|
178
289
|
// last-of-flow arrow.
|
|
179
290
|
const preds = collectMilestonePredecessors(afterRaw, ctx);
|
|
180
291
|
const dedupedPreds = lastPredecessorPerFlow(preds);
|
|
181
|
-
dedupedPreds.sort((a, b) => b.
|
|
182
|
-
const maxEnd = dedupedPreds[0]?.
|
|
292
|
+
dedupedPreds.sort((a, b) => b.endX - a.endX);
|
|
293
|
+
const maxEnd = dedupedPreds[0]?.endX ?? ctx.timeline.originX;
|
|
183
294
|
centerX = maxEnd;
|
|
184
295
|
fixed = false;
|
|
185
|
-
|
|
186
|
-
// tick-order pack across all markers (date-pinned anchors,
|
|
187
|
-
// date-pinned milestones, after-only milestones) once
|
|
188
|
-
// everyone's centerX is known. That pass overwrites the
|
|
189
|
-
// rowIndex / centerY / labelBox / labelSide we set here.
|
|
190
|
-
const provisional = decideLabelBoxForCanvas(
|
|
191
|
-
centerX,
|
|
192
|
-
inRowY,
|
|
193
|
-
radius,
|
|
194
|
-
title,
|
|
195
|
-
10,
|
|
196
|
-
true,
|
|
197
|
-
ctx.timeline.box.x,
|
|
198
|
-
ctx.chartRightX,
|
|
199
|
-
);
|
|
200
|
-
centerY = inRowY;
|
|
201
|
-
labelBox = provisional.box;
|
|
202
|
-
labelSide = provisional.side;
|
|
203
|
-
ctx.markerRowPlacements.set(this.id, {
|
|
204
|
-
rowIndex: 0,
|
|
205
|
-
centerY,
|
|
206
|
-
labelBox,
|
|
207
|
-
labelSide,
|
|
208
|
-
});
|
|
209
|
-
const arrows: Array<{ x: number; y: number }> = [];
|
|
296
|
+
const arrows: MilestoneArrowStart[] = [];
|
|
210
297
|
for (let i = 1; i < dedupedPreds.length; i++) {
|
|
211
|
-
const
|
|
212
|
-
if (
|
|
298
|
+
const arrow = slackArrowStart(dedupedPreds[i], maxEnd);
|
|
299
|
+
if (arrow) arrows.push(arrow);
|
|
213
300
|
}
|
|
214
301
|
if (arrows.length > 0) slackArrows = arrows;
|
|
215
302
|
}
|
|
216
303
|
if (centerX === null) return null;
|
|
217
|
-
if (!labelBox) {
|
|
218
|
-
const fallback = decideLabelBoxForCanvas(
|
|
219
|
-
centerX,
|
|
220
|
-
centerY,
|
|
221
|
-
radius,
|
|
222
|
-
title,
|
|
223
|
-
10,
|
|
224
|
-
true,
|
|
225
|
-
ctx.timeline.box.x,
|
|
226
|
-
ctx.chartRightX,
|
|
227
|
-
);
|
|
228
|
-
labelBox = fallback.box;
|
|
229
|
-
labelSide = fallback.side;
|
|
230
|
-
}
|
|
231
304
|
|
|
232
|
-
const
|
|
233
|
-
ctx.entityLeftEdges.set(this.id,
|
|
234
|
-
ctx.entityRightEdges.set(this.id,
|
|
235
|
-
ctx.entityMidpoints.set(this.id,
|
|
236
|
-
|
|
305
|
+
const midpoint: Point = { x: centerX, y: estimatedCenterY };
|
|
306
|
+
ctx.entityLeftEdges.set(this.id, midpoint.x);
|
|
307
|
+
ctx.entityRightEdges.set(this.id, midpoint.x);
|
|
308
|
+
ctx.entityMidpoints.set(this.id, midpoint);
|
|
309
|
+
const placement: MilestonePlacement = {
|
|
237
310
|
id: this.id,
|
|
238
311
|
title,
|
|
239
|
-
|
|
312
|
+
x: centerX,
|
|
240
313
|
radius,
|
|
241
314
|
fixed,
|
|
242
315
|
slackArrows,
|
|
@@ -244,20 +317,132 @@ export class MilestoneNode {
|
|
|
244
317
|
style,
|
|
245
318
|
cutTopY: ctx.chartTopY,
|
|
246
319
|
cutBottomY: ctx.swimlaneBottomY,
|
|
247
|
-
labelBox,
|
|
248
|
-
labelSide,
|
|
249
320
|
};
|
|
321
|
+
if (slipX !== undefined) placement.slipX = slipX;
|
|
322
|
+
// On a wave boundary the boundary line carries the vertical. A
|
|
323
|
+
// slipped milestone's cut line sits at `slipX`; an overrun one that
|
|
324
|
+
// did not slip keeps its red cut line, so it never counts as on a
|
|
325
|
+
// boundary.
|
|
326
|
+
const cutX = slipX ?? centerX;
|
|
327
|
+
if (ctx.waves && (slipX !== undefined || !isOverrun) && isOnWaveBoundary(ctx.waves, cutX)) {
|
|
328
|
+
placement.onWaveBoundary = true;
|
|
329
|
+
}
|
|
330
|
+
if (overrunByWave !== undefined && date) {
|
|
331
|
+
placement.overrunByWave = overrunByWave;
|
|
332
|
+
placement.overrunDate = formatIsoDate(date);
|
|
333
|
+
}
|
|
334
|
+
// A date the axis hides keeps its own day as a tooltip
|
|
335
|
+
// (specs/working-calendar.md §7.4). Under `show` the day is drawn,
|
|
336
|
+
// and the milestone sits on it.
|
|
337
|
+
if (date && !ctx.scale.showsNonWorking && !ctx.calendar.isWorkingDay(date)) {
|
|
338
|
+
placement.hiddenDate = formatIsoDate(date);
|
|
339
|
+
}
|
|
340
|
+
return placement;
|
|
250
341
|
}
|
|
251
342
|
}
|
|
252
343
|
|
|
253
344
|
export function buildMilestones(
|
|
254
345
|
milestones: Map<string, MilestoneDeclaration>,
|
|
255
346
|
ctx: LayoutContext,
|
|
347
|
+
): MilestonePlacement[] {
|
|
348
|
+
const out: MilestonePlacement[] = [];
|
|
349
|
+
for (const [id, m] of milestones) {
|
|
350
|
+
const placement = new MilestoneNode(id, m).place(ctx);
|
|
351
|
+
if (placement) out.push(placement);
|
|
352
|
+
}
|
|
353
|
+
return out;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Build the positioned milestones from their placements and the unified
|
|
358
|
+
* marker re-pack (`rows`, keyed by id), in placement order. The re-pack
|
|
359
|
+
* packs every placed milestone and anchor, so a missing row is an
|
|
360
|
+
* invariant violation and throws. An arrow that starts on a marker row
|
|
361
|
+
* takes that row's centre, already final; one from an item keeps its
|
|
362
|
+
* chart y. Optional keys stay omitted when the placement omits them.
|
|
363
|
+
*/
|
|
364
|
+
export function positionMilestones(
|
|
365
|
+
placements: readonly MilestonePlacement[],
|
|
366
|
+
rows: ReadonlyMap<string, MarkerRowPlacement>,
|
|
256
367
|
): PositionedMilestone[] {
|
|
257
|
-
const
|
|
368
|
+
const rowOf = (id: string): MarkerRowPlacement => {
|
|
369
|
+
const row = rows.get(id);
|
|
370
|
+
if (!row) {
|
|
371
|
+
throw new Error(`layout: marker "${id}" has no marker-row placement`);
|
|
372
|
+
}
|
|
373
|
+
return row;
|
|
374
|
+
};
|
|
375
|
+
return placements.map((p) => {
|
|
376
|
+
const row = rowOf(p.id);
|
|
377
|
+
const positioned: PositionedMilestone = {
|
|
378
|
+
id: p.id,
|
|
379
|
+
title: p.title,
|
|
380
|
+
center: { x: p.x, y: row.centerY },
|
|
381
|
+
radius: p.radius,
|
|
382
|
+
fixed: p.fixed,
|
|
383
|
+
slackArrows: p.slackArrows?.map((a) =>
|
|
384
|
+
'y' in a ? { x: a.x, y: a.y } : { x: a.x, y: rowOf(a.marker).centerY },
|
|
385
|
+
),
|
|
386
|
+
isOverrun: p.isOverrun,
|
|
387
|
+
style: p.style,
|
|
388
|
+
cutTopY: p.cutTopY,
|
|
389
|
+
cutBottomY: p.cutBottomY,
|
|
390
|
+
labelBox: row.labelBox,
|
|
391
|
+
labelSide: row.labelSide,
|
|
392
|
+
};
|
|
393
|
+
if (p.slipX !== undefined) positioned.slip = { x: p.slipX };
|
|
394
|
+
if (p.onWaveBoundary) positioned.onWaveBoundary = true;
|
|
395
|
+
if (p.overrunByWave !== undefined) positioned.overrunByWave = p.overrunByWave;
|
|
396
|
+
if (p.overrunDate !== undefined) positioned.overrunDate = p.overrunDate;
|
|
397
|
+
if (p.hiddenDate !== undefined) positioned.hiddenDate = p.hiddenDate;
|
|
398
|
+
return positioned;
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Collect slack-arrow corridors from the resolved milestones using the
|
|
404
|
+
* current entity-edge maps. The result mirrors what
|
|
405
|
+
* `MilestoneNode.place` would emit on `slackArrows`, so the row-packer
|
|
406
|
+
* and the renderer agree on every arrow's geometry.
|
|
407
|
+
*
|
|
408
|
+
* Each non-binding item predecessor (`x < maxEnd`) of an after-only
|
|
409
|
+
* milestone becomes one corridor. A date-pinned milestone draws no arrow
|
|
410
|
+
* (an overrun slips it, in the marker band), so it owns none. An arrow
|
|
411
|
+
* from a marker runs in the marker band, above the lanes, so it owns no
|
|
412
|
+
* corridor; nor does a wave predecessor, which draws no slack arrow (a
|
|
413
|
+
* floating milestone bound by a wave still sets `maxEnd`).
|
|
414
|
+
*/
|
|
415
|
+
export function collectSlackCorridors(
|
|
416
|
+
milestones: Map<string, import('@nowline/core').MilestoneDeclaration>,
|
|
417
|
+
ctx: LayoutContext,
|
|
418
|
+
): SlackCorridor[] {
|
|
419
|
+
const out: SlackCorridor[] = [];
|
|
258
420
|
for (const [id, m] of milestones) {
|
|
259
|
-
|
|
260
|
-
if (
|
|
421
|
+
// A dated milestone draws no arrow, so it owns no corridor.
|
|
422
|
+
if (parseIsoDate(propValue(m.properties, 'date'))) continue;
|
|
423
|
+
// Reuse MilestoneNode's helpers so the corridor set agrees
|
|
424
|
+
// exactly with the rendered slack-arrow set: same source x
|
|
425
|
+
// (visual edge for items / cut-line for markers), same
|
|
426
|
+
// attach y, and the same flow-dedupe rule that collapses
|
|
427
|
+
// chained predecessors to one arrow per flow.
|
|
428
|
+
const preds = collectMilestonePredecessors(propValues(m.properties, 'after'), ctx);
|
|
429
|
+
const dedupedPreds = lastPredecessorPerFlow(preds);
|
|
430
|
+
if (dedupedPreds.length === 0) continue;
|
|
431
|
+
dedupedPreds.sort((a, b) => b.endX - a.endX);
|
|
432
|
+
const maxEnd = dedupedPreds[0].endX;
|
|
433
|
+
for (let i = 1; i < dedupedPreds.length; i++) {
|
|
434
|
+
const p = dedupedPreds[i];
|
|
435
|
+
const arrow = slackArrowStart(p, maxEnd);
|
|
436
|
+
if (arrow && 'y' in arrow) {
|
|
437
|
+
out.push({
|
|
438
|
+
xStart: p.endX,
|
|
439
|
+
xEnd: maxEnd,
|
|
440
|
+
y: arrow.y,
|
|
441
|
+
slackPredId: p.ref,
|
|
442
|
+
milestoneId: id,
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
}
|
|
261
446
|
}
|
|
262
447
|
return out;
|
|
263
448
|
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// The now-line (finalize step f12): the line at `today` on the final
|
|
2
|
+
// scale, with its pill in the reserved row above the tick panel.
|
|
3
|
+
|
|
4
|
+
import { localeStrings } from '../i18n.js';
|
|
5
|
+
import type { LayoutContext } from '../layout-context.js';
|
|
6
|
+
import { resolveStyle } from '../style-resolution.js';
|
|
7
|
+
import { estimateTextWidth } from '../text-measure.js';
|
|
8
|
+
import {
|
|
9
|
+
GUTTER_PX,
|
|
10
|
+
NOW_PILL_LABEL_FONT_SIZE_PX,
|
|
11
|
+
NOW_PILL_LABEL_INSET_X_PX,
|
|
12
|
+
NOW_PILL_WIDTH_PX,
|
|
13
|
+
} from '../themes/shared.js';
|
|
14
|
+
import type { PositionedNowline } from '../types.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The now-line on the final scale. `chartRightX` is the final canvas right
|
|
18
|
+
* edge; a pill that would cross it flips to flag mode.
|
|
19
|
+
*/
|
|
20
|
+
export function buildNowline(
|
|
21
|
+
today: Date | undefined,
|
|
22
|
+
ctx: LayoutContext,
|
|
23
|
+
locale: string,
|
|
24
|
+
chartRightX: number,
|
|
25
|
+
): PositionedNowline | null {
|
|
26
|
+
if (!today) return null;
|
|
27
|
+
const x = ctx.scale.forwardWithinDomain(today);
|
|
28
|
+
if (x === null) return null;
|
|
29
|
+
// Pill row is the band reserved at the very top of the timeline area.
|
|
30
|
+
// The line drops from the bottom of the pill (top of the date headers)
|
|
31
|
+
// through any marker row and into the chart, so the pill and line stay
|
|
32
|
+
// visually connected.
|
|
33
|
+
const pillTopY = ctx.timeline.box.y;
|
|
34
|
+
const lineTopY = ctx.timeline.tickPanelY;
|
|
35
|
+
// Pill width is locale-aware: en-US's `'now'` (3 chars) fits the
|
|
36
|
+
// 36 px default trivially; longer strings like fr's `'maint.'`
|
|
37
|
+
// (6 chars) need a wider pill to avoid clipping. Floor at the
|
|
38
|
+
// default so en-US output stays byte-stable; grow when the
|
|
39
|
+
// measured label needs it. The 2× inset matches the renderer's
|
|
40
|
+
// flag-mode label inset (`NOW_PILL_LABEL_INSET_X_PX`) and gives
|
|
41
|
+
// the same visual padding to centered-mode strings.
|
|
42
|
+
const label = localeStrings(locale).nowLabel;
|
|
43
|
+
const labelTextWidth = estimateTextWidth(label, NOW_PILL_LABEL_FONT_SIZE_PX);
|
|
44
|
+
const pillWidth = Math.max(
|
|
45
|
+
NOW_PILL_WIDTH_PX,
|
|
46
|
+
Math.ceil(labelTextWidth + 2 * NOW_PILL_LABEL_INSET_X_PX),
|
|
47
|
+
);
|
|
48
|
+
// The "chart's left edge" the pill must clear is `chartLeftX` (in
|
|
49
|
+
// beside-mode, the right edge of the header card; in above-mode,
|
|
50
|
+
// the canvas left edge at x=0). originX = chartLeftX + GUTTER_PX,
|
|
51
|
+
// so we recover chartLeftX as `originX - GUTTER_PX`.
|
|
52
|
+
const chartLeftX = ctx.timeline.originX - GUTTER_PX;
|
|
53
|
+
const halfPill = pillWidth / 2;
|
|
54
|
+
let pillMode: 'center' | 'flag-right' | 'flag-left';
|
|
55
|
+
if (x - halfPill < chartLeftX) {
|
|
56
|
+
// Centered pill would intrude into the header card / past the
|
|
57
|
+
// canvas left edge — anchor the pill's LEFT side to the line
|
|
58
|
+
// and let it extend right into the chart.
|
|
59
|
+
pillMode = 'flag-right';
|
|
60
|
+
} else if (x + halfPill > chartRightX) {
|
|
61
|
+
// Centered pill would clip past the canvas right edge — anchor
|
|
62
|
+
// the pill's RIGHT side to the line and let it extend left.
|
|
63
|
+
pillMode = 'flag-left';
|
|
64
|
+
} else {
|
|
65
|
+
pillMode = 'center';
|
|
66
|
+
}
|
|
67
|
+
// Bottom-most Y the now-line should reach. When a mirrored bottom
|
|
68
|
+
// tick panel exists, thread the line through it (so the line ties
|
|
69
|
+
// the two date strips together visually). Otherwise stop at the
|
|
70
|
+
// last swimlane — never extend into the footnote area below.
|
|
71
|
+
const bottomTickPanelY = ctx.timeline.bottomTickPanelY;
|
|
72
|
+
const bottomTickPanelHeight = ctx.timeline.bottomTickPanelHeight ?? 0;
|
|
73
|
+
const lineBottomY =
|
|
74
|
+
bottomTickPanelY !== undefined && bottomTickPanelHeight > 0
|
|
75
|
+
? bottomTickPanelY + bottomTickPanelHeight
|
|
76
|
+
: ctx.swimlaneBottomY;
|
|
77
|
+
return {
|
|
78
|
+
x,
|
|
79
|
+
topY: lineTopY,
|
|
80
|
+
bottomY: lineBottomY,
|
|
81
|
+
pillTopY,
|
|
82
|
+
pillMode,
|
|
83
|
+
label,
|
|
84
|
+
pillWidth,
|
|
85
|
+
style: resolveStyle('item', [], ctx.styleCtx),
|
|
86
|
+
};
|
|
87
|
+
}
|