@meltemi/diagrams-layout 1.10.0 → 1.10.18
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/audit.d.ts +8 -6
- package/dist/audit.d.ts.map +1 -1
- package/dist/audit.js +28 -14
- package/dist/audit.js.map +1 -1
- package/dist/checks.d.ts +5 -1
- package/dist/checks.d.ts.map +1 -1
- package/dist/checks.js +11 -3
- package/dist/checks.js.map +1 -1
- package/dist/frame.d.ts +2 -2
- package/dist/frame.d.ts.map +1 -1
- package/dist/frame.js +4 -1
- package/dist/frame.js.map +1 -1
- package/dist/free.d.ts +50 -3
- package/dist/free.d.ts.map +1 -1
- package/dist/free.js +187 -32
- package/dist/free.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/layout.d.ts +203 -7
- package/dist/layout.d.ts.map +1 -1
- package/dist/layout.js +883 -135
- package/dist/layout.js.map +1 -1
- package/dist/measure.d.ts +21 -2
- package/dist/measure.d.ts.map +1 -1
- package/dist/measure.js +30 -7
- package/dist/measure.js.map +1 -1
- package/dist/messages/en.d.ts +24 -1
- package/dist/messages/en.d.ts.map +1 -1
- package/dist/messages/en.js +25 -2
- package/dist/messages/en.js.map +1 -1
- package/dist/messages/es.d.ts.map +1 -1
- package/dist/messages/es.js +8 -1
- package/dist/messages/es.js.map +1 -1
- package/dist/messages/index.d.ts +7 -0
- package/dist/messages/index.d.ts.map +1 -1
- package/dist/scene.d.ts +41 -1
- package/dist/scene.d.ts.map +1 -1
- package/dist/sequence.d.ts +1 -1
- package/dist/sequence.d.ts.map +1 -1
- package/dist/sequence.js +658 -282
- package/dist/sequence.js.map +1 -1
- package/dist/styles.d.ts +5 -1
- package/dist/styles.d.ts.map +1 -1
- package/dist/styles.js +20 -4
- package/dist/styles.js.map +1 -1
- package/dist/text.d.ts +8 -0
- package/dist/text.d.ts.map +1 -1
- package/dist/text.js +132 -0
- package/dist/text.js.map +1 -1
- package/dist/theme.d.ts +5 -0
- package/dist/theme.d.ts.map +1 -1
- package/dist/theme.js +20 -0
- package/dist/theme.js.map +1 -1
- package/package.json +11 -6
package/dist/sequence.js
CHANGED
|
@@ -2,11 +2,13 @@ import { resolveScenario, } from "@meltemi/diagrams-model";
|
|
|
2
2
|
import { brandFrame, round2, textItem } from "./frame.js";
|
|
3
3
|
import { cylinderPaths, unionBoxes } from "./geometry.js";
|
|
4
4
|
import { markerItems, relationEnds, shiftMarker } from "./markers.js";
|
|
5
|
-
import { canvasSize, frameMinWidth, } from "./layout.js";
|
|
5
|
+
import { canvasSize, frameMinWidth, tagPill, tagPillWidth, } from "./layout.js";
|
|
6
|
+
import { textFont, writtenStack } from "./measure.js";
|
|
6
7
|
import { markGradient } from "./mark.js";
|
|
8
|
+
import { messages as words } from "./messages/index.js";
|
|
7
9
|
import { strokeDash, } from "./scene.js";
|
|
8
10
|
import { StyleResolver } from "./styles.js";
|
|
9
|
-
import { splitLines } from "./text.js";
|
|
11
|
+
import { splitLines, wrapLines } from "./text.js";
|
|
10
12
|
const BOX_KINDS = new Set(["participant", "database", "collections", "queue"]);
|
|
11
13
|
/** The horizontal radius of a queue's ends. */
|
|
12
14
|
const QUEUE_CAP = 8;
|
|
@@ -16,8 +18,19 @@ const STACK = 4;
|
|
|
16
18
|
const LIFELINE_OPACITY = 0.5;
|
|
17
19
|
const BAR = 10;
|
|
18
20
|
const BAR_STEP = 5;
|
|
21
|
+
/** The least room between two heads, whatever a negative `gap` asks. */
|
|
22
|
+
const HEAD_CLEARANCE = 8;
|
|
23
|
+
/** How far two distances of the columns may differ by rounding alone. */
|
|
24
|
+
const ROUNDING = 1e-6;
|
|
19
25
|
const SELF_WIDTH = 34;
|
|
20
26
|
const SELF_HEIGHT = 22;
|
|
27
|
+
/** The room between two pills of a row of tags. */
|
|
28
|
+
const TAG_GAP = 4;
|
|
29
|
+
/** How far a message's row of tags stands from the arrow's start, or from its number's disc. */
|
|
30
|
+
const TAG_START = 6;
|
|
31
|
+
const TAG_START_NUMBERED = 12;
|
|
32
|
+
/** How far a figure's badges stand above it. */
|
|
33
|
+
const BADGE_LIFT = 4;
|
|
21
34
|
/** Lays out a sequence diagram (decision 0019): columns in source order, events top to bottom. */
|
|
22
35
|
export function layoutSequence(input) {
|
|
23
36
|
const { compilation, diagram, options, theme, scale, metrics, diagnostics } = input;
|
|
@@ -33,73 +46,188 @@ export function layoutSequence(input) {
|
|
|
33
46
|
const groups = diagram.elements.filter((e) => e.kind === "group");
|
|
34
47
|
const column = new Map(participants.map((p, i) => [p.id, i]));
|
|
35
48
|
// ---------------------------------------------------------------- measures
|
|
36
|
-
|
|
37
|
-
|
|
49
|
+
// A head's label is in the theme's label size and weight unless its style writes them; the row
|
|
50
|
+
// of heads grows to the largest label written (decision 0067).
|
|
51
|
+
const headText = (p) => sequenceText(styles.element(p), scale.label, scale.labelWeight);
|
|
52
|
+
const headSizes = participants
|
|
53
|
+
.map((p) => styles.element(p).fontSize)
|
|
54
|
+
.filter((size) => size !== undefined && size > scale.label);
|
|
55
|
+
const largest = Math.max(scale.label, ...headSizes);
|
|
56
|
+
const labelLine = Math.round(largest * 1.5);
|
|
57
|
+
const boxHeight = Math.max(Math.round(40 * f), Math.round((40 * largest) / 13));
|
|
38
58
|
const figureHeight = Math.round(34 * f);
|
|
39
59
|
const hasFigures = participants.some((p) => !BOX_KINDS.has(p.kind));
|
|
40
60
|
const headHeight = Math.max(boxHeight, hasFigures ? figureHeight + 6 + labelLine : 0);
|
|
41
|
-
const labelWidth = (p) =>
|
|
61
|
+
const labelWidth = (p) => {
|
|
62
|
+
const text = headText(p);
|
|
63
|
+
return metrics.width(p.label, text.font, text.weight, text.size);
|
|
64
|
+
};
|
|
42
65
|
const widths = participants.map((p) => BOX_KINDS.has(p.kind)
|
|
43
66
|
? Math.max(Math.round(96 * f), Math.ceil(labelWidth(p) + 2 * theme.padding)) +
|
|
44
67
|
// A queue's right cap takes room from its body.
|
|
45
68
|
(p.kind === "queue" ? 2 * QUEUE_CAP : 0)
|
|
46
69
|
: Math.max(Math.round(48 * f), Math.ceil(labelWidth(p) + 8)));
|
|
47
|
-
|
|
70
|
+
// A message label is in the body font at weight 400 and the relation label size, broken at `\n`.
|
|
71
|
+
// Between two lifelines it holds its columns apart on one line, and wraps at the room they leave
|
|
72
|
+
// it when a pull brings them closer; a self-call's stays beside its loop. Its lines are 1.25
|
|
73
|
+
// times the size apart, as a sticky's are.
|
|
74
|
+
const messageText = (relation) => labelText(sequenceText(styles.relation(relation), scale.relationLabel, 400));
|
|
75
|
+
const measureLabel = (relation) => {
|
|
76
|
+
const text = messageText(relation);
|
|
77
|
+
return (line) => metrics.width(line, text.font, text.weight, text.size);
|
|
78
|
+
};
|
|
79
|
+
/** The widest word of a label, words split at spaces and line breaks: the least room it needs. */
|
|
80
|
+
const longestWord = (relation) => relation.label === null
|
|
81
|
+
? 0
|
|
82
|
+
: Math.max(0, ...relation.label.split(/[ \n]+/).map(measureLabel(relation)));
|
|
83
|
+
/** The widest line of a label broken at `\n` only: the room it takes unwrapped. */
|
|
84
|
+
const lineWidth = (relation) => relation.label === null
|
|
85
|
+
? 0
|
|
86
|
+
: Math.max(0, ...splitLines(relation.label).map(measureLabel(relation)));
|
|
48
87
|
const noteLines = (note) => splitLines(note.text);
|
|
49
88
|
const noteSize = (note) => {
|
|
50
89
|
const lines = noteLines(note);
|
|
51
|
-
const
|
|
52
|
-
|
|
90
|
+
const text = noteText(styles, scale, note);
|
|
91
|
+
const width = Math.max(...lines.map((line) => metrics.width(line, text.font, text.weight, text.size)), 24);
|
|
92
|
+
return { width: Math.ceil(width + 16), height: lines.length * text.line + 12 };
|
|
53
93
|
};
|
|
54
94
|
const numbered = diagram.properties.autonumber !== null;
|
|
55
95
|
const discRoom = numbered ? 22 : 0;
|
|
96
|
+
// Tags are the flow tag's pills, `tagHeight` high, in rows 4 apart: a message's on its arrow
|
|
97
|
+
// next to its source end, a participant's over its head as badges.
|
|
98
|
+
const tagHeight = scale.tagHeight;
|
|
99
|
+
const pillWidths = (tags) => tags.map((tag) => tagPillWidth(metrics, scale, tag.text));
|
|
100
|
+
/** The width of a row of pills, nothing when there are no tags. */
|
|
101
|
+
const rowWidth = (tags) => {
|
|
102
|
+
const pills = pillWidths(tags);
|
|
103
|
+
return pills.length === 0
|
|
104
|
+
? 0
|
|
105
|
+
: pills.reduce((sum, w) => sum + w, 0) + (pills.length - 1) * TAG_GAP;
|
|
106
|
+
};
|
|
107
|
+
/** Boxes of a row of pills from `x`, centred on `y`. */
|
|
108
|
+
const pillRow = (tags, x, y) => {
|
|
109
|
+
let left = x;
|
|
110
|
+
return pillWidths(tags).map((width) => {
|
|
111
|
+
const box = { x: left, y: y - tagHeight / 2, width, height: tagHeight };
|
|
112
|
+
left += width + TAG_GAP;
|
|
113
|
+
return box;
|
|
114
|
+
});
|
|
115
|
+
};
|
|
116
|
+
/** The length of the head a message draws at its source end: two-way messages draw one. */
|
|
117
|
+
const startHeadRoom = (relation) => messageHeads(relation, styles.relation(relation)).start !== "none" ? theme.arrowSize : 0;
|
|
118
|
+
// A message's row starts 6 from its arrow's start, 12 beside its number's disc, and past the
|
|
119
|
+
// head a two-way message draws there.
|
|
120
|
+
const tagStart = (relation) => (numbered ? TAG_START_NUMBERED : TAG_START) + startHeadRoom(relation);
|
|
121
|
+
/** The loop of a self-call: wide enough for its row of tags on its top segment. */
|
|
122
|
+
const loopWidth = (relation) => relation.tags.length === 0
|
|
123
|
+
? SELF_WIDTH
|
|
124
|
+
: Math.max(SELF_WIDTH, tagStart(relation) + rowWidth(relation.tags) + TAG_START);
|
|
125
|
+
// A head counts as the wider of itself and its badges where heads may not touch.
|
|
126
|
+
const extents = participants.map((p, i) => Math.max(widths[i], rowWidth(p.tags)));
|
|
127
|
+
// The badges stand over the heads: the row of heads starts lower by the room they need above
|
|
128
|
+
// it, a box kind's with their middle on its box's top edge, a figure's 4 above the figure.
|
|
129
|
+
const boxTop = headHeight - boxHeight;
|
|
130
|
+
const figureTop = headHeight - figureHeight - 6 - labelLine;
|
|
131
|
+
/** The room a head's badges need above its cell: none without tags. */
|
|
132
|
+
const badgeNeed = (p) => p.tags.length === 0
|
|
133
|
+
? 0
|
|
134
|
+
: Math.max(0, BOX_KINDS.has(p.kind) ? tagHeight / 2 - boxTop : tagHeight + BADGE_LIFT - figureTop);
|
|
135
|
+
const badgeRoom = Math.max(0, ...participants.map(badgeNeed));
|
|
136
|
+
// `space` above (before) or below (after) an event; `gap` left (before) or right (after) of a
|
|
137
|
+
// participant, both sides of a pair adding up.
|
|
56
138
|
const spaces = new Map();
|
|
57
|
-
const
|
|
139
|
+
const spacesAfter = new Map();
|
|
140
|
+
const gapsBefore = new Map();
|
|
141
|
+
const gapsAfter = new Map();
|
|
58
142
|
const labelEntries = new Map();
|
|
59
143
|
for (const entry of diagram.placement) {
|
|
60
|
-
if (entry.statement === "space")
|
|
61
|
-
spaces.set(entry.target, entry.amount);
|
|
62
|
-
|
|
63
|
-
|
|
144
|
+
if (entry.statement === "space") {
|
|
145
|
+
(entry.side === "after" ? spacesAfter : spaces).set(entry.target, entry.amount);
|
|
146
|
+
}
|
|
147
|
+
else if (entry.statement === "gap") {
|
|
148
|
+
(entry.side === "after" ? gapsAfter : gapsBefore).set(entry.target, entry);
|
|
149
|
+
}
|
|
64
150
|
else if (entry.statement === "label")
|
|
65
151
|
labelEntries.set(entry.target, entry);
|
|
66
152
|
}
|
|
153
|
+
const gapBefore = (id) => gapsBefore.get(id)?.amount ?? 0;
|
|
154
|
+
const gapAfter = (id) => gapsAfter.get(id)?.amount ?? 0;
|
|
67
155
|
const baseKey = (key) => key.replace(/@\d+$/, "").replace(/@\d+$/, "");
|
|
68
156
|
// ---------------------------------------------------------------- columns
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const
|
|
157
|
+
// Two passes (decision 0065, D1-D2). The natural columns come first: half of each head, the
|
|
158
|
+
// `spacing` (40 scaled with the theme's labels when none is written, and never less there) and
|
|
159
|
+
// the two `gap`s of a pair when their sum is positive, pushed apart by every message label on
|
|
160
|
+
// one line, the rows of tags, self-calls and side notes. A pull then brings a pair closer than
|
|
161
|
+
// its natural distance by its amount: a negative sum of its two `gap`s, and a `spacing` below
|
|
162
|
+
// the default by the difference. It stops at the pair's floor: the two half heads (or their rows
|
|
163
|
+
// of badges, when wider) and HEAD_CLEARANCE, so heads never touch, and the longest word of every
|
|
164
|
+
// label between them, which wraps at the room it is left.
|
|
165
|
+
const defaultSpacing = Math.round(40 * f);
|
|
166
|
+
const spacing = diagram.properties.spacing ?? defaultSpacing;
|
|
167
|
+
const base = Math.max(spacing, defaultSpacing);
|
|
168
|
+
const spacingPull = Math.min(0, spacing - defaultSpacing);
|
|
169
|
+
const halves = (i) => widths[i - 1] / 2 + widths[i] / 2;
|
|
170
|
+
const floor = (i) => extents[i - 1] / 2 + extents[i] / 2 + HEAD_CLEARANCE;
|
|
171
|
+
const gapSum = (i) => gapAfter(participants[i - 1].id) + gapBefore(participants[i].id);
|
|
172
|
+
/** A pair's written distance before the labels push it: positive gaps only. */
|
|
173
|
+
const written = (i) => halves(i) + base + Math.max(0, gapSum(i));
|
|
174
|
+
/** How much closer than its natural distance a pair is asked to be: 0 or less. */
|
|
175
|
+
const pull = (i) => Math.min(0, gapSum(i)) + spacingPull;
|
|
176
|
+
// Room left of the first participant, and right of the last, never less than nothing.
|
|
177
|
+
const outerBefore = participants.length > 0 ? Math.max(0, gapBefore(participants[0].id)) : 0;
|
|
178
|
+
const outerAfter = participants.length > 0 ? Math.max(0, gapAfter(participants[participants.length - 1].id)) : 0;
|
|
179
|
+
// What holds the natural columns apart (each label on one line), and what holds the pulled ones
|
|
180
|
+
// (each label's longest word); self-calls, notes and rows of tags hold both.
|
|
181
|
+
const natural = [];
|
|
182
|
+
const floors = [];
|
|
183
|
+
const both = (constraint) => {
|
|
184
|
+
natural.push(constraint);
|
|
185
|
+
floors.push(constraint);
|
|
186
|
+
};
|
|
81
187
|
let leftExtra = 0;
|
|
82
188
|
let rightExtra = 0;
|
|
189
|
+
/**
|
|
190
|
+
* What a creating message (the message right after the `create` of its receiver) leaves out of
|
|
191
|
+
* the distance between the columns: its arrow ends on the near edge of the head it draws, half
|
|
192
|
+
* that head short of the receiver's column. Nothing for any other message.
|
|
193
|
+
*/
|
|
194
|
+
const madeHead = (relation, before) => before?.kind === "create" && before.participant === relation.to && relation.from !== relation.to
|
|
195
|
+
? widths[column.get(relation.to)] / 2
|
|
196
|
+
: 0;
|
|
83
197
|
const walk = (list) => {
|
|
84
|
-
for (const event of list) {
|
|
198
|
+
for (const [index, event] of list.entries()) {
|
|
85
199
|
if (event.kind === "message") {
|
|
86
200
|
const relation = diagram.relationsByKey.get(event.relation);
|
|
87
201
|
if (!relation)
|
|
88
202
|
continue;
|
|
89
203
|
const a = column.get(relation.from);
|
|
90
204
|
const b = column.get(relation.to);
|
|
91
|
-
const room = messageLabel(relation) + discRoom;
|
|
92
205
|
if (a === b) {
|
|
93
|
-
const length =
|
|
206
|
+
const length = loopWidth(relation) + lineWidth(relation) + discRoom + 16;
|
|
94
207
|
if (a < participants.length - 1) {
|
|
95
|
-
|
|
208
|
+
both({ a, b: a + 1, length: length + widths[a + 1] / 2 });
|
|
96
209
|
}
|
|
97
210
|
else
|
|
98
211
|
rightExtra = Math.max(rightExtra, length);
|
|
99
212
|
}
|
|
100
213
|
else {
|
|
101
|
-
|
|
214
|
+
// The label on one line holds the natural pair apart, its longest word a pulled one;
|
|
215
|
+
// the row of its tags holds both, with the disc and the arrowhead beside it. Each counts
|
|
216
|
+
// along the arrow, which a creating message ends on the head it draws.
|
|
217
|
+
const made = madeHead(relation, list[index - 1]);
|
|
218
|
+
const row = rowWidth(relation.tags);
|
|
219
|
+
const tagged = row > 0 ? row + 24 + discRoom + theme.arrowSize + startHeadRoom(relation) + made : 0;
|
|
220
|
+
const pair = { a: Math.min(a, b), b: Math.max(a, b) };
|
|
221
|
+
natural.push({
|
|
222
|
+
...pair,
|
|
223
|
+
length: Math.max(lineWidth(relation) + discRoom + 24 + made, tagged),
|
|
224
|
+
});
|
|
225
|
+
floors.push({
|
|
226
|
+
...pair,
|
|
227
|
+
length: Math.max(longestWord(relation) + 24 + discRoom + made, tagged),
|
|
228
|
+
});
|
|
102
229
|
}
|
|
230
|
+
walk(event.events);
|
|
103
231
|
}
|
|
104
232
|
else if (event.kind === "note") {
|
|
105
233
|
const size = noteSize(event);
|
|
@@ -108,26 +236,26 @@ export function layoutSequence(input) {
|
|
|
108
236
|
const j = indices[indices.length - 1];
|
|
109
237
|
if (event.position === "over" && i === j) {
|
|
110
238
|
if (i > 0)
|
|
111
|
-
|
|
239
|
+
both({ a: i - 1, b: i, length: size.width / 2 + widths[i - 1] / 2 + 10 });
|
|
112
240
|
else
|
|
113
241
|
leftExtra = Math.max(leftExtra, size.width / 2 - widths[i] / 2);
|
|
114
242
|
if (i < participants.length - 1) {
|
|
115
|
-
|
|
243
|
+
both({ a: i, b: i + 1, length: size.width / 2 + widths[i + 1] / 2 + 10 });
|
|
116
244
|
}
|
|
117
245
|
else
|
|
118
246
|
rightExtra = Math.max(rightExtra, size.width / 2 - widths[i] / 2);
|
|
119
247
|
}
|
|
120
248
|
else if (event.position === "over") {
|
|
121
|
-
|
|
249
|
+
both({ a: i, b: j, length: size.width - 48 });
|
|
122
250
|
}
|
|
123
251
|
else if (event.position === "left") {
|
|
124
252
|
if (i > 0)
|
|
125
|
-
|
|
253
|
+
both({ a: i - 1, b: i, length: size.width + 20 + widths[i - 1] / 2 });
|
|
126
254
|
else
|
|
127
255
|
leftExtra = Math.max(leftExtra, size.width + 20 - widths[i] / 2);
|
|
128
256
|
}
|
|
129
257
|
else if (i < participants.length - 1) {
|
|
130
|
-
|
|
258
|
+
both({ a: i, b: i + 1, length: size.width + 20 + widths[i + 1] / 2 });
|
|
131
259
|
}
|
|
132
260
|
else
|
|
133
261
|
rightExtra = Math.max(rightExtra, size.width + 20 - widths[i] / 2);
|
|
@@ -139,30 +267,92 @@ export function layoutSequence(input) {
|
|
|
139
267
|
}
|
|
140
268
|
};
|
|
141
269
|
walk(events);
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
270
|
+
/**
|
|
271
|
+
* Pushes the right column of every constraint, and everything after it, as far as it needs
|
|
272
|
+
* beyond `slack`.
|
|
273
|
+
*/
|
|
274
|
+
const push = (xs, constraints, slack) => {
|
|
275
|
+
constraints.sort((p, q) => p.b - q.b || p.a - q.a);
|
|
276
|
+
for (const { a, b, length } of constraints) {
|
|
277
|
+
const deficit = length - (xs[b] - xs[a]);
|
|
278
|
+
if (deficit > slack)
|
|
279
|
+
for (let k = b; k < xs.length; k++)
|
|
280
|
+
xs[k] = xs[k] + deficit;
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
const naturalXs = [];
|
|
284
|
+
participants.forEach((_p, i) => {
|
|
285
|
+
naturalXs[i] =
|
|
286
|
+
i === 0 ? widths[0] / 2 + outerBefore : naturalXs[i - 1] + Math.max(written(i), floor(i));
|
|
287
|
+
});
|
|
288
|
+
push(naturalXs, natural, 0);
|
|
289
|
+
/** The natural distance of the pair left of column `i`. */
|
|
290
|
+
const naturalDistance = (i) => naturalXs[i] - naturalXs[i - 1];
|
|
291
|
+
// The pulled columns: each pair closer by its pull, down to its heads' floor, then pushed by
|
|
292
|
+
// the longest words, the rows of tags, self-calls and notes. A pair without a pull keeps its
|
|
293
|
+
// natural distance exactly.
|
|
294
|
+
const xs = [];
|
|
295
|
+
let shift = 0;
|
|
296
|
+
participants.forEach((_p, i) => {
|
|
297
|
+
if (i > 0 && pull(i) < 0)
|
|
298
|
+
shift += Math.max(pull(i), floor(i) - naturalDistance(i));
|
|
299
|
+
xs[i] = naturalXs[i] + shift;
|
|
300
|
+
});
|
|
301
|
+
// The natural columns already hold every floor: rounding is no deficit.
|
|
302
|
+
push(xs, floors, ROUNDING);
|
|
303
|
+
/** Whether a pull brings any pair between columns `a` and `b` closer: their labels wrap. */
|
|
304
|
+
const pulled = (a, b) => {
|
|
305
|
+
for (let i = Math.min(a, b) + 1; i <= Math.max(a, b); i++)
|
|
306
|
+
if (pull(i) < 0)
|
|
307
|
+
return true;
|
|
308
|
+
return false;
|
|
309
|
+
};
|
|
310
|
+
// A negative gap that the floor kept from being drawn, measured from the natural distance, is
|
|
311
|
+
// said on the statement (the `before` one when both sides are written).
|
|
312
|
+
for (let i = 1; i < participants.length; i++) {
|
|
313
|
+
const after = gapsAfter.get(participants[i - 1].id);
|
|
314
|
+
const before = gapsBefore.get(participants[i].id);
|
|
315
|
+
const sum = (after?.amount ?? 0) + (before?.amount ?? 0);
|
|
316
|
+
const distance = xs[i] - xs[i - 1];
|
|
317
|
+
if (sum >= 0 || distance <= naturalDistance(i) + pull(i) + ROUNDING)
|
|
318
|
+
continue;
|
|
319
|
+
const entry = before ?? after;
|
|
320
|
+
const other = entry === before ? (after?.amount ?? 0) : (before?.amount ?? 0);
|
|
321
|
+
// `+ 0` writes a drawn 0 as 0, never -0.
|
|
322
|
+
const applied = Math.round(distance - naturalDistance(i) - other - spacingPull) + 0;
|
|
323
|
+
diagnostics.push({
|
|
324
|
+
code: "layout.gap-floor",
|
|
325
|
+
severity: "warning",
|
|
326
|
+
message: words().diagnostics.gapFloor(entry.target, entry.side, entry.amount, applied, participants[i - 1].id, participants[i].id),
|
|
327
|
+
range: entry.range,
|
|
328
|
+
data: {
|
|
329
|
+
gap: { participant: entry.target, side: entry.side, written: entry.amount, applied },
|
|
330
|
+
},
|
|
331
|
+
});
|
|
148
332
|
}
|
|
149
333
|
// ---------------------------------------------------------------- rows
|
|
150
|
-
|
|
334
|
+
// The groups' titles share a band as tall as the largest title written, the head's line at least.
|
|
335
|
+
const groupTitle = groups.length > 0
|
|
336
|
+
? Math.max(labelLine, ...groups.map((g) => Math.round((styles.element(g).fontSize ?? 0) * 1.5))) + 12
|
|
337
|
+
: 0;
|
|
151
338
|
const rowGap = Math.round(14 * f);
|
|
152
|
-
|
|
153
|
-
|
|
339
|
+
// The heads' row, below the groups' titles and the room of the badges over the heads.
|
|
340
|
+
const headTop = groupTitle + badgeRoom;
|
|
341
|
+
let y = headTop + headHeight + Math.round(24 * f);
|
|
154
342
|
const lifelines = new Map();
|
|
155
343
|
for (const p of participants) {
|
|
156
344
|
lifelines.set(p.id, {
|
|
157
345
|
participant: p.id,
|
|
158
346
|
x: xs[column.get(p.id)],
|
|
159
|
-
top:
|
|
347
|
+
top: headTop + headHeight,
|
|
160
348
|
bottom: null,
|
|
161
349
|
});
|
|
162
350
|
}
|
|
163
351
|
const created = new Map();
|
|
164
352
|
const heads = new Map();
|
|
165
353
|
const messages = [];
|
|
354
|
+
const messageTexts = new Map();
|
|
355
|
+
const tags = new Map();
|
|
166
356
|
const notes = [];
|
|
167
357
|
const frames = [];
|
|
168
358
|
const dividers = [];
|
|
@@ -213,222 +403,306 @@ export function layoutSequence(input) {
|
|
|
213
403
|
for (const [index, event] of list.entries()) {
|
|
214
404
|
y += spaces.get(baseKey(event.key)) ?? 0;
|
|
215
405
|
const start = y;
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
406
|
+
// `space … after` is room below the event's row, which the row itself does not hold.
|
|
407
|
+
try {
|
|
408
|
+
switch (event.kind) {
|
|
409
|
+
case "message": {
|
|
410
|
+
const relation = diagram.relationsByKey.get(event.relation);
|
|
411
|
+
if (!relation)
|
|
412
|
+
break;
|
|
413
|
+
const a = relation.from;
|
|
414
|
+
const b = relation.to;
|
|
415
|
+
number += numbered ? 1 : 0;
|
|
416
|
+
const text = relation.label ?? "";
|
|
417
|
+
const self = a === b;
|
|
418
|
+
// The room of a label between two lifelines: the length of its arrow (the distance
|
|
419
|
+
// between them, less half the head a creating message ends on), less 12 on each side
|
|
420
|
+
// and the disc. At the natural distance it holds the label on one line; a pull may
|
|
421
|
+
// leave it less, down to the longest word, and the label wraps there.
|
|
422
|
+
const made = self ? 0 : madeHead(relation, list[index - 1]);
|
|
423
|
+
const room = self
|
|
424
|
+
? 0
|
|
425
|
+
: Math.max(1, Math.abs(xs[column.get(b)] - xs[column.get(a)]) - made - 24 - discRoom);
|
|
426
|
+
const wraps = !self && pulled(column.get(a), column.get(b));
|
|
427
|
+
const written = messageText(relation);
|
|
428
|
+
const measure = measureLabel(relation);
|
|
429
|
+
const labelHeight = written.height;
|
|
430
|
+
const labelStep = written.step;
|
|
431
|
+
const lines = text === ""
|
|
432
|
+
? []
|
|
433
|
+
: self || lineWidth(relation) <= room + ROUNDING
|
|
434
|
+
? splitLines(text)
|
|
435
|
+
: wrapLines(text, room, measure);
|
|
436
|
+
// The block of lines: one label line high, and a step more for each line after it.
|
|
437
|
+
const block = labelHeight + Math.max(0, lines.length - 1) * labelStep;
|
|
438
|
+
// A message's tags sit on its arrow: its label stands higher by half a pill and 2, and
|
|
439
|
+
// its row is that much taller, so that a wrapped label never meets them.
|
|
440
|
+
const lift = relation.tags.length > 0 && !self ? tagHeight / 2 + 2 : 0;
|
|
441
|
+
const loop = self ? loopWidth(relation) : SELF_WIDTH;
|
|
442
|
+
let points;
|
|
443
|
+
let arrowY;
|
|
444
|
+
if (self) {
|
|
445
|
+
// The lines are centred on the loop's middle: the row grows when they are taller.
|
|
446
|
+
const over = Math.max(0, (block - SELF_HEIGHT) / 2);
|
|
447
|
+
const top = y + Math.max(labelHeight, over + 3);
|
|
448
|
+
const x0 = edge(a, 1);
|
|
253
449
|
points = [
|
|
254
|
-
{ x:
|
|
255
|
-
{ x:
|
|
450
|
+
{ x: x0, y: top },
|
|
451
|
+
{ x: x0 + loop, y: top },
|
|
452
|
+
{ x: x0 + loop, y: top + SELF_HEIGHT },
|
|
453
|
+
{ x: x0, y: top + SELF_HEIGHT },
|
|
256
454
|
];
|
|
257
|
-
|
|
455
|
+
arrowY = top;
|
|
456
|
+
y = top + SELF_HEIGHT + over + rowGap;
|
|
258
457
|
}
|
|
259
458
|
else {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
459
|
+
const createdHead = created.get(b);
|
|
460
|
+
const dir = xs[column.get(b)] > xs[column.get(a)] ? 1 : -1;
|
|
461
|
+
if (createdHead !== undefined && createdHead < 0) {
|
|
462
|
+
// The creating message ends on the new head, drawn at this row, lower by the
|
|
463
|
+
// label's lines after the first, the lift of its tags and the room of the head's
|
|
464
|
+
// own badges.
|
|
465
|
+
const element = diagram.elementsById.get(b);
|
|
466
|
+
const top = y + 4 + (block - labelHeight) + lift + badgeNeed(element);
|
|
467
|
+
const head = headBoxAt(element, top);
|
|
468
|
+
heads.set(b, head);
|
|
469
|
+
created.set(b, top);
|
|
470
|
+
lifelines.get(b).top = top + headHeight;
|
|
471
|
+
arrowY = top + headHeight - boxHeight / 2;
|
|
472
|
+
const x1 = edge(a, dir);
|
|
473
|
+
const x2 = dir > 0 ? head.x : head.x + head.width;
|
|
474
|
+
points = [
|
|
475
|
+
{ x: x1, y: arrowY },
|
|
476
|
+
{ x: x2, y: arrowY },
|
|
477
|
+
];
|
|
478
|
+
y = top + headHeight + rowGap;
|
|
479
|
+
}
|
|
480
|
+
else {
|
|
481
|
+
// The arrow under the label's last line: the label keeps its top.
|
|
482
|
+
arrowY = y + block + 4 + lift;
|
|
483
|
+
points = [
|
|
484
|
+
{ x: edge(a, dir), y: arrowY },
|
|
485
|
+
{ x: edge(b, dir === 1 ? -1 : 1), y: arrowY },
|
|
486
|
+
];
|
|
487
|
+
y = arrowY + rowGap;
|
|
488
|
+
}
|
|
266
489
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
490
|
+
// The block sits as a one-line label does: its bottom just above the arrow, centred at
|
|
491
|
+
// `at` along it, or beside a self-call's loop. `label … offset` moves the block, not
|
|
492
|
+
// the room it wraps in.
|
|
270
493
|
const entry = relation.id ? labelEntries.get(relation.id) : undefined;
|
|
271
494
|
const first = points[0];
|
|
272
495
|
const lastPoint = points[points.length - 1];
|
|
273
496
|
const at = entry ? entry.at / 100 : 0.5;
|
|
274
497
|
const offset = entry?.offset ?? { x: 0, y: 0 };
|
|
275
|
-
const width = Math.max(
|
|
276
|
-
const anchor =
|
|
277
|
-
? { x: first.x +
|
|
278
|
-
: { x: first.x + (lastPoint.x - first.x) * at, y: arrowY -
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
height: labelHeight,
|
|
285
|
-
},
|
|
286
|
-
text,
|
|
498
|
+
const width = Math.max(Math.max(0, ...lines.map(measure)) + 8, 8);
|
|
499
|
+
const anchor = self
|
|
500
|
+
? { x: first.x + loop + 8 + width / 2, y: first.y + SELF_HEIGHT / 2 }
|
|
501
|
+
: { x: first.x + (lastPoint.x - first.x) * at, y: arrowY - block / 2 - 1 - lift };
|
|
502
|
+
const box = {
|
|
503
|
+
x: anchor.x - width / 2 + offset.x,
|
|
504
|
+
y: anchor.y - block / 2 + offset.y,
|
|
505
|
+
width,
|
|
506
|
+
height: block,
|
|
287
507
|
};
|
|
508
|
+
const label = text !== "" || numbered ? { box, lines, line: labelStep, text: written } : undefined;
|
|
509
|
+
messageTexts.set(event.key, {
|
|
510
|
+
area: self
|
|
511
|
+
? box
|
|
512
|
+
: { x: box.x + box.width / 2 - room / 2, y: box.y, width: room, height: block },
|
|
513
|
+
lines,
|
|
514
|
+
size: written.size,
|
|
515
|
+
line: labelStep,
|
|
516
|
+
wraps,
|
|
517
|
+
weight: written.weight,
|
|
518
|
+
...(writtenStack(written.font) !== undefined
|
|
519
|
+
? { font: writtenStack(written.font) }
|
|
520
|
+
: {}),
|
|
521
|
+
});
|
|
522
|
+
// The row of tags on the arrow, its end nearest the source `tagStart` from the
|
|
523
|
+
// arrow's start: rightwards from it, or leftwards on a message from right to left.
|
|
524
|
+
const row = rowWidth(relation.tags);
|
|
525
|
+
const toRight = self || lastPoint.x >= first.x;
|
|
526
|
+
const pills = pillRow(relation.tags, toRight ? first.x + tagStart(relation) : first.x - tagStart(relation) - row, first.y);
|
|
527
|
+
if (pills.length > 0)
|
|
528
|
+
tags.set(event.key, pills);
|
|
529
|
+
messages.push({
|
|
530
|
+
key: event.key,
|
|
531
|
+
relation,
|
|
532
|
+
points,
|
|
533
|
+
self,
|
|
534
|
+
label,
|
|
535
|
+
number: numbered ? number : null,
|
|
536
|
+
tags: pills,
|
|
537
|
+
});
|
|
538
|
+
const xsOf = [...points.map((p) => p.x), ...pills.flatMap((b) => [b.x, b.x + b.width])];
|
|
539
|
+
extend(Math.min(...xsOf) - 8, Math.max(...xsOf, label ? label.box.x + label.box.width : -Infinity) + 8);
|
|
540
|
+
last = { participants: new Set([a, b]), y: a === b ? points[3].y : arrowY };
|
|
541
|
+
rows.set(event.key, { y: start, height: y - start });
|
|
542
|
+
if (event.events.length > 0) {
|
|
543
|
+
// The receiver is active for the call's block: a bar from the arrow to the block's
|
|
544
|
+
// last event, which moves the ends of the nested messages to its edge.
|
|
545
|
+
const stack = stacks.get(b) ?? [];
|
|
546
|
+
const top = last.y;
|
|
547
|
+
const bar = {
|
|
548
|
+
key: event.key,
|
|
549
|
+
participant: b,
|
|
550
|
+
depth: stack.length,
|
|
551
|
+
top,
|
|
552
|
+
bottom: top,
|
|
553
|
+
to: null,
|
|
554
|
+
block: true,
|
|
555
|
+
};
|
|
556
|
+
stack.push(bar);
|
|
557
|
+
stacks.set(b, stack);
|
|
558
|
+
bars.push(bar);
|
|
559
|
+
process(event.events);
|
|
560
|
+
(stacks.get(b) ?? []).pop();
|
|
561
|
+
const bottom = last && last.participants.has(b) ? last.y : y;
|
|
562
|
+
bar.bottom = Math.max(bottom, bar.top + 12);
|
|
563
|
+
}
|
|
564
|
+
continue;
|
|
288
565
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
const bar = {
|
|
307
|
-
participant: event.participant,
|
|
308
|
-
depth: stack.length,
|
|
309
|
-
top,
|
|
310
|
-
bottom: top,
|
|
311
|
-
};
|
|
312
|
-
stack.push(bar);
|
|
313
|
-
stacks.set(event.participant, stack);
|
|
314
|
-
bars.push(bar);
|
|
315
|
-
rows.set(event.key, { y: top, height: 0 });
|
|
316
|
-
continue;
|
|
317
|
-
}
|
|
318
|
-
case "deactivate": {
|
|
319
|
-
const stack = stacks.get(event.participant) ?? [];
|
|
320
|
-
const bar = stack.pop();
|
|
321
|
-
const bottom = last && last.participants.has(event.participant) ? last.y : y;
|
|
322
|
-
if (bar)
|
|
323
|
-
bar.bottom = Math.max(bottom, bar.top + 12);
|
|
324
|
-
rows.set(event.key, { y: bottom, height: 0 });
|
|
325
|
-
continue;
|
|
326
|
-
}
|
|
327
|
-
case "create": {
|
|
328
|
-
const next = list[index + 1];
|
|
329
|
-
const relation = next?.kind === "message" ? diagram.relationsByKey.get(next.relation) : undefined;
|
|
330
|
-
if (relation && relation.to === event.participant && relation.from !== relation.to) {
|
|
331
|
-
created.set(event.participant, -1); // drawn by the next message
|
|
566
|
+
case "activate": {
|
|
567
|
+
const top = last && last.participants.has(event.participant) ? last.y : y;
|
|
568
|
+
const stack = stacks.get(event.participant) ?? [];
|
|
569
|
+
const bar = {
|
|
570
|
+
key: event.key,
|
|
571
|
+
participant: event.participant,
|
|
572
|
+
depth: stack.length,
|
|
573
|
+
top,
|
|
574
|
+
bottom: top,
|
|
575
|
+
to: null,
|
|
576
|
+
block: false,
|
|
577
|
+
};
|
|
578
|
+
stack.push(bar);
|
|
579
|
+
stacks.set(event.participant, stack);
|
|
580
|
+
bars.push(bar);
|
|
581
|
+
rows.set(event.key, { y: top, height: 0 });
|
|
582
|
+
continue;
|
|
332
583
|
}
|
|
333
|
-
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
584
|
+
case "deactivate": {
|
|
585
|
+
const stack = stacks.get(event.participant) ?? [];
|
|
586
|
+
const bar = stack.pop();
|
|
587
|
+
const bottom = last && last.participants.has(event.participant) ? last.y : y;
|
|
588
|
+
if (bar) {
|
|
589
|
+
bar.bottom = Math.max(bottom, bar.top + 12);
|
|
590
|
+
bar.to = event.key;
|
|
591
|
+
}
|
|
592
|
+
rows.set(event.key, { y: bottom, height: 0 });
|
|
593
|
+
continue;
|
|
339
594
|
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
const size = noteSize(event);
|
|
358
|
-
const indices = event.participants.map((p) => column.get(p)).sort((p, q) => p - q);
|
|
359
|
-
const i = indices[0];
|
|
360
|
-
const j = indices[indices.length - 1];
|
|
361
|
-
const top = y + 4;
|
|
362
|
-
let x;
|
|
363
|
-
let width = size.width;
|
|
364
|
-
if (event.position === "over") {
|
|
365
|
-
const left = xs[i] - (i === j ? 0 : 24);
|
|
366
|
-
const right = xs[j] + (i === j ? 0 : 24);
|
|
367
|
-
width = Math.max(size.width, right - left);
|
|
368
|
-
x = (left + right) / 2 - width / 2;
|
|
595
|
+
case "create": {
|
|
596
|
+
const next = list[index + 1];
|
|
597
|
+
const relation = next?.kind === "message" ? diagram.relationsByKey.get(next.relation) : undefined;
|
|
598
|
+
if (relation && relation.to === event.participant && relation.from !== relation.to) {
|
|
599
|
+
created.set(event.participant, -1); // drawn by the next message
|
|
600
|
+
}
|
|
601
|
+
else {
|
|
602
|
+
// A created head takes the room of its badges above it, as the heads' row does.
|
|
603
|
+
const element = diagram.elementsById.get(event.participant);
|
|
604
|
+
const top = y + 4 + badgeNeed(element);
|
|
605
|
+
heads.set(event.participant, headBoxAt(element, top));
|
|
606
|
+
created.set(event.participant, top);
|
|
607
|
+
lifelines.get(event.participant).top = top + headHeight;
|
|
608
|
+
y = top + headHeight + rowGap;
|
|
609
|
+
}
|
|
610
|
+
rows.set(event.key, { y: start, height: y - start });
|
|
611
|
+
continue;
|
|
369
612
|
}
|
|
370
|
-
|
|
371
|
-
x = xs[
|
|
613
|
+
case "destroy": {
|
|
614
|
+
const x = xs[column.get(event.participant)];
|
|
615
|
+
const at = y + 8;
|
|
616
|
+
crosses.push({ key: event.key, x, y: at });
|
|
617
|
+
lifelines.get(event.participant).bottom = at;
|
|
618
|
+
for (const bar of stacks.get(event.participant) ?? [])
|
|
619
|
+
bar.bottom = Math.max(at, bar.top + 12);
|
|
620
|
+
stacks.set(event.participant, []);
|
|
621
|
+
y = at + rowGap + 4;
|
|
622
|
+
rows.set(event.key, { y: start, height: y - start });
|
|
623
|
+
last = undefined;
|
|
624
|
+
continue;
|
|
372
625
|
}
|
|
373
|
-
|
|
374
|
-
|
|
626
|
+
case "note": {
|
|
627
|
+
const size = noteSize(event);
|
|
628
|
+
const indices = event.participants.map((p) => column.get(p)).sort((p, q) => p - q);
|
|
629
|
+
const i = indices[0];
|
|
630
|
+
const j = indices[indices.length - 1];
|
|
631
|
+
const top = y + 4;
|
|
632
|
+
let x;
|
|
633
|
+
let width = size.width;
|
|
634
|
+
if (event.position === "over") {
|
|
635
|
+
const left = xs[i] - (i === j ? 0 : 24);
|
|
636
|
+
const right = xs[j] + (i === j ? 0 : 24);
|
|
637
|
+
width = Math.max(size.width, right - left);
|
|
638
|
+
x = (left + right) / 2 - width / 2;
|
|
639
|
+
}
|
|
640
|
+
else if (event.position === "left") {
|
|
641
|
+
x = xs[i] - 10 - width;
|
|
642
|
+
}
|
|
643
|
+
else {
|
|
644
|
+
x = xs[i] + 10;
|
|
645
|
+
}
|
|
646
|
+
const box = { x, y: top, width, height: size.height };
|
|
647
|
+
notes.push({ key: event.key, event, box });
|
|
648
|
+
extend(box.x - 8, box.x + box.width + 8);
|
|
649
|
+
y = top + size.height + rowGap;
|
|
650
|
+
rows.set(event.key, { y: start, height: y - start });
|
|
651
|
+
last = undefined;
|
|
652
|
+
continue;
|
|
375
653
|
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
for (const [b, branch] of event.branches.entries()) {
|
|
392
|
-
if (b > 0) {
|
|
393
|
-
const sep = y + 2;
|
|
394
|
-
separators.push({ y: sep, condition: branch.condition });
|
|
395
|
-
y = sep + Math.round(20 * f);
|
|
654
|
+
case "fragment": {
|
|
655
|
+
const top = y + 4;
|
|
656
|
+
const headerHeight = Math.round(22 * f);
|
|
657
|
+
const frame = { minX: Infinity, maxX: -Infinity };
|
|
658
|
+
open.push(frame);
|
|
659
|
+
const separators = [];
|
|
660
|
+
y = top + headerHeight + 6;
|
|
661
|
+
for (const [b, branch] of event.branches.entries()) {
|
|
662
|
+
if (b > 0) {
|
|
663
|
+
const sep = y + 2;
|
|
664
|
+
separators.push({ y: sep, condition: branch.condition });
|
|
665
|
+
y = sep + Math.round(20 * f);
|
|
666
|
+
}
|
|
667
|
+
last = undefined;
|
|
668
|
+
process(branch.events);
|
|
396
669
|
}
|
|
670
|
+
open.pop();
|
|
671
|
+
const bottom = y + 4;
|
|
672
|
+
const headerText = fragmentHeader(event, metrics, scale, styles);
|
|
673
|
+
if (!Number.isFinite(frame.minX)) {
|
|
674
|
+
frame.minX = xs[0] - 24;
|
|
675
|
+
frame.maxX = xs[xs.length - 1] + 24;
|
|
676
|
+
}
|
|
677
|
+
const minX = frame.minX - 16;
|
|
678
|
+
const maxX = Math.max(frame.maxX + 16, minX + headerText + 24);
|
|
679
|
+
const box = { x: minX, y: top, width: maxX - minX, height: bottom - top };
|
|
680
|
+
frames.push({ key: event.key, event, box, headerHeight, separators });
|
|
681
|
+
extend(box.x - 8, box.x + box.width + 8);
|
|
682
|
+
y = bottom + rowGap;
|
|
683
|
+
rows.set(event.key, { y: start, height: y - start });
|
|
397
684
|
last = undefined;
|
|
398
|
-
|
|
685
|
+
continue;
|
|
399
686
|
}
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
687
|
+
case "divider": {
|
|
688
|
+
dividers.push({ key: event.key, y: y + 12, label: event.label });
|
|
689
|
+
y += 24 + rowGap;
|
|
690
|
+
rows.set(event.key, { y: start, height: y - start });
|
|
691
|
+
last = undefined;
|
|
692
|
+
continue;
|
|
693
|
+
}
|
|
694
|
+
case "delay": {
|
|
695
|
+
delays.push({ key: event.key, y: y + 14, label: event.label });
|
|
696
|
+
y += 28 + rowGap;
|
|
697
|
+
rows.set(event.key, { y: start, height: y - start });
|
|
698
|
+
last = undefined;
|
|
699
|
+
continue;
|
|
406
700
|
}
|
|
407
|
-
const minX = frame.minX - 16;
|
|
408
|
-
const maxX = Math.max(frame.maxX + 16, minX + headerText + 24);
|
|
409
|
-
const box = { x: minX, y: top, width: maxX - minX, height: bottom - top };
|
|
410
|
-
frames.push({ key: event.key, event, box, headerHeight, separators });
|
|
411
|
-
extend(box.x - 8, box.x + box.width + 8);
|
|
412
|
-
y = bottom + rowGap;
|
|
413
|
-
rows.set(event.key, { y: start, height: y - start });
|
|
414
|
-
last = undefined;
|
|
415
|
-
continue;
|
|
416
|
-
}
|
|
417
|
-
case "divider": {
|
|
418
|
-
dividers.push({ key: event.key, y: y + 12, label: event.label });
|
|
419
|
-
y += 24 + rowGap;
|
|
420
|
-
rows.set(event.key, { y: start, height: y - start });
|
|
421
|
-
last = undefined;
|
|
422
|
-
continue;
|
|
423
|
-
}
|
|
424
|
-
case "delay": {
|
|
425
|
-
delays.push({ key: event.key, y: y + 14, label: event.label });
|
|
426
|
-
y += 28 + rowGap;
|
|
427
|
-
rows.set(event.key, { y: start, height: y - start });
|
|
428
|
-
last = undefined;
|
|
429
|
-
continue;
|
|
430
701
|
}
|
|
431
702
|
}
|
|
703
|
+
finally {
|
|
704
|
+
y += spacesAfter.get(baseKey(event.key)) ?? 0;
|
|
705
|
+
}
|
|
432
706
|
}
|
|
433
707
|
};
|
|
434
708
|
process(events);
|
|
@@ -441,15 +715,35 @@ export function layoutSequence(input) {
|
|
|
441
715
|
bar.bottom = Math.max(bar.bottom, end);
|
|
442
716
|
for (const p of participants) {
|
|
443
717
|
if (!heads.has(p.id))
|
|
444
|
-
heads.set(p.id, headBoxAt(p,
|
|
718
|
+
heads.set(p.id, headBoxAt(p, headTop));
|
|
445
719
|
const line = lifelines.get(p.id);
|
|
446
720
|
if (line.bottom === null)
|
|
447
721
|
line.bottom = end;
|
|
448
722
|
}
|
|
723
|
+
// A participant's badges: a row centred on its head, a box kind's with their middle on the top
|
|
724
|
+
// edge of its (front) box, a figure's 4 above the figure.
|
|
725
|
+
const badges = new Map();
|
|
726
|
+
for (const p of participants) {
|
|
727
|
+
if (p.tags.length === 0)
|
|
728
|
+
continue;
|
|
729
|
+
const cell = heads.get(p.id);
|
|
730
|
+
const middle = BOX_KINDS.has(p.kind)
|
|
731
|
+
? cell.y + boxTop
|
|
732
|
+
: cell.y + figureTop - BADGE_LIFT - tagHeight / 2;
|
|
733
|
+
badges.set(p.id, pillRow(p.tags, cell.x + cell.width / 2 - rowWidth(p.tags) / 2, middle));
|
|
734
|
+
}
|
|
735
|
+
/** A bar's rectangle on its lifeline, nested bars stepping right. */
|
|
736
|
+
const barBox = (bar) => ({
|
|
737
|
+
x: xs[column.get(bar.participant)] - BAR / 2 + bar.depth * BAR_STEP,
|
|
738
|
+
y: bar.top,
|
|
739
|
+
width: BAR,
|
|
740
|
+
height: bar.bottom - bar.top,
|
|
741
|
+
});
|
|
449
742
|
const groupBoxes = new Map();
|
|
450
743
|
for (const group of groups) {
|
|
451
744
|
const members = participants.filter((p) => p.groupPath.includes(group.id));
|
|
452
|
-
|
|
745
|
+
// A group holds its members' heads and their badges.
|
|
746
|
+
const boxes = members.flatMap((m) => [heads.get(m.id), ...(badges.get(m.id) ?? [])]);
|
|
453
747
|
const union = unionBoxes(boxes);
|
|
454
748
|
if (!union)
|
|
455
749
|
continue;
|
|
@@ -462,14 +756,18 @@ export function layoutSequence(input) {
|
|
|
462
756
|
}
|
|
463
757
|
// ---------------------------------------------------------------- canvas
|
|
464
758
|
// A diagram with no participant yet (a new one) has an empty width, not an unknown one.
|
|
465
|
-
const leftmost = xs.length > 0 ? xs[0] - widths[0] / 2 - leftExtra : 0;
|
|
466
|
-
const rightmost = xs.length > 0
|
|
759
|
+
const leftmost = xs.length > 0 ? xs[0] - widths[0] / 2 - outerBefore - leftExtra : 0;
|
|
760
|
+
const rightmost = xs.length > 0
|
|
761
|
+
? xs[xs.length - 1] + widths[widths.length - 1] / 2 + rightExtra + outerAfter
|
|
762
|
+
: 0;
|
|
467
763
|
const left = Math.min(leftmost, ...frames.map((fr) => fr.box.x));
|
|
468
764
|
const right = Math.max(rightmost, ...frames.map((fr) => fr.box.x + fr.box.width), ...notes.map((n) => n.box.x + n.box.width), ...messages.flatMap((m) => (m.label ? [m.label.box.x + m.label.box.width] : [])));
|
|
469
765
|
const everything = [
|
|
470
766
|
{ x: left, y: 0, width: right - left, height: end + 8 },
|
|
471
767
|
...groupBoxes.values(),
|
|
472
768
|
...heads.values(),
|
|
769
|
+
...[...badges.values()].flat(),
|
|
770
|
+
...[...tags.values()].flat(),
|
|
473
771
|
];
|
|
474
772
|
const bounds = unionBoxes(everything);
|
|
475
773
|
const margin = theme.margin;
|
|
@@ -499,7 +797,9 @@ export function layoutSequence(input) {
|
|
|
499
797
|
items.push(emit.frame(frame));
|
|
500
798
|
const headers = frames.map((frame) => emit.frameHeader(frame));
|
|
501
799
|
for (const p of participants) {
|
|
502
|
-
items.push(emit.lifeline(p, lifelines.get(p.id), bars
|
|
800
|
+
items.push(emit.lifeline(p, lifelines.get(p.id), bars
|
|
801
|
+
.filter((b) => b.participant === p.id)
|
|
802
|
+
.map((bar) => ({ key: bar.key, depth: bar.depth, box: barBox(bar) })), xs[column.get(p.id)]));
|
|
503
803
|
}
|
|
504
804
|
items.push(...headers);
|
|
505
805
|
for (const message of messages)
|
|
@@ -514,7 +814,7 @@ export function layoutSequence(input) {
|
|
|
514
814
|
items.push(emit.cross(cross));
|
|
515
815
|
for (const p of participants) {
|
|
516
816
|
const cell = heads.get(p.id);
|
|
517
|
-
items.push(emit.head(p, cell, drawnHead(p, cell), boxHeight, figureHeight, labelLine));
|
|
817
|
+
items.push(emit.head(p, cell, drawnHead(p, cell), boxHeight, figureHeight, labelLine, badges.get(p.id) ?? []));
|
|
518
818
|
}
|
|
519
819
|
const gradients = [];
|
|
520
820
|
if (framed) {
|
|
@@ -545,6 +845,22 @@ export function layoutSequence(input) {
|
|
|
545
845
|
boxes.set(note.key, note.box);
|
|
546
846
|
for (const frame of frames)
|
|
547
847
|
boxes.set(frame.key, frame.box);
|
|
848
|
+
// Every bar by the key of its `activate`, or of the call whose block draws it; a written bar is
|
|
849
|
+
// also a box, which editors select and outline.
|
|
850
|
+
const barGeometry = new Map();
|
|
851
|
+
for (const bar of bars) {
|
|
852
|
+
const box = barBox(bar);
|
|
853
|
+
barGeometry.set(bar.key, {
|
|
854
|
+
participant: bar.participant,
|
|
855
|
+
box,
|
|
856
|
+
depth: bar.depth,
|
|
857
|
+
from: bar.key,
|
|
858
|
+
to: bar.to,
|
|
859
|
+
block: bar.block,
|
|
860
|
+
});
|
|
861
|
+
if (!bar.block)
|
|
862
|
+
boxes.set(bar.key, box);
|
|
863
|
+
}
|
|
548
864
|
const connectors = new Map();
|
|
549
865
|
const labels = new Map();
|
|
550
866
|
for (const message of messages) {
|
|
@@ -578,6 +894,10 @@ export function layoutSequence(input) {
|
|
|
578
894
|
boxes,
|
|
579
895
|
offset: { x: dx, y: dy },
|
|
580
896
|
labels,
|
|
897
|
+
messageTexts,
|
|
898
|
+
bars: barGeometry,
|
|
899
|
+
// The participants' badges, then the messages' pills.
|
|
900
|
+
tags: new Map([...badges, ...tags]),
|
|
581
901
|
connectors,
|
|
582
902
|
origins: new Map(),
|
|
583
903
|
theme,
|
|
@@ -586,11 +906,45 @@ export function layoutSequence(input) {
|
|
|
586
906
|
radii,
|
|
587
907
|
};
|
|
588
908
|
}
|
|
909
|
+
/**
|
|
910
|
+
* The heads a message draws: calls a filled one, replies and async calls an open one, two-way
|
|
911
|
+
* messages one at both ends; styles replace either.
|
|
912
|
+
*/
|
|
913
|
+
function messageHeads(relation, style) {
|
|
914
|
+
const end = style.arrowhead ??
|
|
915
|
+
(relation.heads === "none"
|
|
916
|
+
? "none"
|
|
917
|
+
: relation.line === "dashed" || relation.async
|
|
918
|
+
? "open"
|
|
919
|
+
: "triangle");
|
|
920
|
+
return { start: style.arrowtail ?? (relation.heads === "both" ? end : "none"), end };
|
|
921
|
+
}
|
|
922
|
+
function sequenceText(style, size, weight) {
|
|
923
|
+
return {
|
|
924
|
+
size: style.fontSize ?? size,
|
|
925
|
+
weight: style.fontWeight ?? weight,
|
|
926
|
+
font: textFont(style.font),
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
/** A label's lines 1.25 times its size apart, its first line as tall as a relation label's. */
|
|
930
|
+
function labelText(text) {
|
|
931
|
+
return { ...text, step: text.size * 1.25, height: Math.round((text.size * 18) / 11) };
|
|
932
|
+
}
|
|
933
|
+
/** A note's text: the theme's note size at 400 or its style's, its lines the note scale's ratio. */
|
|
934
|
+
function noteText(styles, scale, note) {
|
|
935
|
+
const text = sequenceText(styles.event(note.id ?? note.key, "note"), scale.note, 400);
|
|
936
|
+
return { ...text, line: Math.round((text.size * scale.noteLine) / scale.note) };
|
|
937
|
+
}
|
|
938
|
+
/** A fragment's text: its operator's size (the relation label size) and font. */
|
|
939
|
+
function fragmentText(styles, scale, event, key) {
|
|
940
|
+
return sequenceText(styles.event(event.id ?? key, "fragment"), scale.relationLabel, 600);
|
|
941
|
+
}
|
|
589
942
|
/** Width of a fragment's header: the operator tab and the first condition. */
|
|
590
|
-
function fragmentHeader(event, metrics, scale) {
|
|
591
|
-
const
|
|
943
|
+
function fragmentHeader(event, metrics, scale, styles) {
|
|
944
|
+
const text = fragmentText(styles, scale, event, event.key);
|
|
945
|
+
const tab = metrics.width(event.operator, text.font, text.weight, text.size) + 18;
|
|
592
946
|
const condition = event.branches[0]?.condition;
|
|
593
|
-
return
|
|
947
|
+
return tab + (condition ? metrics.width(`[${condition}]`, text.font, 400, text.size) + 12 : 0);
|
|
594
948
|
}
|
|
595
949
|
class SequenceEmitter {
|
|
596
950
|
theme;
|
|
@@ -623,8 +977,8 @@ class SequenceEmitter {
|
|
|
623
977
|
height: round2(box.height),
|
|
624
978
|
};
|
|
625
979
|
}
|
|
626
|
-
text(x, y, text, size, weight, fill, anchor = "middle") {
|
|
627
|
-
return textItem(this.metrics, x + this.dx, y + this.dy, text, size, weight, fill, anchor);
|
|
980
|
+
text(x, y, text, size, weight, fill, anchor = "middle", font = "body") {
|
|
981
|
+
return textItem(this.metrics, x + this.dx, y + this.dy, text, size, weight, fill, anchor, font);
|
|
628
982
|
}
|
|
629
983
|
rect(box, radius, fill, stroke, width, dashed = false) {
|
|
630
984
|
const b = this.box(box);
|
|
@@ -647,7 +1001,7 @@ class SequenceEmitter {
|
|
|
647
1001
|
const style = this.styles.element(group);
|
|
648
1002
|
const items = [
|
|
649
1003
|
this.rect(box, theme.groupRadius, style.fill ?? theme.band, style.stroke ?? theme.bandLine, style.width ?? theme.strokeWidth, strokeDash(style.line)),
|
|
650
|
-
this.text(box.x + 12, box.y + 14, group.label, scale.groupTitle, scale.labelWeight, style.color ?? theme.ink, "start"),
|
|
1004
|
+
this.text(box.x + 12, box.y + 14 + Math.max(0, (style.fontSize ?? scale.groupTitle) - scale.groupTitle) / 2, group.label, style.fontSize ?? scale.groupTitle, style.fontWeight ?? scale.labelWeight, style.color ?? theme.ink, "start", textFont(style.font)),
|
|
651
1005
|
];
|
|
652
1006
|
return {
|
|
653
1007
|
kind: "group",
|
|
@@ -658,6 +1012,10 @@ class SequenceEmitter {
|
|
|
658
1012
|
...(group.url ? { href: group.url } : {}),
|
|
659
1013
|
};
|
|
660
1014
|
}
|
|
1015
|
+
/**
|
|
1016
|
+
* A participant's lifeline and its activation bars, each bar its own group of role `event` with
|
|
1017
|
+
* the key of its `activate` (or of the call whose block draws it) and its box as bounds.
|
|
1018
|
+
*/
|
|
661
1019
|
lifeline(participant, line, bars, x) {
|
|
662
1020
|
const { theme } = this;
|
|
663
1021
|
const style = this.styles.element(participant);
|
|
@@ -674,22 +1032,26 @@ class SequenceEmitter {
|
|
|
674
1032
|
},
|
|
675
1033
|
];
|
|
676
1034
|
for (const bar of [...bars].sort((a, b) => a.depth - b.depth)) {
|
|
677
|
-
items.push(
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
1035
|
+
items.push({
|
|
1036
|
+
kind: "group",
|
|
1037
|
+
id: bar.key,
|
|
1038
|
+
role: "event",
|
|
1039
|
+
items: [
|
|
1040
|
+
this.rect(bar.box, 0, style.fill ?? theme.surface, style.stroke ?? theme.muted, theme.strokeWidth),
|
|
1041
|
+
],
|
|
1042
|
+
bounds: this.bounds(bar.box, 0),
|
|
1043
|
+
});
|
|
683
1044
|
}
|
|
684
1045
|
return { kind: "group", id: participant.id, role: "lifeline", items };
|
|
685
1046
|
}
|
|
686
1047
|
/**
|
|
687
1048
|
* A participant's head in its row's cell `head`; `drawn` is the box it is drawn in, which the
|
|
688
|
-
* group carries as its bounds.
|
|
1049
|
+
* group carries as its bounds. Its badges, the pills of its tags, are drawn over it.
|
|
689
1050
|
*/
|
|
690
|
-
head(participant, head, drawn, boxHeight, figureHeight, labelLine) {
|
|
1051
|
+
head(participant, head, drawn, boxHeight, figureHeight, labelLine, badges) {
|
|
691
1052
|
const { theme, scale } = this;
|
|
692
1053
|
const style = this.styles.element(participant);
|
|
1054
|
+
const text = sequenceText(style, scale.label, scale.labelWeight);
|
|
693
1055
|
const fill = style.fill ?? theme.surface;
|
|
694
1056
|
const stroke = style.stroke ?? theme.muted;
|
|
695
1057
|
const strokeWidth = style.width ?? theme.strokeWidth;
|
|
@@ -726,7 +1088,7 @@ class SequenceEmitter {
|
|
|
726
1088
|
}
|
|
727
1089
|
// A queue's label is centred on the body, left of its right cap.
|
|
728
1090
|
const capRoom = participant.kind === "queue" ? Math.min(QUEUE_CAP, box.width * 0.1) : 0;
|
|
729
|
-
items.push(this.text(cx - capRoom, box.y + box.height / 2, participant.label,
|
|
1091
|
+
items.push(this.text(cx - capRoom, box.y + box.height / 2, participant.label, text.size, text.weight, color, "middle", text.font));
|
|
730
1092
|
}
|
|
731
1093
|
else {
|
|
732
1094
|
const top = head.y + head.height - figureHeight - 6 - labelLine;
|
|
@@ -761,8 +1123,9 @@ class SequenceEmitter {
|
|
|
761
1123
|
items.push(this.path(d, fill, figure, Math.max(strokeWidth, 1.5)));
|
|
762
1124
|
}
|
|
763
1125
|
}
|
|
764
|
-
items.push(this.text(cx, head.y + head.height - labelLine / 2, participant.label,
|
|
1126
|
+
items.push(this.text(cx, head.y + head.height - labelLine / 2, participant.label, text.size, text.weight, color, "middle", text.font));
|
|
765
1127
|
}
|
|
1128
|
+
items.push(...this.tags(participant.id, participant.tags, badges));
|
|
766
1129
|
return {
|
|
767
1130
|
kind: "group",
|
|
768
1131
|
id: participant.id,
|
|
@@ -779,16 +1142,8 @@ class SequenceEmitter {
|
|
|
779
1142
|
const stroke = style.stroke ?? theme.connector;
|
|
780
1143
|
const strokeWidth = style.width ?? theme.connectorWidth;
|
|
781
1144
|
const dashed = strokeDash(style.line ?? relation.line);
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
const endKind = style.arrowhead ??
|
|
785
|
-
(relation.heads === "none"
|
|
786
|
-
? "none"
|
|
787
|
-
: relation.line === "dashed" || relation.async
|
|
788
|
-
? "open"
|
|
789
|
-
: "triangle");
|
|
790
|
-
const startKind = style.arrowtail ?? (relation.heads === "both" ? endKind : "none");
|
|
791
|
-
const arrow = relationEnds(message.points, startKind, endKind, theme.arrowSize, (theme.arrowSize * 7) / 8);
|
|
1145
|
+
const heads = messageHeads(relation, style);
|
|
1146
|
+
const arrow = relationEnds(message.points, heads.start, heads.end, theme.arrowSize, (theme.arrowSize * 7) / 8);
|
|
792
1147
|
const items = [this.line(arrow.line, stroke, strokeWidth, dashed)];
|
|
793
1148
|
const local = (parts) => shiftMarker(parts, (q) => this.p(q));
|
|
794
1149
|
items.push(...markerItems(local(arrow.start), stroke, strokeWidth), ...markerItems(local(arrow.end), stroke, strokeWidth));
|
|
@@ -797,17 +1152,18 @@ class SequenceEmitter {
|
|
|
797
1152
|
items.push(this.rect({ x: start.x - 8, y: start.y - 8, width: 16, height: 16 }, 8, style.stroke ?? theme.ink, "none", 0));
|
|
798
1153
|
items.push(this.text(start.x, start.y, String(message.number), Math.round(scale.relationLabel * 0.85), 700, theme.surface));
|
|
799
1154
|
}
|
|
800
|
-
if (message.label && message.label.
|
|
801
|
-
|
|
1155
|
+
if (message.label && message.label.lines.length > 0) {
|
|
1156
|
+
// One text a line, centred, the first where a one-line label is drawn.
|
|
1157
|
+
const { box, lines, line, text: written } = message.label;
|
|
802
1158
|
items.push({
|
|
803
1159
|
kind: "group",
|
|
804
1160
|
id: message.key,
|
|
805
1161
|
role: "label",
|
|
806
|
-
items:
|
|
807
|
-
this.text(box.x + box.width / 2, box.y + box.height / 2, message.label.text, scale.relationLabel, 400, style.color ?? theme.ink),
|
|
808
|
-
],
|
|
1162
|
+
items: lines.map((text, i) => this.text(box.x + box.width / 2, box.y + written.height / 2 + i * line, text, written.size, written.weight, style.color ?? theme.ink, "middle", written.font)),
|
|
809
1163
|
});
|
|
810
1164
|
}
|
|
1165
|
+
// The pills of its tags, over the arrow and after the label, so that particles run under both.
|
|
1166
|
+
items.push(...this.tags(message.key, relation.tags, message.tags));
|
|
811
1167
|
return {
|
|
812
1168
|
kind: "group",
|
|
813
1169
|
id: message.key,
|
|
@@ -816,6 +1172,23 @@ class SequenceEmitter {
|
|
|
816
1172
|
track: arrow.line.map((q) => this.p(q)),
|
|
817
1173
|
};
|
|
818
1174
|
}
|
|
1175
|
+
/**
|
|
1176
|
+
* The pills of an owner's tags in their boxes, each a group of role `tag` with the owner's id or
|
|
1177
|
+
* key and its index: the flow tag's pill in the theme's `tag` and `tagText`.
|
|
1178
|
+
*/
|
|
1179
|
+
tags(owner, tags, boxes) {
|
|
1180
|
+
const { theme } = this;
|
|
1181
|
+
return boxes.map((box, index) => {
|
|
1182
|
+
const { outline, label } = tagPill(this.metrics, this.scale, this.box(box), tags[index].text, {
|
|
1183
|
+
fill: theme.tag,
|
|
1184
|
+
stroke: theme.tag,
|
|
1185
|
+
strokeWidth: theme.strokeWidth,
|
|
1186
|
+
dashed: false,
|
|
1187
|
+
color: theme.tagText,
|
|
1188
|
+
});
|
|
1189
|
+
return { kind: "group", id: owner, role: "tag", tag: index, items: [outline, label] };
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
819
1192
|
note(note) {
|
|
820
1193
|
const { theme, scale } = this;
|
|
821
1194
|
const style = note.event.id
|
|
@@ -829,8 +1202,9 @@ class SequenceEmitter {
|
|
|
829
1202
|
const items = [
|
|
830
1203
|
this.path(d, style.fill ?? theme.surface, style.stroke ?? theme.muted, style.width ?? theme.strokeWidth, strokeDash(style.line)),
|
|
831
1204
|
];
|
|
1205
|
+
const text = noteText(this.styles, scale, { id: note.event.id, key: note.key });
|
|
832
1206
|
splitLines(note.event.text).forEach((line, i) => {
|
|
833
|
-
items.push(this.text(b.x + 8, b.y + 6 +
|
|
1207
|
+
items.push(this.text(b.x + 8, b.y + 6 + text.line / 2 + i * text.line, line, text.size, text.weight, style.color ?? theme.ink, "start", text.font));
|
|
834
1208
|
});
|
|
835
1209
|
return { kind: "group", id: note.key, role: "event", items, bounds: this.bounds(b, 0) };
|
|
836
1210
|
}
|
|
@@ -860,28 +1234,30 @@ class SequenceEmitter {
|
|
|
860
1234
|
const style = this.styles.event(event.id ?? frame.key, "fragment");
|
|
861
1235
|
const b = frame.box;
|
|
862
1236
|
const stroke = style.stroke ?? theme.muted;
|
|
863
|
-
const
|
|
1237
|
+
const written = fragmentText(this.styles, scale, event, frame.key);
|
|
1238
|
+
const size = written.size;
|
|
1239
|
+
const operatorWeight = written.weight;
|
|
864
1240
|
const color = style.color ?? theme.ink;
|
|
865
1241
|
const muted = style.color ?? theme.muted;
|
|
866
1242
|
const first = event.branches[0]?.condition ?? null;
|
|
867
1243
|
const h = frame.headerHeight;
|
|
868
1244
|
const items = [];
|
|
869
1245
|
const label = (x, y, text, weight, fill) => {
|
|
870
|
-
const width = metrics.width(text,
|
|
1246
|
+
const width = metrics.width(text, written.font, weight, size) + 6;
|
|
871
1247
|
items.push(this.rect({ x: x - 3, y: y - h / 2 + 4, width, height: h - 8 }, 2, theme.background, "none", 0));
|
|
872
|
-
items.push(this.text(x, y, text, size, weight, fill, "start"));
|
|
1248
|
+
items.push(this.text(x, y, text, size, weight, fill, "start", written.font));
|
|
873
1249
|
};
|
|
874
1250
|
if (event.operator === "region") {
|
|
875
1251
|
if (first)
|
|
876
|
-
label(b.x + 8, b.y + h / 2, first,
|
|
1252
|
+
label(b.x + 8, b.y + h / 2, first, operatorWeight, color);
|
|
877
1253
|
}
|
|
878
1254
|
else {
|
|
879
|
-
const tabWidth = metrics.width(event.operator,
|
|
1255
|
+
const tabWidth = metrics.width(event.operator, written.font, operatorWeight, size) + 18;
|
|
880
1256
|
const s = this.box(b);
|
|
881
1257
|
const n = (v) => String(round2(v));
|
|
882
1258
|
const tab = `M${n(s.x)} ${n(s.y)} L${n(s.x + tabWidth)} ${n(s.y)} L${n(s.x + tabWidth)} ${n(s.y + h - 6)} L${n(s.x + tabWidth - 6)} ${n(s.y + h)} L${n(s.x)} ${n(s.y + h)} Z`;
|
|
883
1259
|
items.push(this.path(tab, theme.surface, stroke, style.width ?? theme.strokeWidth));
|
|
884
|
-
items.push(this.text(b.x + 8, b.y + h / 2, event.operator, size,
|
|
1260
|
+
items.push(this.text(b.x + 8, b.y + h / 2, event.operator, size, operatorWeight, color, "start", written.font));
|
|
885
1261
|
if (first)
|
|
886
1262
|
label(b.x + tabWidth + 8, b.y + h / 2, `[${first}]`, 400, muted);
|
|
887
1263
|
}
|