@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
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
// Row track — the child loop that `SwimlaneNode` and `GroupNode` share
|
|
2
|
+
// (specs/layout-phases.md §3.8). Feeds a lane's or a group's content to
|
|
3
|
+
// a `RowPacker` in DSL order: an item is measured (`measureItem`) so the
|
|
4
|
+
// packer can bump it to a fresh row before `sequenceItem` places it with
|
|
5
|
+
// the same measure; a parallel or nested group takes a fresh row of its own through
|
|
6
|
+
// `sequenceOne`. The callers keep what differs between them: the lane
|
|
7
|
+
// its title tab and top-pad collapse, the group its header and body. Both
|
|
8
|
+
// read the packer's used height and right edge, and the track's span in
|
|
9
|
+
// time, from the result.
|
|
10
|
+
|
|
11
|
+
import type { GroupBlock, SwimlaneDeclaration } from '@nowline/core';
|
|
12
|
+
import { isItemDeclaration } from '@nowline/core';
|
|
13
|
+
import { type TrackNode, trackChildren } from '@nowline/core/internal';
|
|
14
|
+
import {
|
|
15
|
+
CONTAINER_HEADER_CLEARANCE_PX,
|
|
16
|
+
containerHeaderRightX,
|
|
17
|
+
} from '../container-header-geometry.js';
|
|
18
|
+
import { type LayoutContext, type LayoutHelpers, newCursor } from '../layout-context.js';
|
|
19
|
+
import { RowPacker } from '../row-packer.js';
|
|
20
|
+
import { resolveChildStart } from '../start-rules.js';
|
|
21
|
+
import type { PositionedItem, PositionedTrackChild } from '../types.js';
|
|
22
|
+
import { itemSpanPx } from '../working-span.js';
|
|
23
|
+
import { trackChildPaintedRightX } from './content-extent.js';
|
|
24
|
+
import { measureItem } from './item-measure.js';
|
|
25
|
+
import { sequenceItem } from './sequence-item.js';
|
|
26
|
+
|
|
27
|
+
/** Ids of every named item under `node`, `node` included. */
|
|
28
|
+
function itemIdsUnder(node: TrackNode): string[] {
|
|
29
|
+
if (isItemDeclaration(node)) return node.name ? [node.name] : [];
|
|
30
|
+
return trackChildren(node).flatMap(itemIdsUnder);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface RowTrackOrigin {
|
|
34
|
+
/** Left edge of the track: the lane's left x, or the group's content left x. */
|
|
35
|
+
leftX: number;
|
|
36
|
+
/** Top of the first row (the packer's `originY`). */
|
|
37
|
+
firstRowY: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface RowTrackResult {
|
|
41
|
+
children: PositionedTrackChild[];
|
|
42
|
+
/**
|
|
43
|
+
* Compact cursor, in time: item duration ends and blocks' compact ends
|
|
44
|
+
* only, never a caption spill.
|
|
45
|
+
*/
|
|
46
|
+
timeCursorX: number;
|
|
47
|
+
/**
|
|
48
|
+
* Earliest child start, in time: where a group's bracket and body open.
|
|
49
|
+
* The track's left x when it has no children.
|
|
50
|
+
*/
|
|
51
|
+
timeStartX: number;
|
|
52
|
+
/** The packer the children were fed to; read `usedHeight()` and `usedRightX()` from it. */
|
|
53
|
+
packer: RowPacker;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Sequence a lane's or a group's track children (core's `trackChildren`:
|
|
58
|
+
* description directives skipped) into rows starting at `origin`, in DSL
|
|
59
|
+
* order. Nested blocks recurse through `sequenceOne`, which carries the
|
|
60
|
+
* inherited state (cursor, flow key) the walk alone does not.
|
|
61
|
+
*/
|
|
62
|
+
export function sequenceRowTrack(
|
|
63
|
+
container: SwimlaneDeclaration | GroupBlock,
|
|
64
|
+
origin: RowTrackOrigin,
|
|
65
|
+
ctx: LayoutContext,
|
|
66
|
+
helpers: LayoutHelpers,
|
|
67
|
+
): RowTrackResult {
|
|
68
|
+
const step = ctx.bandScale.step();
|
|
69
|
+
const leftX = origin.leftX;
|
|
70
|
+
|
|
71
|
+
// Topmost-fit row pack. See `RowPacker` for the full contract.
|
|
72
|
+
// The packer owns the rows; we feed it children in DSL order and
|
|
73
|
+
// it returns each child's resolved (rowIndex, y).
|
|
74
|
+
const packer = new RowPacker({
|
|
75
|
+
laneLeftX: leftX,
|
|
76
|
+
originY: origin.firstRowY,
|
|
77
|
+
minRowHeight: step,
|
|
78
|
+
slackCorridors: ctx.slackCorridors,
|
|
79
|
+
});
|
|
80
|
+
let timeCursorX = leftX;
|
|
81
|
+
let timeStartX = Number.POSITIVE_INFINITY;
|
|
82
|
+
// Items not yet placed in this track: an item never bumps for the slack
|
|
83
|
+
// arrow of one still to come, since rows never move up and that item
|
|
84
|
+
// would follow it down.
|
|
85
|
+
const pendingIds = new Set(trackChildren(container).flatMap(itemIdsUnder));
|
|
86
|
+
|
|
87
|
+
const children: PositionedTrackChild[] = [];
|
|
88
|
+
for (const child of trackChildren(container)) {
|
|
89
|
+
for (const id of itemIdsUnder(child)) pendingIds.delete(id);
|
|
90
|
+
if (!isItemDeclaration(child)) {
|
|
91
|
+
const blockStart = resolveChildStart(child, timeCursorX, ctx);
|
|
92
|
+
const { rowIndex, y: blockY } = packer.placeBlock();
|
|
93
|
+
const cursor = newCursor(blockStart, blockY);
|
|
94
|
+
const positioned = helpers.sequenceOne(child, cursor, ctx);
|
|
95
|
+
children.push(positioned);
|
|
96
|
+
// A block's box spans only its dates, so a caption its children
|
|
97
|
+
// spill and a header title that runs past its end are outside
|
|
98
|
+
// it; the row reserves them, as it does an item's spill, so the
|
|
99
|
+
// next child drops a row rather than landing on them
|
|
100
|
+
// (specs/true-width-layout.md §3.2, §3.3). The time cursor takes
|
|
101
|
+
// the block's end in time, so that child still starts on its
|
|
102
|
+
// date, as in engine C (specs/scheduler-unification.md §8
|
|
103
|
+
// decision 7).
|
|
104
|
+
const blockEnd = positioned.box.x + positioned.box.width;
|
|
105
|
+
const blockHeight = Math.max(step, cursor.height);
|
|
106
|
+
const contentRight = reservedRightX(positioned);
|
|
107
|
+
packer.commitBlock({
|
|
108
|
+
rowIndex,
|
|
109
|
+
placed: positioned,
|
|
110
|
+
blockHeight,
|
|
111
|
+
blockEnd,
|
|
112
|
+
contentOffsetY: firstTrackOffsetY(positioned),
|
|
113
|
+
spillReservation: contentRight > blockEnd ? contentRight : null,
|
|
114
|
+
paintedRight: trackChildPaintedRightX(positioned),
|
|
115
|
+
});
|
|
116
|
+
timeStartX = Math.min(timeStartX, cursor.startX);
|
|
117
|
+
timeCursorX = Math.max(timeCursorX, cursor.x);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const item = child;
|
|
122
|
+
const desiredStart = resolveChildStart(item, timeCursorX, ctx);
|
|
123
|
+
// Measure the item where `sequenceItem` will place it, so the row
|
|
124
|
+
// pack can decide BEFORE handing off: the measure gives the logical
|
|
125
|
+
// extent and the bar's growth, and `sequenceItem` reuses it. Under
|
|
126
|
+
// `show` the bar snaps to a working day; the cursor below keeps the
|
|
127
|
+
// raw start.
|
|
128
|
+
const predictedStart = ctx.scale.startX(desiredStart);
|
|
129
|
+
const measure = measureItem(item, predictedStart, ctx);
|
|
130
|
+
const desiredEnd = predictedStart + measure.naturalWidth;
|
|
131
|
+
// Row pitch = `step()` + the bar's growth. Keeps the inter-row
|
|
132
|
+
// visible gap (= step - bandwidth) intact when a title wraps or an
|
|
133
|
+
// item's labels wrap and grow the bar. The placed box is
|
|
134
|
+
// `bandwidth + barExtra` tall, so this is also the placed pitch.
|
|
135
|
+
const pitch = step + measure.barExtra;
|
|
136
|
+
const { rowIndex, y: rowY } = packer.placeItem({
|
|
137
|
+
childId: item.name ?? '',
|
|
138
|
+
desiredStart: predictedStart,
|
|
139
|
+
desiredEnd,
|
|
140
|
+
predictedHeight: pitch,
|
|
141
|
+
pendingIds,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const cursor = newCursor(desiredStart, rowY);
|
|
145
|
+
const positioned = sequenceItem(item, cursor, ctx, measure);
|
|
146
|
+
children.push(positioned);
|
|
147
|
+
timeStartX = Math.min(timeStartX, cursor.startX);
|
|
148
|
+
|
|
149
|
+
// The painted footprint in LOGICAL space: the item's span, or the
|
|
150
|
+
// painted box when the 1 px floor makes it wider. The row packer
|
|
151
|
+
// keeps it, so a following bar never overlaps it.
|
|
152
|
+
const itemLogicalEnd = Math.max(
|
|
153
|
+
predictedStart + measure.naturalWidth,
|
|
154
|
+
positioned.box.x + positioned.box.width,
|
|
155
|
+
);
|
|
156
|
+
// The time cursor advances by the duration, so the next chained
|
|
157
|
+
// item starts on its date, as in engine C.
|
|
158
|
+
const durationEnd = predictedStart + itemSpanPx(ctx, predictedStart, measure.durationDays);
|
|
159
|
+
timeCursorX = Math.max(timeCursorX, durationEnd);
|
|
160
|
+
|
|
161
|
+
packer.commitItem({
|
|
162
|
+
rowIndex,
|
|
163
|
+
placed: positioned,
|
|
164
|
+
logicalEnd: itemLogicalEnd,
|
|
165
|
+
spillReservation: itemSpillReservation(positioned),
|
|
166
|
+
rowHeight: pitch,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (timeStartX === Number.POSITIVE_INFINITY) timeStartX = ctx.scale.startX(leftX);
|
|
171
|
+
return { children, timeCursorX, timeStartX, packer };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* How far below a block's top its first track starts: its header band plus
|
|
176
|
+
* body pad, read off the first child so a block that reserves no chrome (a
|
|
177
|
+
* wave-only group) reads 0.
|
|
178
|
+
*/
|
|
179
|
+
function firstTrackOffsetY(block: PositionedTrackChild): number {
|
|
180
|
+
if (block.kind === 'item') return 0;
|
|
181
|
+
return (block.children[0]?.box.y ?? block.box.y) - block.box.y;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* The row reservation an item's spill claims, or null when nothing spills.
|
|
186
|
+
* `decorationsRightX` already aggregates the spilled dot / icon / footnote /
|
|
187
|
+
* caption right edges from `sequenceItem`; the chip column tracks separately
|
|
188
|
+
* via `chipsRightX`. A 6 px buffer leaves the next chained item's bar a
|
|
189
|
+
* visible gutter past the spilled cluster instead of butting against it.
|
|
190
|
+
*/
|
|
191
|
+
function itemSpillReservation(item: PositionedItem): number | null {
|
|
192
|
+
const spills =
|
|
193
|
+
item.textSpills ||
|
|
194
|
+
item.chipsOutside ||
|
|
195
|
+
item.dotSpills ||
|
|
196
|
+
item.iconSpills ||
|
|
197
|
+
item.footnoteSpills;
|
|
198
|
+
if (!spills) return null;
|
|
199
|
+
return Math.max(item.decorationsRightX, item.chipsOutside ? item.chipsRightX : 0) + 6;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Rightmost x a placed child reserves on its row: an item's bar or spill
|
|
204
|
+
* reservation; for a group or parallel, whose box spans only its dates, the
|
|
205
|
+
* furthest of its header (title and glyphs, plus clearance) and its
|
|
206
|
+
* children's reservations.
|
|
207
|
+
*/
|
|
208
|
+
function reservedRightX(child: PositionedTrackChild): number {
|
|
209
|
+
const boxRight = child.box.x + child.box.width;
|
|
210
|
+
if (child.kind === 'item') return Math.max(boxRight, itemSpillReservation(child) ?? boxRight);
|
|
211
|
+
const headerRight = containerHeaderRightX(child);
|
|
212
|
+
return Math.max(
|
|
213
|
+
boxRight,
|
|
214
|
+
headerRight > boxRight ? headerRight + CONTAINER_HEADER_CLEARANCE_PX : boxRight,
|
|
215
|
+
...child.children.map(reservedRightX),
|
|
216
|
+
);
|
|
217
|
+
}
|
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
// sequenceItem — place one item on its track: resolve its start (pins,
|
|
2
|
+
// `after:` chains, the wave floor), take its measure (`measureItem`, the
|
|
3
|
+
// row track hands in the one it packed the row with), apply the `before:`
|
|
4
|
+
// caps, build its `ItemNode` and lay out the chips and the spill column,
|
|
5
|
+
// then register it in the context's entity maps.
|
|
6
|
+
|
|
7
|
+
import type { ItemDeclaration } from '@nowline/core';
|
|
8
|
+
import { parseIsoDate, propValue, propValues } from '../dsl-utils.js';
|
|
9
|
+
import { computeItemInlineDatePins, pickInlineDate } from '../inline-date-pin-geometry.js';
|
|
10
|
+
import {
|
|
11
|
+
ITEM_CAPTION_INSET_X_PX,
|
|
12
|
+
ITEM_CAPTION_META_BASELINE_OFFSET_PX,
|
|
13
|
+
ITEM_CAPTION_SPILL_GAP_PX,
|
|
14
|
+
ITEM_CAPTION_TITLE_FONT_SIZE_PX,
|
|
15
|
+
LABEL_CHIP_GAP_ABOVE_PROGRESS_STRIP_PX,
|
|
16
|
+
LABEL_CHIP_GAP_BETWEEN_PX,
|
|
17
|
+
LABEL_CHIP_HEIGHT_PX,
|
|
18
|
+
LABEL_CHIP_ROW_STEP_PX,
|
|
19
|
+
layoutSpillCluster,
|
|
20
|
+
MIN_BAR_WIDTH_FOR_DOT_PX,
|
|
21
|
+
MIN_BAR_WIDTH_FOR_FOOTNOTE_PX,
|
|
22
|
+
} from '../item-bar-geometry.js';
|
|
23
|
+
import type { LayoutContext, TrackCursor } from '../layout-context.js';
|
|
24
|
+
import { nonWorkingPinOf, resolvePinnedOrSequentialStart } from '../start-rules.js';
|
|
25
|
+
import { statusDotColor } from '../status-dot.js';
|
|
26
|
+
import { estimateTextWidth } from '../text-measure.js';
|
|
27
|
+
import { PROGRESS_STRIP_HEIGHT_PX } from '../themes/shared.js';
|
|
28
|
+
import type { BoundingBox, PositionedItem, PositionedLabelChip } from '../types.js';
|
|
29
|
+
import { waveHatchKind } from '../wave-hatch.js';
|
|
30
|
+
import { accumulateWaveMember, waveFloorX, waveRoleOf } from '../wave-layout.js';
|
|
31
|
+
import { itemSpanPx } from '../working-span.js';
|
|
32
|
+
import { buildLabelChip, type ItemMeasure, measureItem } from './item-measure.js';
|
|
33
|
+
import { ItemNode, traceItemNode } from './item-node.js';
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Place `node` at `cursor` and advance the cursor: `cursor.x` to its
|
|
37
|
+
* duration end.
|
|
38
|
+
* `measured` is the item's measure at the snapped start it is placed at,
|
|
39
|
+
* when the caller (a lane or group row track) already took it to pack the
|
|
40
|
+
* row; a parallel track's child is measured here.
|
|
41
|
+
*/
|
|
42
|
+
export function sequenceItem(
|
|
43
|
+
node: ItemDeclaration,
|
|
44
|
+
cursor: TrackCursor,
|
|
45
|
+
ctx: LayoutContext,
|
|
46
|
+
measured?: ItemMeasure,
|
|
47
|
+
): PositionedItem {
|
|
48
|
+
const props = node.properties;
|
|
49
|
+
const afterRaw = propValues(props, 'after');
|
|
50
|
+
const beforeRaw = propValues(props, 'before');
|
|
51
|
+
|
|
52
|
+
// Resolve start x with the swimlane child rule: the latest of the
|
|
53
|
+
// after-chain and the cursor position. A lane or group child's cursor
|
|
54
|
+
// already sits at that start; a direct parallel track's cursor sits at
|
|
55
|
+
// the block's start, so this is where its `after:` applies.
|
|
56
|
+
let startX = resolvePinnedOrSequentialStart(props, cursor.x, ctx);
|
|
57
|
+
// Wave barrier (specs/waves.md §5.1, §8.4): the floor is one more term
|
|
58
|
+
// in the max, after the `after:` logic. Parallel tracks reach this
|
|
59
|
+
// function directly, so this is where their floor applies.
|
|
60
|
+
if (ctx.waves) startX = waveFloorX(node, startX, ctx);
|
|
61
|
+
const nonWorkingPin = nonWorkingPinOf(props, startX, ctx);
|
|
62
|
+
|
|
63
|
+
// Under `show` a start that lands in a non-working day moves to the next
|
|
64
|
+
// working day. The floor and NL.I1008 above read the unsnapped start,
|
|
65
|
+
// so their ties and texts match `hide`; the identity under `hide`.
|
|
66
|
+
const placedStartX = ctx.scale.startX(startX);
|
|
67
|
+
// Width, style and tint, progress, meta line, chips, caption fit and
|
|
68
|
+
// the bar's growth (specs/layout-phases.md §3.6). A lane or group row
|
|
69
|
+
// track measured the item at this same start to size its row (the
|
|
70
|
+
// cursor sits at `resolveChildStart`, which an `after:` chain and the
|
|
71
|
+
// wave floor both return unchanged); a parallel track measures here.
|
|
72
|
+
const measure = measured ?? measureItem(node, placedStartX, ctx);
|
|
73
|
+
if (measure.startX !== placedStartX) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
`layout: item "${node.name ?? node.title ?? '(unnamed)'}" was measured at x ${measure.startX} but placed at x ${placedStartX}`,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
const { naturalWidth, style, status, progress, footnoteIndicators, caption } = measure;
|
|
79
|
+
const { metaText, ownerDisplay, capacity, capacityTrailingWidth } = measure.meta;
|
|
80
|
+
const linkInfo = measure.link;
|
|
81
|
+
const hasLinkIcon = linkInfo.icon !== 'none';
|
|
82
|
+
const chipSamples = measure.chips.samples;
|
|
83
|
+
const chipsOutside = measure.chips.outside;
|
|
84
|
+
// Logical extent: the item's span in time; the bar box is inset from it
|
|
85
|
+
// by its seams (specs/true-width-layout.md §3.1).
|
|
86
|
+
const logicalLeft = placedStartX;
|
|
87
|
+
const logicalRight = placedStartX + naturalWidth;
|
|
88
|
+
// Duration end: where the item ends in time. The time paths take it
|
|
89
|
+
// (the cursor, `after:` references, the wave barrier), so what follows
|
|
90
|
+
// starts on its date, as in engine C; the bar is drawn inside it
|
|
91
|
+
// (specs/true-width-layout.md §3.1).
|
|
92
|
+
const durationEndX = placedStartX + itemSpanPx(ctx, placedStartX, measure.durationDays);
|
|
93
|
+
|
|
94
|
+
// Handle `before:` — item must end by the earliest cap. Each cap is
|
|
95
|
+
// either an entity id (looked up in entityLeftEdges) or an inline ISO
|
|
96
|
+
// date literal (resolved through the time scale). The validator enforces
|
|
97
|
+
// "at most one inline date per direction" so at most one element per
|
|
98
|
+
// before-list will hit the date path. The cap is a time path: the item
|
|
99
|
+
// overflows when its duration end passes it; the red tail runs to the
|
|
100
|
+
// painted end.
|
|
101
|
+
let hasOverflow = false;
|
|
102
|
+
let overflowBox: BoundingBox | undefined;
|
|
103
|
+
let overflowAnchorId: string | undefined;
|
|
104
|
+
if (beforeRaw.length > 0) {
|
|
105
|
+
let earliestCapX: number | undefined;
|
|
106
|
+
let earliestCapRef: string | undefined;
|
|
107
|
+
for (const ref of beforeRaw) {
|
|
108
|
+
const inlineDate = parseIsoDate(ref);
|
|
109
|
+
const capX = inlineDate
|
|
110
|
+
? (ctx.scale.forwardWithinDomain(inlineDate) ?? undefined)
|
|
111
|
+
: ctx.entityLeftEdges.get(ref);
|
|
112
|
+
if (capX === undefined) continue;
|
|
113
|
+
if (earliestCapX === undefined || capX < earliestCapX) {
|
|
114
|
+
earliestCapX = capX;
|
|
115
|
+
earliestCapRef = ref;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (earliestCapX !== undefined && durationEndX > earliestCapX) {
|
|
119
|
+
hasOverflow = true;
|
|
120
|
+
overflowBox = {
|
|
121
|
+
x: earliestCapX,
|
|
122
|
+
y: cursor.y,
|
|
123
|
+
width: logicalRight - earliestCapX,
|
|
124
|
+
height: ctx.bandScale.bandwidth(),
|
|
125
|
+
};
|
|
126
|
+
overflowAnchorId = earliestCapRef;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// The bar box (grown by `measure.barExtra`) and the caption's x, inset
|
|
131
|
+
// from the logical extent logicalLeft/logicalRight.
|
|
132
|
+
const titleStr = caption.title;
|
|
133
|
+
const itemNode = new ItemNode(measure);
|
|
134
|
+
const placeCtx = { time: ctx.scale, bands: ctx.bandScale, style };
|
|
135
|
+
const placed = itemNode.place({ x: logicalLeft, y: cursor.y }, placeCtx);
|
|
136
|
+
const itemBox = placed.box;
|
|
137
|
+
const bandwidth = ctx.bandScale.bandwidth();
|
|
138
|
+
|
|
139
|
+
// Narrow-bar decoration spill — when a bar is too narrow to host
|
|
140
|
+
// the dot, link icon, or footnote with its full inset, those
|
|
141
|
+
// glyphs render in the same spill column as the (already-
|
|
142
|
+
// spilling) caption text, in reading order
|
|
143
|
+
// `[bar][dot][icon][title][footnote#…][meta]`. Each decoration's
|
|
144
|
+
// threshold is independent (a 20-px-wide bar can host the dot
|
|
145
|
+
// but not the icon, etc.); see the `MIN_BAR_WIDTH_FOR_*`
|
|
146
|
+
// constants in `item-bar-geometry`.
|
|
147
|
+
//
|
|
148
|
+
// Forcing `textSpills` when `iconSpills` keeps the icon and
|
|
149
|
+
// title visually adjacent — otherwise a spilled icon would
|
|
150
|
+
// float at `bar.right + 6` while the title stayed inside the
|
|
151
|
+
// bar, breaking the icon→title affordance.
|
|
152
|
+
const dotSpills = itemBox.width < MIN_BAR_WIDTH_FOR_DOT_PX;
|
|
153
|
+
// Measured on the same box width as the other two.
|
|
154
|
+
const iconSpills = caption.iconSpills;
|
|
155
|
+
const footnoteSpillsForNarrow = itemBox.width < MIN_BAR_WIDTH_FOR_FOOTNOTE_PX;
|
|
156
|
+
const textSpills = placed.textSpills || iconSpills;
|
|
157
|
+
// The lines the title paints on (`resolveCaptionTitleLines`).
|
|
158
|
+
const captionLines = caption.lines;
|
|
159
|
+
|
|
160
|
+
// Label chips lay out left → right at natural text width.
|
|
161
|
+
//
|
|
162
|
+
// INSIDE the bar (chipsOutside === false): single row,
|
|
163
|
+
// left-aligned at the caption inset, anchored just above the
|
|
164
|
+
// bottom progress strip.
|
|
165
|
+
//
|
|
166
|
+
// OUTSIDE the bar (chipsOutside === true): the whole chip set
|
|
167
|
+
// moves to the spill column at `bar.right + 6`. Within the
|
|
168
|
+
// column, chips pack into rows capped at the bar's visual
|
|
169
|
+
// width — see `packSpillChips`. Row 0 sits at the same y the
|
|
170
|
+
// single-row would have used; subsequent rows stack DOWNWARD by
|
|
171
|
+
// one `LABEL_CHIP_ROW_STEP_PX`.
|
|
172
|
+
//
|
|
173
|
+
// When chips spill OR the title wrapped over a meta line, the BAR
|
|
174
|
+
// ITSELF GROWS DOWNWARD so the chip column (or the second title
|
|
175
|
+
// line plus meta) reads as enclosed by the bar — the painted
|
|
176
|
+
// footprint of the bar is `bandwidth + barExtra` (the larger of
|
|
177
|
+
// the title and chip growth, `measure.barExtra`, which `ItemNode`
|
|
178
|
+
// has already applied to the box) and the bottom progress strip
|
|
179
|
+
// moves with the new bottom edge. Chip Y is anchored to the
|
|
180
|
+
// ORIGINAL bandwidth (relative to the bar's top), not to the grown
|
|
181
|
+
// box.height, so row 0 stays where a single-row chip would
|
|
182
|
+
// naturally render and rows 1..N grow downward into the new bar
|
|
183
|
+
// area.
|
|
184
|
+
//
|
|
185
|
+
// When the caption ALSO spills (`textSpills && chipsOutside`),
|
|
186
|
+
// row 0's y drops below the meta baseline so the spilled stack
|
|
187
|
+
// reads `title → meta → chip-row-0 → chip-row-1 → ...` at a
|
|
188
|
+
// single column inside the (now-taller) bar.
|
|
189
|
+
const chipPack = measure.chips.pack;
|
|
190
|
+
// Baseline (px from the bar's top) of the caption's last line: the
|
|
191
|
+
// meta line when present, else the last title line. Chips stacked
|
|
192
|
+
// inside the bar must clear it.
|
|
193
|
+
const captionLastBaseline = caption.lastBaseline;
|
|
194
|
+
const barExtra = measure.barExtra;
|
|
195
|
+
|
|
196
|
+
const labelChips: PositionedLabelChip[] = [];
|
|
197
|
+
const baseChipY =
|
|
198
|
+
itemBox.y +
|
|
199
|
+
bandwidth -
|
|
200
|
+
PROGRESS_STRIP_HEIGHT_PX -
|
|
201
|
+
LABEL_CHIP_HEIGHT_PX -
|
|
202
|
+
LABEL_CHIP_GAP_ABOVE_PROGRESS_STRIP_PX;
|
|
203
|
+
// Chips under a SPILLED caption clear the classic meta baseline (38),
|
|
204
|
+
// or the caption's own last baseline when explicit breaks make the
|
|
205
|
+
// spilled block taller than that; an in-bar caption is cleared at its
|
|
206
|
+
// own last baseline (which a wrapped title pushes down).
|
|
207
|
+
const captionStackChipY =
|
|
208
|
+
itemBox.y +
|
|
209
|
+
(chipsOutside && textSpills
|
|
210
|
+
? Math.max(ITEM_CAPTION_META_BASELINE_OFFSET_PX, captionLastBaseline)
|
|
211
|
+
: captionLastBaseline) +
|
|
212
|
+
LABEL_CHIP_GAP_ABOVE_PROGRESS_STRIP_PX;
|
|
213
|
+
// Inside-bar chips need to clear the caption's last baseline —
|
|
214
|
+
// the natural `baseChipY` (anchored to bar bottom) sits above
|
|
215
|
+
// the meta line at typical bandwidths, and above line 2 of a
|
|
216
|
+
// wrapped title, so the chip rect would overlap the text
|
|
217
|
+
// vertically. Use whichever Y is lower. With no meta and a
|
|
218
|
+
// single-line title the caption bottom is above `baseChipY`, so
|
|
219
|
+
// this resolves to `baseChipY` as before.
|
|
220
|
+
// Outside-bar chips already reuse `captionStackChipY` when the
|
|
221
|
+
// caption ALSO spills; with caption inside we don't need to
|
|
222
|
+
// shift them since they're horizontally separated from the meta.
|
|
223
|
+
const stackBelowCaption =
|
|
224
|
+
(chipsOutside && textSpills) || (!chipsOutside && chipSamples.length > 0);
|
|
225
|
+
const chipRow0Y = stackBelowCaption ? Math.max(baseChipY, captionStackChipY) : baseChipY;
|
|
226
|
+
const chipStartX = chipsOutside
|
|
227
|
+
? itemBox.x + itemBox.width + ITEM_CAPTION_SPILL_GAP_PX
|
|
228
|
+
: itemBox.x + ITEM_CAPTION_INSET_X_PX;
|
|
229
|
+
|
|
230
|
+
let chipsRightX = chipStartX;
|
|
231
|
+
if (chipPack) {
|
|
232
|
+
for (let r = 0; r < chipPack.rows.length; r += 1) {
|
|
233
|
+
const rowY = chipRow0Y + r * LABEL_CHIP_ROW_STEP_PX;
|
|
234
|
+
let rowCursorX = chipStartX;
|
|
235
|
+
for (const sample of chipPack.rows[r]) {
|
|
236
|
+
const chip = buildLabelChip(sample.id, ctx.styleCtx, rowCursorX, rowY);
|
|
237
|
+
labelChips.push(chip);
|
|
238
|
+
rowCursorX += chip.box.width + LABEL_CHIP_GAP_BETWEEN_PX;
|
|
239
|
+
}
|
|
240
|
+
const rowRight = rowCursorX - LABEL_CHIP_GAP_BETWEEN_PX;
|
|
241
|
+
if (rowRight > chipsRightX) chipsRightX = rowRight;
|
|
242
|
+
}
|
|
243
|
+
} else {
|
|
244
|
+
let rowCursorX = chipStartX;
|
|
245
|
+
for (const sample of chipSamples) {
|
|
246
|
+
const chip = buildLabelChip(sample.id, ctx.styleCtx, rowCursorX, chipRow0Y);
|
|
247
|
+
labelChips.push(chip);
|
|
248
|
+
rowCursorX += chip.box.width + LABEL_CHIP_GAP_BETWEEN_PX;
|
|
249
|
+
}
|
|
250
|
+
chipsRightX = chipSamples.length > 0 ? rowCursorX - LABEL_CHIP_GAP_BETWEEN_PX : chipStartX;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// `footnoteIndicators` (one superscript per footnote that names this
|
|
254
|
+
// item) was resolved above, before the caption fit.
|
|
255
|
+
|
|
256
|
+
const owner = ownerDisplay ?? propValue(props, 'owner');
|
|
257
|
+
const description = node.description?.text;
|
|
258
|
+
|
|
259
|
+
// Footnote glyphs only need to spill when there's at least one
|
|
260
|
+
// indicator AND the bar is too narrow to host them at the inset-
|
|
261
|
+
// right anchor. Compute the final boolean here once we know the
|
|
262
|
+
// indicator count.
|
|
263
|
+
const footnoteSpills = footnoteIndicators.length > 0 && footnoteSpillsForNarrow;
|
|
264
|
+
|
|
265
|
+
// Spill-column x positions for the decorations. The cluster
|
|
266
|
+
// mirrors the in-bar reading order so users see the same visual
|
|
267
|
+
// hierarchy whether everything fits inside or trails off to the
|
|
268
|
+
// right:
|
|
269
|
+
//
|
|
270
|
+
// In-bar (default): [icon] [title] [¹²] [dot]
|
|
271
|
+
// Spilled (narrow): [bar] [icon?] [title][¹²?] [dot?]
|
|
272
|
+
//
|
|
273
|
+
// The dot lives at the trailing edge in BOTH cases — pushing it
|
|
274
|
+
// to the LEFT of the title (with the title trailing it) read as
|
|
275
|
+
// the dot belonging to the next item, not this one. A missing
|
|
276
|
+
// decoration just collapses out of the row; e.g. an item with
|
|
277
|
+
// no link AND a too-narrow bar gives `[bar] [title] [dot]`.
|
|
278
|
+
//
|
|
279
|
+
// `decorationsRightX` is the furthest right edge any spilled
|
|
280
|
+
// glyph reaches; the row-packer uses it (alongside spilled-chip
|
|
281
|
+
// width) to reserve x-extent so the next chained item bumps to
|
|
282
|
+
// a fresh row instead of landing under the spilled cluster.
|
|
283
|
+
let captionSpillWidth: number | null = null;
|
|
284
|
+
if (textSpills) {
|
|
285
|
+
// The widest title LINE: a multi-line spill is as wide as its
|
|
286
|
+
// longest line, not as the raw string (newlines included). A
|
|
287
|
+
// caption capped at the chart's end arrives with its lines already
|
|
288
|
+
// truncated (`measureItem`).
|
|
289
|
+
const titleW = Math.max(
|
|
290
|
+
...captionLines.map((line) => estimateTextWidth(line, ITEM_CAPTION_TITLE_FONT_SIZE_PX)),
|
|
291
|
+
);
|
|
292
|
+
// Spill column width is the wider of the title and the *full* meta
|
|
293
|
+
// line (text + capacity suffix). `capacityTrailingWidth` is 0 when
|
|
294
|
+
// no capacity suffix is rendered, so this stays a no-op for items
|
|
295
|
+
// without `capacity:`.
|
|
296
|
+
const metaW = (metaText ? estimateTextWidth(metaText, 11) : 0) + capacityTrailingWidth;
|
|
297
|
+
captionSpillWidth = Math.max(titleW, metaW);
|
|
298
|
+
}
|
|
299
|
+
const spill = layoutSpillCluster({
|
|
300
|
+
barRightX: itemBox.x + itemBox.width,
|
|
301
|
+
iconSpills,
|
|
302
|
+
captionWidth: captionSpillWidth,
|
|
303
|
+
footnoteCount: footnoteSpills ? footnoteIndicators.length : 0,
|
|
304
|
+
dotSpills,
|
|
305
|
+
});
|
|
306
|
+
const { iconSpillX, footnoteSpillStartX, dotSpillCx } = spill;
|
|
307
|
+
const decorationsRightX = Math.max(itemBox.x + itemBox.width, spill.rightX);
|
|
308
|
+
|
|
309
|
+
const id = node.name;
|
|
310
|
+
// Reference-target edges resolve `after:` / `before:` lookups, so only
|
|
311
|
+
// EXPLICIT ids register here — an id-less item never becomes a
|
|
312
|
+
// referenceable target. Entity edges live in LOGICAL space so chained
|
|
313
|
+
// items / `after:` references sit on the column boundary, not on the
|
|
314
|
+
// visually inset bar edge. The visible 12 px gutter between bars then
|
|
315
|
+
// becomes a clean attach corridor. The right edge is the duration end:
|
|
316
|
+
// `after:` is a time path (decision 6). Items attach their arrows off
|
|
317
|
+
// their final box (`placedItems`), never off this map.
|
|
318
|
+
if (id) {
|
|
319
|
+
ctx.entityLeftEdges.set(id, logicalLeft);
|
|
320
|
+
ctx.entityRightEdges.set(id, durationEndX);
|
|
321
|
+
}
|
|
322
|
+
// A member's end feeds its wave's barrier: its duration end, the lane
|
|
323
|
+
// cursor's advance in `sequenceRowTrack`, never a group box or a
|
|
324
|
+
// caption spill (specs/waves.md §8.4).
|
|
325
|
+
accumulateWaveMember(node, durationEndX, ctx);
|
|
326
|
+
// Drawing / flow maps key on a registration handle EVERY item has: the
|
|
327
|
+
// explicit id when present, else a synthetic, non-referenceable handle
|
|
328
|
+
// (see `syntheticItemKey`). This lets a title-only item register its own
|
|
329
|
+
// dependency-arrow target geometry and join flow-key dedup without
|
|
330
|
+
// entering the human-referenceable namespace above. The item itself is
|
|
331
|
+
// registered in `ctx.placedItems` once it is built (below); its arrow
|
|
332
|
+
// and slack-arrow attach points are derived from its final box later.
|
|
333
|
+
const drawKey = id ?? syntheticItemKey(node);
|
|
334
|
+
ctx.itemFlowKey.set(drawKey, ctx.currentFlowKey);
|
|
335
|
+
|
|
336
|
+
// A parallel track joins, and its container's bracket spans, from
|
|
337
|
+
// `cursor.startX` to `cursor.x`.
|
|
338
|
+
cursor.startX = logicalLeft;
|
|
339
|
+
cursor.x = durationEndX;
|
|
340
|
+
// The next row in a parallel/group/lane starts at
|
|
341
|
+
// `cursor.y + cursor.height`. Default pitch is `bandScale.step()`
|
|
342
|
+
// (bandwidth + inter-row gap). When the bar grew to enclose a
|
|
343
|
+
// spilled chip column or a wrapped title, the pitch grows by the
|
|
344
|
+
// SAME amount so the inter-row gap stays constant — the next row's
|
|
345
|
+
// bar starts `step − bandwidth` px below the (now-taller) bar bottom.
|
|
346
|
+
cursor.height = Math.max(cursor.height, ctx.bandScale.step() + barExtra);
|
|
347
|
+
|
|
348
|
+
const inlineDatePins = computeItemInlineDatePins({
|
|
349
|
+
box: itemBox,
|
|
350
|
+
afterDate: pickInlineDate(afterRaw),
|
|
351
|
+
beforeDate: pickInlineDate(beforeRaw),
|
|
352
|
+
hasLinkIcon,
|
|
353
|
+
footnoteCount: footnoteIndicators.length,
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
const result: PositionedItem = {
|
|
357
|
+
kind: 'item',
|
|
358
|
+
id,
|
|
359
|
+
title: titleStr,
|
|
360
|
+
// Only present when the painted lines differ from `[title]`: an
|
|
361
|
+
// auto-wrap, explicit breaks, or a stray leading/trailing break
|
|
362
|
+
// that was trimmed away. A plain one-line title carries no key.
|
|
363
|
+
...(captionLines.length >= 2 || captionLines[0] !== titleStr
|
|
364
|
+
? { titleLines: captionLines }
|
|
365
|
+
: {}),
|
|
366
|
+
box: itemBox,
|
|
367
|
+
dateSpan: { startX: logicalLeft, endX: logicalRight },
|
|
368
|
+
status,
|
|
369
|
+
statusDotColor: statusDotColor(status, style.bg, ctx.styleCtx.theme),
|
|
370
|
+
progressFraction: progress,
|
|
371
|
+
footnoteIndicators,
|
|
372
|
+
labelChips,
|
|
373
|
+
chipsOutside,
|
|
374
|
+
chipsRightX,
|
|
375
|
+
linkIcon: linkInfo.icon,
|
|
376
|
+
linkHref: linkInfo.href,
|
|
377
|
+
hasOverflow,
|
|
378
|
+
overflowBox,
|
|
379
|
+
overflowAnchorId,
|
|
380
|
+
owner,
|
|
381
|
+
description,
|
|
382
|
+
metaText,
|
|
383
|
+
textSpills,
|
|
384
|
+
dotSpills,
|
|
385
|
+
iconSpills,
|
|
386
|
+
footnoteSpills,
|
|
387
|
+
dotSpillCx,
|
|
388
|
+
iconSpillX,
|
|
389
|
+
footnoteSpillStartX,
|
|
390
|
+
decorationsRightX,
|
|
391
|
+
capacity,
|
|
392
|
+
size: measure.size,
|
|
393
|
+
style,
|
|
394
|
+
inlineDatePins: inlineDatePins.length > 0 ? inlineDatePins : undefined,
|
|
395
|
+
};
|
|
396
|
+
// Wave fields exist only in a roadmap with waves (omitted, never
|
|
397
|
+
// undefined, so wave-free models keep their exact shape).
|
|
398
|
+
const waveRole = waveRoleOf(node, ctx);
|
|
399
|
+
if (waveRole !== undefined) result.waveRole = waveRole;
|
|
400
|
+
if (waveRole === 'background') result.waveHatch = waveHatchKind(style.bg);
|
|
401
|
+
if (nonWorkingPin !== undefined) result.nonWorkingPin = nonWorkingPin;
|
|
402
|
+
// The caption cap at the chart's end truncated a title line; the full
|
|
403
|
+
// title stays the bar's tooltip.
|
|
404
|
+
if (caption.truncated) result.titleTruncated = true;
|
|
405
|
+
// A background item carries its localized hover line from placement
|
|
406
|
+
// on (specs/waves.md §9.3); members and wave-free items carry none.
|
|
407
|
+
if (waveRole === 'background' && ctx.waves) {
|
|
408
|
+
result.waveTooltip = ctx.waves.backgroundTooltip;
|
|
409
|
+
}
|
|
410
|
+
// Register the item object itself, not coordinates sampled from it:
|
|
411
|
+
// the row packer and the marker-band shift may still move this box
|
|
412
|
+
// down, and every attach port is read off the final box (see
|
|
413
|
+
// `item-port-geometry.ts`).
|
|
414
|
+
ctx.placedItems.set(drawKey, result);
|
|
415
|
+
traceItemNode(itemNode, placeCtx, result);
|
|
416
|
+
return result;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Internal drawing handle for an id-less item. Title-only items have no
|
|
421
|
+
* `node.name`, yet they still need a stable key to register their own
|
|
422
|
+
* dependency-arrow target geometry and join flow-key dedup. The handle is
|
|
423
|
+
* derived from the item's source position so it is deterministic and
|
|
424
|
+
* byte-stable, and the `#item@line:col` form cannot be produced by the
|
|
425
|
+
* grammar's id rule — so `after:` / `before:` / `on:` can never resolve to it
|
|
426
|
+
* (an id-less item stays non-referenceable; declare an explicit id to
|
|
427
|
+
* reference it). Distinct items always start at distinct positions, so the
|
|
428
|
+
* handle is unique within a file.
|
|
429
|
+
*/
|
|
430
|
+
export function syntheticItemKey(node: ItemDeclaration): string {
|
|
431
|
+
const start = node.$cstNode?.range.start;
|
|
432
|
+
return `#item@${start ? start.line + 1 : 0}:${start ? start.character + 1 : 0}`;
|
|
433
|
+
}
|