@meltemi/diagrams-layout 1.9.0 → 1.10.17
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 +34 -17
- 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 +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/dist/layout.d.ts +203 -6
- package/dist/layout.d.ts.map +1 -1
- package/dist/layout.js +1308 -138
- 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 +37 -0
- package/dist/messages/en.d.ts.map +1 -1
- package/dist/messages/en.js +37 -0
- package/dist/messages/en.js.map +1 -1
- package/dist/messages/es.d.ts.map +1 -1
- package/dist/messages/es.js +14 -0
- package/dist/messages/es.js.map +1 -1
- package/dist/messages/index.d.ts +14 -0
- package/dist/messages/index.d.ts.map +1 -1
- package/dist/scene.d.ts +46 -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 +719 -286
- package/dist/sequence.js.map +1 -1
- package/dist/shapes.d.ts +19 -0
- package/dist/shapes.d.ts.map +1 -1
- package/dist/shapes.js +58 -0
- package/dist/shapes.js.map +1 -1
- package/dist/styles.d.ts +7 -1
- package/dist/styles.d.ts.map +1 -1
- package/dist/styles.js +22 -2
- 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 +13 -1
- package/dist/theme.d.ts.map +1 -1
- package/dist/theme.js +69 -30
- package/dist/theme.js.map +1 -1
- package/package.json +6 -6
package/dist/sequence.js
CHANGED
|
@@ -2,18 +2,35 @@ 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;
|
|
15
|
+
/** How far the second box of a collections head stands up and to the right of the first. */
|
|
16
|
+
const STACK = 4;
|
|
17
|
+
/** The opacity of a lifeline's stroke: half of `muted`, behind the messages. */
|
|
18
|
+
const LIFELINE_OPACITY = 0.5;
|
|
13
19
|
const BAR = 10;
|
|
14
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;
|
|
15
25
|
const SELF_WIDTH = 34;
|
|
16
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;
|
|
17
34
|
/** Lays out a sequence diagram (decision 0019): columns in source order, events top to bottom. */
|
|
18
35
|
export function layoutSequence(input) {
|
|
19
36
|
const { compilation, diagram, options, theme, scale, metrics, diagnostics } = input;
|
|
@@ -29,73 +46,188 @@ export function layoutSequence(input) {
|
|
|
29
46
|
const groups = diagram.elements.filter((e) => e.kind === "group");
|
|
30
47
|
const column = new Map(participants.map((p, i) => [p.id, i]));
|
|
31
48
|
// ---------------------------------------------------------------- measures
|
|
32
|
-
|
|
33
|
-
|
|
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));
|
|
34
58
|
const figureHeight = Math.round(34 * f);
|
|
35
59
|
const hasFigures = participants.some((p) => !BOX_KINDS.has(p.kind));
|
|
36
60
|
const headHeight = Math.max(boxHeight, hasFigures ? figureHeight + 6 + labelLine : 0);
|
|
37
|
-
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
|
+
};
|
|
38
65
|
const widths = participants.map((p) => BOX_KINDS.has(p.kind)
|
|
39
66
|
? Math.max(Math.round(96 * f), Math.ceil(labelWidth(p) + 2 * theme.padding)) +
|
|
40
67
|
// A queue's right cap takes room from its body.
|
|
41
68
|
(p.kind === "queue" ? 2 * QUEUE_CAP : 0)
|
|
42
69
|
: Math.max(Math.round(48 * f), Math.ceil(labelWidth(p) + 8)));
|
|
43
|
-
|
|
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)));
|
|
44
87
|
const noteLines = (note) => splitLines(note.text);
|
|
45
88
|
const noteSize = (note) => {
|
|
46
89
|
const lines = noteLines(note);
|
|
47
|
-
const
|
|
48
|
-
|
|
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 };
|
|
49
93
|
};
|
|
50
94
|
const numbered = diagram.properties.autonumber !== null;
|
|
51
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.
|
|
52
138
|
const spaces = new Map();
|
|
53
|
-
const
|
|
139
|
+
const spacesAfter = new Map();
|
|
140
|
+
const gapsBefore = new Map();
|
|
141
|
+
const gapsAfter = new Map();
|
|
54
142
|
const labelEntries = new Map();
|
|
55
143
|
for (const entry of diagram.placement) {
|
|
56
|
-
if (entry.statement === "space")
|
|
57
|
-
spaces.set(entry.target, entry.amount);
|
|
58
|
-
|
|
59
|
-
|
|
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
|
+
}
|
|
60
150
|
else if (entry.statement === "label")
|
|
61
151
|
labelEntries.set(entry.target, entry);
|
|
62
152
|
}
|
|
153
|
+
const gapBefore = (id) => gapsBefore.get(id)?.amount ?? 0;
|
|
154
|
+
const gapAfter = (id) => gapsAfter.get(id)?.amount ?? 0;
|
|
63
155
|
const baseKey = (key) => key.replace(/@\d+$/, "").replace(/@\d+$/, "");
|
|
64
156
|
// ---------------------------------------------------------------- columns
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
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
|
+
};
|
|
77
187
|
let leftExtra = 0;
|
|
78
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;
|
|
79
197
|
const walk = (list) => {
|
|
80
|
-
for (const event of list) {
|
|
198
|
+
for (const [index, event] of list.entries()) {
|
|
81
199
|
if (event.kind === "message") {
|
|
82
200
|
const relation = diagram.relationsByKey.get(event.relation);
|
|
83
201
|
if (!relation)
|
|
84
202
|
continue;
|
|
85
203
|
const a = column.get(relation.from);
|
|
86
204
|
const b = column.get(relation.to);
|
|
87
|
-
const room = messageLabel(relation) + discRoom;
|
|
88
205
|
if (a === b) {
|
|
89
|
-
const length =
|
|
206
|
+
const length = loopWidth(relation) + lineWidth(relation) + discRoom + 16;
|
|
90
207
|
if (a < participants.length - 1) {
|
|
91
|
-
|
|
208
|
+
both({ a, b: a + 1, length: length + widths[a + 1] / 2 });
|
|
92
209
|
}
|
|
93
210
|
else
|
|
94
211
|
rightExtra = Math.max(rightExtra, length);
|
|
95
212
|
}
|
|
96
213
|
else {
|
|
97
|
-
|
|
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
|
+
});
|
|
98
229
|
}
|
|
230
|
+
walk(event.events);
|
|
99
231
|
}
|
|
100
232
|
else if (event.kind === "note") {
|
|
101
233
|
const size = noteSize(event);
|
|
@@ -104,26 +236,26 @@ export function layoutSequence(input) {
|
|
|
104
236
|
const j = indices[indices.length - 1];
|
|
105
237
|
if (event.position === "over" && i === j) {
|
|
106
238
|
if (i > 0)
|
|
107
|
-
|
|
239
|
+
both({ a: i - 1, b: i, length: size.width / 2 + widths[i - 1] / 2 + 10 });
|
|
108
240
|
else
|
|
109
241
|
leftExtra = Math.max(leftExtra, size.width / 2 - widths[i] / 2);
|
|
110
242
|
if (i < participants.length - 1) {
|
|
111
|
-
|
|
243
|
+
both({ a: i, b: i + 1, length: size.width / 2 + widths[i + 1] / 2 + 10 });
|
|
112
244
|
}
|
|
113
245
|
else
|
|
114
246
|
rightExtra = Math.max(rightExtra, size.width / 2 - widths[i] / 2);
|
|
115
247
|
}
|
|
116
248
|
else if (event.position === "over") {
|
|
117
|
-
|
|
249
|
+
both({ a: i, b: j, length: size.width - 48 });
|
|
118
250
|
}
|
|
119
251
|
else if (event.position === "left") {
|
|
120
252
|
if (i > 0)
|
|
121
|
-
|
|
253
|
+
both({ a: i - 1, b: i, length: size.width + 20 + widths[i - 1] / 2 });
|
|
122
254
|
else
|
|
123
255
|
leftExtra = Math.max(leftExtra, size.width + 20 - widths[i] / 2);
|
|
124
256
|
}
|
|
125
257
|
else if (i < participants.length - 1) {
|
|
126
|
-
|
|
258
|
+
both({ a: i, b: i + 1, length: size.width + 20 + widths[i + 1] / 2 });
|
|
127
259
|
}
|
|
128
260
|
else
|
|
129
261
|
rightExtra = Math.max(rightExtra, size.width + 20 - widths[i] / 2);
|
|
@@ -135,30 +267,92 @@ export function layoutSequence(input) {
|
|
|
135
267
|
}
|
|
136
268
|
};
|
|
137
269
|
walk(events);
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
+
});
|
|
144
332
|
}
|
|
145
333
|
// ---------------------------------------------------------------- rows
|
|
146
|
-
|
|
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;
|
|
147
338
|
const rowGap = Math.round(14 * f);
|
|
148
|
-
|
|
149
|
-
|
|
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);
|
|
150
342
|
const lifelines = new Map();
|
|
151
343
|
for (const p of participants) {
|
|
152
344
|
lifelines.set(p.id, {
|
|
153
345
|
participant: p.id,
|
|
154
346
|
x: xs[column.get(p.id)],
|
|
155
|
-
top:
|
|
347
|
+
top: headTop + headHeight,
|
|
156
348
|
bottom: null,
|
|
157
349
|
});
|
|
158
350
|
}
|
|
159
351
|
const created = new Map();
|
|
160
352
|
const heads = new Map();
|
|
161
353
|
const messages = [];
|
|
354
|
+
const messageTexts = new Map();
|
|
355
|
+
const tags = new Map();
|
|
162
356
|
const notes = [];
|
|
163
357
|
const frames = [];
|
|
164
358
|
const dividers = [];
|
|
@@ -187,226 +381,328 @@ export function layoutSequence(input) {
|
|
|
187
381
|
const width = widths[i];
|
|
188
382
|
return { x: xs[i] - width / 2, y: top, width, height: headHeight };
|
|
189
383
|
};
|
|
384
|
+
/**
|
|
385
|
+
* The box a head is drawn in, within its row's cell: a box kind's box at the bottom of the cell
|
|
386
|
+
* (a collections head with its second box), a figure's whole cell. Editors select, halos pulse
|
|
387
|
+
* and highlights frame this box.
|
|
388
|
+
*/
|
|
389
|
+
const drawnHead = (p, cell) => {
|
|
390
|
+
if (!BOX_KINDS.has(p.kind))
|
|
391
|
+
return cell;
|
|
392
|
+
const box = {
|
|
393
|
+
x: cell.x,
|
|
394
|
+
y: cell.y + cell.height - boxHeight,
|
|
395
|
+
width: cell.width,
|
|
396
|
+
height: boxHeight,
|
|
397
|
+
};
|
|
398
|
+
return p.kind === "collections"
|
|
399
|
+
? { x: box.x, y: box.y - STACK, width: box.width + STACK, height: box.height + STACK }
|
|
400
|
+
: box;
|
|
401
|
+
};
|
|
190
402
|
const process = (list) => {
|
|
191
403
|
for (const [index, event] of list.entries()) {
|
|
192
404
|
y += spaces.get(baseKey(event.key)) ?? 0;
|
|
193
405
|
const start = y;
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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);
|
|
231
449
|
points = [
|
|
232
|
-
{ x:
|
|
233
|
-
{ 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 },
|
|
234
454
|
];
|
|
235
|
-
|
|
455
|
+
arrowY = top;
|
|
456
|
+
y = top + SELF_HEIGHT + over + rowGap;
|
|
236
457
|
}
|
|
237
458
|
else {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
+
}
|
|
244
489
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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.
|
|
248
493
|
const entry = relation.id ? labelEntries.get(relation.id) : undefined;
|
|
249
494
|
const first = points[0];
|
|
250
495
|
const lastPoint = points[points.length - 1];
|
|
251
496
|
const at = entry ? entry.at / 100 : 0.5;
|
|
252
497
|
const offset = entry?.offset ?? { x: 0, y: 0 };
|
|
253
|
-
const width = Math.max(
|
|
254
|
-
const anchor =
|
|
255
|
-
? { x: first.x +
|
|
256
|
-
: { x: first.x + (lastPoint.x - first.x) * at, y: arrowY -
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
height: labelHeight,
|
|
263
|
-
},
|
|
264
|
-
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,
|
|
265
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;
|
|
266
565
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
const bar = {
|
|
285
|
-
participant: event.participant,
|
|
286
|
-
depth: stack.length,
|
|
287
|
-
top,
|
|
288
|
-
bottom: top,
|
|
289
|
-
};
|
|
290
|
-
stack.push(bar);
|
|
291
|
-
stacks.set(event.participant, stack);
|
|
292
|
-
bars.push(bar);
|
|
293
|
-
rows.set(event.key, { y: top, height: 0 });
|
|
294
|
-
continue;
|
|
295
|
-
}
|
|
296
|
-
case "deactivate": {
|
|
297
|
-
const stack = stacks.get(event.participant) ?? [];
|
|
298
|
-
const bar = stack.pop();
|
|
299
|
-
const bottom = last && last.participants.has(event.participant) ? last.y : y;
|
|
300
|
-
if (bar)
|
|
301
|
-
bar.bottom = Math.max(bottom, bar.top + 12);
|
|
302
|
-
rows.set(event.key, { y: bottom, height: 0 });
|
|
303
|
-
continue;
|
|
304
|
-
}
|
|
305
|
-
case "create": {
|
|
306
|
-
const next = list[index + 1];
|
|
307
|
-
const relation = next?.kind === "message" ? diagram.relationsByKey.get(next.relation) : undefined;
|
|
308
|
-
if (relation && relation.to === event.participant && relation.from !== relation.to) {
|
|
309
|
-
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;
|
|
310
583
|
}
|
|
311
|
-
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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;
|
|
317
594
|
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
const size = noteSize(event);
|
|
336
|
-
const indices = event.participants.map((p) => column.get(p)).sort((p, q) => p - q);
|
|
337
|
-
const i = indices[0];
|
|
338
|
-
const j = indices[indices.length - 1];
|
|
339
|
-
const top = y + 4;
|
|
340
|
-
let x;
|
|
341
|
-
let width = size.width;
|
|
342
|
-
if (event.position === "over") {
|
|
343
|
-
const left = xs[i] - (i === j ? 0 : 24);
|
|
344
|
-
const right = xs[j] + (i === j ? 0 : 24);
|
|
345
|
-
width = Math.max(size.width, right - left);
|
|
346
|
-
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;
|
|
347
612
|
}
|
|
348
|
-
|
|
349
|
-
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;
|
|
350
625
|
}
|
|
351
|
-
|
|
352
|
-
|
|
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;
|
|
353
653
|
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
for (const [b, branch] of event.branches.entries()) {
|
|
370
|
-
if (b > 0) {
|
|
371
|
-
const sep = y + 2;
|
|
372
|
-
separators.push({ y: sep, condition: branch.condition });
|
|
373
|
-
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);
|
|
374
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 });
|
|
375
684
|
last = undefined;
|
|
376
|
-
|
|
685
|
+
continue;
|
|
377
686
|
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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;
|
|
384
700
|
}
|
|
385
|
-
const minX = frame.minX - 16;
|
|
386
|
-
const maxX = Math.max(frame.maxX + 16, minX + headerText + 24);
|
|
387
|
-
const box = { x: minX, y: top, width: maxX - minX, height: bottom - top };
|
|
388
|
-
frames.push({ key: event.key, event, box, headerHeight, separators });
|
|
389
|
-
extend(box.x - 8, box.x + box.width + 8);
|
|
390
|
-
y = bottom + rowGap;
|
|
391
|
-
rows.set(event.key, { y: start, height: y - start });
|
|
392
|
-
last = undefined;
|
|
393
|
-
continue;
|
|
394
|
-
}
|
|
395
|
-
case "divider": {
|
|
396
|
-
dividers.push({ key: event.key, y: y + 12, label: event.label });
|
|
397
|
-
y += 24 + rowGap;
|
|
398
|
-
rows.set(event.key, { y: start, height: y - start });
|
|
399
|
-
last = undefined;
|
|
400
|
-
continue;
|
|
401
|
-
}
|
|
402
|
-
case "delay": {
|
|
403
|
-
delays.push({ key: event.key, y: y + 14, label: event.label });
|
|
404
|
-
y += 28 + rowGap;
|
|
405
|
-
rows.set(event.key, { y: start, height: y - start });
|
|
406
|
-
last = undefined;
|
|
407
|
-
continue;
|
|
408
701
|
}
|
|
409
702
|
}
|
|
703
|
+
finally {
|
|
704
|
+
y += spacesAfter.get(baseKey(event.key)) ?? 0;
|
|
705
|
+
}
|
|
410
706
|
}
|
|
411
707
|
};
|
|
412
708
|
process(events);
|
|
@@ -419,15 +715,35 @@ export function layoutSequence(input) {
|
|
|
419
715
|
bar.bottom = Math.max(bar.bottom, end);
|
|
420
716
|
for (const p of participants) {
|
|
421
717
|
if (!heads.has(p.id))
|
|
422
|
-
heads.set(p.id, headBoxAt(p,
|
|
718
|
+
heads.set(p.id, headBoxAt(p, headTop));
|
|
423
719
|
const line = lifelines.get(p.id);
|
|
424
720
|
if (line.bottom === null)
|
|
425
721
|
line.bottom = end;
|
|
426
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
|
+
});
|
|
427
742
|
const groupBoxes = new Map();
|
|
428
743
|
for (const group of groups) {
|
|
429
744
|
const members = participants.filter((p) => p.groupPath.includes(group.id));
|
|
430
|
-
|
|
745
|
+
// A group holds its members' heads and their badges.
|
|
746
|
+
const boxes = members.flatMap((m) => [heads.get(m.id), ...(badges.get(m.id) ?? [])]);
|
|
431
747
|
const union = unionBoxes(boxes);
|
|
432
748
|
if (!union)
|
|
433
749
|
continue;
|
|
@@ -439,12 +755,19 @@ export function layoutSequence(input) {
|
|
|
439
755
|
});
|
|
440
756
|
}
|
|
441
757
|
// ---------------------------------------------------------------- canvas
|
|
442
|
-
|
|
443
|
-
const
|
|
758
|
+
// A diagram with no participant yet (a new one) has an empty width, not an unknown one.
|
|
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;
|
|
763
|
+
const left = Math.min(leftmost, ...frames.map((fr) => fr.box.x));
|
|
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] : [])));
|
|
444
765
|
const everything = [
|
|
445
766
|
{ x: left, y: 0, width: right - left, height: end + 8 },
|
|
446
767
|
...groupBoxes.values(),
|
|
447
768
|
...heads.values(),
|
|
769
|
+
...[...badges.values()].flat(),
|
|
770
|
+
...[...tags.values()].flat(),
|
|
448
771
|
];
|
|
449
772
|
const bounds = unionBoxes(everything);
|
|
450
773
|
const margin = theme.margin;
|
|
@@ -474,7 +797,9 @@ export function layoutSequence(input) {
|
|
|
474
797
|
items.push(emit.frame(frame));
|
|
475
798
|
const headers = frames.map((frame) => emit.frameHeader(frame));
|
|
476
799
|
for (const p of participants) {
|
|
477
|
-
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)]));
|
|
478
803
|
}
|
|
479
804
|
items.push(...headers);
|
|
480
805
|
for (const message of messages)
|
|
@@ -488,7 +813,8 @@ export function layoutSequence(input) {
|
|
|
488
813
|
for (const cross of crosses)
|
|
489
814
|
items.push(emit.cross(cross));
|
|
490
815
|
for (const p of participants) {
|
|
491
|
-
|
|
816
|
+
const cell = heads.get(p.id);
|
|
817
|
+
items.push(emit.head(p, cell, drawnHead(p, cell), boxHeight, figureHeight, labelLine, badges.get(p.id) ?? []));
|
|
492
818
|
}
|
|
493
819
|
const gradients = [];
|
|
494
820
|
if (framed) {
|
|
@@ -498,14 +824,43 @@ export function layoutSequence(input) {
|
|
|
498
824
|
gradients.push(markGradient(theme.accent, theme.accentEnd));
|
|
499
825
|
}
|
|
500
826
|
const boxes = new Map();
|
|
501
|
-
|
|
502
|
-
|
|
827
|
+
// Box kinds are drawn as rounded boxes and databases as cylinders; figures have no outline.
|
|
828
|
+
const shapes = new Map();
|
|
829
|
+
const radii = new Map();
|
|
830
|
+
for (const p of participants) {
|
|
831
|
+
boxes.set(p.id, drawnHead(p, heads.get(p.id)));
|
|
832
|
+
if (!BOX_KINDS.has(p.kind))
|
|
833
|
+
continue;
|
|
834
|
+
if (p.kind === "database") {
|
|
835
|
+
shapes.set(p.id, "cylinder");
|
|
836
|
+
}
|
|
837
|
+
else {
|
|
838
|
+
shapes.set(p.id, "rounded");
|
|
839
|
+
radii.set(p.id, theme.radius);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
503
842
|
for (const [id, box] of groupBoxes)
|
|
504
843
|
boxes.set(id, box);
|
|
505
844
|
for (const note of notes)
|
|
506
845
|
boxes.set(note.key, note.box);
|
|
507
846
|
for (const frame of frames)
|
|
508
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
|
+
}
|
|
509
864
|
const connectors = new Map();
|
|
510
865
|
const labels = new Map();
|
|
511
866
|
for (const message of messages) {
|
|
@@ -533,22 +888,63 @@ export function layoutSequence(input) {
|
|
|
533
888
|
gradients: [...gradients, ...emit.gradients],
|
|
534
889
|
patterns: [],
|
|
535
890
|
items,
|
|
891
|
+
...(theme.sketch !== "none" ? { sketch: theme.sketch } : {}),
|
|
536
892
|
},
|
|
537
893
|
diagnostics,
|
|
538
894
|
boxes,
|
|
539
895
|
offset: { x: dx, y: dy },
|
|
540
896
|
labels,
|
|
897
|
+
messageTexts,
|
|
898
|
+
bars: barGeometry,
|
|
899
|
+
// The participants' badges, then the messages' pills.
|
|
900
|
+
tags: new Map([...badges, ...tags]),
|
|
541
901
|
connectors,
|
|
542
902
|
origins: new Map(),
|
|
543
903
|
theme,
|
|
544
904
|
events: rows,
|
|
905
|
+
shapes,
|
|
906
|
+
radii,
|
|
545
907
|
};
|
|
546
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
|
+
}
|
|
547
942
|
/** Width of a fragment's header: the operator tab and the first condition. */
|
|
548
|
-
function fragmentHeader(event, metrics, scale) {
|
|
549
|
-
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;
|
|
550
946
|
const condition = event.branches[0]?.condition;
|
|
551
|
-
return
|
|
947
|
+
return tab + (condition ? metrics.width(`[${condition}]`, text.font, 400, text.size) + 12 : 0);
|
|
552
948
|
}
|
|
553
949
|
class SequenceEmitter {
|
|
554
950
|
theme;
|
|
@@ -581,8 +977,8 @@ class SequenceEmitter {
|
|
|
581
977
|
height: round2(box.height),
|
|
582
978
|
};
|
|
583
979
|
}
|
|
584
|
-
text(x, y, text, size, weight, fill, anchor = "middle") {
|
|
585
|
-
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);
|
|
586
982
|
}
|
|
587
983
|
rect(box, radius, fill, stroke, width, dashed = false) {
|
|
588
984
|
const b = this.box(box);
|
|
@@ -605,7 +1001,7 @@ class SequenceEmitter {
|
|
|
605
1001
|
const style = this.styles.element(group);
|
|
606
1002
|
const items = [
|
|
607
1003
|
this.rect(box, theme.groupRadius, style.fill ?? theme.band, style.stroke ?? theme.bandLine, style.width ?? theme.strokeWidth, strokeDash(style.line)),
|
|
608
|
-
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)),
|
|
609
1005
|
];
|
|
610
1006
|
return {
|
|
611
1007
|
kind: "group",
|
|
@@ -616,30 +1012,48 @@ class SequenceEmitter {
|
|
|
616
1012
|
...(group.url ? { href: group.url } : {}),
|
|
617
1013
|
};
|
|
618
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
|
+
*/
|
|
619
1019
|
lifeline(participant, line, bars, x) {
|
|
620
1020
|
const { theme } = this;
|
|
621
1021
|
const style = this.styles.element(participant);
|
|
1022
|
+
// Lifelines, bars, heads and notes take `muted`: the quiet `line` of boxes is too pale for the
|
|
1023
|
+
// long thin strokes a sequence diagram is made of (decision 0052). A lifeline is a guide, drawn
|
|
1024
|
+
// at half strength so the messages that cross it stay in front.
|
|
622
1025
|
const items = [
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
1026
|
+
{
|
|
1027
|
+
...this.line([
|
|
1028
|
+
{ x, y: line.top },
|
|
1029
|
+
{ x, y: line.bottom ?? line.top },
|
|
1030
|
+
], style.stroke ?? theme.muted, theme.strokeWidth, true),
|
|
1031
|
+
opacity: LIFELINE_OPACITY,
|
|
1032
|
+
},
|
|
627
1033
|
];
|
|
628
1034
|
for (const bar of [...bars].sort((a, b) => a.depth - b.depth)) {
|
|
629
|
-
items.push(
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
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
|
+
});
|
|
635
1044
|
}
|
|
636
1045
|
return { kind: "group", id: participant.id, role: "lifeline", items };
|
|
637
1046
|
}
|
|
638
|
-
|
|
1047
|
+
/**
|
|
1048
|
+
* A participant's head in its row's cell `head`; `drawn` is the box it is drawn in, which the
|
|
1049
|
+
* group carries as its bounds. Its badges, the pills of its tags, are drawn over it.
|
|
1050
|
+
*/
|
|
1051
|
+
head(participant, head, drawn, boxHeight, figureHeight, labelLine, badges) {
|
|
639
1052
|
const { theme, scale } = this;
|
|
640
1053
|
const style = this.styles.element(participant);
|
|
1054
|
+
const text = sequenceText(style, scale.label, scale.labelWeight);
|
|
641
1055
|
const fill = style.fill ?? theme.surface;
|
|
642
|
-
const stroke = style.stroke ?? theme.
|
|
1056
|
+
const stroke = style.stroke ?? theme.muted;
|
|
643
1057
|
const strokeWidth = style.width ?? theme.strokeWidth;
|
|
644
1058
|
const dashed = strokeDash(style.line);
|
|
645
1059
|
const color = style.color ?? theme.ink;
|
|
@@ -658,7 +1072,7 @@ class SequenceEmitter {
|
|
|
658
1072
|
items.push(this.path(body, fill, stroke, strokeWidth, dashed), this.path(top, fill, stroke, strokeWidth, dashed));
|
|
659
1073
|
}
|
|
660
1074
|
else if (participant.kind === "collections") {
|
|
661
|
-
items.push(this.rect({ ...box, x: box.x +
|
|
1075
|
+
items.push(this.rect({ ...box, x: box.x + STACK, y: box.y - STACK }, theme.radius, fill, stroke, strokeWidth, dashed));
|
|
662
1076
|
items.push(this.rect(box, theme.radius, fill, stroke, strokeWidth, dashed));
|
|
663
1077
|
}
|
|
664
1078
|
else if (participant.kind === "queue") {
|
|
@@ -674,7 +1088,7 @@ class SequenceEmitter {
|
|
|
674
1088
|
}
|
|
675
1089
|
// A queue's label is centred on the body, left of its right cap.
|
|
676
1090
|
const capRoom = participant.kind === "queue" ? Math.min(QUEUE_CAP, box.width * 0.1) : 0;
|
|
677
|
-
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));
|
|
678
1092
|
}
|
|
679
1093
|
else {
|
|
680
1094
|
const top = head.y + head.height - figureHeight - 6 - labelLine;
|
|
@@ -709,14 +1123,15 @@ class SequenceEmitter {
|
|
|
709
1123
|
items.push(this.path(d, fill, figure, Math.max(strokeWidth, 1.5)));
|
|
710
1124
|
}
|
|
711
1125
|
}
|
|
712
|
-
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));
|
|
713
1127
|
}
|
|
1128
|
+
items.push(...this.tags(participant.id, participant.tags, badges));
|
|
714
1129
|
return {
|
|
715
1130
|
kind: "group",
|
|
716
1131
|
id: participant.id,
|
|
717
1132
|
role: "element",
|
|
718
1133
|
items,
|
|
719
|
-
bounds: this.bounds(
|
|
1134
|
+
bounds: this.bounds(drawn, theme.radius),
|
|
720
1135
|
...(participant.url ? { href: participant.url } : {}),
|
|
721
1136
|
};
|
|
722
1137
|
}
|
|
@@ -727,11 +1142,8 @@ class SequenceEmitter {
|
|
|
727
1142
|
const stroke = style.stroke ?? theme.connector;
|
|
728
1143
|
const strokeWidth = style.width ?? theme.connectorWidth;
|
|
729
1144
|
const dashed = strokeDash(style.line ?? relation.line);
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
const endKind = style.arrowhead ?? (relation.line === "dashed" || relation.async ? "open" : "triangle");
|
|
733
|
-
const startKind = style.arrowtail ?? (relation.twoWay ? endKind : "none");
|
|
734
|
-
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);
|
|
735
1147
|
const items = [this.line(arrow.line, stroke, strokeWidth, dashed)];
|
|
736
1148
|
const local = (parts) => shiftMarker(parts, (q) => this.p(q));
|
|
737
1149
|
items.push(...markerItems(local(arrow.start), stroke, strokeWidth), ...markerItems(local(arrow.end), stroke, strokeWidth));
|
|
@@ -740,17 +1152,18 @@ class SequenceEmitter {
|
|
|
740
1152
|
items.push(this.rect({ x: start.x - 8, y: start.y - 8, width: 16, height: 16 }, 8, style.stroke ?? theme.ink, "none", 0));
|
|
741
1153
|
items.push(this.text(start.x, start.y, String(message.number), Math.round(scale.relationLabel * 0.85), 700, theme.surface));
|
|
742
1154
|
}
|
|
743
|
-
if (message.label && message.label.
|
|
744
|
-
|
|
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;
|
|
745
1158
|
items.push({
|
|
746
1159
|
kind: "group",
|
|
747
1160
|
id: message.key,
|
|
748
1161
|
role: "label",
|
|
749
|
-
items:
|
|
750
|
-
this.text(box.x + box.width / 2, box.y + box.height / 2, message.label.text, scale.relationLabel, 400, style.color ?? theme.ink),
|
|
751
|
-
],
|
|
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)),
|
|
752
1163
|
});
|
|
753
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));
|
|
754
1167
|
return {
|
|
755
1168
|
kind: "group",
|
|
756
1169
|
id: message.key,
|
|
@@ -759,6 +1172,23 @@ class SequenceEmitter {
|
|
|
759
1172
|
track: arrow.line.map((q) => this.p(q)),
|
|
760
1173
|
};
|
|
761
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
|
+
}
|
|
762
1192
|
note(note) {
|
|
763
1193
|
const { theme, scale } = this;
|
|
764
1194
|
const style = note.event.id
|
|
@@ -770,10 +1200,11 @@ class SequenceEmitter {
|
|
|
770
1200
|
const n = (v) => String(round2(v));
|
|
771
1201
|
const d = `M${n(s.x)} ${n(s.y)} L${n(s.x + s.width - fold)} ${n(s.y)} L${n(s.x + s.width)} ${n(s.y + fold)} L${n(s.x + s.width)} ${n(s.y + s.height)} L${n(s.x)} ${n(s.y + s.height)} Z`;
|
|
772
1202
|
const items = [
|
|
773
|
-
this.path(d, style.fill ?? theme.surface, style.stroke ?? theme.
|
|
1203
|
+
this.path(d, style.fill ?? theme.surface, style.stroke ?? theme.muted, style.width ?? theme.strokeWidth, strokeDash(style.line)),
|
|
774
1204
|
];
|
|
1205
|
+
const text = noteText(this.styles, scale, { id: note.event.id, key: note.key });
|
|
775
1206
|
splitLines(note.event.text).forEach((line, i) => {
|
|
776
|
-
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));
|
|
777
1208
|
});
|
|
778
1209
|
return { kind: "group", id: note.key, role: "event", items, bounds: this.bounds(b, 0) };
|
|
779
1210
|
}
|
|
@@ -803,28 +1234,30 @@ class SequenceEmitter {
|
|
|
803
1234
|
const style = this.styles.event(event.id ?? frame.key, "fragment");
|
|
804
1235
|
const b = frame.box;
|
|
805
1236
|
const stroke = style.stroke ?? theme.muted;
|
|
806
|
-
const
|
|
1237
|
+
const written = fragmentText(this.styles, scale, event, frame.key);
|
|
1238
|
+
const size = written.size;
|
|
1239
|
+
const operatorWeight = written.weight;
|
|
807
1240
|
const color = style.color ?? theme.ink;
|
|
808
1241
|
const muted = style.color ?? theme.muted;
|
|
809
1242
|
const first = event.branches[0]?.condition ?? null;
|
|
810
1243
|
const h = frame.headerHeight;
|
|
811
1244
|
const items = [];
|
|
812
1245
|
const label = (x, y, text, weight, fill) => {
|
|
813
|
-
const width = metrics.width(text,
|
|
1246
|
+
const width = metrics.width(text, written.font, weight, size) + 6;
|
|
814
1247
|
items.push(this.rect({ x: x - 3, y: y - h / 2 + 4, width, height: h - 8 }, 2, theme.background, "none", 0));
|
|
815
|
-
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));
|
|
816
1249
|
};
|
|
817
1250
|
if (event.operator === "region") {
|
|
818
1251
|
if (first)
|
|
819
|
-
label(b.x + 8, b.y + h / 2, first,
|
|
1252
|
+
label(b.x + 8, b.y + h / 2, first, operatorWeight, color);
|
|
820
1253
|
}
|
|
821
1254
|
else {
|
|
822
|
-
const tabWidth = metrics.width(event.operator,
|
|
1255
|
+
const tabWidth = metrics.width(event.operator, written.font, operatorWeight, size) + 18;
|
|
823
1256
|
const s = this.box(b);
|
|
824
1257
|
const n = (v) => String(round2(v));
|
|
825
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`;
|
|
826
1259
|
items.push(this.path(tab, theme.surface, stroke, style.width ?? theme.strokeWidth));
|
|
827
|
-
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));
|
|
828
1261
|
if (first)
|
|
829
1262
|
label(b.x + tabWidth + 8, b.y + h / 2, `[${first}]`, 400, muted);
|
|
830
1263
|
}
|