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/explain.js
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { edgeId, isGroup, isLanesLayout, nodes, str, toBeat } from './model.js';
|
|
2
|
+
const CAST_MAX = 6;
|
|
3
|
+
const isStore = (n) => n.shape === 'store';
|
|
4
|
+
const isDecision = (n) => n?.shape === 'decision';
|
|
5
|
+
const dead = (t) => t === 'red' || t === 'gray';
|
|
6
|
+
export const isCheck = (n) => !!n && (n.shape === 'decision' || !!n.check);
|
|
7
|
+
export const question = (n) => n.check || (n.shape === 'decision' ? str(n.label) : '');
|
|
8
|
+
export const failed = (byId, h) => isCheck(byId.get(h.source)) && (dead(byId.get(h.target)?.tone) || h.tone === 'red');
|
|
9
|
+
const labeledTop = (g) => g.children.flatMap((c) => (!isGroup(c) ? [] : c.label != null ? [c] : labeledTop(c)));
|
|
10
|
+
export const hopsOf = (fig, step) => {
|
|
11
|
+
const byId = new Map(fig.edges.map((e) => [edgeId(e), e]));
|
|
12
|
+
return step.flow.map((b) => {
|
|
13
|
+
const beat = toBeat(b);
|
|
14
|
+
const hops = beat.hops.flatMap((h) => {
|
|
15
|
+
const edge = byId.get(h.edge);
|
|
16
|
+
return edge ? [{ ...h, edge, source: h.back ? edge.to : edge.from, target: h.back ? edge.from : edge.to }] : [];
|
|
17
|
+
});
|
|
18
|
+
return { beat, hops };
|
|
19
|
+
});
|
|
20
|
+
};
|
|
21
|
+
export function cast(fig) {
|
|
22
|
+
const all = nodes(fig.layout);
|
|
23
|
+
const stores = all.filter(isStore);
|
|
24
|
+
const byId = new Map(all.map((n) => [n.id, n]));
|
|
25
|
+
const groups = fig.lanes && isLanesLayout(fig.layout) ? fig.layout.children.filter(isGroup) : labeledTop(fig.layout);
|
|
26
|
+
let actors = [];
|
|
27
|
+
for (const g of groups) {
|
|
28
|
+
const boxes = nodes(g).filter((n) => !isStore(n));
|
|
29
|
+
if (boxes.length)
|
|
30
|
+
actors.push({ name: str(g.label), look: g.look, boxes: boxes.map((n) => n.id) });
|
|
31
|
+
}
|
|
32
|
+
if (actors.length) {
|
|
33
|
+
const owned = new Set(actors.flatMap((a) => a.boxes));
|
|
34
|
+
for (const step of fig.steps ?? [])
|
|
35
|
+
for (const { hops } of hopsOf(fig, step))
|
|
36
|
+
for (const id of hops.flatMap((h) => [h.source, h.target])) {
|
|
37
|
+
const n = byId.get(id);
|
|
38
|
+
if (!n || owned.has(id) || isStore(n) || isDecision(n))
|
|
39
|
+
continue;
|
|
40
|
+
owned.add(id);
|
|
41
|
+
actors.push({ name: str(n.label), look: n.look, boxes: [id] });
|
|
42
|
+
}
|
|
43
|
+
const rank = new Map(all.map((n, i) => [n.id, i]));
|
|
44
|
+
actors.sort((a, b) => rank.get(a.boxes[0]) - rank.get(b.boxes[0]));
|
|
45
|
+
}
|
|
46
|
+
if (!actors.length) {
|
|
47
|
+
const seen = new Set();
|
|
48
|
+
for (const step of fig.steps ?? [])
|
|
49
|
+
for (const { hops } of hopsOf(fig, step))
|
|
50
|
+
for (const h of hops)
|
|
51
|
+
for (const id of [h.source, h.target])
|
|
52
|
+
if (byId.has(id) && !isStore(byId.get(id)) && !isDecision(byId.get(id)))
|
|
53
|
+
seen.add(id);
|
|
54
|
+
actors = [...seen].slice(0, stores.length ? CAST_MAX - 1 : CAST_MAX).map((id) => {
|
|
55
|
+
const n = byId.get(id);
|
|
56
|
+
return { name: str(n.label), look: n.look, boxes: [id] };
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
if (stores.length)
|
|
60
|
+
actors.push({ name: 'Records', look: stores.find((s) => s.look)?.look, boxes: stores.map((s) => s.id) });
|
|
61
|
+
return actors;
|
|
62
|
+
}
|
|
63
|
+
export function moments(fig, step) {
|
|
64
|
+
const s = fig.steps?.find((x) => str(x.label) === step);
|
|
65
|
+
if (!s)
|
|
66
|
+
return [];
|
|
67
|
+
const actors = cast(fig);
|
|
68
|
+
const byId = new Map(nodes(fig.layout).map((n) => [n.id, n]));
|
|
69
|
+
const actorOf = new Map(actors.flatMap((a) => a.boxes.map((id) => [id, a.name])));
|
|
70
|
+
const groupLabels = new Map();
|
|
71
|
+
const walk = (g) => {
|
|
72
|
+
for (const c of g.children) {
|
|
73
|
+
if (!isGroup(c))
|
|
74
|
+
continue;
|
|
75
|
+
if (c.id)
|
|
76
|
+
groupLabels.set(c.id, c.label);
|
|
77
|
+
walk(c);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
walk(fig.layout);
|
|
81
|
+
const labelOf = (id) => str(byId.get(id)?.label ?? groupLabels.get(id));
|
|
82
|
+
const beats = hopsOf(fig, s);
|
|
83
|
+
if (!beats.length)
|
|
84
|
+
return [];
|
|
85
|
+
const last = beats.flatMap((b) => b.hops).at(-1);
|
|
86
|
+
const outcome = last ? labelOf(last.target) : '';
|
|
87
|
+
let prev = actors[0]?.name ?? '';
|
|
88
|
+
const out = beats.map(({ beat, hops }, i) => {
|
|
89
|
+
const end = hops.at(-1);
|
|
90
|
+
const own = (id) => actorOf.get(id) ?? labelOf(id);
|
|
91
|
+
const actor = !end ? prev : actorOf.has(end.target) || !isDecision(byId.get(end.target)) ? own(end.target) : i ? prev : own(end.source);
|
|
92
|
+
prev = actor;
|
|
93
|
+
const labels = hops.map((h) => str(h.edge.label)).filter(Boolean);
|
|
94
|
+
const say = str(beat.say) || (end ? (labels.length ? `${labels.join(', ')}: ${labelOf(end.target)}` : labelOf(end.target)) : '');
|
|
95
|
+
const moment = { actor, say, note: str(beat.note), records: [] };
|
|
96
|
+
const asks = hops.filter((h) => isCheck(byId.get(h.source)));
|
|
97
|
+
const ask = asks.find((h) => failed(byId, h)) ?? asks.at(-1);
|
|
98
|
+
if (ask) {
|
|
99
|
+
moment.stamp = { mark: failed(byId, ask) ? '✗' : '✓', label: str(ask.edge.label) };
|
|
100
|
+
moment.question = question(byId.get(ask.source));
|
|
101
|
+
}
|
|
102
|
+
for (const h of hops) {
|
|
103
|
+
const store = byId.get(h.target);
|
|
104
|
+
if (store && isStore(store))
|
|
105
|
+
moment.records.push(str(h.data) || str(h.edge.label) || str(store.label));
|
|
106
|
+
}
|
|
107
|
+
return moment;
|
|
108
|
+
});
|
|
109
|
+
const mine = new Set(beats.flatMap((b) => b.hops.flatMap((h) => [h.source, h.target])));
|
|
110
|
+
const refusal = beats.flatMap((b) => b.hops).find((h) => failed(byId, h));
|
|
111
|
+
const order = [];
|
|
112
|
+
if (refusal) {
|
|
113
|
+
const live = fig.edges.filter((e) => e !== refusal.edge && byId.has(e.to) && !dead(byId.get(e.to).tone));
|
|
114
|
+
const reach = new Set([refusal.source]);
|
|
115
|
+
for (let grew = true; grew;) {
|
|
116
|
+
grew = false;
|
|
117
|
+
for (const e of live)
|
|
118
|
+
if (reach.has(e.from) && !reach.has(e.to)) {
|
|
119
|
+
reach.add(e.to);
|
|
120
|
+
grew = true;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const inner = live.filter((e) => reach.has(e.from) && reach.has(e.to) && e.to !== refusal.source);
|
|
124
|
+
const need = new Map();
|
|
125
|
+
for (const e of inner)
|
|
126
|
+
need.set(e.to, (need.get(e.to) ?? 0) + 1);
|
|
127
|
+
for (const queue = [refusal.source]; queue.length;) {
|
|
128
|
+
const id = queue.shift();
|
|
129
|
+
order.push(id);
|
|
130
|
+
for (const e of inner) {
|
|
131
|
+
if (e.from !== id)
|
|
132
|
+
continue;
|
|
133
|
+
need.set(e.to, need.get(e.to) - 1);
|
|
134
|
+
if (!need.get(e.to))
|
|
135
|
+
queue.push(e.to);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
for (const id of reach)
|
|
139
|
+
if (!order.includes(id))
|
|
140
|
+
order.push(id);
|
|
141
|
+
}
|
|
142
|
+
const notReached = order
|
|
143
|
+
.slice(1)
|
|
144
|
+
.filter((id) => !mine.has(id))
|
|
145
|
+
.map(labelOf);
|
|
146
|
+
out.forEach((m, i) => {
|
|
147
|
+
const next = out.slice(i + 1).find((o) => o.actor !== m.actor);
|
|
148
|
+
if (!m.note && next)
|
|
149
|
+
m.note = `${next.actor} acts next.`;
|
|
150
|
+
});
|
|
151
|
+
out.push({
|
|
152
|
+
actor: out.at(-1).actor,
|
|
153
|
+
say: s.outcome || (/[.?!]$/.test(outcome) ? outcome : `${outcome}.`),
|
|
154
|
+
note: '',
|
|
155
|
+
records: [],
|
|
156
|
+
summary: { outcome, notReached },
|
|
157
|
+
});
|
|
158
|
+
return out;
|
|
159
|
+
}
|
|
160
|
+
const PLACES = ['person', 'shop', 'truck'];
|
|
161
|
+
export const worldReady = (fig) => fig.edges.some((e) => !!e.carries && typeof e.carries === 'object') || cast(fig).some((a) => PLACES.includes(a.look));
|
package/dist/frames.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type FlowProps } from './model.ts';
|
|
2
|
+
export type Tile = {
|
|
3
|
+
frame: number;
|
|
4
|
+
kinds: string[];
|
|
5
|
+
step: number;
|
|
6
|
+
beat: number;
|
|
7
|
+
text: string;
|
|
8
|
+
say: string;
|
|
9
|
+
};
|
|
10
|
+
type Grid = {
|
|
11
|
+
cols: number;
|
|
12
|
+
rows: number;
|
|
13
|
+
tile: number;
|
|
14
|
+
tileH: number;
|
|
15
|
+
width: number;
|
|
16
|
+
height: number;
|
|
17
|
+
warning?: string;
|
|
18
|
+
};
|
|
19
|
+
export declare function keyFrames(fig: FlowProps, o: {
|
|
20
|
+
fps: number;
|
|
21
|
+
every?: number;
|
|
22
|
+
step?: number;
|
|
23
|
+
}): {
|
|
24
|
+
tiles: Tile[];
|
|
25
|
+
frames: number;
|
|
26
|
+
};
|
|
27
|
+
export declare function sheetGrid(count: number, width: number, height: number, scale?: number): Grid;
|
|
28
|
+
export declare function sheetSvg(tiles: Tile[], pngs: Buffer[], grid: Grid, o: {
|
|
29
|
+
header: string;
|
|
30
|
+
fps: number;
|
|
31
|
+
dark: boolean;
|
|
32
|
+
scale: number;
|
|
33
|
+
}): string;
|
|
34
|
+
export {};
|
package/dist/frames.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { beatTimes, DARK, edgeId, LIGHT, n2, nodes, scenes, str, toBeat, tourPlan } from './model.js';
|
|
2
|
+
import { esc } from './page.js';
|
|
3
|
+
import { textWidth } from './text.js';
|
|
4
|
+
export function keyFrames(fig, o) {
|
|
5
|
+
const steps = fig.steps ?? [];
|
|
6
|
+
const beats = steps.map((s) => s.flow.map(toBeat));
|
|
7
|
+
const tour = scenes(fig).length > 1 ? tourPlan(fig) : undefined;
|
|
8
|
+
const model = tour?.times ?? beatTimes(beats, fig.speed ?? 900);
|
|
9
|
+
const loop = tour ? tour.ms / 1000 : (model.ends.at(-1) ?? 0);
|
|
10
|
+
const dur = n2(loop || 1);
|
|
11
|
+
const scale = loop ? dur / loop : 1;
|
|
12
|
+
const times = {
|
|
13
|
+
beats: model.beats.map((step) => step.map((b) => ({ t0: b.t0 * scale, t1: b.t1 * scale, tEnd: b.tEnd * scale }))),
|
|
14
|
+
ends: model.ends.map((e) => e * scale),
|
|
15
|
+
};
|
|
16
|
+
const frames = Math.max(1, Math.round(dur * o.fps));
|
|
17
|
+
const first = o.step ?? 1;
|
|
18
|
+
const name = new Map(nodes(fig.layout).map((n) => [n.id, str(n.label) || n.id]));
|
|
19
|
+
const edges = new Map(fig.edges.map((e) => [edgeId(e), e]));
|
|
20
|
+
const hopText = (si, bi) => beats[si][bi].hops
|
|
21
|
+
.map((h) => {
|
|
22
|
+
const e = edges.get(h.edge);
|
|
23
|
+
if (!e)
|
|
24
|
+
return h.edge;
|
|
25
|
+
const [a, b] = h.back ? [e.to, e.from] : [e.from, e.to];
|
|
26
|
+
const label = str(e.label);
|
|
27
|
+
return `${name.get(a) ?? a} → ${name.get(b) ?? b}${label ? ` (${label})` : ''}`;
|
|
28
|
+
})
|
|
29
|
+
.join('; ') || 'no hop';
|
|
30
|
+
const said = (si, bi) => {
|
|
31
|
+
const at = beats[si].slice(0, bi + 1).findLastIndex((b) => b.say != null);
|
|
32
|
+
return str(at === -1 ? steps[si].caption : beats[si][at].say);
|
|
33
|
+
};
|
|
34
|
+
const tiles = [];
|
|
35
|
+
const add = (at, kind, si, bi, text, say) => {
|
|
36
|
+
const last = tiles.at(-1);
|
|
37
|
+
const frame = Math.min(frames - 1, Math.max(at, last?.frame ?? 0));
|
|
38
|
+
if (last && frame === last.frame) {
|
|
39
|
+
if (!last.kinds.includes(kind))
|
|
40
|
+
last.kinds.push(kind);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
tiles.push({ frame, kinds: [kind], step: si + first, beat: bi + 1, text, say });
|
|
44
|
+
};
|
|
45
|
+
const f = o.fps;
|
|
46
|
+
if (o.every) {
|
|
47
|
+
const flat = beats.flatMap((step, si) => step.map((b, bi) => ({ si, bi, hop: b.hops.length > 0, ...times.beats[si][bi] })));
|
|
48
|
+
for (let k = 0; k * o.every < dur * 1000; k++) {
|
|
49
|
+
if (Math.round((k * o.every * f) / 1000) >= frames)
|
|
50
|
+
break;
|
|
51
|
+
const t = (k * o.every) / 1000;
|
|
52
|
+
const at = flat.findLast((x) => x.t0 <= t) ?? flat[0];
|
|
53
|
+
const kind = at.hop && t < Math.min(at.t1, at.tEnd) ? 'move' : t < at.tEnd ? 'read' : 'hold';
|
|
54
|
+
add(Math.round((k * o.every * f) / 1000), kind, at.si, at.bi, hopText(at.si, at.bi), said(at.si, at.bi));
|
|
55
|
+
}
|
|
56
|
+
return { tiles, frames };
|
|
57
|
+
}
|
|
58
|
+
beats.forEach((step, si) => {
|
|
59
|
+
step.forEach((b, bi) => {
|
|
60
|
+
const { t0, t1, tEnd } = times.beats[si][bi];
|
|
61
|
+
const text = hopText(si, bi), say = said(si, bi);
|
|
62
|
+
if (b.hops.length) {
|
|
63
|
+
add(Math.round(((t0 + Math.min(t1, tEnd)) / 2) * f), 'move', si, bi, text, say);
|
|
64
|
+
if (t1 <= tEnd)
|
|
65
|
+
add(Math.ceil(t1 * f + 0.5), 'arrive', si, bi, text, say);
|
|
66
|
+
}
|
|
67
|
+
add(Math.floor(tEnd * f - 0.5), 'read', si, bi, text, say);
|
|
68
|
+
});
|
|
69
|
+
add(Math.floor(times.ends[si] * f - 0.5), 'end', si, -1, `end of step ${si + first}: ${str(steps[si].label)}`, '');
|
|
70
|
+
});
|
|
71
|
+
return { tiles, frames };
|
|
72
|
+
}
|
|
73
|
+
export function sheetGrid(count, width, height, scale = 1) {
|
|
74
|
+
const [limit, gap, head, band] = [1568, 8, 24, 34].map((n) => Math.round(n * scale));
|
|
75
|
+
const size = (cols, tile) => {
|
|
76
|
+
const rows = Math.ceil(count / cols), tileH = Math.round((tile * height) / width);
|
|
77
|
+
return { cols, rows, tile, tileH, width: cols * tile + (cols + 1) * gap, height: head + rows * (tileH + band) + (rows + 1) * gap };
|
|
78
|
+
};
|
|
79
|
+
let best = size(1, 0);
|
|
80
|
+
for (let c = 1; c <= count; c++) {
|
|
81
|
+
const rows = Math.ceil(count / c);
|
|
82
|
+
let tile = Math.floor(Math.min((limit - (c + 1) * gap) / c, (((limit - head - (rows + 1) * gap) / rows - band) * width) / height));
|
|
83
|
+
while (tile > 1 && Math.max(size(c, tile).width, size(c, tile).height) > limit)
|
|
84
|
+
tile--;
|
|
85
|
+
if (tile > best.tile)
|
|
86
|
+
best = size(c, tile);
|
|
87
|
+
}
|
|
88
|
+
const small = 160 * scale;
|
|
89
|
+
return best.tile < small
|
|
90
|
+
? { ...best, warning: `warning: the tiles are ${best.tile} px wide, under ${small} px. Use --step <n> to show fewer tiles.` }
|
|
91
|
+
: best;
|
|
92
|
+
}
|
|
93
|
+
export function sheetSvg(tiles, pngs, grid, o) {
|
|
94
|
+
const c = o.dark ? DARK : LIGHT;
|
|
95
|
+
const [gap, head, band, font, pad] = [8, 24, 34, 12, 4].map((n) => Math.round(n * o.scale));
|
|
96
|
+
const fit = (line) => {
|
|
97
|
+
let cut = line;
|
|
98
|
+
while (cut && textWidth(cut === line ? cut : cut + '…', font, true) > grid.tile - 2 * pad)
|
|
99
|
+
cut = cut.slice(0, -1);
|
|
100
|
+
return cut === line ? line : cut + '…';
|
|
101
|
+
};
|
|
102
|
+
const text = (x, y, s) => `<text x="${x}" y="${y}">${esc(s)}</text>`;
|
|
103
|
+
const cells = tiles.map((t, i) => {
|
|
104
|
+
const x = gap + (i % grid.cols) * (grid.tile + gap), y = head + gap + Math.floor(i / grid.cols) * (grid.tileH + band + gap);
|
|
105
|
+
const line1 = `#${t.frame} · ${Math.round((t.frame * 1000) / o.fps)} ms · step ${t.step}${t.beat ? ` beat ${t.beat}` : ''} · ${t.kinds.join('+')}`;
|
|
106
|
+
return (`<image x="${x}" y="${y}" width="${grid.tile}" height="${grid.tileH}" href="data:image/png;base64,${pngs[i].toString('base64')}"/>` +
|
|
107
|
+
`<rect x="${x - 0.5}" y="${y - 0.5}" width="${grid.tile + 1}" height="${grid.tileH + 1}" fill="none" stroke="${c.border}"/>` +
|
|
108
|
+
text(x + pad, y + grid.tileH + Math.round(14 * o.scale), fit(line1)) +
|
|
109
|
+
text(x + pad, y + grid.tileH + Math.round(29 * o.scale), fit(t.text)));
|
|
110
|
+
});
|
|
111
|
+
return (`<svg xmlns="http://www.w3.org/2000/svg" width="${grid.width}" height="${grid.height}" viewBox="0 0 ${grid.width} ${grid.height}" ` +
|
|
112
|
+
`font-family="ui-monospace, Menlo, Consolas, monospace" font-size="${font}" fill="${c.fg}">` +
|
|
113
|
+
`<rect width="${grid.width}" height="${grid.height}" fill="${c.surface}"/>` +
|
|
114
|
+
text(gap, head - Math.round(7 * o.scale), o.header) +
|
|
115
|
+
cells.join('') +
|
|
116
|
+
`</svg>`);
|
|
117
|
+
}
|