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/cli.js
CHANGED
|
@@ -13,6 +13,7 @@ import { tmpdir } from 'node:os';
|
|
|
13
13
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
14
14
|
import { pathToFileURL } from 'node:url';
|
|
15
15
|
import { captureFrames, findBrowser, launch } from './browser.js';
|
|
16
|
+
import { keyFrames, sheetGrid, sheetSvg } from './frames.js';
|
|
16
17
|
import { delays, encodeGif } from './gif.js';
|
|
17
18
|
import { GUIDE, TOPICS } from './guide.js';
|
|
18
19
|
import { runDraw } from './draw.js';
|
|
@@ -20,14 +21,14 @@ import { runInit } from './init.js';
|
|
|
20
21
|
import { serve } from './mcp.js';
|
|
21
22
|
import { runAtlas } from './atlas.js';
|
|
22
23
|
import { runFromMermaid } from './mermaid.js';
|
|
23
|
-
import { openedLine, openSvg, runOpen } from './open.js';
|
|
24
|
+
import { openedLine, openFile, openSvg, runOpen } from './open.js';
|
|
24
25
|
import { pageHtml } from './page.js';
|
|
25
26
|
import { decodePng } from './png.js';
|
|
26
27
|
import { diff, formatDiff, mergeFigures } from './diff.js';
|
|
27
28
|
import { loadSpec, reportLines, sortFindings, specOf, summaryLines, svgWithSpec, verifyLines } from './load.js';
|
|
28
29
|
import { check, render, toSvg } from './svg.js';
|
|
29
30
|
import { checkRendered } from './check.js';
|
|
30
|
-
import { coverageLine, links, parseSource, unsureLines, verifyReport } from './verify.js';
|
|
31
|
+
import { coverageLine, levelLines, links, parseSource, unsureLines, verifyReport } from './verify.js';
|
|
31
32
|
import { VERSION } from './version.js';
|
|
32
33
|
import { trace, traceLines } from './trace.js';
|
|
33
34
|
import { coverageLines, coverageReport } from './coverage.js';
|
|
@@ -46,12 +47,72 @@ const USAGE = `usage: flowfig <-|spec.json|figure.ts> [out.svg] [--open] rende
|
|
|
46
47
|
flowfig open <figure.svg> [--html <path>] show the figure in the default browser
|
|
47
48
|
flowfig from-mermaid <file.mmd|file.md|-> [--out <dir>] convert Mermaid diagrams to specs
|
|
48
49
|
flowfig gif <figure.svg> [out.gif] [--step <n>] [--dark] [--fps <n>] [--scale <n>] [--mp4] write an animated GIF
|
|
49
|
-
|
|
50
|
+
flowfig frames <figure.svg> [out.png] [--step <n>] [--dark] [--fps <n>] [--every <ms>] [--scale <n>] [--open] write one PNG sheet of the key frames
|
|
51
|
+
flags for render and check: --strict (warnings are errors), --json, --width <px>, --min-text <px>, --no-check (render only), --no-verify (render only), --scene <id> (render one level), --script (add the script that opens a box on a click)
|
|
50
52
|
flowfig --version, -v print the version`;
|
|
51
53
|
function usage(message) {
|
|
52
54
|
console.error(message);
|
|
53
55
|
process.exit(2);
|
|
54
56
|
}
|
|
57
|
+
async function withBrowser(name, work) {
|
|
58
|
+
const found = findBrowser({ platform: process.platform, env: process.env, exists: existsSync });
|
|
59
|
+
if (!found.path)
|
|
60
|
+
usage([`${name} needs Chrome, Edge, Chromium or Brave. Checked:`, ...found.checked, 'Set CHROME_PATH to the browser program.'].join('\n'));
|
|
61
|
+
const temp = mkdtempSync(join(tmpdir(), `flowfig-${name}-`));
|
|
62
|
+
let launching;
|
|
63
|
+
let cleaning;
|
|
64
|
+
const cleanup = () => (cleaning ??= (async () => {
|
|
65
|
+
await (await launching?.catch(() => undefined))?.close();
|
|
66
|
+
try {
|
|
67
|
+
// Windows can hold a file lock briefly after the exit, so retry.
|
|
68
|
+
rmSync(temp, { recursive: true, force: true, maxRetries: 10 });
|
|
69
|
+
}
|
|
70
|
+
catch (e) {
|
|
71
|
+
console.error(`warning: could not remove ${temp}: ${e.message}`);
|
|
72
|
+
}
|
|
73
|
+
})());
|
|
74
|
+
let stopped = 0;
|
|
75
|
+
const yieldLoop = () => new Promise((done) => setImmediate(done));
|
|
76
|
+
// A write comes right after this check, so a signal must run its handler first.
|
|
77
|
+
const run = {
|
|
78
|
+
written: [],
|
|
79
|
+
proceed: async () => {
|
|
80
|
+
await yieldLoop();
|
|
81
|
+
if (stopped)
|
|
82
|
+
throw new Error('stopped');
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
// Keep the handlers: a second signal must not end the process mid-cleanup.
|
|
86
|
+
const onSignal = (signal) => {
|
|
87
|
+
if (stopped)
|
|
88
|
+
return;
|
|
89
|
+
stopped = signal === 'SIGINT' ? 130 : 143;
|
|
90
|
+
void (run.onStop?.() ?? Promise.resolve())
|
|
91
|
+
.then(() => run.written.forEach((f) => rmSync(f, { force: true })))
|
|
92
|
+
.then(cleanup)
|
|
93
|
+
.finally(() => process.exit(stopped));
|
|
94
|
+
};
|
|
95
|
+
process.on('SIGINT', onSignal);
|
|
96
|
+
process.on('SIGTERM', onSignal);
|
|
97
|
+
let code = 0;
|
|
98
|
+
try {
|
|
99
|
+
launching = launch(found.path, join(temp, 'profile'));
|
|
100
|
+
await work((await launching).cdp, run);
|
|
101
|
+
}
|
|
102
|
+
catch (e) {
|
|
103
|
+
if (!stopped)
|
|
104
|
+
console.error(`${name}: ${e.message}`);
|
|
105
|
+
code = 1;
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
await cleanup();
|
|
109
|
+
}
|
|
110
|
+
// A late signal runs its handler in this await, before the exit.
|
|
111
|
+
await yieldLoop();
|
|
112
|
+
if (stopped)
|
|
113
|
+
run.written.forEach((f) => rmSync(f, { force: true }));
|
|
114
|
+
process.exit(stopped || code);
|
|
115
|
+
}
|
|
55
116
|
const args = process.argv.slice(2);
|
|
56
117
|
const flag = (name) => {
|
|
57
118
|
const i = args.indexOf(name);
|
|
@@ -160,10 +221,12 @@ if (args[0] === 'verify') {
|
|
|
160
221
|
else {
|
|
161
222
|
const n = (k, word) => `${k} ${word}${k === 1 ? '' : 's'}`;
|
|
162
223
|
for (const f of findings)
|
|
163
|
-
console.log(`${f.severity.padEnd(8)} ${f.rule.padEnd(18)} ${f.figure}: ${f.message}`);
|
|
224
|
+
console.log(`${f.severity.padEnd(8)} ${f.rule.padEnd(18)} ${f.figure}: ${f.scene ? `[${f.scene}] ` : ''}${f.message}`);
|
|
164
225
|
console.log(`${n(errors, 'error')}, ${n(findings.length - errors, 'warning')}`);
|
|
165
226
|
for (const c of coverage) {
|
|
166
227
|
console.log(coverageLine(c.figure, c));
|
|
228
|
+
for (const l of levelLines(c))
|
|
229
|
+
console.log(l);
|
|
167
230
|
for (const l of unsureLines(c))
|
|
168
231
|
console.log(l);
|
|
169
232
|
}
|
|
@@ -272,56 +335,20 @@ if (args[0] === 'gif') {
|
|
|
272
335
|
usage(`${path}: no width and height on the <svg> element`);
|
|
273
336
|
if (!existsSync(dirname(resolve(out))))
|
|
274
337
|
usage(`${out}: the folder does not exist`);
|
|
275
|
-
const found = findBrowser({ platform: process.platform, env: process.env, exists: existsSync });
|
|
276
|
-
if (!found.path)
|
|
277
|
-
usage(['gif needs Chrome, Edge, Chromium or Brave. Checked:', ...found.checked, 'Set CHROME_PATH to the browser program.'].join('\n'));
|
|
278
338
|
const line = (file, frames, bytes) => `${file} — ${frames} frame${frames === 1 ? '' : 's'}, ${(frames / fps).toFixed(1)} s, ${(bytes / 1048576).toFixed(1)} MB`;
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
// Windows can hold a file lock briefly after the exit, so retry.
|
|
286
|
-
rmSync(temp, { recursive: true, force: true, maxRetries: 10 });
|
|
287
|
-
}
|
|
288
|
-
catch (e) {
|
|
289
|
-
console.error(`warning: could not remove ${temp}: ${e.message}`);
|
|
290
|
-
}
|
|
291
|
-
})());
|
|
292
|
-
const written = [];
|
|
293
|
-
let ffmpeg, ffmpegDone = Promise.resolve(), stopped = 0;
|
|
294
|
-
const yieldLoop = () => new Promise((done) => setImmediate(done));
|
|
295
|
-
// Keep the handlers: a second signal must not end the process mid-cleanup.
|
|
296
|
-
const onSignal = (signal) => {
|
|
297
|
-
if (stopped)
|
|
298
|
-
return;
|
|
299
|
-
stopped = signal === 'SIGINT' ? 130 : 143;
|
|
300
|
-
ffmpeg?.kill('SIGKILL');
|
|
301
|
-
void ffmpegDone
|
|
302
|
-
.then(() => written.forEach((f) => rmSync(f, { force: true })))
|
|
303
|
-
.then(cleanup)
|
|
304
|
-
.finally(() => process.exit(stopped));
|
|
305
|
-
};
|
|
306
|
-
process.on('SIGINT', onSignal);
|
|
307
|
-
process.on('SIGTERM', onSignal);
|
|
308
|
-
// A write comes right after this check, so a signal must run its handler first.
|
|
309
|
-
const proceed = async () => {
|
|
310
|
-
await yieldLoop();
|
|
311
|
-
if (stopped)
|
|
312
|
-
throw new Error('stopped');
|
|
313
|
-
};
|
|
314
|
-
let code = 0;
|
|
315
|
-
try {
|
|
316
|
-
launching = launch(found.path, join(temp, 'profile'));
|
|
317
|
-
const browser = await launching;
|
|
339
|
+
await withBrowser('gif', async (cdp, run) => {
|
|
340
|
+
let ffmpeg, ffmpegDone = Promise.resolve();
|
|
341
|
+
run.onStop = () => {
|
|
342
|
+
ffmpeg?.kill('SIGKILL');
|
|
343
|
+
return ffmpegDone;
|
|
344
|
+
};
|
|
318
345
|
const viewport = { width: Math.ceil(Number(size[1])), height: Math.ceil(Number(size[2])) };
|
|
319
|
-
const { pngs } = await captureFrames(
|
|
346
|
+
const { pngs } = await captureFrames(cdp, pageHtml(svg, basename(path)), { ...viewport, scale, fps, dark });
|
|
320
347
|
const wait = delays(pngs.length, fps);
|
|
321
348
|
// The GIF keeps the PNG bytes: a decoded 2400 x 1600 frame is 15 MB.
|
|
322
349
|
const gif = await encodeGif(pngs.map((png, i) => ({ load: () => decodePng(png), delay: wait[i], same: i > 0 && png.equals(pngs[i - 1]) })));
|
|
323
|
-
await proceed();
|
|
324
|
-
written.push(out);
|
|
350
|
+
await run.proceed();
|
|
351
|
+
run.written.push(out);
|
|
325
352
|
writeFileSync(out, gif);
|
|
326
353
|
console.log(line(out, pngs.length, gif.length));
|
|
327
354
|
if (gif.length > 10485760)
|
|
@@ -332,8 +359,8 @@ if (args[0] === 'gif') {
|
|
|
332
359
|
if (spawnSync(ffmpegBin, ['-version']).error)
|
|
333
360
|
console.error('gif: no ffmpeg on the PATH, so no MP4. The GIF is written.');
|
|
334
361
|
else {
|
|
335
|
-
await proceed();
|
|
336
|
-
written.push(mp4Path);
|
|
362
|
+
await run.proceed();
|
|
363
|
+
run.written.push(mp4Path);
|
|
337
364
|
// yuv420p needs an even width and height, so the pad filter adds a pixel.
|
|
338
365
|
ffmpeg = spawn(ffmpegBin, [
|
|
339
366
|
'-y',
|
|
@@ -364,7 +391,7 @@ if (args[0] === 'gif') {
|
|
|
364
391
|
ffmpeg.once('error', () => done(-1));
|
|
365
392
|
ffmpeg.once('close', done);
|
|
366
393
|
}));
|
|
367
|
-
await proceed();
|
|
394
|
+
await run.proceed();
|
|
368
395
|
if (status !== 0) {
|
|
369
396
|
rmSync(mp4Path, { force: true });
|
|
370
397
|
throw new Error(`ffmpeg failed: ${stderr.trim()}`);
|
|
@@ -372,26 +399,93 @@ if (args[0] === 'gif') {
|
|
|
372
399
|
console.log(line(mp4Path, pngs.length, statSync(mp4Path).size));
|
|
373
400
|
}
|
|
374
401
|
}
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
if (args[0] === 'frames') {
|
|
405
|
+
args.shift();
|
|
406
|
+
const dark = flag('--dark'), opening = flag('--open');
|
|
407
|
+
const fps = value('--fps') ?? 20, scale = value('--scale') ?? 1, step = value('--step'), every = value('--every');
|
|
408
|
+
if (!Number.isInteger(fps) || fps > 50)
|
|
409
|
+
usage('--fps needs a whole number from 1 to 50');
|
|
410
|
+
const unknown = args.find((a) => a.startsWith('-'));
|
|
411
|
+
if (unknown)
|
|
412
|
+
usage(`unknown flag ${unknown}`);
|
|
413
|
+
const [path, outArg] = args;
|
|
414
|
+
if (!path)
|
|
415
|
+
usage('usage: flowfig frames <figure.svg> [out.png] [--step <n>] [--dark] [--fps <n>] [--every <ms>] [--scale <n>] [--open]');
|
|
416
|
+
if (!path.endsWith('.svg'))
|
|
417
|
+
usage(`${path}: expected a .svg path`);
|
|
418
|
+
const props = (() => {
|
|
419
|
+
try {
|
|
420
|
+
return loadSpec(path);
|
|
421
|
+
}
|
|
422
|
+
catch (e) {
|
|
423
|
+
return usage(e.message);
|
|
424
|
+
}
|
|
425
|
+
})();
|
|
426
|
+
const count = props.steps?.length ?? 0;
|
|
427
|
+
if (!count)
|
|
428
|
+
usage(`${path}: frames needs a figure with steps; this figure has 0`);
|
|
429
|
+
if (step !== undefined && (!Number.isInteger(step) || step > count))
|
|
430
|
+
usage(`--step needs a whole number from 1 to ${count}`);
|
|
431
|
+
const fig = step === undefined ? props : { ...props, steps: [props.steps[step - 1]] };
|
|
432
|
+
const svg = step === undefined ? readFileSync(path, 'utf8') : toSvg(fig);
|
|
433
|
+
const { tiles, frames } = keyFrames(fig, { fps, every, step });
|
|
434
|
+
if (tiles.length > 64)
|
|
435
|
+
usage(`${path}: ${tiles.length} tiles; the sheet holds 64 or fewer. Use --step <n> or a larger --every <ms>.`);
|
|
436
|
+
const out = outArg ?? path.replace(/\.svg$/, step === undefined ? '-frames.png' : `-step${step}-frames.png`);
|
|
437
|
+
const size = /<svg\b[^>]*?\swidth="([\d.]+)"[^>]*?\sheight="([\d.]+)"/.exec(svg);
|
|
438
|
+
if (!size)
|
|
439
|
+
usage(`${path}: no width and height on the <svg> element`);
|
|
440
|
+
if (!existsSync(dirname(resolve(out))))
|
|
441
|
+
usage(`${out}: the folder does not exist`);
|
|
442
|
+
const [w, h] = [Number(size[1]), Number(size[2])];
|
|
443
|
+
const grid = sheetGrid(tiles.length, w, h, scale);
|
|
444
|
+
const loopMs = Math.round((frames * 1000) / fps);
|
|
445
|
+
await withBrowser('frames', async (cdp, run) => {
|
|
446
|
+
// A fractional device scale draws paused opacity animations two ways on alternate frames.
|
|
447
|
+
const shot = await captureFrames(cdp, pageHtml(svg, basename(path), '', '', '<style>svg { width: 100% }</style>'), {
|
|
448
|
+
width: grid.tile,
|
|
449
|
+
height: grid.tileH,
|
|
450
|
+
scale: 1,
|
|
451
|
+
fps,
|
|
452
|
+
dark,
|
|
453
|
+
frames: tiles.map((t) => t.frame),
|
|
454
|
+
});
|
|
455
|
+
if (Math.abs(Math.round((shot.loopMs * fps) / 1000) - frames) > 1)
|
|
456
|
+
throw new Error(`${path}: the SVG loop is ${Math.round(shot.loopMs)} ms but its spec gives ${loopMs} ms. Render the figure again.`);
|
|
457
|
+
const header = `${basename(path)} · ${tiles.length} tiles of ${frames} frames at ${fps} fps · loop ${loopMs} ms`;
|
|
458
|
+
const sheet = await captureFrames(cdp, pageHtml(sheetSvg(tiles, shot.pngs, grid, { header, fps, dark, scale }), basename(out)), {
|
|
459
|
+
width: grid.width,
|
|
460
|
+
height: grid.height,
|
|
461
|
+
scale: 1,
|
|
462
|
+
fps,
|
|
463
|
+
dark,
|
|
464
|
+
});
|
|
465
|
+
const png = sheet.pngs[0];
|
|
466
|
+
await run.proceed();
|
|
467
|
+
run.written.push(out);
|
|
468
|
+
writeFileSync(out, png);
|
|
469
|
+
const rows = tiles.map((t) => [
|
|
470
|
+
`#${t.frame}`,
|
|
471
|
+
`${Math.round((t.frame * 1000) / fps)} ms`,
|
|
472
|
+
`step ${t.step}${t.beat ? ` beat ${t.beat}` : ''}`,
|
|
473
|
+
t.kinds.join('+'),
|
|
474
|
+
]);
|
|
475
|
+
const widths = [0, 1, 2, 3].map((c) => Math.max(...rows.map((r) => r[c].length)));
|
|
476
|
+
tiles.forEach((t, i) => console.log([...rows[i].map((v, c) => v.padEnd(widths[c])), t.text].join(' ') + (t.say ? ` "${t.say}"` : '')));
|
|
477
|
+
console.log(`${out} — ${tiles.length} tiles of ${frames} frames at ${fps} fps, ${grid.cols} x ${grid.rows} grid, ${grid.width} x ${grid.height} px, ${(png.length / 1048576).toFixed(1)} MB`);
|
|
478
|
+
if (grid.warning)
|
|
479
|
+
console.error(grid.warning);
|
|
480
|
+
if (opening)
|
|
481
|
+
await openFile(out);
|
|
482
|
+
});
|
|
389
483
|
}
|
|
390
484
|
const command = args[0] === 'check' ? args.shift() : 'render';
|
|
391
485
|
const json = flag('--json'), strict = flag('--strict'), skip = flag('--no-check'), skipVerify = flag('--no-verify'), open = flag('--open');
|
|
392
486
|
if (command === 'check' && open)
|
|
393
487
|
usage('--open works with a render, not with check');
|
|
394
|
-
const opts = { width: value('--width'), minText: value('--min-text') };
|
|
488
|
+
const opts = { width: value('--width'), minText: value('--min-text'), scene: option('--scene'), script: flag('--script') };
|
|
395
489
|
const unknown = args.find((a) => a.startsWith('-') && a !== '-');
|
|
396
490
|
if (unknown)
|
|
397
491
|
usage(`unknown flag ${unknown}`);
|
|
@@ -399,8 +493,18 @@ const [input, out] = args;
|
|
|
399
493
|
if (!input)
|
|
400
494
|
usage(USAGE);
|
|
401
495
|
const props = await load(input);
|
|
402
|
-
const
|
|
403
|
-
|
|
496
|
+
const drawn = () => {
|
|
497
|
+
try {
|
|
498
|
+
return render(props, opts);
|
|
499
|
+
}
|
|
500
|
+
catch (e) {
|
|
501
|
+
if (!e.message.endsWith('has no level to open'))
|
|
502
|
+
throw e;
|
|
503
|
+
return usage(e.message);
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
const rendered = skip ? undefined : drawn();
|
|
507
|
+
const findings = rendered ? sortFindings(checkRendered(props, opts, rendered.scene, rendered.levels), strict) : [];
|
|
404
508
|
const errors = findings.filter((f) => f.severity === 'error').length;
|
|
405
509
|
const report = (print, tty, extra = []) => {
|
|
406
510
|
const paint = (s) => tty && !process.env.NO_COLOR ? s.replace(/^(error {3}|warning )/, (w) => `\x1b[${w[0] === 'e' ? 31 : 33}m${w}\x1b[0m`) : s;
|
|
@@ -418,7 +522,7 @@ const dest = out ?? (input === '-' ? 'figure.svg' : input.replace(/\.[^./\\]+$/,
|
|
|
418
522
|
report(console.error, process.stderr.isTTY, errors ? [] : [...summaryLines(props), ...(skipVerify ? [] : verifyLines(props, dest))]);
|
|
419
523
|
if (errors)
|
|
420
524
|
process.exit(1);
|
|
421
|
-
const withSpec = svgWithSpec(props, opts, rendered
|
|
525
|
+
const withSpec = svgWithSpec(props, opts, (rendered ?? drawn()).svg);
|
|
422
526
|
writeFileSync(dest, withSpec);
|
|
423
527
|
console.log(`${dest} — ${(withSpec.length / 1024).toFixed(1)} kB`);
|
|
424
528
|
if (open) {
|
package/dist/diff.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ export type Change = {
|
|
|
4
4
|
op: 'added' | 'removed' | 'changed';
|
|
5
5
|
id: string;
|
|
6
6
|
detail?: string;
|
|
7
|
+
/** The path line of the level, for a change inside a box. */
|
|
8
|
+
path?: string;
|
|
7
9
|
};
|
|
8
10
|
export declare function diff(before: FlowProps, after: FlowProps): Change[];
|
|
9
11
|
export declare function formatDiff(changes: Change[], as: 'text' | 'md'): string;
|
package/dist/diff.js
CHANGED
|
@@ -1,49 +1,94 @@
|
|
|
1
|
-
import { edgeId, isGroup, laneColumns, nodes, str, toBeat } from './model.js';
|
|
1
|
+
import { edgeId, isGroup, laneColumns, nodes, scenes, str, toBeat, } from './model.js';
|
|
2
2
|
const show = (v) => (v == null ? '(none)' : `"${str(v)}"`);
|
|
3
3
|
const fields = (a, b, names) => names.filter((k) => str(a[k]) !== str(b[k])).map((k) => `${String(k)} ${show(a[k])} -> ${show(b[k])}`);
|
|
4
4
|
function compare(kind, before, after, fieldsOf) {
|
|
5
|
+
const at = (key, v) => ({ kind, op: 'added', id: v.id ?? key, ...(v.path && { path: v.path }) });
|
|
5
6
|
const out = [];
|
|
6
|
-
for (const [
|
|
7
|
-
const b = after.get(
|
|
7
|
+
for (const [key, a] of before) {
|
|
8
|
+
const b = after.get(key);
|
|
8
9
|
if (!b)
|
|
9
10
|
continue;
|
|
10
11
|
const d = fieldsOf(a, b);
|
|
11
12
|
if (d.length)
|
|
12
|
-
out.push({
|
|
13
|
+
out.push({ ...at(key, b), op: 'changed', detail: d.join('; ') });
|
|
13
14
|
}
|
|
14
|
-
for (const
|
|
15
|
-
if (!after.has(
|
|
16
|
-
out.push({
|
|
17
|
-
for (const
|
|
18
|
-
if (!before.has(
|
|
19
|
-
out.push(
|
|
15
|
+
for (const [key, a] of before)
|
|
16
|
+
if (!after.has(key))
|
|
17
|
+
out.push({ ...at(key, a), op: 'removed' });
|
|
18
|
+
for (const [key, b] of after)
|
|
19
|
+
if (!before.has(key))
|
|
20
|
+
out.push(at(key, b));
|
|
20
21
|
return out;
|
|
21
22
|
}
|
|
22
23
|
const messages = (fig) => {
|
|
23
24
|
const seen = new Map();
|
|
24
|
-
|
|
25
|
+
return new Map(scenes(fig).flatMap((sc) => (sc.props.steps ?? []).flatMap((s) => s.flow.flatMap((b) => toBeat(b).hops.map((h) => {
|
|
25
26
|
const key = `${str(s.label)}: ${h.edge}${h.back ? ' back' : ''}${h.data != null ? ` "${str(h.data)}"` : ''}${h.tone ? ` [${h.tone}]` : ''}${h.source ? ` <${h.source}>` : ''}${h.via ? ` via ${h.via}` : ''}`;
|
|
26
|
-
const
|
|
27
|
-
seen.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
const full = `${sc.path.join('>')}\n${key}`;
|
|
28
|
+
const n = (seen.get(full) ?? 0) + 1;
|
|
29
|
+
seen.set(full, n);
|
|
30
|
+
const id = n > 1 ? `${key} #${n}` : key;
|
|
31
|
+
return [`${sc.path.join('>')}\n${id}`, { id, path: sc.line }];
|
|
32
|
+
})))));
|
|
31
33
|
};
|
|
32
34
|
const boxes = (f) => {
|
|
33
35
|
const m = new Map();
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
for (const sc of scenes(f)) {
|
|
37
|
+
const ports = new Set(sc.ports.map((p) => p.id));
|
|
38
|
+
const walk = (g, parent) => g.children.forEach((c) => isGroup(c)
|
|
39
|
+
? walk(c, c.label != null ? str(c.label) : c.id)
|
|
40
|
+
: ports.has(c.id) || m.set(c.id, { ...c, group: parent, path: sc.line, owner: sc.path.at(-1), top: sc.path[0] }));
|
|
41
|
+
walk(sc.props.layout);
|
|
42
|
+
}
|
|
36
43
|
return m;
|
|
37
44
|
};
|
|
45
|
+
const BOX_FIELDS = ['label', 'sub', 'shape', 'tone', 'mark', 'from', 'to', 'source', 'group'];
|
|
46
|
+
const touched = (a, b) => !a || !b || a.owner !== b.owner || fields(a, b, BOX_FIELDS).length > 0;
|
|
38
47
|
export function diff(before, after) {
|
|
39
|
-
const edges = (f) => new Map(f.edges.map((e) => [edgeId(e), e]));
|
|
40
|
-
const steps = (f) => new Map((f.steps ?? []).map((s) => [str(s.label), s]));
|
|
48
|
+
const edges = (f) => new Map(scenes(f).flatMap((sc) => sc.props.edges.map((e) => [edgeId(e), { ...e, path: sc.line }])));
|
|
49
|
+
const steps = (f) => new Map(scenes(f).flatMap((sc) => (sc.props.steps ?? []).map((s) => [`${sc.path.join('>')}\n${str(s.label)}`, { ...s, id: str(s.label), path: sc.line }])));
|
|
50
|
+
const was = boxes(before);
|
|
51
|
+
const now = boxes(after);
|
|
41
52
|
const out = [
|
|
42
|
-
...compare('box',
|
|
53
|
+
...compare('box', was, now, (a, b) => [
|
|
54
|
+
...(a.owner !== b.owner ? [`moved from ${a.path || 'the root'} to ${b.path || 'the root'}`] : []),
|
|
55
|
+
...fields(a, b, BOX_FIELDS),
|
|
56
|
+
]),
|
|
43
57
|
...compare('edge', edges(before), edges(after), (a, b) => fields(a, b, ['from', 'to', 'label', 'source', 'via'])),
|
|
44
58
|
...compare('step', steps(before), steps(after), (a, b) => fields(a, b, ['caption'])),
|
|
45
59
|
...compare('message', messages(before), messages(after), () => []),
|
|
46
60
|
];
|
|
61
|
+
const parts = new Map();
|
|
62
|
+
const tally = (owner, k) => {
|
|
63
|
+
if (owner)
|
|
64
|
+
parts.set(owner, { added: 0, removed: 0, changed: 0, ...parts.get(owner), [k]: (parts.get(owner)?.[k] ?? 0) + 1 });
|
|
65
|
+
};
|
|
66
|
+
for (const id of new Set([...was.keys(), ...now.keys()])) {
|
|
67
|
+
const a = was.get(id);
|
|
68
|
+
const b = now.get(id);
|
|
69
|
+
if (!touched(a, b))
|
|
70
|
+
continue;
|
|
71
|
+
if (!a)
|
|
72
|
+
tally(b.owner, 'added');
|
|
73
|
+
else if (!b)
|
|
74
|
+
tally(a.owner, 'removed');
|
|
75
|
+
else {
|
|
76
|
+
tally(b.owner, 'changed');
|
|
77
|
+
if (a.owner !== b.owner)
|
|
78
|
+
tally(a.owner, 'removed');
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
for (const [owner, c] of parts) {
|
|
82
|
+
if (!was.has(owner) || !now.has(owner))
|
|
83
|
+
continue;
|
|
84
|
+
const text = `parts: ${c.added} added, ${c.removed} removed, ${c.changed} changed`;
|
|
85
|
+
const hit = out.find((x) => x.kind === 'box' && x.id === owner);
|
|
86
|
+
const path = now.get(owner).path;
|
|
87
|
+
if (hit)
|
|
88
|
+
hit.detail = hit.detail ? `${hit.detail}; ${text}` : text;
|
|
89
|
+
else
|
|
90
|
+
out.push({ kind: 'box', op: 'changed', id: owner, detail: text, ...(path && { path }) });
|
|
91
|
+
}
|
|
47
92
|
// The rail is a boolean or 'only', so str() would hide it.
|
|
48
93
|
if ((before.rail ?? false) !== (after.rail ?? false))
|
|
49
94
|
out.push({ kind: 'rail', op: 'changed', id: 'rail', detail: `${before.rail ?? '(none)'} -> ${after.rail ?? '(none)'}` });
|
|
@@ -55,7 +100,7 @@ export function formatDiff(changes, as) {
|
|
|
55
100
|
return changes
|
|
56
101
|
.map((c) => {
|
|
57
102
|
const id = as === 'md' ? `\`${c.id}\`` : c.id;
|
|
58
|
-
const line = `${c.kind} ${c.op}: ${id}${c.detail ? ` (${c.detail})` : ''}`;
|
|
103
|
+
const line = `${c.path ? `[${c.path}] ` : ''}${c.kind} ${c.op}: ${id}${c.detail ? ` (${c.detail})` : ''}`;
|
|
59
104
|
return as === 'md' ? `- ${line}` : line;
|
|
60
105
|
})
|
|
61
106
|
.join('\n');
|
|
@@ -73,8 +118,11 @@ function find(root, hit) {
|
|
|
73
118
|
}
|
|
74
119
|
}
|
|
75
120
|
export function mergeFigures(before, after) {
|
|
76
|
-
const
|
|
77
|
-
const
|
|
121
|
+
const rootBoxes = (f) => new Map([...boxes(f)].filter(([, n]) => !n.path));
|
|
122
|
+
const was = rootBoxes(before);
|
|
123
|
+
const now = rootBoxes(after);
|
|
124
|
+
const innerBefore = boxes(before);
|
|
125
|
+
const innerAfter = boxes(after);
|
|
78
126
|
const gone = (c) => ids(c).every((id) => !now.has(id));
|
|
79
127
|
const layout = cloneGroup(after.layout);
|
|
80
128
|
const placed = new Set();
|
|
@@ -119,6 +167,13 @@ export function mergeFigures(before, after) {
|
|
|
119
167
|
for (const id of was.keys())
|
|
120
168
|
if (!now.has(id))
|
|
121
169
|
marks.boxes[id] = 'removed';
|
|
170
|
+
for (const id of new Set([...innerBefore.keys(), ...innerAfter.keys()])) {
|
|
171
|
+
const [a, b] = [innerBefore.get(id), innerAfter.get(id)];
|
|
172
|
+
if (touched(a, b))
|
|
173
|
+
for (const top of [a?.top, b?.top])
|
|
174
|
+
if (top && now.has(top) && !marks.boxes[top])
|
|
175
|
+
marks.boxes[top] = 'changed';
|
|
176
|
+
}
|
|
122
177
|
const colAt = new Map();
|
|
123
178
|
if (after.lanes) {
|
|
124
179
|
const colsAfter = laneColumns(after);
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
var e=/[ᄀ-ᅟ⺀-가-힣豈-︰-﹏-⦆¢-₩]|\p{Extended_Pictographic}/u,t=/[ilj.,:;'|!()[\]ftrI ]/,n=/[WMmw@%]/,r=/[A-Z0-9#&?]/;function i(i,a,o=!1){let s=0;for(let a of i)s+=e.test(a)?1:o?.6:n.test(a)?.85:t.test(a)?.3:r.test(a)?.64:.53;return s*a}function a(e,t,n,r=!1){let a=[];for(let o of e.split(`
|
|
2
|
+
`)){let e=``;for(let s of o.split(` `))e?i(e+` `+s,n,r)<=t?e+=` `+s:(a.push(e),e=s):e=s;a.push(e)}return a}var o=e=>{let{edges:t,...n}=typeof e==`object`&&!Array.isArray(e)&&!(`edge`in e)?e:{edges:e},r=t==null?[]:Array.isArray(t)?t:[t];return{...n,hops:r.map(e=>typeof e==`string`?{edge:e,back:!1}:{back:!1,...e})}},s=(e,t,n)=>{let r=new Set(t?[t]:[]);for(let t of n)for(let n of t.hops)n.edge===e&&n.source&&r.add(n.source);return r.size?[...r].join(`
|
|
3
|
+
`):void 0},c=e=>Array.isArray(e)&&e.every(e=>typeof e==`object`&&!!e&&`text`in e),l=e=>e.id??`${e.from}->${e.to}`,u=e=>`children`in e,d=1.25,f={blue:`#3b82f6`,purple:`#8b5cf6`,green:`#10b981`,orange:`#f59e0b`,red:`#ef4444`,gray:`#8b949e`},p=e=>`color-mix(in srgb, ${e} 64%, #000)`,m=(e,t,n=8)=>`color-mix(in srgb, ${e} ${n}%, ${t})`,h=e=>i(e,11,!0)+14,g=i(`ASYNC`,9)+8,_=1.25,v={accent:`#0074d9`,fg:`#1c1e21`,muted:`#606770`,bg:`#ffffff`,surface:`#f5f7fa`,border:`#d0d7de`},y={accent:`#1f78c8`,fg:`#e3e3e3`,muted:`#9aa0a6`,bg:`#1b1b1d`,surface:`#242526`,border:`#3a3b3c`},b=`#ffffff`,x=e=>typeof e==`string`||typeof e==`number`?String(e):``,S=e=>{let t=x(e).split(/\s+/).filter(Boolean).length;return t?400+240*t:0},C=(e,t)=>e.ms??t+S(e.say),w=(e,t,n)=>{for(let r=Math.min(n,t.length-1);r>=0;r--){let n=(t[r].focus??[]).find(t=>e.some(e=>e.id===t));if(n!=null)return e.find(e=>e.id===n)}},T=(e,t)=>w(e,(t[0]?.flow??[]).slice(0,1).map(o),0),ee=(e,t)=>({x:e.x+e.w+6,y:e.y,w:i(t,13),h:e.h}),E=(e,t,n)=>[Math.min(e+n,t),t],D=(e,t,n,r)=>t!=null&&e+3<t-3&&e+3+n>t-3-r,O=2e3,k=e=>Math.round(e*10)/10,A=(e,t)=>{let n=t/1e3/d,r=O/1e3/d,i=[],a=0;return{beats:e.map(e=>{let o=e.map((i,o)=>{let s=a,c=C(i,t)/1e3/d;return a+=c+(o===e.length-1?r:0),{t0:s,t1:s+n,tEnd:s+c}});return i.push(a),o}),ends:i}},j=e=>e.children.flatMap(e=>u(e)?j(e):[e]),M=e=>e.way===`in`?`in: from ${e.name}`:e.way===`out`?`out: to ${e.name}`:`in and out: ${e.name}`;function N(e){let t={path:[],name:``,line:``,props:e,ports:[],parts:0};if(e.lanes||e.timeline||e.rail===`only`)return[t];let n=/* @__PURE__ */ new Map,r=(e,t)=>j(e).forEach(e=>{n.set(e.id,e),e.inside&&t<3&&r(e.inside.layout,t+1)});r(e.layout,1);let i=e=>x(n.get(e)?.label)||e,{theme:a,speed:o,rail:s}=e,c=[t],l=e=>{for(let t of j(e.props.layout)){let r=t.inside?j(t.inside.layout):[];if(!t.inside||!r.length)continue;let u=new Set(r.map(e=>e.id)),d=t.inside.edges??[],f=[],p=[];for(let e of d)u.has(e.from)!==u.has(e.to)&&(!u.has(e.from)&&!f.includes(e.from)&&f.push(e.from),!u.has(e.to)&&!p.includes(e.to)&&p.push(e.to));let m=[...f,...p.filter(e=>!f.includes(e))].map(e=>({id:e,way:f.includes(e)?p.includes(e)?`in and out`:`in`:`out`,name:i(e)})),h=e=>{let t=n.get(e.id)?.source;return{id:e.id,label:M(e),...t!=null&&{source:t}}},g=m.filter(e=>e.way!==`out`).map(h),_=m.filter(e=>e.way===`out`).map(h),v=t.inside.layout,y=v.auto?{...v,children:[...g,...v.children,..._]}:{direction:v.direction===`column`?`column`:`row`,gap:28,children:[...g,v,..._]},b=[...e.path,t.id],x={path:b,name:i(t.id),line:b.map(i).join(` > `),props:{layout:y,edges:d,steps:t.inside.steps,theme:a,speed:o,rail:s},ports:m,parts:u.size};c.push(x),b.length+1<3&&l(x)}};return l(t),c}var P={fade:120,grow:360,stagger:50,hide:120,shrink:300},F=(e,t)=>t?[e.to,e.from]:[e.from,e.to];function te(e,t){let n=new Map(e.props.edges.map(e=>[l(e),e])),r=(e.props.steps??[]).findIndex(e=>{let r=e.flow.length?o(e.flow[0]).hops[0]:void 0,i=r&&n.get(r.edge);return i!=null&&F(i,r.back)[0]===t});return Math.max(r,0)}function ne(e){let t=ae(e.props),n=j(e.props.layout).map(e=>e.id).sort((e,n)=>t.get(e)-t.get(n)),r=new Set(e.ports.map(e=>e.id));return[...n.filter(e=>r.has(e)),...n.filter(e=>!r.has(e))]}function re(e,t=e.speed??900){let n=N(e),r=new Map(n.map(e=>[e.path.join(`>`),e])),i=[],a=/* @__PURE__ */ new Set,s=(e,t,n)=>{let r=[],i=e;for(let[e,t]of n)e>i&&r.push([i,e]),i=t;return t>i&&r.push([i,t]),r},c=(e,n,i)=>{let s=n.map(e=>e.flow.map(o)),c=A(s,t),d=new Map(e.props.edges.map(e=>[l(e),e])),f=[],p=0,m=(t,n,i)=>{let o=[...e.path,t].join(`>`),s=r.get(o);if(!s||a.has(o))return;let c=u(s,n,i);f.push([i,i+c]),p+=c},h={beats:[],ends:[]};s.forEach((e,t)=>{h.beats.push(e.map((e,n)=>{let r=c.beats[t][n],a=i+r.t0*1e3+p,o=i+r.t1*1e3+p;for(let t of e.hops){let e=d.get(t.edge);e&&m(F(e,t.back)[1],F(e,t.back)[0],i+r.tEnd*1e3+p)}return{t0:a/1e3,t1:o/1e3,tEnd:(i+r.tEnd*1e3+p)/1e3}})),h.ends.push((i+c.ends[t]*1e3+p)/1e3)});let g=i+(c.ends.at(-1)??0)*1e3;for(let t of j(e.props.layout))m(t.id,void 0,g+p);return h.ends.length&&(h.ends[h.ends.length-1]=(g+p)/1e3),{times:h,end:g+p,spans:f}},u=(e,t,n)=>{a.add(e.path.join(`>`));let o=n+S(e.name)+P.fade+P.grow,l=j(e.props.layout).length,u=o+P.stagger*(l-1)+S([e.line,...e.ports.map(M)].join(` `)),d=e.props.steps??[],f=d.length?te(e,t):-1,p={path:e.path,step:f,open:[n,u],grown:o,loop:[],back:[0,0],times:{beats:[],ends:[]}};i.push(p);let m=c(e,f===-1?[]:[d[f]],u),h=e.path.length>1?`back to ${r.get(e.path.slice(0,-1).join(`>`)).name}`:`Back`,g=m.end+S(h)+P.hide+P.shrink+P.fade;return p.loop=s(u,m.end,m.spans),p.back=[m.end,g],p.times=m.times,g-n},d=c(n[0],e.steps??[],0);return{ms:d.end,loop:s(0,d.end,d.spans),times:d.times,opens:i}}var I=e=>e.direction===`column`&&e.children.every(e=>u(e)&&e.label!=null&&e.children.every(e=>!u(e))),ie=e=>typeof e==`number`&&Number.isInteger(e)&&e>=0;function ae(e){let t=new Map(e.edges.map(e=>[l(e),e])),n=/* @__PURE__ */ new Map,r=e=>{n.has(e)||n.set(e,n.size)};for(let n of e.steps??[])for(let e of n.flow.map(o))for(let n of e.hops){let e=t.get(n.edge);if(e)for(let t of n.back?[e.to,e.from]:[e.from,e.to])r(t)}for(let t of j(e.layout))r(t.id);for(let t of j(e.layout))ie(t.at)&&n.set(t.id,t.at);return n}function L(e,t,n=190){if(e.width!=null)return e.width;if(t)return 166;let r=i(x(e.label),14)+32,a=i(x(e.sub),12)+32;return Math.min(e.shape===`decision`?170:n,Math.max(100,r,a))}function R(e,t){let n=x(e.label);if(e.shape===`decision`)return a(n,t-102+.5,14);if(i(n,14)<=t-32+.5)return[n];let r=a(n,t-32,14);return r.every(e=>i(e,14)<=t-32)?r:[n]}function z(e,t,n,r=190){if(!u(e))return L(e,t.has(e.id),r)+(e.shape===`decision`?70:0);let i=e.children.map(e=>z(e,t,n,r));return(e.direction===`column`?Math.max(...i):i.reduce((e,t)=>e+t,0)+_e(e,n)*(i.length-1))+(e.label==null?0:36)}function oe(e,t,n=24){if((e.lanes||e.timeline)&&I(e.layout))return 190;let r=new Set((e.steps??[]).flatMap(e=>e.flow.flatMap(e=>Object.keys(o(e).show??{})))),a=(e,t)=>{let n=L(e,r.has(e.id),t);return Math.max(i(x(e.sub),12),...R(e,n).map(e=>i(e,14)))+32<=n+.5},s=j(e.layout).filter(e=>a(e,190)),c=190;for(;c>110&&z(e.layout,r,e.edges,c)+n*2>t&&s.every(e=>a(e,c-20));)c-=20;return c}var B=(e,t,n)=>e*Math.max(0,1-n/(t/2))-8;function se(e,t){let n=e.sub?x(e.sub):``,r=R(e,t),o=r.length-1,s=(e,t)=>Math.max(Math.abs(e-.75*t),e+.25*t);for(let e=+!!n,c=0;;){c=Math.max(c,62+15*e+18*o);let l=(e?5.5:5)-7.5*e-9*o,u=e?s(l+18*o+15*e,12):0,d=e?a(n,B(t,c,u),12):[],f=r.map((e,t)=>({text:e,dy:l+18*t,far:s(l+18*t,14)})),p=d.map((e,t)=>({text:e,dy:l+18*o+15*(t+1),far:s(l+18*o+15*(t+1),12)}));if(d.length>e&&e<6)e++;else if(c<t&&f.some(e=>i(e.text,14)>B(t,c,e.far)))c+=4;else return{h:c,label:f,subs:p}}}function ce(e,t,n,r){let i=t=>e.findIndex(e=>e.id===t||e.children.some(e=>e.id===t)),a=/* @__PURE__ */ new Set;for(let e of r){let r=n.find(t=>l(t)===e.id),[o,s]=t.get(e.id)??[];r&&e.stub&&(e.stub.tight[0]&&o!=null&&a.add(`${i(r.from)}@${o}`),e.stub.tight[1]&&s!=null&&a.add(`${i(r.to)}@${s}`))}return a}var le=e=>Math.max(0,...e.map(e=>i(x(e.label).toUpperCase(),12)))+36,ue=(e,t)=>{let n=0;for(;n<e.length-2&&t>=e[n+1];)n++;return n},de=(e,t,n)=>[`${x(e)?x(e)+` `:``}→ ${x(n)}`,`from ${x(t)}`,...x(e)?[`→ ${x(n)}`]:[]];function fe(e,{width:t=830,minText:n=10,padding:r=24,floor:i=0}={}){let a=ae(e),s=Math.max(-1,...a.values())+1,u=(e.steps??[]).flatMap(e=>e.flow.flatMap(e=>Object.entries(o(e).show??{}))),d=new Set(u.map(([e])=>e)),f=u.some(([,e])=>c(e)&&e.some(e=>e.mono)),p=Array.from({length:s},()=>0),m=new Map(j(e.layout).map(e=>[e.id,e]));for(let e of m.values()){let t=a.get(e.id);p[t]=Math.max(p[t],L(e,d.has(e.id))+(e.shape===`decision`?70:0))}let g=f?10.5:11,_=t*g/n,v=_-r*2,y=e.layout.children,b=new Map(y.filter(e=>e.id!=null).map(e=>[e.id,e])),S=le(y),C=t=>{let n=[0];for(let e=0;e<t;e++)n.push(n[e]+Math.floor(s/t)+ +(e<s%t));let r=e=>ue(n,e),i=e=>r(a.get(e)),o=[...new Set([...m.keys()].map(i))].sort((e,t)=>e-t),c=o.length?o:[0],u=c.length>1,d=e=>u?[...new Set(e.children.map(e=>i(e.id)))].sort((e,t)=>e-t):c,f=/* @__PURE__ */ new Map,g=/* @__PURE__ */ new Map,_=[];e.edges.forEach(e=>{let t=l(e),[n,r]=[b.get(e.from),b.get(e.to)];if(!(m.has(e.from)||n)||!(m.has(e.to)||r))return;let a=!1,o=(e,t,n)=>{let r=d(e);return(n?r.find(e=>e>=t):r.findLast(e=>e<=t))??(a=!0,n?r.at(-1):r[0])},s,c;if(n||(s=i(e.from)),r||(c=i(e.to)),n&&r&&(s=d(n)[0]),n&&!r&&(s=o(n,c,!1)),r&&(c=o(r,s,!0)),s==null||c==null)return void(u&&_.push(t));if(a&&_.push(t),g.set(t,[s,c]),s===c)return;let p=de(e.label,(m.get(e.from)??n).label,(m.get(e.to)??r).label);f.set(t,p)});let C=Array.from({length:s},()=>0);for(let t of e.edges){let[e,n]=[a.get(t.from),a.get(t.to)];if(t.label==null||e==null||n==null||Math.abs(e-n)!==1||r(e)!==r(n))continue;let i=h(x(t.label));i-2>56&&(C[Math.min(e,n)]=Math.max(C[Math.min(e,n)],i+8))}let T=p.map((e,t)=>t===n[r(t)+1]-1?18:Math.max(56,C[t])),ee=Math.max(...n.slice(1).map((e,t)=>e-n[t])),E=Array.from({length:t},()=>0),D=e=>12+h(e)+8,O=c.every(e=>w(n[e],n[e+1],T,0)<=v),k=/* @__PURE__ */ new Map,A=/* @__PURE__ */ new Map,j=/* @__PURE__ */ new Set,M=/* @__PURE__ */ new Set;for(let[t,n]of f){let r=e.edges.find(e=>l(e)===t),[i,o]=[a.get(r.from),a.get(r.to)];i!=null&&k.set(i,Math.max(k.get(i)??0,D(n[0]))),o!=null&&A.set(o,Math.max(A.get(o)??0,D(n[1])))}for(let e of c){let[t,r]=[n[e],n[e+1]],i=v-w(t,r,T,0),a=(e,t)=>{e>0&&e<=i&&(i-=e,t())};a(A.get(t)??0,()=>E[e]=A.get(t)),(!A.has(t)||E[e])&&M.add(t);for(let e=t;e<r;e++){let t=(k.get(e)??0)+(e<r-1?A.get(e+1)??0:0);a(t-T[e],()=>T[e]=t),T[e]>=t&&j.add(e).add(-1-(e+1))}}let N=e=>y.findIndex(t=>t.id===e||t.children.some(t=>t.id===e)),P=/* @__PURE__ */ new Set;for(let[t]of f){let r=e.edges.find(e=>l(e)===t),[i,o]=[a.get(r.from),a.get(r.to)],[s,c]=g.get(t);(i==null||!j.has(i))&&P.add(`${N(r.from)}@${s}`),(o==null||!(o===n[c]?M.has(o):j.has(-1-o)))&&P.add(`${N(r.to)}@${c}`)}let F=new Map(y.flatMap(e=>e.children.map(t=>[t.id,e]))),te=new Set(e.edges.filter(e=>{let[t,n,i]=[a.get(e.from),a.get(e.to),F.get(e.from)];return e.around||t==null||n==null||!i||i!==F.get(e.to)||r(t)!==r(n)?!1:i.children.some(e=>a.get(e.id)>Math.min(t,n)&&a.get(e.id)<Math.max(t,n))}).map(l));return[{cols:a,starts:n,per:ee,blocks:c,gutter:S,gaps:T,lead:E,ends:g,stubs:f,around:te,tall:P,lost:_},O]},w=(e,t,n,r)=>S+r+p.slice(e,t).reduce((e,t)=>e+t,0)+n.slice(e,t).reduce((e,t)=>e+t,0);if(g<n||i>_)return C(1)[0];for(let e=1;e<=Math.max(1,Math.floor(s/2));e++){let[t,n]=C(e);if(n)return t}return C(1)[0]}function pe(e,t,n,r,i,a){let o=e.ends.get(t)?.[+!r],s=o==null?void 0:i(n,o);if(e.blocks.length<2||e.cols.has(n)||!s)return n;let c=e.stubs.get(t),l=c?`${t}@${r?`s`:`t`}`:`${n}@${o}`,u=r?s.x+s.w-(12+h(c?.[0]??``)+8):s.x+e.gutter+e.lead[o];return a[l]=c?{x:u,y:s.y,w:0,h:s.h}:s,l}var V=e=>{let t=typeof e==`string`?/^(\d{4})-(\d{2})-(\d{2})$/.exec(e):null;if(!t)return null;let n=Date.UTC(+t[1],t[2]-1,+t[3]);return new Date(n).toISOString().slice(0,10)===e?n/864e5:null},H=[`Jan`,`Feb`,`Mar`,`Apr`,`May`,`Jun`,`Jul`,`Aug`,`Sep`,`Oct`,`Nov`,`Dec`],U=e=>/* @__PURE__ */ new Date(e*864e5),W=e=>((e+3)%7+7)%7,me=e=>{let t=e-W(e)+3,n=Date.UTC(U(t).getUTCFullYear(),0,1)/864e5;return Math.floor((t-n)/7)+1},G=e=>{let t=[];return e.layout.children.forEach((e,n)=>j(u(e)?e:{children:[e]}).forEach(e=>{let r=e.from==null?null:V(e.from);if(r==null)return;let i=e.to==null?null:V(e.to);t.push({id:e.id,track:n,from:r,to:i!=null&&i>=r?i:null,label:e})})),t};function he(e,t){let n=G(e),r=e.today==null?null:V(e.today),a=[...n.flatMap(e=>[e.from,e.to??e.from]),...r==null?[]:[r]],o=a.length?Math.min(...a):0,s=a.length?Math.max(...a):0,c=o-W(o),l=s+(6-W(s)),u=e=>(e-c)/(l-c+1)*t;for(let e=0;e<4;e++){let e=Math.max(0,...n.map(e=>{let n=e.to==null?u(e.from)-7:u(e.from),r=e.to==null?14:Math.max(24,u(e.to+1)-u(e.from)),a=i(x(e.label.label),13);return e.to!=null&&a+16<=r?0:n+r+6+a-t}));if(e<=0)break;l+=Math.ceil(e/(t/(l-c+1))/7)*7}let d=[];if((l-c+1)/7<=16)for(let e=c;e<=l;e+=7)d.push({x:u(e),label:`W${me(e)}`});else for(let e=c;e<=l;e++)U(e).getUTCDate()===1&&d.push({x:u(e),label:H[U(e).getUTCMonth()]});let f=e.layout.children.map(()=>1),p=[],m=e.layout.children.map(()=>[]);for(let e of[...n].sort((e,t)=>e.from-t.from)){let t=e.to!=null,n=t?u(e.from):u(e.from)-7,r=t?Math.max(24,u(e.to+1)-u(e.from)):14,a=i(x(e.label.label),13),o=t&&a+16<=r,s=m[e.track].findIndex(e=>e+8<=n);s<0&&(s=m[e.track].length),m[e.track][s]=o?n+r:n+r+6+a,f[e.track]=Math.max(f[e.track],s+1),p.push({id:e.id,track:e.track,row:s,x:n,w:r,milestone:!t,labelInside:o,date:K(e.from)})}let h=new Map(n.map((e,t)=>[e.id,t]));return p.sort((e,t)=>h.get(e.id)-h.get(t.id)),{start:c,end:l,ticks:d,items:p,rows:f,today:n.length&&r!=null?u(r):null,last:n.length?u(s):null,lastDate:K(s)}}var K=e=>`${U(e).getUTCDate()} ${H[U(e).getUTCMonth()]}`;function ge(e){let t=G(e).sort((e,t)=>e.from-t.from);return[{label:`timeline`,flow:t.map((e,n)=>({focus:[e.id],light:t.slice(0,n+1).map(e=>e.id),say:`${x(e.label.label)}, ${K(e.from)}${e.to==null?``:` to ${K(e.to)}`}${e.label.sub==null?``:` · ${x(e.label.sub)}`}`}))}]}var q=e=>u(e)?[...e.id?[e.id]:[],...e.children.flatMap(q)]:[e.id];function _e(e,t){let n=/* @__PURE__ */ new Map;e.children.forEach((e,t)=>q(e).forEach(e=>n.set(e,t)));let r=t.filter(t=>t.label!=null&&n.has(t.from)&&n.has(t.to)&&n.get(t.from)!==n.get(t.to)&&!(e.gap!=null&&t.around));if(e.gap!=null&&!r.length)return e.gap;let i=Math.max(0,...r.map(e=>h(x(e.label))+32));if(e.gap!=null&&e.direction!==`column`)return Math.max(e.gap,i);let a;if(e.direction===`column`){let t=r.map(e=>[n.get(e.from),n.get(e.to)].sort((e,t)=>e-t)),i=Math.max(0,...e.children.map((e,n)=>t.filter(([e,t])=>e<=n&&n<t).length));a=i?Math.min(i,3)*22+32-4:28}else a=Math.max(56,i);return Math.max(e.gap??0,a)}var ve=e=>{let t=/* @__PURE__ */ new Map,n=e=>e.children.forEach(e=>{e.id&&t.set(e.id,x(e.label)||e.id),u(e)&&n(e)});n(e.layout);let r=e=>t.get(e)??e,i=new Map(e.edges.map(e=>[l(e),e])),a=j(e.layout).map(e=>r(e.id)),s=e.steps??[],c=s.flatMap(e=>[`${x(e.label)}.`,...e.flow.map(o).flatMap(e=>x(e.say)?[x(e.say)]:e.hops.flatMap(e=>{let t=i.get(e.edge);return t?[e.back?`${r(t.to)} to ${r(t.from)}`:`${r(t.from)} to ${r(t.to)}`]:[]}))]),d=N(e).slice(1).map(e=>{let t=new Set(e.ports.map(e=>e.id)),n=j(e.props.layout).filter(e=>!t.has(e.id));return`${e.line}: ${n.map(e=>x(e.label)||e.id).join(`, `)}.${e.ports.length?` Ports: ${e.ports.map(M).join(`, `)}.`:``}`});return{title:`Flow figure: ${a.join(`, `)}.${s.length?` Steps: ${s.map(e=>x(e.label)).join(`, `)}.`:``}`,desc:[`The figure has these boxes: ${a.join(`, `)}.`,...c,...d].join(`
|
|
4
|
+
`)}},ye=6,J=e=>e.shape===`store`,Y=e=>e?.shape===`decision`,be=e=>e===`red`||e===`gray`,X=e=>!!e&&(e.shape===`decision`||!!e.check),xe=e=>e.check||(e.shape===`decision`?x(e.label):``),Z=(e,t)=>X(e.get(t.source))&&(be(e.get(t.target)?.tone)||t.tone===`red`),Se=e=>e.children.flatMap(e=>u(e)?e.label==null?Se(e):[e]:[]),Q=(e,t)=>{let n=new Map(e.edges.map(e=>[l(e),e]));return t.flow.map(e=>{let t=o(e);return{beat:t,hops:t.hops.flatMap(e=>{let t=n.get(e.edge);return t?[{...e,edge:t,source:e.back?t.to:t.from,target:e.back?t.from:t.to}]:[]})}})};function $(e){let t=j(e.layout),n=t.filter(J),r=new Map(t.map(e=>[e.id,e])),i=e.lanes&&I(e.layout)?e.layout.children.filter(u):Se(e.layout),a=[];for(let e of i){let t=j(e).filter(e=>!J(e));t.length&&a.push({name:x(e.label),look:e.look,boxes:t.map(e=>e.id)})}if(a.length){let n=new Set(a.flatMap(e=>e.boxes));for(let t of e.steps??[])for(let{hops:i}of Q(e,t))for(let e of i.flatMap(e=>[e.source,e.target])){let t=r.get(e);!t||n.has(e)||J(t)||Y(t)||(n.add(e),a.push({name:x(t.label),look:t.look,boxes:[e]}))}let i=new Map(t.map((e,t)=>[e.id,t]));a.sort((e,t)=>i.get(e.boxes[0])-i.get(t.boxes[0]))}if(!a.length){let t=/* @__PURE__ */ new Set;for(let n of e.steps??[])for(let{hops:i}of Q(e,n))for(let e of i)for(let n of[e.source,e.target])r.has(n)&&!J(r.get(n))&&!Y(r.get(n))&&t.add(n);a=[...t].slice(0,n.length?5:ye).map(e=>{let t=r.get(e);return{name:x(t.label),look:t.look,boxes:[e]}})}return n.length&&a.push({name:`Records`,look:n.find(e=>e.look)?.look,boxes:n.map(e=>e.id)}),a}function Ce(e,t){let n=e.steps?.find(e=>x(e.label)===t);if(!n)return[];let r=$(e),i=new Map(j(e.layout).map(e=>[e.id,e])),a=new Map(r.flatMap(e=>e.boxes.map(t=>[t,e.name]))),o=/* @__PURE__ */ new Map,s=e=>{for(let t of e.children)u(t)&&(t.id&&o.set(t.id,t.label),s(t))};s(e.layout);let c=e=>x(i.get(e)?.label??o.get(e)),l=Q(e,n);if(!l.length)return[];let d=l.flatMap(e=>e.hops).at(-1),f=d?c(d.target):``,p=r[0]?.name??``,m=l.map(({beat:e,hops:t},n)=>{let r=t.at(-1),o=e=>a.get(e)??c(e),s=r?a.has(r.target)||!Y(i.get(r.target))?o(r.target):n?p:o(r.source):p;p=s;let l=t.map(e=>x(e.edge.label)).filter(Boolean),u={actor:s,say:x(e.say)||(r?l.length?`${l.join(`, `)}: ${c(r.target)}`:c(r.target):``),note:x(e.note),records:[]},d=t.filter(e=>X(i.get(e.source))),f=d.find(e=>Z(i,e))??d.at(-1);f&&(u.stamp={mark:Z(i,f)?`✗`:`✓`,label:x(f.edge.label)},u.question=xe(i.get(f.source)));for(let e of t){let t=i.get(e.target);t&&J(t)&&u.records.push(x(e.data)||x(e.edge.label)||x(t.label))}return u}),h=new Set(l.flatMap(e=>e.hops.flatMap(e=>[e.source,e.target]))),g=l.flatMap(e=>e.hops).find(e=>Z(i,e)),_=[];if(g){let t=e.edges.filter(e=>e!==g.edge&&i.has(e.to)&&!be(i.get(e.to).tone)),n=/* @__PURE__ */ new Set([g.source]);for(let e=!0;e;){e=!1;for(let r of t)n.has(r.from)&&!n.has(r.to)&&(n.add(r.to),e=!0)}let r=t.filter(e=>n.has(e.from)&&n.has(e.to)&&e.to!==g.source),a=/* @__PURE__ */ new Map;for(let e of r)a.set(e.to,(a.get(e.to)??0)+1);for(let e=[g.source];e.length;){let t=e.shift();_.push(t);for(let n of r)n.from===t&&(a.set(n.to,a.get(n.to)-1),a.get(n.to)||e.push(n.to))}for(let e of n)_.includes(e)||_.push(e)}let v=_.slice(1).filter(e=>!h.has(e)).map(c);return m.forEach((e,t)=>{let n=m.slice(t+1).find(t=>t.actor!==e.actor);!e.note&&n&&(e.note=`${n.actor} acts next.`)}),m.push({actor:m.at(-1).actor,say:n.outcome||(/[.?!]$/.test(f)?f:`${f}.`),note:``,records:[],summary:{outcome:f,notReached:v}}),m}var we=[`person`,`shop`,`truck`],Te=e=>e.edges.some(e=>!!e.carries&&typeof e.carries==`object`)||$(e).some(e=>we.includes(e.look)),Ee={font:`system-ui, -apple-system, "Segoe UI", Roboto, sans-serif`},De={light:{...Ee,bg:`#fafbfd`,surface:`#ffffff`,border:`#dfe7f0`,ink:`#1d2433`,muted:`#5d687a`,accent:`#0074d9`,"on-accent":`#ffffff`,bad:`#c8352f`,good:`#23704e`,play:`#1d2433`},dark:{...Ee,bg:`#131820`,surface:`#1c2330`,border:`#2a3343`,ink:`#e9eef6`,muted:`#9aa6b8`,accent:`#4da3f0`,"on-accent":`#0b1220`,bad:`#ee5d57`,good:`#5cc48f`,play:`#6b778c`}},Oe={wide:880,roomy:1200,panel:376,slim:300,pad:16,chips:52,sheet:160,short:560,tall:760};export{p as $,q as A,le as B,se as C,oe as D,s as E,R as F,ee as G,T as H,h as I,x as J,w as K,E as L,I as M,c as N,ne as O,z as P,o as Q,ue as R,D as S,l as T,k as U,fe as V,j as W,ge as X,ce as Y,he as Z,O as _,Q as a,C as b,xe as c,d,m as et,y as f,P as g,b as h,Z as i,u as j,_e as k,Te as l,v as m,De as n,i as nt,X as o,_ as p,N as q,$ as r,a as rt,Ce as s,Oe as t,re as tt,g as u,f as v,B as w,A as x,ve as y,pe as z};
|