flowfig 0.10.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 +152 -23
- package/dist/atlas.js +6 -2
- package/dist/auto.js +104 -9
- package/dist/browser.d.ts +1 -0
- package/dist/browser.js +9 -4
- package/dist/check.d.ts +4 -3
- package/dist/check.js +143 -8
- package/dist/cli.js +180 -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 +328 -152
- 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 +105 -3
- package/dist/model.js +221 -21
- 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 +245 -81
- 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/load.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
|
-
import { altText, counts, toBeat } from './model.js';
|
|
3
|
-
import { coverageLine, detailFindings, owners, unsureLines, verifyReport } from './verify.js';
|
|
2
|
+
import { altText, counts, scenes, toBeat, tourPlan } from './model.js';
|
|
3
|
+
import { coverageLine, detailFindings, levelLines, owners, unsureLines, verifyReport } from './verify.js';
|
|
4
4
|
import { toSvg } from './svg.js';
|
|
5
5
|
const SPEC_OPEN = '<metadata id="figure-spec"><![CDATA[';
|
|
6
6
|
const SPEC_CLOSE = ']]></metadata>';
|
|
@@ -47,12 +47,16 @@ export function sortFindings(findings, strict) {
|
|
|
47
47
|
const n = (k, word) => `${k} ${word}${k === 1 ? '' : 's'}`;
|
|
48
48
|
/** One line per finding, the counts line (unless `withCounts` is false), and the `figure:` line. No color: the CLI adds it. */
|
|
49
49
|
export function reportLines(props, findings, withCounts = true) {
|
|
50
|
-
const out = findings.map((f) => `${f.severity.padEnd(8)} ${f.rule.padEnd(18)} ${f.message}`);
|
|
50
|
+
const out = findings.map((f) => `${f.severity.padEnd(8)} ${f.rule.padEnd(18)} ${f.scene ? `[${f.scene}] ` : ''}${f.message}`);
|
|
51
51
|
const errors = findings.filter((f) => f.severity === 'error').length;
|
|
52
52
|
if (withCounts)
|
|
53
53
|
out.push(`${n(errors, 'error')}, ${n(findings.length - errors, 'warning')}`);
|
|
54
54
|
const c = counts(props);
|
|
55
|
-
|
|
55
|
+
const all = scenes(props);
|
|
56
|
+
const levels = all.length > 1
|
|
57
|
+
? `; ${1 + Math.max(...all.map((s) => s.path.length))} levels, ${all.length} scenes, ${all.reduce((k, s) => k + s.parts, 0)} parts; tour ${Math.round(tourPlan(props).ms / 1000)} s`
|
|
58
|
+
: '';
|
|
59
|
+
out.push(`figure: ${n(c.boxes, 'box').replace('boxs', 'boxes')}, ${n(c.groups, 'group')}, ${n(c.edges, 'edge')}, ${n(c.steps, 'step')}, ${n(c.messages, 'message')}${levels}`);
|
|
56
60
|
return out;
|
|
57
61
|
}
|
|
58
62
|
const text = (x) => (typeof x === 'string' ? x : '');
|
|
@@ -75,6 +79,7 @@ export function verifyLines(props, figure, root = process.cwd()) {
|
|
|
75
79
|
return [
|
|
76
80
|
...findings.map((f) => `${f.severity.padEnd(8)} ${f.rule.padEnd(18)} ${figure}: ${f.message}`),
|
|
77
81
|
coverageLine(figure, coverage),
|
|
82
|
+
...levelLines(coverage),
|
|
78
83
|
...unsureLines(coverage),
|
|
79
84
|
];
|
|
80
85
|
}
|
package/dist/looks.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare const TOKENS: {
|
|
2
|
+
light: Record<string, string>;
|
|
3
|
+
dark: Record<string, string>;
|
|
4
|
+
};
|
|
5
|
+
export declare const HUD: {
|
|
6
|
+
wide: number;
|
|
7
|
+
roomy: number;
|
|
8
|
+
panel: number;
|
|
9
|
+
slim: number;
|
|
10
|
+
pad: number;
|
|
11
|
+
chips: number;
|
|
12
|
+
sheet: number;
|
|
13
|
+
short: number;
|
|
14
|
+
tall: number;
|
|
15
|
+
};
|
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,14 +269,73 @@ 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;
|
|
238
335
|
/** The time column of each box: first use in the steps, then layout order. `at` wins. */
|
|
239
336
|
export declare function laneColumns(fig: FlowProps): Map<string, number>;
|
|
240
337
|
export declare const LABEL_LINE = 18;
|
|
241
|
-
export declare const FRAME_SIDE = 18, NODE_MIN_W = 100, NODE_MAX_W = 190;
|
|
338
|
+
export declare const FRAME_SIDE = 18, NODE_MIN_W = 100, NODE_MAX_W = 190, DIAMOND_MAX_W = 170;
|
|
242
339
|
export declare const LANE_BLOCK_GAP = 40;
|
|
243
340
|
export declare const STUB = 12, STUB_CLEAR = 8, STUB_ROOM = 30;
|
|
244
341
|
/** The width of a box as the SVG draws it, before a diamond adds its 70 px. */
|
|
@@ -249,13 +346,13 @@ export declare function itemWidth(item: FigNode | FigGroup, carded: {
|
|
|
249
346
|
}, edges: FigEdge[], cap?: number): number;
|
|
250
347
|
export declare function fitCap(fig: FlowProps, width: number, padding?: number): number;
|
|
251
348
|
export declare const diamondRoom: (w: number, h: number, far: number) => number;
|
|
252
|
-
/** The lines of a diamond with no card. The diamond grows 15 px for each sub line that wraps. */
|
|
253
349
|
export declare function diamondLines(item: FigNode, w: number): {
|
|
254
350
|
h: number;
|
|
255
351
|
label: {
|
|
352
|
+
text: string;
|
|
256
353
|
dy: number;
|
|
257
354
|
far: number;
|
|
258
|
-
};
|
|
355
|
+
}[];
|
|
259
356
|
subs: {
|
|
260
357
|
text: string;
|
|
261
358
|
dy: number;
|
|
@@ -361,5 +458,10 @@ export declare const altText: (fig: FlowProps) => {
|
|
|
361
458
|
desc: string;
|
|
362
459
|
};
|
|
363
460
|
export declare const specKey: (...parts: unknown[]) => string;
|
|
461
|
+
export declare const textFloor: (box: number) => 9 | 11;
|
|
364
462
|
export declare const fitScale: (wide: number, box: number, map: number) => number;
|
|
463
|
+
export declare const shownFont: (size: number, place: 'map' | 'rail' | 'plain', scale: {
|
|
464
|
+
map: number;
|
|
465
|
+
rail: number;
|
|
466
|
+
}) => number;
|
|
365
467
|
export declare const nextWide: (wide: number, box: number, map: number) => 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;
|
|
@@ -95,7 +269,7 @@ export function laneColumns(fig) {
|
|
|
95
269
|
return cols;
|
|
96
270
|
}
|
|
97
271
|
export const LABEL_LINE = 18;
|
|
98
|
-
export const FRAME_SIDE = 18, NODE_MIN_W = 100, NODE_MAX_W = 190;
|
|
272
|
+
export const FRAME_SIDE = 18, NODE_MIN_W = 100, NODE_MAX_W = 190, DIAMOND_MAX_W = 170;
|
|
99
273
|
export const LANE_BLOCK_GAP = 40;
|
|
100
274
|
export const STUB = 12, STUB_CLEAR = 8, STUB_ROOM = 30;
|
|
101
275
|
/** The width of a box as the SVG draws it, before a diamond adds its 70 px. */
|
|
@@ -106,14 +280,16 @@ export function nodeWidth(item, carded, cap = NODE_MAX_W) {
|
|
|
106
280
|
return CARD_WIDTH;
|
|
107
281
|
const label = textWidth(str(item.label), 14) + 32;
|
|
108
282
|
const sub = textWidth(str(item.sub), 12) + 32;
|
|
109
|
-
return Math.min(item.shape === 'decision' ?
|
|
283
|
+
return Math.min(item.shape === 'decision' ? DIAMOND_MAX_W : cap, Math.max(NODE_MIN_W, label, sub));
|
|
110
284
|
}
|
|
111
285
|
export function labelLines(item, w) {
|
|
112
286
|
const label = str(item.label);
|
|
113
|
-
if (item.shape === 'decision'
|
|
287
|
+
if (item.shape === 'decision')
|
|
288
|
+
return wrap(label, w - 102 + 0.5, 14);
|
|
289
|
+
if (textWidth(label, 14) <= w - 32 + 0.5)
|
|
114
290
|
return [label];
|
|
115
291
|
const lines = wrap(label, w - 32, 14);
|
|
116
|
-
return lines.
|
|
292
|
+
return lines.every((l) => textWidth(l, 14) <= w - 32) ? lines : [label];
|
|
117
293
|
}
|
|
118
294
|
export function itemWidth(item, carded, edges, cap = NODE_MAX_W) {
|
|
119
295
|
if (!isGroup(item))
|
|
@@ -137,21 +313,28 @@ export function fitCap(fig, width, padding = 24) {
|
|
|
137
313
|
return cap;
|
|
138
314
|
}
|
|
139
315
|
export const diamondRoom = (w, h, far) => w * Math.max(0, 1 - far / (h / 2)) - 8;
|
|
140
|
-
/** The lines of a diamond with no card. The diamond grows 15 px for each sub line that wraps. */
|
|
141
316
|
export function diamondLines(item, w) {
|
|
142
317
|
const sub = item.sub ? str(item.sub) : '';
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
318
|
+
const label = labelLines(item, w);
|
|
319
|
+
const k = label.length - 1;
|
|
320
|
+
const at = (dy, size) => Math.max(Math.abs(dy - 0.75 * size), dy + 0.25 * size);
|
|
321
|
+
for (let n = sub ? 1 : 0, h = 0;;) {
|
|
322
|
+
h = Math.max(h, 62 + 15 * n + LABEL_LINE * k);
|
|
323
|
+
const dy0 = (n ? 5.5 : 5) - 7.5 * n - (LABEL_LINE / 2) * k;
|
|
324
|
+
const last = n ? at(dy0 + LABEL_LINE * k + 15 * n, 12) : 0;
|
|
148
325
|
const lines = n ? wrap(sub, diamondRoom(w, h, last), 12) : [];
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
326
|
+
const labels = label.map((text, i) => ({ text, dy: dy0 + LABEL_LINE * i, far: at(dy0 + LABEL_LINE * i, 14) }));
|
|
327
|
+
const subs = lines.map((text, j) => ({
|
|
328
|
+
text,
|
|
329
|
+
dy: dy0 + LABEL_LINE * k + 15 * (j + 1),
|
|
330
|
+
far: at(dy0 + LABEL_LINE * k + 15 * (j + 1), 12),
|
|
331
|
+
}));
|
|
332
|
+
if (lines.length > n && n < 6)
|
|
333
|
+
n++;
|
|
334
|
+
else if (h < w && labels.some((l) => textWidth(l.text, 14) > diamondRoom(w, h, l.far)))
|
|
335
|
+
h += 4;
|
|
336
|
+
else
|
|
337
|
+
return { h, label: labels, subs };
|
|
155
338
|
}
|
|
156
339
|
}
|
|
157
340
|
/** The lane copies whose stub end has no clear place. The renderer grows each by STUB_ROOM. */
|
|
@@ -498,9 +681,16 @@ export const altText = (fig) => {
|
|
|
498
681
|
});
|
|
499
682
|
}),
|
|
500
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
|
+
});
|
|
501
691
|
return {
|
|
502
692
|
title: `Flow figure: ${boxes.join(', ')}.${steps.length ? ` Steps: ${steps.map((s) => str(s.label)).join(', ')}.` : ''}`,
|
|
503
|
-
desc: [`The figure has these boxes: ${boxes.join(', ')}.`, ...lines].join('\n'),
|
|
693
|
+
desc: [`The figure has these boxes: ${boxes.join(', ')}.`, ...lines, ...levels].join('\n'),
|
|
504
694
|
};
|
|
505
695
|
};
|
|
506
696
|
const keyOf = (v, path) => {
|
|
@@ -523,7 +713,17 @@ const keyOf = (v, path) => {
|
|
|
523
713
|
return Object.fromEntries(Object.entries(src).flatMap(([k, x]) => (k === 'ref' || k.startsWith('_') ? [] : [[k, keyOf(x, at)]])));
|
|
524
714
|
};
|
|
525
715
|
export const specKey = (...parts) => JSON.stringify(keyOf(parts, []));
|
|
526
|
-
|
|
527
|
-
const
|
|
528
|
-
const
|
|
529
|
-
|
|
716
|
+
// Edge labels are 11 px, the smallest reader text at scale 1.
|
|
717
|
+
const READER_PX = 11;
|
|
718
|
+
const DESKTOP_PX = 9;
|
|
719
|
+
const DESKTOP_BOX = 600;
|
|
720
|
+
export const textFloor = (box) => (box < DESKTOP_BOX ? READER_PX : DESKTOP_PX);
|
|
721
|
+
export const fitScale = (wide, box, map) => Math.max(wide ? 1 : textFloor(box) / READER_PX, Math.min(1, box / map));
|
|
722
|
+
export const shownFont = (size, place, scale) => size * (place === 'map' ? scale.map : place === 'rail' ? scale.rail : 1);
|
|
723
|
+
const folds = (box, row) => box * READER_PX < row * textFloor(box);
|
|
724
|
+
export const nextWide = (wide, box, map) => {
|
|
725
|
+
const row = Math.abs(wide) || map;
|
|
726
|
+
if (!folds(box, row))
|
|
727
|
+
return 0;
|
|
728
|
+
return wide > 0 && map >= wide ? -wide : wide || map;
|
|
729
|
+
};
|
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 {};
|