flowfig 0.11.0 → 0.12.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 +148 -21
- package/dist/atlas.js +6 -2
- package/dist/browser.d.ts +1 -0
- package/dist/browser.js +9 -4
- package/dist/check.d.ts +2 -2
- package/dist/check.js +135 -5
- package/dist/cli.js +174 -70
- package/dist/diff.d.ts +2 -0
- package/dist/diff.js +79 -24
- package/dist/element-looks.js +4 -0
- package/dist/element-world.js +6 -0
- package/dist/element.d.ts +27 -0
- package/dist/element.js +67 -0
- package/dist/explain.d.ts +45 -0
- package/dist/explain.js +161 -0
- package/dist/frames.d.ts +34 -0
- package/dist/frames.js +117 -0
- package/dist/index.js +283 -131
- package/dist/load.js +9 -4
- package/dist/looks.d.ts +15 -0
- package/dist/looks.js +32 -0
- package/dist/mcp.d.ts +4 -0
- package/dist/mcp.js +14 -6
- package/dist/model.d.ts +97 -0
- package/dist/model.js +182 -1
- package/dist/scene.d.ts +2 -0
- package/dist/source.d.ts +4 -2
- package/dist/source.js +15 -9
- package/dist/svg.d.ts +12 -0
- package/dist/svg.js +235 -75
- package/dist/verify.d.ts +6 -0
- package/dist/verify.js +104 -56
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/world-art.d.ts +59 -0
- package/dist/world-art.js +854 -0
- package/dist/world.d.ts +233 -0
- package/dist/world.js +1577 -0
- package/package.json +9 -3
package/dist/looks.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const base = {
|
|
2
|
+
font: 'system-ui, -apple-system, "Segoe UI", Roboto, sans-serif',
|
|
3
|
+
};
|
|
4
|
+
export const TOKENS = {
|
|
5
|
+
light: {
|
|
6
|
+
...base,
|
|
7
|
+
bg: '#fafbfd',
|
|
8
|
+
surface: '#ffffff',
|
|
9
|
+
border: '#dfe7f0',
|
|
10
|
+
ink: '#1d2433',
|
|
11
|
+
muted: '#5d687a',
|
|
12
|
+
accent: '#0074d9',
|
|
13
|
+
'on-accent': '#ffffff',
|
|
14
|
+
bad: '#c8352f',
|
|
15
|
+
good: '#23704e',
|
|
16
|
+
play: '#1d2433',
|
|
17
|
+
},
|
|
18
|
+
dark: {
|
|
19
|
+
...base,
|
|
20
|
+
bg: '#131820',
|
|
21
|
+
surface: '#1c2330',
|
|
22
|
+
border: '#2a3343',
|
|
23
|
+
ink: '#e9eef6',
|
|
24
|
+
muted: '#9aa6b8',
|
|
25
|
+
accent: '#4da3f0',
|
|
26
|
+
'on-accent': '#0b1220',
|
|
27
|
+
bad: '#ee5d57',
|
|
28
|
+
good: '#5cc48f',
|
|
29
|
+
play: '#6b778c',
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
export const HUD = { wide: 880, roomy: 1200, panel: 376, slim: 300, pad: 16, chips: 52, sheet: 160, short: 560, tall: 760 };
|
package/dist/mcp.d.ts
CHANGED
package/dist/mcp.js
CHANGED
|
@@ -6,7 +6,7 @@ import { GUIDE, TOPICS } from './guide.js';
|
|
|
6
6
|
import { loadSpec, reportLines, sortFindings, summaryLines, svgWithSpec, verifyLines } from './load.js';
|
|
7
7
|
import { check, render } from './svg.js';
|
|
8
8
|
import { checkRendered } from './check.js';
|
|
9
|
-
import { coverageLine, unsureLines, verifyReport } from './verify.js';
|
|
9
|
+
import { coverageLine, levelLines, unsureLines, verifyReport } from './verify.js';
|
|
10
10
|
import { VERSION } from './version.js';
|
|
11
11
|
const specSchema = {
|
|
12
12
|
description: 'The figure: the props object, { props }, or a path to a .json file or an SVG that flowfig wrote.',
|
|
@@ -34,7 +34,11 @@ export const TOOLS = [
|
|
|
34
34
|
description: 'Check a figure, then write it as one animated SVG file. Returns the check lines and the path. On an error, writes nothing.',
|
|
35
35
|
inputSchema: {
|
|
36
36
|
type: 'object',
|
|
37
|
-
properties: {
|
|
37
|
+
properties: {
|
|
38
|
+
...checkProps,
|
|
39
|
+
out: { type: 'string', description: 'The SVG path to write.' },
|
|
40
|
+
scene: { type: 'string', description: 'The id of a box with inside: render that level as a plain figure.' },
|
|
41
|
+
},
|
|
38
42
|
required: ['spec', 'out'],
|
|
39
43
|
},
|
|
40
44
|
},
|
|
@@ -98,11 +102,15 @@ function run(name, args) {
|
|
|
98
102
|
}
|
|
99
103
|
if (name === 'check' || name === 'render') {
|
|
100
104
|
need(args, name, name === 'render' ? ['spec', 'out'] : ['spec']);
|
|
101
|
-
types(args, name, { width: 'number', minText: 'number', out: 'string', strict: 'boolean' });
|
|
105
|
+
types(args, name, { width: 'number', minText: 'number', out: 'string', scene: 'string', strict: 'boolean' });
|
|
102
106
|
const props = spec(args.spec, name);
|
|
103
|
-
const opts = {
|
|
107
|
+
const opts = {
|
|
108
|
+
width: args.width,
|
|
109
|
+
minText: args.minText,
|
|
110
|
+
...(name === 'render' && { scene: args.scene }),
|
|
111
|
+
};
|
|
104
112
|
const rendered = render(props, opts);
|
|
105
|
-
const findings = sortFindings(checkRendered(props, opts, rendered.scene), args.strict === true);
|
|
113
|
+
const findings = sortFindings(checkRendered(props, opts, rendered.scene, rendered.levels), args.strict === true);
|
|
106
114
|
const errors = findings.some((f) => f.severity === 'error');
|
|
107
115
|
const lines = reportLines(props, findings);
|
|
108
116
|
if (name === 'check' || errors)
|
|
@@ -140,7 +148,7 @@ function run(name, args) {
|
|
|
140
148
|
continue;
|
|
141
149
|
}
|
|
142
150
|
const report = verifyReport(props, { root });
|
|
143
|
-
counts.push(coverageLine(p, report.coverage), ...unsureLines(report.coverage));
|
|
151
|
+
counts.push(coverageLine(p, report.coverage), ...levelLines(report.coverage), ...unsureLines(report.coverage));
|
|
144
152
|
const findings = sortFindings([...check(props), ...report.findings], args.strict === true);
|
|
145
153
|
errors += findings.filter((f) => f.severity === 'error').length;
|
|
146
154
|
lines.push(...findings.map((f) => `${f.severity.padEnd(8)} ${f.rule.padEnd(18)} ${p}: ${f.message}`));
|
package/dist/model.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
2
|
import type { Rect } from './geometry.ts';
|
|
3
|
+
export declare const LOOKS: readonly ['person', 'shop', 'desk', 'truck', 'phone', 'server', 'cabinet', 'team', 'tower', 'queue', 'gear', 'cloud'];
|
|
4
|
+
export type FigLook = (typeof LOOKS)[number];
|
|
3
5
|
/** A box. A box with `show` in a step becomes a content card. */
|
|
4
6
|
export type FigNode = {
|
|
5
7
|
/** Name that edges, steps and beats use for this box. Unique in the figure. */
|
|
@@ -28,6 +30,14 @@ export type FigNode = {
|
|
|
28
30
|
source?: string;
|
|
29
31
|
/** A figure with more detail: an SVG path from the repo root. */
|
|
30
32
|
detail?: string;
|
|
33
|
+
/** The parts inside the box, down to 3 levels. */
|
|
34
|
+
inside?: FigInside;
|
|
35
|
+
/** The picture of this box or group in <flowfig-explain>. The diagram ignores it. */
|
|
36
|
+
look?: FigLook;
|
|
37
|
+
/** The question on the gate in <flowfig-explain>. Default: the label of a `decision` box. */
|
|
38
|
+
check?: string;
|
|
39
|
+
/** The state after this box, such as "Oil in stock". The <flowfig-explain> ending shows it. */
|
|
40
|
+
done?: string;
|
|
31
41
|
};
|
|
32
42
|
/** A frame that lays out its children in a row or a column. */
|
|
33
43
|
export type FigGroup = {
|
|
@@ -45,6 +55,28 @@ export type FigGroup = {
|
|
|
45
55
|
children: (FigNode | FigGroup)[];
|
|
46
56
|
/** On the root layout: flowfig places the boxes. A labeled group is a frame. */
|
|
47
57
|
auto?: true;
|
|
58
|
+
/** The picture of this box or group in <flowfig-explain>. The diagram ignores it. */
|
|
59
|
+
look?: FigLook;
|
|
60
|
+
};
|
|
61
|
+
/** A physical thing that moves on an edge in <flowfig-explain>. */
|
|
62
|
+
export type FigCarry = {
|
|
63
|
+
/** The name of the thing, in plain words. */
|
|
64
|
+
thing: string;
|
|
65
|
+
/** The picture of one unit. Default: `box`. */
|
|
66
|
+
form?: 'box' | 'barrel' | 'notes';
|
|
67
|
+
/** How many units move, 1 to 4. Default: 1. */
|
|
68
|
+
count?: number;
|
|
69
|
+
/** The id of the box or group where the thing starts. Default: the edge `from`. */
|
|
70
|
+
from?: string;
|
|
71
|
+
};
|
|
72
|
+
/** The parts inside a box. A reader opens the box to see them. */
|
|
73
|
+
export type FigInside = {
|
|
74
|
+
/** The parts and how they line up. The rules of `FlowProps.layout` apply, `auto` too. */
|
|
75
|
+
layout: FigGroup;
|
|
76
|
+
/** The arrows between the parts. One end can name an outer neighbor. */
|
|
77
|
+
edges?: FigEdge[];
|
|
78
|
+
/** The stories inside the box. Each step is a tab when the box is open. */
|
|
79
|
+
steps?: FigStep[];
|
|
48
80
|
};
|
|
49
81
|
/** An arrow from one box to another. */
|
|
50
82
|
export type FigEdge = {
|
|
@@ -64,6 +96,8 @@ export type FigEdge = {
|
|
|
64
96
|
source?: string;
|
|
65
97
|
/** The route, queue, topic or table that carries this edge. Checked by `flowfig verify`. */
|
|
66
98
|
via?: string;
|
|
99
|
+
/** What moves on this edge in <flowfig-explain>: `data`, or a thing. Default: `data`. */
|
|
100
|
+
carries?: 'data' | FigCarry;
|
|
67
101
|
};
|
|
68
102
|
/** One packet on one edge. A string is the edge `id`. */
|
|
69
103
|
export type FigHop = string | {
|
|
@@ -88,6 +122,8 @@ export type FigBeat = {
|
|
|
88
122
|
edges?: FigHop | FigHop[];
|
|
89
123
|
/** The line of narration for this beat. */
|
|
90
124
|
say?: ReactNode;
|
|
125
|
+
/** One line under the say in <flowfig-explain>, 15 words or fewer. */
|
|
126
|
+
note?: ReactNode;
|
|
91
127
|
/** Fills the content card of each named box until the step ends. */
|
|
92
128
|
show?: Record<string, FigContent>;
|
|
93
129
|
/** Ids of boxes to highlight for this beat, without a packet or a content card. */
|
|
@@ -126,6 +162,8 @@ export type FigStep = {
|
|
|
126
162
|
flow: (FigHop | FigHop[] | FigBeat)[];
|
|
127
163
|
/** Ids of boxes to highlight for the whole step. */
|
|
128
164
|
nodes?: string[];
|
|
165
|
+
/** One sentence, 15 words or fewer, on how the story ends in <flowfig-explain>. */
|
|
166
|
+
outcome?: string;
|
|
129
167
|
};
|
|
130
168
|
/** Colors for the figure. A key you leave out keeps the built-in color. */
|
|
131
169
|
export type FigTheme = Partial<Record<'accent' | 'fg' | 'muted' | 'bg' | 'surface' | 'border' | 'font', string>>;
|
|
@@ -231,7 +269,66 @@ export declare const outsideLabelRect: (bar: {
|
|
|
231
269
|
export declare const labelSpan: (t0: number, end: number, ramp: number) => [number, number];
|
|
232
270
|
export declare const dateLabelRaised: (x: number, today: number | null | undefined, w: number, todayW: number) => boolean;
|
|
233
271
|
export declare const STEP_HOLD_MS = 2000;
|
|
272
|
+
export declare const n2: (v: number) => number;
|
|
273
|
+
export declare const beatTimes: (beats: Beat[][], speedMs: number) => {
|
|
274
|
+
beats: {
|
|
275
|
+
t0: number;
|
|
276
|
+
t1: number;
|
|
277
|
+
tEnd: number;
|
|
278
|
+
}[][];
|
|
279
|
+
ends: number[];
|
|
280
|
+
};
|
|
234
281
|
export declare const nodes: (g: FigGroup) => FigNode[];
|
|
282
|
+
export type FigPort = {
|
|
283
|
+
id: string;
|
|
284
|
+
way: 'in' | 'out' | 'in and out';
|
|
285
|
+
name: string;
|
|
286
|
+
};
|
|
287
|
+
export type FigScene = {
|
|
288
|
+
path: string[];
|
|
289
|
+
name: string;
|
|
290
|
+
line: string;
|
|
291
|
+
props: FlowProps;
|
|
292
|
+
ports: FigPort[];
|
|
293
|
+
parts: number;
|
|
294
|
+
};
|
|
295
|
+
export declare const PORT_GAP = 28;
|
|
296
|
+
export declare const MAX_LEVEL = 3;
|
|
297
|
+
export declare const portLabel: (p: FigPort) => string;
|
|
298
|
+
export declare function scenes(fig: FlowProps): FigScene[];
|
|
299
|
+
export declare const OPEN_MS: {
|
|
300
|
+
fade: number;
|
|
301
|
+
grow: number;
|
|
302
|
+
stagger: number;
|
|
303
|
+
hide: number;
|
|
304
|
+
shrink: number;
|
|
305
|
+
};
|
|
306
|
+
export type Times = {
|
|
307
|
+
beats: {
|
|
308
|
+
t0: number;
|
|
309
|
+
t1: number;
|
|
310
|
+
tEnd: number;
|
|
311
|
+
}[][];
|
|
312
|
+
ends: number[];
|
|
313
|
+
};
|
|
314
|
+
export type TourOpen = {
|
|
315
|
+
path: string[];
|
|
316
|
+
step: number;
|
|
317
|
+
open: [number, number];
|
|
318
|
+
grown: number;
|
|
319
|
+
loop: [number, number][];
|
|
320
|
+
back: [number, number];
|
|
321
|
+
times: Times;
|
|
322
|
+
};
|
|
323
|
+
export type Tour = {
|
|
324
|
+
ms: number;
|
|
325
|
+
loop: [number, number][];
|
|
326
|
+
times: Times;
|
|
327
|
+
opens: TourOpen[];
|
|
328
|
+
};
|
|
329
|
+
export declare function insideStep(s: FigScene, port?: string): number;
|
|
330
|
+
export declare function flowOrder(s: FigScene): string[];
|
|
331
|
+
export declare function tourPlan(fig: FlowProps, speed?: number): Tour;
|
|
235
332
|
export declare const LANE_GAP = 56, LANE_PAD = 24, LANE_ROW_GAP = 20;
|
|
236
333
|
export declare const isLanesLayout: (g: FigGroup) => boolean;
|
|
237
334
|
export declare const validAt: (at: unknown) => at is number;
|
package/dist/model.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { textWidth, wrap } from './text.js';
|
|
2
|
+
export const LOOKS = ['person', 'shop', 'desk', 'truck', 'phone', 'server', 'cabinet', 'team', 'tower', 'queue', 'gear', 'cloud'];
|
|
2
3
|
export const toBeat = (b) => {
|
|
3
4
|
const isBeat = typeof b === 'object' && !Array.isArray(b) && !('edge' in b);
|
|
4
5
|
const { edges, ...rest } = isBeat ? b : { edges: b };
|
|
@@ -66,7 +67,180 @@ export const outsideLabelRect = (bar, label) => ({
|
|
|
66
67
|
export const labelSpan = (t0, end, ramp) => [Math.min(t0 + ramp, end), end];
|
|
67
68
|
export const dateLabelRaised = (x, today, w, todayW) => today != null && x + 3 < today - 3 && x + 3 + w > today - 3 - todayW;
|
|
68
69
|
export const STEP_HOLD_MS = 2000;
|
|
70
|
+
export const n2 = (v) => Math.round(v * 10) / 10;
|
|
71
|
+
export const beatTimes = (beats, speedMs) => {
|
|
72
|
+
const travel = speedMs / 1000 / BASE_RATE, hold = STEP_HOLD_MS / 1000 / BASE_RATE;
|
|
73
|
+
const ends = [];
|
|
74
|
+
let t = 0;
|
|
75
|
+
const times = beats.map((step) => {
|
|
76
|
+
const out = step.map((b, bi) => {
|
|
77
|
+
const t0 = t, dur = beatMs(b, speedMs) / 1000 / BASE_RATE;
|
|
78
|
+
t += dur + (bi === step.length - 1 ? hold : 0);
|
|
79
|
+
return { t0, t1: t0 + travel, tEnd: t0 + dur };
|
|
80
|
+
});
|
|
81
|
+
ends.push(t);
|
|
82
|
+
return out;
|
|
83
|
+
});
|
|
84
|
+
return { beats: times, ends };
|
|
85
|
+
};
|
|
69
86
|
export const nodes = (g) => g.children.flatMap((c) => (isGroup(c) ? nodes(c) : [c]));
|
|
87
|
+
// Measured: a 56 px gap makes the Orders level of figures/inside-shop.ts fail small-text.
|
|
88
|
+
export const PORT_GAP = 28;
|
|
89
|
+
export const MAX_LEVEL = 3;
|
|
90
|
+
export const portLabel = (p) => p.way === 'in' ? `in: from ${p.name}` : p.way === 'out' ? `out: to ${p.name}` : `in and out: ${p.name}`;
|
|
91
|
+
export function scenes(fig) {
|
|
92
|
+
const root = { path: [], name: '', line: '', props: fig, ports: [], parts: 0 };
|
|
93
|
+
if (fig.lanes || fig.timeline || fig.rail === 'only')
|
|
94
|
+
return [root];
|
|
95
|
+
const real = new Map();
|
|
96
|
+
const index = (g, level) => nodes(g).forEach((n) => {
|
|
97
|
+
real.set(n.id, n);
|
|
98
|
+
if (n.inside && level < MAX_LEVEL)
|
|
99
|
+
index(n.inside.layout, level + 1);
|
|
100
|
+
});
|
|
101
|
+
index(fig.layout, 1);
|
|
102
|
+
const name = (id) => str(real.get(id)?.label) || id;
|
|
103
|
+
const { theme, speed, rail } = fig;
|
|
104
|
+
const out = [root];
|
|
105
|
+
const open = (parent) => {
|
|
106
|
+
for (const b of nodes(parent.props.layout)) {
|
|
107
|
+
const parts = b.inside ? nodes(b.inside.layout) : [];
|
|
108
|
+
if (!b.inside || !parts.length)
|
|
109
|
+
continue;
|
|
110
|
+
const ids = new Set(parts.map((n) => n.id));
|
|
111
|
+
const edges = b.inside.edges ?? [];
|
|
112
|
+
const ins = [], outs = [];
|
|
113
|
+
for (const e of edges) {
|
|
114
|
+
if (ids.has(e.from) === ids.has(e.to))
|
|
115
|
+
continue;
|
|
116
|
+
if (!ids.has(e.from) && !ins.includes(e.from))
|
|
117
|
+
ins.push(e.from);
|
|
118
|
+
if (!ids.has(e.to) && !outs.includes(e.to))
|
|
119
|
+
outs.push(e.to);
|
|
120
|
+
}
|
|
121
|
+
const ports = [...ins, ...outs.filter((id) => !ins.includes(id))].map((id) => ({
|
|
122
|
+
id,
|
|
123
|
+
way: !ins.includes(id) ? 'out' : outs.includes(id) ? 'in and out' : 'in',
|
|
124
|
+
name: name(id),
|
|
125
|
+
}));
|
|
126
|
+
const box = (p) => {
|
|
127
|
+
const source = real.get(p.id)?.source;
|
|
128
|
+
return { id: p.id, label: portLabel(p), ...(source != null && { source }) };
|
|
129
|
+
};
|
|
130
|
+
const before = ports.filter((p) => p.way !== 'out').map(box), after = ports.filter((p) => p.way === 'out').map(box);
|
|
131
|
+
const l = b.inside.layout;
|
|
132
|
+
const layout = l.auto
|
|
133
|
+
? { ...l, children: [...before, ...l.children, ...after] }
|
|
134
|
+
: { direction: l.direction === 'column' ? 'column' : 'row', gap: PORT_GAP, children: [...before, l, ...after] };
|
|
135
|
+
const path = [...parent.path, b.id];
|
|
136
|
+
const s = {
|
|
137
|
+
path,
|
|
138
|
+
name: name(b.id),
|
|
139
|
+
line: path.map(name).join(' > '),
|
|
140
|
+
props: { layout, edges, steps: b.inside.steps, theme, speed, rail },
|
|
141
|
+
ports,
|
|
142
|
+
parts: ids.size,
|
|
143
|
+
};
|
|
144
|
+
out.push(s);
|
|
145
|
+
if (path.length + 1 < MAX_LEVEL)
|
|
146
|
+
open(s);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
open(root);
|
|
150
|
+
return out;
|
|
151
|
+
}
|
|
152
|
+
export const OPEN_MS = { fade: 120, grow: 360, stagger: 50, hide: 120, shrink: 300 };
|
|
153
|
+
const hopEnds = (e, back) => (back ? [e.to, e.from] : [e.from, e.to]);
|
|
154
|
+
export function insideStep(s, port) {
|
|
155
|
+
const byId = new Map(s.props.edges.map((e) => [edgeId(e), e]));
|
|
156
|
+
const at = (s.props.steps ?? []).findIndex((st) => {
|
|
157
|
+
const h = st.flow.length ? toBeat(st.flow[0]).hops[0] : undefined;
|
|
158
|
+
const e = h && byId.get(h.edge);
|
|
159
|
+
return e != null && hopEnds(e, h.back)[0] === port;
|
|
160
|
+
});
|
|
161
|
+
return Math.max(at, 0);
|
|
162
|
+
}
|
|
163
|
+
export function flowOrder(s) {
|
|
164
|
+
const cols = laneColumns(s.props);
|
|
165
|
+
const ids = nodes(s.props.layout)
|
|
166
|
+
.map((n) => n.id)
|
|
167
|
+
.sort((a, b) => cols.get(a) - cols.get(b));
|
|
168
|
+
const port = new Set(s.ports.map((p) => p.id));
|
|
169
|
+
return [...ids.filter((id) => port.has(id)), ...ids.filter((id) => !port.has(id))];
|
|
170
|
+
}
|
|
171
|
+
export function tourPlan(fig, speed = fig.speed ?? 900) {
|
|
172
|
+
const all = scenes(fig);
|
|
173
|
+
const byKey = new Map(all.map((s) => [s.path.join('>'), s]));
|
|
174
|
+
const opens = [];
|
|
175
|
+
const done = new Set();
|
|
176
|
+
const cut = (from, to, spans) => {
|
|
177
|
+
const out = [];
|
|
178
|
+
let at = from;
|
|
179
|
+
for (const [a, b] of spans) {
|
|
180
|
+
if (a > at)
|
|
181
|
+
out.push([at, a]);
|
|
182
|
+
at = b;
|
|
183
|
+
}
|
|
184
|
+
if (to > at)
|
|
185
|
+
out.push([at, to]);
|
|
186
|
+
return out;
|
|
187
|
+
};
|
|
188
|
+
const play = (s, steps, at) => {
|
|
189
|
+
const beats = steps.map((st) => st.flow.map(toBeat));
|
|
190
|
+
const base = beatTimes(beats, speed);
|
|
191
|
+
const byId = new Map(s.props.edges.map((e) => [edgeId(e), e]));
|
|
192
|
+
const spans = [];
|
|
193
|
+
let shift = 0;
|
|
194
|
+
const openAt = (id, port, p) => {
|
|
195
|
+
const key = [...s.path, id].join('>');
|
|
196
|
+
const inner = byKey.get(key);
|
|
197
|
+
if (!inner || done.has(key))
|
|
198
|
+
return;
|
|
199
|
+
const L = openBox(inner, port, p);
|
|
200
|
+
spans.push([p, p + L]);
|
|
201
|
+
shift += L;
|
|
202
|
+
};
|
|
203
|
+
const times = { beats: [], ends: [] };
|
|
204
|
+
beats.forEach((row, si) => {
|
|
205
|
+
times.beats.push(row.map((b, bi) => {
|
|
206
|
+
const t = base.beats[si][bi];
|
|
207
|
+
const t0 = at + t.t0 * 1000 + shift, t1 = at + t.t1 * 1000 + shift;
|
|
208
|
+
for (const h of b.hops) {
|
|
209
|
+
const e = byId.get(h.edge);
|
|
210
|
+
if (e)
|
|
211
|
+
openAt(hopEnds(e, h.back)[1], hopEnds(e, h.back)[0], at + t.tEnd * 1000 + shift);
|
|
212
|
+
}
|
|
213
|
+
return { t0: t0 / 1000, t1: t1 / 1000, tEnd: (at + t.tEnd * 1000 + shift) / 1000 };
|
|
214
|
+
}));
|
|
215
|
+
times.ends.push((at + base.ends[si] * 1000 + shift) / 1000);
|
|
216
|
+
});
|
|
217
|
+
const last = at + (base.ends.at(-1) ?? 0) * 1000;
|
|
218
|
+
for (const n of nodes(s.props.layout))
|
|
219
|
+
openAt(n.id, undefined, last + shift);
|
|
220
|
+
if (times.ends.length)
|
|
221
|
+
times.ends[times.ends.length - 1] = (last + shift) / 1000;
|
|
222
|
+
return { times, end: last + shift, spans };
|
|
223
|
+
};
|
|
224
|
+
const openBox = (sc, port, p) => {
|
|
225
|
+
done.add(sc.path.join('>'));
|
|
226
|
+
const grown = p + readMs(sc.name) + OPEN_MS.fade + OPEN_MS.grow;
|
|
227
|
+
const count = nodes(sc.props.layout).length;
|
|
228
|
+
const loopAt = grown + OPEN_MS.stagger * (count - 1) + readMs([sc.line, ...sc.ports.map(portLabel)].join(' '));
|
|
229
|
+
const steps = sc.props.steps ?? [];
|
|
230
|
+
const step = steps.length ? insideStep(sc, port) : -1;
|
|
231
|
+
const entry = { path: sc.path, step, open: [p, loopAt], grown, loop: [], back: [0, 0], times: { beats: [], ends: [] } };
|
|
232
|
+
opens.push(entry);
|
|
233
|
+
const r = play(sc, step === -1 ? [] : [steps[step]], loopAt);
|
|
234
|
+
const up = sc.path.length > 1 ? `back to ${byKey.get(sc.path.slice(0, -1).join('>')).name}` : 'Back';
|
|
235
|
+
const end = r.end + readMs(up) + OPEN_MS.hide + OPEN_MS.shrink + OPEN_MS.fade;
|
|
236
|
+
entry.loop = cut(loopAt, r.end, r.spans);
|
|
237
|
+
entry.back = [r.end, end];
|
|
238
|
+
entry.times = r.times;
|
|
239
|
+
return end - p;
|
|
240
|
+
};
|
|
241
|
+
const root = play(all[0], fig.steps ?? [], 0);
|
|
242
|
+
return { ms: root.end, loop: cut(0, root.end, root.spans), times: root.times, opens };
|
|
243
|
+
}
|
|
70
244
|
export const LANE_GAP = 56, LANE_PAD = 24, LANE_ROW_GAP = 20;
|
|
71
245
|
export const isLanesLayout = (g) => g.direction === 'column' && g.children.every((c) => isGroup(c) && c.label != null && c.children.every((k) => !isGroup(k)));
|
|
72
246
|
export const validAt = (at) => typeof at === 'number' && Number.isInteger(at) && at >= 0;
|
|
@@ -507,9 +681,16 @@ export const altText = (fig) => {
|
|
|
507
681
|
});
|
|
508
682
|
}),
|
|
509
683
|
]);
|
|
684
|
+
const levels = scenes(fig)
|
|
685
|
+
.slice(1)
|
|
686
|
+
.map((s) => {
|
|
687
|
+
const port = new Set(s.ports.map((p) => p.id));
|
|
688
|
+
const parts = nodes(s.props.layout).filter((n) => !port.has(n.id));
|
|
689
|
+
return `${s.line}: ${parts.map((n) => str(n.label) || n.id).join(', ')}.${s.ports.length ? ` Ports: ${s.ports.map(portLabel).join(', ')}.` : ''}`;
|
|
690
|
+
});
|
|
510
691
|
return {
|
|
511
692
|
title: `Flow figure: ${boxes.join(', ')}.${steps.length ? ` Steps: ${steps.map((s) => str(s.label)).join(', ')}.` : ''}`,
|
|
512
|
-
desc: [`The figure has these boxes: ${boxes.join(', ')}.`, ...lines].join('\n'),
|
|
693
|
+
desc: [`The figure has these boxes: ${boxes.join(', ')}.`, ...lines, ...levels].join('\n'),
|
|
513
694
|
};
|
|
514
695
|
};
|
|
515
696
|
const keyOf = (v, path) => {
|
package/dist/scene.d.ts
CHANGED
package/dist/source.d.ts
CHANGED
|
@@ -10,8 +10,10 @@ export type Link = {
|
|
|
10
10
|
source: string;
|
|
11
11
|
path: string;
|
|
12
12
|
symbol?: string;
|
|
13
|
+
level?: string;
|
|
13
14
|
};
|
|
14
|
-
|
|
15
|
-
export declare function owners(fig: FlowProps): [
|
|
15
|
+
type Owner = [string, string | undefined, string | undefined, string];
|
|
16
|
+
export declare function owners(fig: FlowProps): Owner[];
|
|
16
17
|
/** Every well-formed link in the figure, with the box, edge or hop that carries it. `bad-source` covers the rest. */
|
|
17
18
|
export declare function links(fig: FlowProps): Link[];
|
|
19
|
+
export {};
|
package/dist/source.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// No Node import: the React and SVG entries must stay browser-safe.
|
|
2
|
-
import { edgeId, nodes, toBeat } from './model.js';
|
|
2
|
+
import { edgeId, nodes, scenes, toBeat } from './model.js';
|
|
3
3
|
/** `path` or `path#symbol`. No spaces, one `#` at most, both parts non-empty. `null` for a bad form. */
|
|
4
4
|
export function parseSource(s) {
|
|
5
5
|
const m = /^([^\s#]+)(?:#([^\s#]+))?$/.exec(s);
|
|
@@ -12,18 +12,24 @@ export const headingSlug = (text) => text
|
|
|
12
12
|
.toLowerCase()
|
|
13
13
|
.replace(/[^\p{L}\p{N} -]/gu, '')
|
|
14
14
|
.replace(/ /g, '-');
|
|
15
|
-
/** Every box, edge and hop with its raw `source`, well-formed or not. The one owner list that `links` and `checkSpec` share. */
|
|
16
15
|
export function owners(fig) {
|
|
17
|
-
return
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
return scenes(fig).flatMap((s) => {
|
|
17
|
+
const ports = new Set(s.ports.map((p) => p.id));
|
|
18
|
+
const at = s.line ? ` [${s.line}]` : '';
|
|
19
|
+
const kind = s.line ? 'part' : 'box';
|
|
20
|
+
return [
|
|
21
|
+
...nodes(s.props.layout)
|
|
22
|
+
.filter((n) => !ports.has(n.id))
|
|
23
|
+
.map((n) => [`${kind} "${n.id}"${at}`, n.source, undefined, s.path.join('>')]),
|
|
24
|
+
...s.props.edges.map((e) => [`edge "${edgeId(e)}"${at}`, e.source, e.via, s.path.join('>')]),
|
|
25
|
+
...(s.props.steps ?? []).flatMap((st) => st.flow.flatMap((b) => toBeat(b).hops.map((h) => [`hop on "${h.edge}"${at}`, h.source, h.via, s.path.join('>')]))),
|
|
26
|
+
];
|
|
27
|
+
});
|
|
22
28
|
}
|
|
23
29
|
/** Every well-formed link in the figure, with the box, edge or hop that carries it. `bad-source` covers the rest. */
|
|
24
30
|
export function links(fig) {
|
|
25
|
-
return owners(fig).flatMap(([owner, source]) => {
|
|
31
|
+
return owners(fig).flatMap(([owner, source, , level]) => {
|
|
26
32
|
const p = source == null ? null : parseSource(source);
|
|
27
|
-
return p ? [{ owner, source: source, ...p }] : [];
|
|
33
|
+
return p ? [{ owner, source: source, ...p, ...(level && { level }) }] : [];
|
|
28
34
|
});
|
|
29
35
|
}
|
package/dist/svg.d.ts
CHANGED
|
@@ -23,11 +23,23 @@ export type SvgOptions = {
|
|
|
23
23
|
marks?: DiffMarks;
|
|
24
24
|
/** Links a box to an href, by box id. `flowfig atlas` sets it. */
|
|
25
25
|
links?: Record<string, string>;
|
|
26
|
+
/** Draws the level of this box as a plain figure: no tour, no script. */
|
|
27
|
+
scene?: string;
|
|
28
|
+
/** Adds the script that opens a box on a click, for a figure with parts. */
|
|
29
|
+
script?: boolean;
|
|
30
|
+
/** A prefix for animation names and path ids. Each page SVG needs its own. */
|
|
31
|
+
scope?: string;
|
|
32
|
+
};
|
|
33
|
+
export type Level = {
|
|
34
|
+
line: string;
|
|
35
|
+
props: FlowProps;
|
|
36
|
+
scene: Scene;
|
|
26
37
|
};
|
|
27
38
|
/** The SVG string and the scene it drew. The scene shape may change; use `toSvg` for the SVG only. */
|
|
28
39
|
export declare function render(spec: FlowProps, opts?: SvgOptions): {
|
|
29
40
|
svg: string;
|
|
30
41
|
scene: Scene;
|
|
42
|
+
levels?: Level[];
|
|
31
43
|
};
|
|
32
44
|
/** One self-contained animated SVG string for the figure. Needs no React and no browser. */
|
|
33
45
|
export declare function toSvg(fig: FlowProps, opts?: SvgOptions): string;
|