flowfig 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +185 -114
- package/dist/browser.d.ts +1 -3
- package/dist/browser.js +72 -60
- package/dist/check.d.ts +5 -3
- package/dist/check.js +28 -21
- package/dist/cli.js +33 -47
- package/dist/diff.js +0 -4
- package/dist/draw.d.ts +1 -3
- package/dist/draw.js +3 -7
- package/dist/geometry.d.ts +13 -5
- package/dist/geometry.js +164 -70
- package/dist/gif.d.ts +1 -2
- package/dist/gif.js +115 -42
- package/dist/guide.d.ts +1 -4
- package/dist/guide.js +7 -4
- package/dist/index.d.ts +2 -2
- package/dist/index.js +69 -105
- package/dist/init.d.ts +3 -13
- package/dist/init.js +17 -19
- package/dist/load.d.ts +1 -3
- package/dist/load.js +2 -5
- package/dist/mcp.d.ts +1 -2
- package/dist/mcp.js +9 -12
- package/dist/model.d.ts +57 -108
- package/dist/model.js +21 -88
- package/dist/open.d.ts +2 -5
- package/dist/open.js +6 -11
- package/dist/page.d.ts +0 -1
- package/dist/page.js +1 -3
- package/dist/picker.d.ts +2 -4
- package/dist/picker.js +11 -14
- package/dist/png.js +9 -10
- package/dist/rail.d.ts +1 -3
- package/dist/rail.js +2 -16
- package/dist/scene.d.ts +9 -7
- package/dist/source.d.ts +0 -1
- package/dist/source.js +2 -2
- package/dist/svg.d.ts +5 -8
- package/dist/svg.js +81 -121
- package/dist/text.d.ts +0 -2
- package/dist/text.js +1 -4
- package/dist/verify.d.ts +1 -1
- package/dist/verify.js +3 -6
- package/dist/version.d.ts +1 -2
- package/dist/version.js +2 -2
- package/package.json +4 -2
package/dist/svg.js
CHANGED
|
@@ -1,34 +1,24 @@
|
|
|
1
|
-
//
|
|
2
|
-
// inside a markdown image on GitHub, GitLab and Notion, where the React player cannot go. It gives
|
|
3
|
-
// up what needs a reader: no hover, no tabs, no pause — every step runs in one loop.
|
|
4
|
-
//
|
|
5
|
-
// It reads the same spec and the same `route()` as the React renderer, so the two cannot drift.
|
|
6
|
-
//
|
|
7
|
-
// Limit: text is measured by character class (src/text.ts) rather than by a browser, so this runs anywhere with plain node.
|
|
8
|
-
// Wrapping is therefore approximate; `flowfig check` uses the same measure, and the player check measures real text.
|
|
1
|
+
// Wrapping is approximate: text is measured by character class, not by a browser.
|
|
9
2
|
import { route } from './geometry.js';
|
|
10
3
|
import { foldedLabel, groupBox, layoutRail, railState, RAIL } from './rail.js';
|
|
11
4
|
import { textWidth, wrap } from './text.js';
|
|
12
|
-
import {
|
|
5
|
+
import { checkRendered, planFor } from './check.js';
|
|
13
6
|
export { parseSource, links } from './source.js';
|
|
14
7
|
export { diff, formatDiff } from './diff.js';
|
|
15
|
-
import { ASYNC_TAG_W, BASE_RATE, CARD_LINE, CARD_PAD, CARD_WIDTH, DARK, EDGE_OFF, EDGE_ON, LIGHT, TONES, toneFill, toneTint, labelPillW, ON_ACCENT, str, edgeId, groupGap, isGroup, isRows, LANE_PAD, LANE_ROW_GAP, LANE_BLOCK_GAP, FRAME_SIDE, laneGutter as gutterOf,
|
|
8
|
+
import { ASYNC_TAG_W, BASE_RATE, CARD_LINE, CARD_PAD, CARD_WIDTH, DARK, EDGE_OFF, EDGE_ON, LIGHT, TONES, toneFill, toneTint, labelPillW, ON_ACCENT, str, edgeId, groupGap, isGroup, isRows, LANE_PAD, LANE_ROW_GAP, LANE_BLOCK_GAP, FRAME_SIDE, laneGutter as gutterOf, STUB_ROOM, laneBlock, laneEnd, tightCopies, nodeWidth, diamondLines, diamondRoom, isLanesLayout, timelineBeats, timelineLayout, TL_AXIS_H, TL_AXIS_W, TL_BAR_H, TL_ROW_GAP, toBeat, beatMs, STEP_HOLD_MS, playheadItem, loopStartItem, dateLabelRaised, labelSpan, outsideLabelRect, } from './model.js';
|
|
16
9
|
export { LIGHT, DARK } from './model.js';
|
|
17
10
|
const LINE = CARD_LINE, CARD_SIDE = 8, ROW_GAP = 4;
|
|
18
11
|
const LABEL_LINE = 18, SUB_LINE = 15;
|
|
19
12
|
const FRAME_TOP = 37, FRAME_BOTTOM = 18;
|
|
20
|
-
// The card-on fills
|
|
13
|
+
// The card-on fills approximate the player's 8 % accent tint.
|
|
21
14
|
const vars = (t, cardOn) => `--accent:${t.accent}; --fg:${t.fg}; --muted:${t.muted}; --bg:${t.bg}; --surface:${t.surface}; --border:${t.border}; --card-on:${cardOn}; --tint:color-mix(in srgb, var(--accent) 10%, var(--surface));`;
|
|
22
|
-
/** What a card actually holds: rows, or its text. Only text survives outside React. */
|
|
23
15
|
const content = (c) => (isRows(c) ? c : str(c));
|
|
24
16
|
const esc = (s) => s.replace(/[<>&"]/g, (c) => ({ '<': '<', '>': '>', '&': '&', '"': '"' })[c]);
|
|
25
17
|
const n2 = (v) => Math.round(v * 10) / 10;
|
|
26
|
-
//
|
|
18
|
+
// Four decimals: one decimal rounds a 0.7 s hop away and the packet jumps.
|
|
27
19
|
const n4 = (v) => Math.round(v * 10000) / 10000;
|
|
28
20
|
const pct = (v) => Math.round(v * 10000) / 100 + '%';
|
|
29
|
-
/** A tag pill. The tag is drawn in bold capitals with letter spacing, so the width counts both. */
|
|
30
21
|
const pillW = (tag) => textWidth(tag.toUpperCase(), 9) + tag.length * 0.27 + 8;
|
|
31
|
-
/** A card's rows, wrapped for `width`, with the height they need. */
|
|
32
22
|
function layoutCard(c, width) {
|
|
33
23
|
const inner = width - CARD_SIDE * 2;
|
|
34
24
|
if (c == null)
|
|
@@ -39,7 +29,6 @@ function layoutCard(c, width) {
|
|
|
39
29
|
return { rows: [{ row: { text: body }, heads: false, lines, room: inner }], height: lines.length * LINE + CARD_PAD * 2 };
|
|
40
30
|
}
|
|
41
31
|
const rows = body.map((row) => {
|
|
42
|
-
// A word-sized tag heads its row so the text keeps the full width; a number or no tag sits inline.
|
|
43
32
|
const heads = (row.tag?.length ?? 0) > 2;
|
|
44
33
|
const tagW = row.tag && !heads ? pillW(row.tag) + 5 : 0;
|
|
45
34
|
const markW = row.mark && !heads ? textWidth(str(row.mark), 11) + 6 : 0;
|
|
@@ -68,8 +57,6 @@ function size(item, s) {
|
|
|
68
57
|
const inner = item.direction === 'column' ? { w: across, h: along } : { w: along, h: across };
|
|
69
58
|
return item.label != null ? { w: inner.w + FRAME_SIDE * 2, h: inner.h + FRAME_TOP + FRAME_BOTTOM } : inner;
|
|
70
59
|
}
|
|
71
|
-
/** Swimlanes: the bands span the width, the label sits in a left gutter, and a box sits at its time column. Columns past
|
|
72
|
-
* the plan wrap into blocks under the first. A block holds only the lanes with a box in it; all bands share one width. */
|
|
73
60
|
function placeLanes(fig, x, y, s, out) {
|
|
74
61
|
const lanes = fig.layout.children;
|
|
75
62
|
const { cols, starts, gaps, lead, blocks } = s.plan;
|
|
@@ -87,7 +74,6 @@ function placeLanes(fig, x, y, s, out) {
|
|
|
87
74
|
.slice(0, c - starts[k])
|
|
88
75
|
.reduce((a, w, i) => a + w + gaps[starts[k] + i], 0));
|
|
89
76
|
});
|
|
90
|
-
// A figure with no box has no column: its band keeps the frame side on the right.
|
|
91
77
|
const width = Math.max(...blocks.map((k) => {
|
|
92
78
|
const ws = colsOf(k);
|
|
93
79
|
return (gutter +
|
|
@@ -104,7 +90,6 @@ function placeLanes(fig, x, y, s, out) {
|
|
|
104
90
|
ly += LANE_BLOCK_GAP - LANE_ROW_GAP;
|
|
105
91
|
for (const lane of lanes) {
|
|
106
92
|
const kids = lane.children.filter((b) => laneBlock(starts, cols.get(b.id)) === bk).map((b) => ({ b, ...size(b, s) }));
|
|
107
|
-
// A wrapped block draws only its own lanes; one block draws every lane, an empty one too.
|
|
108
93
|
if (blocks.length > 1 && !kids.length)
|
|
109
94
|
continue;
|
|
110
95
|
const inner = Math.max(LABEL_LINE + 20, ...kids.map((k) => k.h));
|
|
@@ -117,7 +102,6 @@ function placeLanes(fig, x, y, s, out) {
|
|
|
117
102
|
});
|
|
118
103
|
out[topAt].h = ly - LANE_ROW_GAP - y;
|
|
119
104
|
}
|
|
120
|
-
/** A timeline: the axis strip on top, one band per track, and the bars and milestones at the shared layout x. */
|
|
121
105
|
function placeTimeline(fig, x, y, out) {
|
|
122
106
|
const lanes = fig.layout.children;
|
|
123
107
|
const gutter = gutterOf(lanes);
|
|
@@ -178,24 +162,18 @@ function place(item, x, y, s, out) {
|
|
|
178
162
|
}
|
|
179
163
|
}
|
|
180
164
|
const SYSTEM_FONT = "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif";
|
|
181
|
-
/**
|
|
182
|
-
* The SVG string, and the scene it drew. The scene lets `flowfig check` test the same layout the SVG uses.
|
|
183
|
-
* The scene is for tools, and its shape may change. Use `toSvg` if you only need the SVG.
|
|
184
|
-
*/
|
|
165
|
+
/** The SVG string and the scene it drew. The scene shape may change; use `toSvg` for the SVG only. */
|
|
185
166
|
export function render(fig, opts = {}) {
|
|
186
167
|
const speed = (opts.speed ?? fig.speed ?? 900) / 1000 / BASE_RATE;
|
|
187
168
|
const pad = opts.padding ?? 24;
|
|
188
169
|
const tl = fig.timeline && isLanesLayout(fig.layout) ? timelineLayout(fig, TL_AXIS_W) : null;
|
|
189
|
-
// A timeline with no steps of its own walks its dated items in date order.
|
|
190
170
|
const synthetic = tl != null && !fig.steps?.length;
|
|
191
171
|
const steps = synthetic ? timelineBeats(fig) : (fig.steps ?? []);
|
|
192
172
|
const beats = steps.map((s) => s.flow.map(toBeat));
|
|
193
|
-
// Every content a box will ever show, so its card can be sized to the biggest one up front.
|
|
194
173
|
const cards = new Map();
|
|
195
174
|
for (const b of beats.flat())
|
|
196
175
|
for (const [id, c] of Object.entries(b.show ?? {}))
|
|
197
176
|
cards.set(id, [...(cards.get(id) ?? []), c]);
|
|
198
|
-
// Boxes with many edges on one side get taller so the edges and their labels have room.
|
|
199
177
|
const out = {}, inn = {};
|
|
200
178
|
for (const e of fig.edges) {
|
|
201
179
|
out[e.from] = (out[e.from] ?? 0) + 1;
|
|
@@ -210,14 +188,13 @@ export function render(fig, opts = {}) {
|
|
|
210
188
|
const plan = lanes ? planFor(fig, opts) : null;
|
|
211
189
|
const sizes = { cards, cardH, minH, gap: (g) => groupGap(g, fig.edges), fig, plan, tall: new Set(plan?.tall) };
|
|
212
190
|
for (const [id, contents] of cards) {
|
|
213
|
-
const width = CARD_WIDTH;
|
|
191
|
+
const width = CARD_WIDTH;
|
|
214
192
|
const widths = [width];
|
|
215
193
|
cardH.set(id, Math.max(LINE + CARD_PAD * 2, ...contents.map((c) => layoutCard(c, Math.min(...widths) - 20).height)));
|
|
216
194
|
}
|
|
217
195
|
const placed = [];
|
|
218
196
|
place(fig.layout, pad, pad, sizes, placed);
|
|
219
197
|
let nodes = placed.filter((p) => !isGroup(p.item));
|
|
220
|
-
// A node's own width can differ from CARD_WIDTH (`width` in the spec), so re-measure once placed.
|
|
221
198
|
for (const p of nodes) {
|
|
222
199
|
if (!cards.has(p.item.id))
|
|
223
200
|
continue;
|
|
@@ -227,11 +204,9 @@ export function render(fig, opts = {}) {
|
|
|
227
204
|
const rects = {};
|
|
228
205
|
const tips = new Set(placed.filter((p) => !isGroup(p.item) && (p.item.shape === 'decision' || p.tl?.milestone)).map((p) => p.item.id));
|
|
229
206
|
const ids = fig.edges.map(edgeId);
|
|
230
|
-
// Wrapped lanes: an edge between two blocks is two stubs with pills, clear of the boxes and the lane gutters.
|
|
231
207
|
const stubs = plan?.stubs ?? new Map();
|
|
232
208
|
const gutter = plan ? gutterOf(fig.layout.children) : 0;
|
|
233
209
|
const band = (id, k) => placed.find((p) => p.lane && p.item.id === id && p.block === k);
|
|
234
|
-
// The band of an edge end, in the block of that end: the band of the box's lane, or of the lane itself.
|
|
235
210
|
const laneGroups = fig.layout.children;
|
|
236
211
|
const bandOf = (eid, id, k) => {
|
|
237
212
|
const block = plan?.ends.get(eid)?.[k];
|
|
@@ -239,7 +214,6 @@ export function render(fig, opts = {}) {
|
|
|
239
214
|
return placed.find((p) => p.lane && p.item === lane && p.block === block);
|
|
240
215
|
};
|
|
241
216
|
const end = (eid, id, start) => (plan ? laneEnd(plan, eid, id, start, band, rects) : id);
|
|
242
|
-
// Place and route. A stub end with no place clear of every edge path makes its lane copy taller, then both run again.
|
|
243
217
|
const go = () => {
|
|
244
218
|
placed.length = 0;
|
|
245
219
|
place(fig.layout, pad, pad, sizes, placed);
|
|
@@ -253,12 +227,13 @@ export function render(fig, opts = {}) {
|
|
|
253
227
|
around: e.around ?? (plan?.around.has(ids[i]) ? 'below' : undefined),
|
|
254
228
|
...(tl && { sides: ['r', 'l'], elbow: true }),
|
|
255
229
|
...(stubs.has(ids[i]) && { stub: stubs.get(ids[i]).map(labelPillW), bands: [bandOf(ids[i], e.from, 0), bandOf(ids[i], e.to, 1)] }),
|
|
256
|
-
...(stubs.size && e.label != null && { labelW: labelPillW(str(e.label)) }),
|
|
230
|
+
...((stubs.size || lanes) && e.label != null && { labelW: labelPillW(str(e.label)) }),
|
|
257
231
|
})), rects, tips, stubs.size
|
|
258
232
|
? placed.flatMap((p) => (!isGroup(p.item) ? [p] : p.lane ? [{ x: p.x, y: p.y, w: gutter, h: p.h }] : []))
|
|
259
|
-
:
|
|
260
|
-
|
|
261
|
-
|
|
233
|
+
: [
|
|
234
|
+
...placed.filter((p) => p.tl && !p.tl.labelInside).map((p) => outsideLabelRect(p, str(p.item.label))),
|
|
235
|
+
...placed.filter((p) => p.tl).map((p) => ({ x: p.x, y: p.y, w: p.w, h: p.h, box: true })),
|
|
236
|
+
], stubs.size ? { x: placed[0].x + gutter, y: placed[0].y, w: placed[0].w - gutter, h: placed[0].h } : undefined, lanes ? { bands: placed.filter((p) => p.lane), boxes: placed.filter((p) => !isGroup(p.item)) } : undefined);
|
|
262
237
|
};
|
|
263
238
|
let routed = go();
|
|
264
239
|
for (let pass = 0; plan && pass < 3; pass++) {
|
|
@@ -270,11 +245,9 @@ export function render(fig, opts = {}) {
|
|
|
270
245
|
}
|
|
271
246
|
nodes = placed.filter((p) => !isGroup(p.item));
|
|
272
247
|
const byId = Object.fromEntries(routed.map((r) => [r.id, r]));
|
|
273
|
-
// What this layout drew, in scene form. Tags are left out of minFont: short bold capitals, not reading text.
|
|
274
248
|
const fonts = [14, ...(steps.length ? [13.5] : [])];
|
|
275
249
|
const sceneBoxes = nodes.map((p) => {
|
|
276
250
|
if (p.tl) {
|
|
277
|
-
// A label inside the bar has the bar as room; a label beside the bar has the space to the next item of its row, or to the band edge.
|
|
278
251
|
const label = str(p.item.label);
|
|
279
252
|
const mine = tl.items.find((i) => i.id === p.item.id);
|
|
280
253
|
const next = tl.items.filter((i) => i.track === mine.track && i.row === mine.row && i.x > mine.x).map((i) => p.x - mine.x + i.x - 8);
|
|
@@ -306,7 +279,6 @@ export function render(fig, opts = {}) {
|
|
|
306
279
|
});
|
|
307
280
|
if (placed.some((p) => isGroup(p.item) && p.item.label != null))
|
|
308
281
|
fonts.push(11);
|
|
309
|
-
// The timeline: every beat of every step, in order, with the player's hold at the end of each step.
|
|
310
282
|
const segs = [];
|
|
311
283
|
const hops = [];
|
|
312
284
|
const edgeOf = Object.fromEntries(fig.edges.map((e, i) => [ids[i], e]));
|
|
@@ -337,7 +309,6 @@ export function render(fig, opts = {}) {
|
|
|
337
309
|
});
|
|
338
310
|
});
|
|
339
311
|
const total = t || 1;
|
|
340
|
-
// What the figure shows during each segment, matching the React player exactly.
|
|
341
312
|
const shownAt = segs.map(({ si, bi }) => Object.assign({}, ...beats[si].slice(0, bi + 1).map((b) => b.show)));
|
|
342
313
|
const litEdges = segs.map(({ si, bi }) => new Set(beats[si].slice(0, bi + 1).flatMap((b) => b.hops.map((h) => h.edge))));
|
|
343
314
|
const litNodes = segs.map((seg, i) => {
|
|
@@ -357,10 +328,19 @@ export function render(fig, opts = {}) {
|
|
|
357
328
|
const said = beats[si].slice(0, bi + 1).findLastIndex((b) => b.say != null);
|
|
358
329
|
return str(said === -1 ? steps[si].caption : beats[si][said].say);
|
|
359
330
|
});
|
|
360
|
-
// One class per on/off pattern over the segments, so 30 boxes share a handful of keyframes.
|
|
361
331
|
const css = [];
|
|
362
332
|
const seen = new Map();
|
|
363
|
-
|
|
333
|
+
const runs = (values) => {
|
|
334
|
+
const out = [];
|
|
335
|
+
for (let i = 0; i < segs.length;) {
|
|
336
|
+
let j = i;
|
|
337
|
+
while (j + 1 < segs.length && values[j + 1] === values[i])
|
|
338
|
+
j++;
|
|
339
|
+
out.push(`${pct(segs[i].t0 / total)},${pct(segs[j].t1 / total - 0.0001)} { ${values[i]} }`);
|
|
340
|
+
i = j + 1;
|
|
341
|
+
}
|
|
342
|
+
return out.join(' ');
|
|
343
|
+
};
|
|
364
344
|
const anim = (on, onCss, offCss, prefix) => {
|
|
365
345
|
if (!segs.length || on.every((x) => !x))
|
|
366
346
|
return '';
|
|
@@ -368,17 +348,16 @@ export function render(fig, opts = {}) {
|
|
|
368
348
|
if (!seen.has(key)) {
|
|
369
349
|
const name = `a${seen.size}`;
|
|
370
350
|
seen.set(key, name);
|
|
371
|
-
const frames =
|
|
351
|
+
const frames = runs(on.map((x) => (x ? onCss : offCss)));
|
|
372
352
|
css.push(`@keyframes ${name} { ${frames} }\n.${name} { animation: ${name} ${n2(total)}s infinite step-end; }`);
|
|
373
353
|
}
|
|
374
354
|
return seen.get(key);
|
|
375
355
|
};
|
|
376
|
-
|
|
356
|
+
// One class attribute only: two would be invalid XML.
|
|
377
357
|
const cls = (...names) => {
|
|
378
358
|
const list = names.filter(Boolean).join(' ');
|
|
379
359
|
return list ? ` class="${list}"` : '';
|
|
380
360
|
};
|
|
381
|
-
/** Like `anim`, for a property that takes a different value in each segment (a row that moves as phases fold). */
|
|
382
361
|
const frames = (values, prefix, base = values[0]) => {
|
|
383
362
|
if (!segs.length || values.every((v) => v === base))
|
|
384
363
|
return '';
|
|
@@ -386,20 +365,14 @@ export function render(fig, opts = {}) {
|
|
|
386
365
|
if (!seen.has(key)) {
|
|
387
366
|
const name = `a${seen.size}`;
|
|
388
367
|
seen.set(key, name);
|
|
389
|
-
const kf =
|
|
368
|
+
const kf = runs(values);
|
|
390
369
|
css.push(`@keyframes ${name} { ${kf} }\n.${name} { animation: ${name} ${n2(total)}s infinite step-end; }`);
|
|
391
370
|
}
|
|
392
371
|
return seen.get(key);
|
|
393
372
|
};
|
|
394
|
-
|
|
395
|
-
* The look of one box over the loop: off, trail (visited) or active (the packet arrived, until the segment ends).
|
|
396
|
-
* A segment ends at the beat end, or after the step hold for the last beat.
|
|
397
|
-
* Each frame holds one look. The gap before the frame that follows an active frame gives the FADE.
|
|
398
|
-
* FADE is in timeline seconds, which are wall seconds.
|
|
399
|
-
*/
|
|
373
|
+
// FADE and RAMP are wall seconds; the player matches them in ms.
|
|
400
374
|
const FADE = 0.4;
|
|
401
|
-
const RAMP = 0.4;
|
|
402
|
-
// A box tone is a permanent state: a 1 px border and a light tint in the off and trail looks. The active look uses the hop tone, else the box tone.
|
|
375
|
+
const RAMP = 0.4;
|
|
403
376
|
const looks = (look, boxTone, hopTone) => {
|
|
404
377
|
const tint = boxTone ? toneTint(boxTone, 'var(--bg)') : 'var(--bg)';
|
|
405
378
|
if (look === 'off')
|
|
@@ -411,13 +384,21 @@ export function render(fig, opts = {}) {
|
|
|
411
384
|
? [toneTint(c, 'var(--surface)', 10), c, 2, `drop-shadow(0 0 4px ${c})`]
|
|
412
385
|
: ['var(--tint)', 'var(--accent)', 2, 'drop-shadow(0 0 4px var(--accent))'];
|
|
413
386
|
};
|
|
387
|
+
const hopsAt = new Map();
|
|
388
|
+
for (const h of hops) {
|
|
389
|
+
const k = `${h.si}:${h.bi}:${h.dest}`;
|
|
390
|
+
const list = hopsAt.get(k);
|
|
391
|
+
if (list)
|
|
392
|
+
list.push(h);
|
|
393
|
+
else
|
|
394
|
+
hopsAt.set(k, [h]);
|
|
395
|
+
}
|
|
414
396
|
const boxAnim = (id, shape, boxTone) => {
|
|
415
397
|
const pieces = [];
|
|
416
398
|
segs.forEach((s, i) => {
|
|
417
399
|
if (!litNodes[i].has(id))
|
|
418
400
|
return void pieces.push({ a: s.t0, b: s.t1, look: 'off' });
|
|
419
|
-
const here =
|
|
420
|
-
// A focused box turns active at the beat start; in a timeline, when the today line arrives.
|
|
401
|
+
const here = hopsAt.get(`${s.si}:${s.bi}:${id}`) ?? [];
|
|
421
402
|
const focused = beats[s.si][s.bi].focus?.includes(id);
|
|
422
403
|
const arrive = Math.min(...here.map((h) => h.t1), focused ? Math.min(s.t0 + (tl ? RAMP : 0), s.t1) : s.t1, s.t1);
|
|
423
404
|
if (arrive > s.t0)
|
|
@@ -427,18 +408,23 @@ export function render(fig, opts = {}) {
|
|
|
427
408
|
});
|
|
428
409
|
if (!pieces.length || pieces.every((q) => q.look === 'off'))
|
|
429
410
|
return '';
|
|
430
|
-
// The last frame is active: the fade ends at 100 %, so the loop wraps with no jump.
|
|
431
411
|
const end = pieces.at(-1);
|
|
432
412
|
if (end.look === 'active') {
|
|
433
413
|
end.b -= FADE;
|
|
434
414
|
pieces.push({ a: end.b, b: end.b + FADE, look: pieces[0].look, hop: pieces[0].hop, ramp: FADE - 0.0002 * total });
|
|
435
415
|
}
|
|
436
|
-
const
|
|
416
|
+
const merged = [];
|
|
417
|
+
pieces.forEach((q, k) => {
|
|
437
418
|
const ramp = q.ramp ?? (q.look !== 'active' && pieces[k - 1]?.look === 'active' ? Math.min(FADE, (q.b - q.a) / 2) : 0);
|
|
438
419
|
const [fill, stroke, width, filter] = looks(q.look, boxTone, q.hop);
|
|
439
420
|
const css = `${shape ? `fill: ${fill}; ` : ''}stroke: ${stroke}; stroke-width: ${width}${shape ? `; filter: ${filter}` : ''}`;
|
|
440
|
-
|
|
421
|
+
const last = merged.at(-1);
|
|
422
|
+
if (last?.css === css)
|
|
423
|
+
last.b = q.b;
|
|
424
|
+
else
|
|
425
|
+
merged.push({ a: q.a + ramp, b: q.b, css });
|
|
441
426
|
});
|
|
427
|
+
const kf = merged.map((f) => `${pct(f.a / total)},${pct(f.b / total - 0.0001)} { ${f.css} }`);
|
|
442
428
|
const key = (shape ? 'B' : 'R') + kf.join(' ');
|
|
443
429
|
if (!seen.has(key)) {
|
|
444
430
|
const name = `a${seen.size}`;
|
|
@@ -491,7 +477,6 @@ export function render(fig, opts = {}) {
|
|
|
491
477
|
: item.sub
|
|
492
478
|
? `<text x="${n2(cx)}" y="${n2(labelY + SUB_LINE)}" class="sub">${esc(str(item.sub))}</text>`
|
|
493
479
|
: '';
|
|
494
|
-
// The mark sits in the gap beside the box, so it changes no size and adds nothing to the scene.
|
|
495
480
|
const dot = bt ?? 'var(--accent)';
|
|
496
481
|
const my = n2(p.y + p.h / 2);
|
|
497
482
|
const mark = item.mark === 'start'
|
|
@@ -501,7 +486,6 @@ export function render(fig, opts = {}) {
|
|
|
501
486
|
: '';
|
|
502
487
|
return shape + label + sub + mark + (contents ? card(p, cardTop, contents) : '');
|
|
503
488
|
});
|
|
504
|
-
/** The dashed content card: one group per content it will ever hold, each visible on its own beats. */
|
|
505
489
|
function card(p, top, contents) {
|
|
506
490
|
const id = p.item.id;
|
|
507
491
|
const x = p.x + 10, w = p.w - 20, h = cardH.get(id);
|
|
@@ -509,7 +493,6 @@ export function render(fig, opts = {}) {
|
|
|
509
493
|
const box = `<rect x="${n2(x)}" y="${n2(top)}" width="${n2(w)}" height="${n2(h)}" rx="6" fill="var(--surface)" stroke="var(--border)" stroke-dasharray="3 3"` +
|
|
510
494
|
cls(anim(filled, 'stroke: var(--accent); fill: var(--card-on)', 'stroke: var(--border); fill: var(--surface)', 'c')) +
|
|
511
495
|
`/>`;
|
|
512
|
-
// Every distinct content gets a layer; the segments decide which one is showing.
|
|
513
496
|
const layers = contents
|
|
514
497
|
.map((c, ci) => {
|
|
515
498
|
const on = segs.map((_, i) => contents.indexOf(shownAt[i][id]) === ci);
|
|
@@ -524,7 +507,6 @@ export function render(fig, opts = {}) {
|
|
|
524
507
|
: '';
|
|
525
508
|
return box + layers + dash;
|
|
526
509
|
}
|
|
527
|
-
/** One card's rows: a colored tag pill, the text, its muted meta, and the mark on the right. */
|
|
528
510
|
function rows(c, x, top, w) {
|
|
529
511
|
const { rows } = layoutCard(c, w);
|
|
530
512
|
let y = top + CARD_PAD;
|
|
@@ -561,22 +543,18 @@ export function render(fig, opts = {}) {
|
|
|
561
543
|
const on = segs.map((_, i) => litEdges[i].has(r.id));
|
|
562
544
|
const hidden = e.quiet && !on.every(Boolean);
|
|
563
545
|
const shown = hidden ? cls(anim(on, 'opacity: 1', 'opacity: 0', 'q')) : '';
|
|
564
|
-
// A toned hop colors its edge for its own beat only; the trail after it is the accent.
|
|
565
546
|
const tone = segs.map(({ si, bi }) => (beats[si][bi].hops.find((h) => h.edge === r.id && h.tone)?.tone ?? undefined));
|
|
566
547
|
const col = tone.map((x) => (x ? TONES[x] : 'var(--accent)'));
|
|
567
548
|
const off = `stroke: var(--muted); stroke-width: ${EDGE_OFF}`;
|
|
568
|
-
// With no tone the map and the rail share one keyframe (same key); a toned edge needs its own values.
|
|
569
549
|
const toned = tone.some(Boolean);
|
|
570
550
|
const lit = cls(toned
|
|
571
551
|
? frames(on.map((x, i) => (x ? `stroke: ${col[i]}; stroke-width: ${EDGE_ON}` : off)), 'e', off)
|
|
572
552
|
: anim(on, `stroke: var(--accent); stroke-width: ${EDGE_ON}`, off, 'e'));
|
|
573
|
-
// A cross-block edge draws its two stubs; its full path only guides the packet, so the packet jumps between the blocks.
|
|
574
553
|
const path = r.stub
|
|
575
554
|
? r.stub.parts
|
|
576
555
|
.map((d) => `<path d="${d}" fill="none" stroke="var(--muted)" stroke-width="${EDGE_OFF}" marker-end="url(#arrow)"${lit}/>`)
|
|
577
556
|
.join('') + `<path id="p-${esc(r.id)}" d="${r.d}" fill="none" stroke="none"/>`
|
|
578
557
|
: `<path id="p-${esc(r.id)}" d="${r.d}" fill="none" stroke="var(--muted)" stroke-width="${EDGE_OFF}" marker-end="url(#arrow)"${lit}/>`;
|
|
579
|
-
// A quiet edge is only drawn while a step uses it, so wrap the whole thing rather than the stroke.
|
|
580
558
|
const pill = (x, y, lw, text) => `<rect x="${n2(x - lw / 2)}" y="${n2(y - 9)}" width="${n2(lw)}" height="18" rx="9" fill="var(--bg)" stroke="var(--border)"` +
|
|
581
559
|
cls(toned
|
|
582
560
|
? frames(on.map((x, i) => x ? `fill: ${tone[i] ? toneFill(col[i]) : col[i]}; stroke: ${col[i]}` : 'fill: var(--bg); stroke: var(--border)'), 'l', 'fill: var(--bg); stroke: var(--border)')
|
|
@@ -596,8 +574,6 @@ export function render(fig, opts = {}) {
|
|
|
596
574
|
}
|
|
597
575
|
return hidden ? `<g opacity="0"${shown}>${path}${label}</g>` : path + label;
|
|
598
576
|
});
|
|
599
|
-
// A packet per hop: it waits offstage, crosses its edge in `speed`, rests at the end of the edge to the end of its beat, then leaves. Any `data`
|
|
600
|
-
// rides above it in a chip, which is how the figure says what is moving.
|
|
601
577
|
const packets = hops.map((h, i) => {
|
|
602
578
|
const t0 = h.t0 / total, t1 = h.t1 / total, tEnd = h.tEnd / total;
|
|
603
579
|
const name = `p${i}`;
|
|
@@ -618,8 +594,7 @@ export function render(fig, opts = {}) {
|
|
|
618
594
|
`<animateMotion dur="${n2(total)}s" repeatCount="indefinite" keyTimes="0;${n4(t0)};${n4(t1)};1" keyPoints="${h.back ? '1;1;0;0' : '0;0;1;1'}" calcMode="linear">` +
|
|
619
595
|
`<mpath href="#p-${esc(h.edge)}" xlink:href="#p-${esc(h.edge)}"/></animateMotion></g>`);
|
|
620
596
|
});
|
|
621
|
-
|
|
622
|
-
let axisSvg = '', todaySvg = '';
|
|
597
|
+
let axisSvg = '', lineSvg = '', todaySvg = '';
|
|
623
598
|
if (tl) {
|
|
624
599
|
const top = placed[0];
|
|
625
600
|
const x0 = top.x + gutterOf(fig.layout.children);
|
|
@@ -632,38 +607,40 @@ export function render(fig, opts = {}) {
|
|
|
632
607
|
`<text x="${n2(x0 + k.x + 3)}" y="${n2(top.y + 10)}" class="tick">${esc(k.label)}</text>`)
|
|
633
608
|
.join('');
|
|
634
609
|
const bottom = n2(top.y + top.h);
|
|
635
|
-
// The today marker is fixed at the figure's `today`.
|
|
636
610
|
if (tl.today != null) {
|
|
637
611
|
const tx = x0 + tl.today;
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
`<text x="${n2(tx - 3)}" y="${n2(top.y + 22)}" text-anchor="end" class="today">today</text>`;
|
|
612
|
+
lineSvg = `<path d="M ${n2(tx)} ${n2(top.y + 12)} V ${bottom}" stroke="var(--accent)" stroke-opacity="0.6" stroke-width="1" stroke-dasharray="3 3"/>`;
|
|
613
|
+
todaySvg = `<text x="${n2(tx - 3)}" y="${n2(top.y + 22)}" text-anchor="end" class="today">today</text>`;
|
|
641
614
|
}
|
|
642
|
-
// The playhead moves to the start of each beat's item, then to the last date in the step hold.
|
|
643
615
|
if (tl.last != null && segs.length) {
|
|
644
616
|
const home = tl.last;
|
|
645
617
|
const startOf = new Map(tl.items.map((i) => [i.id, i.x + (i.milestone ? i.w / 2 : 0)]));
|
|
646
618
|
const dateOf = new Map(tl.items.map((i) => [i.id, i.date]));
|
|
647
619
|
const hold = STEP_HOLD_MS / 1000 / BASE_RATE;
|
|
648
|
-
const
|
|
649
|
-
const
|
|
650
|
-
|
|
620
|
+
const first = loopStartItem(tl.items, steps);
|
|
621
|
+
const start = first ? startOf.get(first.id) : home;
|
|
622
|
+
const pts = [[0, start]];
|
|
623
|
+
const dates = [];
|
|
624
|
+
let at = start;
|
|
651
625
|
segs.forEach((s, i) => {
|
|
652
626
|
const item = playheadItem(tl.items, beats[s.si], s.bi);
|
|
653
627
|
const to = item ? startOf.get(item.id) : home;
|
|
654
628
|
const date = item ? dateOf.get(item.id) : tl.lastDate;
|
|
655
629
|
const last = i === segs.length - 1 || segs[i + 1].si !== s.si;
|
|
656
|
-
const end = last ? s.t1 - hold : s.t1;
|
|
630
|
+
const end = last ? s.t1 - hold : s.t1;
|
|
657
631
|
pts.push([s.t0, at], [Math.min(s.t0 + RAMP, end), to], [end, to]);
|
|
658
|
-
const [la, lb] = labelSpan(s.t0, end, RAMP);
|
|
632
|
+
const [la, lb] = i === 0 && to === start ? [s.t0, end] : labelSpan(s.t0, end, RAMP);
|
|
659
633
|
if (la < lb)
|
|
660
634
|
dates.push({ a: la, b: lb, d: date, x: to });
|
|
661
635
|
at = to;
|
|
662
636
|
if (last) {
|
|
663
|
-
|
|
664
|
-
|
|
637
|
+
const back = i === segs.length - 1 ? Math.max(end + RAMP, s.t1 - RAMP) : s.t1;
|
|
638
|
+
pts.push([Math.min(end + RAMP, s.t1), home], [back, home]);
|
|
639
|
+
const [ha, hb] = labelSpan(end, back, RAMP);
|
|
665
640
|
if (ha < hb)
|
|
666
641
|
dates.push({ a: ha, b: hb, d: tl.lastDate, x: home });
|
|
642
|
+
if (back < s.t1)
|
|
643
|
+
pts.push([s.t1, start]);
|
|
667
644
|
at = home;
|
|
668
645
|
}
|
|
669
646
|
});
|
|
@@ -678,36 +655,32 @@ export function render(fig, opts = {}) {
|
|
|
678
655
|
if (!seen.has(key)) {
|
|
679
656
|
const nm = `a${seen.size}`;
|
|
680
657
|
seen.set(key, nm);
|
|
681
|
-
// Each span ends with a hidden frame: the label stays off while the line moves.
|
|
682
658
|
const fr = dates
|
|
683
659
|
.map((x) => `${pct(x.a / total)},${pct(x.b / total - 0.0001)} { opacity: ${x.d === d ? 1 : 0} } ${pct(x.b / total)} { opacity: 0 }`)
|
|
684
660
|
.join(' ');
|
|
685
661
|
css.push(`@keyframes ${nm} { ${fr} }\n.${nm} { animation: ${nm} ${n2(total)}s infinite step-end; }`);
|
|
686
662
|
}
|
|
687
|
-
// A label that would meet "today" moves to the tick row.
|
|
688
663
|
const raised = dateLabelRaised(dates.find((x) => x.d === d).x, tl.today, textWidth(d, 11), textWidth('today', 11));
|
|
689
664
|
return `<text x="${n2(lx + 3)}" y="${n2(top.y + (raised ? 10 : 22))}" opacity="0"${cls('today', seen.get(key))}>${esc(d)}</text>`;
|
|
690
665
|
});
|
|
691
|
-
|
|
666
|
+
lineSvg += `<g${cls(name)}><path d="M ${n2(lx)} ${n2(top.y + 12)} V ${bottom}" stroke="var(--accent)" stroke-width="1.5"/></g>`;
|
|
667
|
+
todaySvg += `<g${cls(name)}>${labels.join('')}</g>`;
|
|
692
668
|
}
|
|
693
669
|
}
|
|
694
670
|
const bounds = placed[0];
|
|
695
|
-
const arcs = fig.edges.some((e, i) => (e.around || plan?.around.has(ids[i])) && !stubs.has(ids[i]));
|
|
671
|
+
const arcs = fig.edges.some((e, i) => (e.around || plan?.around.has(ids[i])) && !stubs.has(ids[i]));
|
|
696
672
|
const capLines = [...new Set(captions)].flatMap((c) => wrap(c, Math.max(560, bounds.w), 13.5).length);
|
|
697
673
|
const mapW = Math.max(bounds.w + pad * 2, 560);
|
|
698
|
-
// `rail: 'only'` drops the map, but only when the rail has a hop to draw; a figure never renders empty.
|
|
699
674
|
const rail = fig.rail ? layoutRail(fig, fig.rail === 'only' ? 560 : mapW) : null;
|
|
700
675
|
const only = fig.rail === 'only' && rail != null;
|
|
701
|
-
// With the rail, the rail shows the step label, so the caption holds the narration only.
|
|
702
676
|
const capTop = rail ? 20 : 26;
|
|
703
677
|
const capH = steps.length ? capTop + 4 + Math.max(0, ...capLines) * 20 : 0;
|
|
704
678
|
const W = only ? Math.max(560, rail.width) : Math.max(mapW, rail?.width ?? 0);
|
|
705
679
|
const mapH = only ? 0 : bounds.h + pad * 2 + (arcs ? 44 : 0);
|
|
706
|
-
const top = only ? pad : mapH + RAIL.gap;
|
|
680
|
+
const top = only ? pad : mapH + RAIL.gap;
|
|
707
681
|
const H = only ? pad + rail.height + capH : mapH + (rail ? RAIL.gap + rail.height : 0) + capH;
|
|
708
682
|
const shift = (W - (bounds.w + pad * 2)) / 2;
|
|
709
683
|
const railX = rail ? (W - rail.width) / 2 : 0;
|
|
710
|
-
// The step label and its narration, both switching with the beats.
|
|
711
684
|
const labels = rail || synthetic
|
|
712
685
|
? []
|
|
713
686
|
: steps.map((_, si) => {
|
|
@@ -721,23 +694,19 @@ export function render(fig, opts = {}) {
|
|
|
721
694
|
lines.map((l, li) => `<text x="${n2(W / 2)}" y="${n2(H - capH + capTop + 16 + li * 20)}" class="caption">${esc(l)}</text>`).join('') +
|
|
722
695
|
`</g>`);
|
|
723
696
|
});
|
|
724
|
-
// The rail: lifelines under the map. Every part reuses a map part: the frame for a band, the edge-label pill for a payload, the card
|
|
725
|
-
// tag for `async`, the filled card for a parallel group. So the rail adds no color and no font size.
|
|
726
697
|
const railSvg = !rail
|
|
727
698
|
? ''
|
|
728
699
|
: (() => {
|
|
729
700
|
const parts = [];
|
|
730
|
-
const cols = rail.head - RAIL.cols / 2 + 4;
|
|
701
|
+
const cols = rail.head - RAIL.cols / 2 + 4;
|
|
731
702
|
for (const b of rail.bands)
|
|
732
703
|
parts.push(`<rect x="${n2(b.rect.x)}" y="${n2(top + b.rect.y)}" width="${n2(b.rect.w)}" height="${n2(b.rect.h)}" rx="10" fill="var(--surface)" stroke="var(--border)"/>` +
|
|
733
704
|
`<text x="${n2(b.rect.x + 10)}" y="${n2(top + b.rect.y + 14)}" class="frame">${esc(b.label.toUpperCase())}</text>`);
|
|
734
705
|
for (const c of rail.columns)
|
|
735
706
|
parts.push(`<text x="${n2(c.x)}" y="${n2(top + cols)}" class="railcol">${esc(c.label)}</text>`);
|
|
736
|
-
// Where each row sits in each segment: it moves when another phase opens, and hides while its phase is folded.
|
|
737
707
|
const at = (i) => segs.map((s) => railState(rail, s.si)[i]);
|
|
738
708
|
const base = (i) => railState(rail, 0)[i].y;
|
|
739
709
|
const moveRow = (i) => frames(at(i).map((st) => `transform: translateY(${n2(st.y - base(i))}px); opacity: ${st.shown ? 1 : 0}`), 'm');
|
|
740
|
-
// The lifelines run through the message rows only, so a phase header reads as one clear divider.
|
|
741
710
|
rail.rows.forEach((row, i) => {
|
|
742
711
|
if (row.kind !== 'message')
|
|
743
712
|
return;
|
|
@@ -755,8 +724,7 @@ export function render(fig, opts = {}) {
|
|
|
755
724
|
if (row.kind === 'phase') {
|
|
756
725
|
const folded = segs.map((s) => rail.folds && s.si !== row.step);
|
|
757
726
|
const rule = (x1) => `<path d="M ${n2(x1)} ${n2(y + 15)} H ${n2(row.line.end)}" stroke="var(--border)"/>`;
|
|
758
|
-
|
|
759
|
-
const first = new Map(); // beat -> the n of its first message
|
|
727
|
+
const first = new Map();
|
|
760
728
|
for (const r of rail.rows)
|
|
761
729
|
if (r.kind === 'message' && r.step === row.step && !first.has(r.beat))
|
|
762
730
|
first.set(r.beat, r.n);
|
|
@@ -780,7 +748,6 @@ export function render(fig, opts = {}) {
|
|
|
780
748
|
const col = row.tone ? TONES[row.tone] : 'var(--accent)';
|
|
781
749
|
const lit = anim(now, `stroke: ${col}; stroke-width: ${EDGE_ON}`, `stroke: var(--muted); stroke-width: ${EDGE_OFF}`, row.tone ? 'e' + col : 'e');
|
|
782
750
|
const tagW = ASYNC_TAG_W;
|
|
783
|
-
// The tag sits on the tail side of the pill, so it never hides the arrowhead.
|
|
784
751
|
const tagX = !row.pill ? (x1 + x2) / 2 - tagW / 2 : x2 > x1 ? row.pill.x - tagW - 4 : row.pill.x + row.pill.w + 4;
|
|
785
752
|
const tag = row.async
|
|
786
753
|
? `<rect x="${n2(tagX)}" y="${n2(ly - 6)}" width="${n2(tagW)}" height="12" rx="4" fill="var(--bg)"/>` +
|
|
@@ -831,7 +798,7 @@ ${only
|
|
|
831
798
|
? ''
|
|
832
799
|
: `<g transform="translate(${n2(shift)} ${arcs ? 44 : 0})">
|
|
833
800
|
${axisSvg}
|
|
834
|
-
${tl ? boxes.filter((b, i) => b && isGroup(placed[i].item)).join('\n') : ''}
|
|
801
|
+
${tl ? boxes.filter((b, i) => b && isGroup(placed[i].item)).join('\n') : ''}${lineSvg && `\n${lineSvg}`}
|
|
835
802
|
${tl ? edgeSvg.join('\n') : ''}
|
|
836
803
|
${boxes.filter((b, i) => b && !(tl && isGroup(placed[i].item))).join('\n')}
|
|
837
804
|
${tl ? '' : edgeSvg.join('\n')}
|
|
@@ -851,16 +818,22 @@ ${said.join('\n')}
|
|
|
851
818
|
edges: [
|
|
852
819
|
...(only ? [] : routed).flatMap((r) => {
|
|
853
820
|
const e = fig.edges[ids.indexOf(r.id)];
|
|
854
|
-
// A cross-block edge is two scene edges: each stub with its pill.
|
|
855
821
|
if (r.stub)
|
|
856
822
|
return r.stub.pts.map((pts, k) => ({ id: r.id, from: e.from, to: e.to, curve: r.curve, pts, label: r.stub.pills[k] }));
|
|
857
|
-
return [
|
|
823
|
+
return [
|
|
824
|
+
{
|
|
825
|
+
id: r.id,
|
|
826
|
+
from: e.from,
|
|
827
|
+
to: e.to,
|
|
828
|
+
curve: r.curve,
|
|
829
|
+
label: labelRects[r.id],
|
|
830
|
+
...(tl && { behind: true, elbow: r.elbow }),
|
|
831
|
+
},
|
|
832
|
+
];
|
|
858
833
|
}),
|
|
859
|
-
// Each rail payload is a label too, in its own open-phase position, so label-overlap covers the rail.
|
|
860
834
|
...(rail?.rows ?? []).flatMap((row, i) => {
|
|
861
835
|
if (row.kind !== 'message')
|
|
862
836
|
return [];
|
|
863
|
-
// Scene coordinates are the map's layout coordinates: the page minus the map's translate(shift, arcs).
|
|
864
837
|
const dx = only ? railX : railX - shift, dy = only ? top : top - (arcs ? 44 : 0);
|
|
865
838
|
const y = dy + railState(rail, row.step)[i].y + RAIL.row / 2;
|
|
866
839
|
const [x1, x2] = [dx + rail.columns[row.from].x, dx + rail.columns[row.to].x];
|
|
@@ -878,30 +851,17 @@ ${said.join('\n')}
|
|
|
878
851
|
}),
|
|
879
852
|
],
|
|
880
853
|
minFont: Math.min(...fonts),
|
|
881
|
-
...(
|
|
854
|
+
...(lanes && {
|
|
882
855
|
lanes: placed.filter((p) => p.lane).map((p) => ({ id: str(p.item.label), rect: { x: p.x, y: p.y, w: p.w, h: p.h } })),
|
|
883
856
|
}),
|
|
884
857
|
};
|
|
885
858
|
return { svg, scene };
|
|
886
859
|
}
|
|
887
|
-
/** The lanes wrap plan at the width and text size of `opts`, or null if the figure does not draw as lanes. A rail sets a least width. */
|
|
888
|
-
function planFor(fig, opts) {
|
|
889
|
-
if (fig.timeline || !fig.lanes || !isLanesLayout(fig.layout))
|
|
890
|
-
return null;
|
|
891
|
-
const floor = fig.rail ? (layoutRail(fig, 560)?.width ?? 0) : 0;
|
|
892
|
-
return lanePlan(fig, { width: opts.width, minText: opts.minText, padding: opts.padding ?? 24, floor });
|
|
893
|
-
}
|
|
894
860
|
/** One self-contained animated SVG string for the figure. Needs no React and no browser. */
|
|
895
861
|
export function toSvg(fig, opts = {}) {
|
|
896
862
|
return render(fig, opts).svg;
|
|
897
863
|
}
|
|
898
|
-
/** Every fault `flowfig check` knows about, for this figure as the SVG lays it out.
|
|
864
|
+
/** Every fault `flowfig check` knows about, for this figure as the SVG lays it out. */
|
|
899
865
|
export function check(fig, opts = {}) {
|
|
900
|
-
|
|
901
|
-
rule: 'lane-end-block',
|
|
902
|
-
severity: 'warning',
|
|
903
|
-
ids: [id],
|
|
904
|
-
message: `edge "${id}" ends at a lane that no block on its side shows; the edge uses the nearest block`,
|
|
905
|
-
}));
|
|
906
|
-
return [...checkSpec(fig), ...checkScene(render(fig, opts).scene, opts), ...lost, ...checkTheme({ ...fig.theme, ...opts.theme })];
|
|
866
|
+
return checkRendered(fig, opts, render(fig, opts).scene);
|
|
907
867
|
}
|
package/dist/text.d.ts
CHANGED
|
@@ -1,4 +1,2 @@
|
|
|
1
|
-
/** The width of `s` in px at `fontSize`. */
|
|
2
1
|
export declare function textWidth(s: string, fontSize: number, mono?: boolean): number;
|
|
3
|
-
/** Break a string into lines that fit `width`, keeping the newlines it already has. */
|
|
4
2
|
export declare function wrap(s: string, width: number, fontSize: number, mono?: boolean): string[];
|
package/dist/text.js
CHANGED
|
@@ -1,17 +1,14 @@
|
|
|
1
|
-
//
|
|
2
|
-
// the system sans-serif stack. Monospace is exact at 0.6em. The React player measures real text instead.
|
|
1
|
+
// Estimated widths: no browser here. The React player measures real text.
|
|
3
2
|
const FULL = /[ᄀ-ᅟ⺀-가-힣豈-︰-﹏-⦆¢-₩]|\p{Extended_Pictographic}/u;
|
|
4
3
|
const NARROW = /[ilj.,:;'|!()[\]ftrI ]/;
|
|
5
4
|
const WIDE = /[WMmw@%]/;
|
|
6
5
|
const TALL = /[A-Z0-9#&?]/;
|
|
7
|
-
/** The width of `s` in px at `fontSize`. */
|
|
8
6
|
export function textWidth(s, fontSize, mono = false) {
|
|
9
7
|
let em = 0;
|
|
10
8
|
for (const ch of s)
|
|
11
9
|
em += FULL.test(ch) ? 1 : mono ? 0.6 : WIDE.test(ch) ? 0.85 : NARROW.test(ch) ? 0.3 : TALL.test(ch) ? 0.64 : 0.53;
|
|
12
10
|
return em * fontSize;
|
|
13
11
|
}
|
|
14
|
-
/** Break a string into lines that fit `width`, keeping the newlines it already has. */
|
|
15
12
|
export function wrap(s, width, fontSize, mono = false) {
|
|
16
13
|
const out = [];
|
|
17
14
|
for (const para of s.split('\n')) {
|
package/dist/verify.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { FlowProps } from './model.ts';
|
|
2
2
|
import type { Finding } from './scene.ts';
|
|
3
3
|
export { parseSource, headingSlug, owners, links, type Link } from './source.ts';
|
|
4
|
-
/** The links against the files under `root`. A path outside
|
|
4
|
+
/** The links against the files under `root`. A path outside `root` counts as a missing file. */
|
|
5
5
|
export declare function verify(fig: FlowProps, { root }?: {
|
|
6
6
|
root?: string;
|
|
7
7
|
}): Finding[];
|