flowfig 0.8.0 → 0.8.1
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 +29 -17
- package/dist/check.js +64 -5
- package/dist/cli.js +13 -10
- package/dist/diff.js +12 -5
- package/dist/draw.js +1 -1
- package/dist/edges.js +53 -6
- package/dist/geometry.d.ts +11 -2
- package/dist/geometry.js +255 -67
- package/dist/guide.d.ts +22 -2
- package/dist/guide.js +282 -222
- package/dist/index.js +36 -28
- package/dist/load.d.ts +2 -0
- package/dist/load.js +24 -1
- package/dist/mcp.d.ts +7 -0
- package/dist/mcp.js +16 -6
- package/dist/model.d.ts +8 -2
- package/dist/model.js +54 -11
- package/dist/scene.d.ts +2 -0
- package/dist/svg.js +49 -32
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { Fragment, useEffect, useMemo, useRef, useState } from 'react';
|
|
4
|
-
import { route } from './geometry.js';
|
|
4
|
+
import { arcRoom, avoidOf, route } from './geometry.js';
|
|
5
5
|
import { foldedLabel, groupBox, layoutRail, railState, RAIL } from './rail.js';
|
|
6
6
|
import { textWidth } from './text.js';
|
|
7
7
|
import { checkScene, checkSpec, checkTheme } from './check.js';
|
|
8
|
-
import { ASYNC_TAG_W, BASE_RATE, CARD_LINE, CARD_PAD,
|
|
8
|
+
import { ASYNC_TAG_W, BASE_RATE, CARD_LINE, CARD_PAD, EDGE_OFF, EDGE_ON, LIGHT, TONES, ON_ACCENT, toneFill, toneTint, decisions, edgeId, groupGap, isGroup, isRows, lanePlan as planLanes, laneEnd, laneBlock, STUB_ROOM, labelPillW, str, LANE_BLOCK_GAP, LANE_PAD, LANE_ROW_GAP, isLanesLayout, nodes, timelineBeats, timelineLayout, TL_AXIS_H, TL_AXIS_W, TL_BAR_H, TL_ROW_GAP, toBeat, diamondLines, diamondRoom, tightCopies, nodeWidth, labelLines, fitCap, LABEL_LINE, beatMs, STEP_HOLD_MS, playheadItem, loopStartItem, dateLabelRaised, edgeTip, } from './model.js';
|
|
9
9
|
export { LIGHT, DARK } from './model.js';
|
|
10
10
|
const DEFAULTS = { ...LIGHT, font: 'inherit' };
|
|
11
11
|
const v = (k) => `var(--fig-${k}, ${DEFAULTS[k]})`;
|
|
@@ -95,7 +95,9 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
95
95
|
: []),
|
|
96
96
|
]), [layout, tl, edges, timeline, today]);
|
|
97
97
|
const step = active == null ? null : steps[active];
|
|
98
|
+
const allBeats = useMemo(() => steps.flatMap((s) => s.flow.map(toBeat)), [steps]);
|
|
98
99
|
const beats = useMemo(() => (step?.flow ?? []).map(toBeat), [step]);
|
|
100
|
+
const cap = useMemo(() => fitCap({ layout, edges, steps, lanes, timeline }, 830), [layout, edges, steps, lanes, timeline]);
|
|
99
101
|
const carded = useMemo(() => {
|
|
100
102
|
const all = new Map();
|
|
101
103
|
for (const b of steps.flatMap((s) => s.flow.map(toBeat)))
|
|
@@ -155,25 +157,18 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
155
157
|
h: r.height / k,
|
|
156
158
|
};
|
|
157
159
|
});
|
|
158
|
-
const
|
|
160
|
+
const extra = [];
|
|
159
161
|
el.querySelectorAll('[data-fig-outside]').forEach((n) => {
|
|
160
162
|
const r = n.getBoundingClientRect();
|
|
161
|
-
|
|
163
|
+
extra.push({ x: (r.left - base.left) / k, y: (r.top - base.top) / k, w: r.width / k, h: r.height / k });
|
|
162
164
|
});
|
|
163
|
-
if (tl)
|
|
164
|
-
for (const n of nodes(layout))
|
|
165
|
-
if (rects[n.id])
|
|
166
|
-
avoid.push({ ...rects[n.id], box: true });
|
|
167
165
|
const stubs = lanePlan?.stubs ?? new Map();
|
|
168
|
-
if (stubs.size)
|
|
169
|
-
for (const n of nodes(layout))
|
|
170
|
-
if (rects[n.id])
|
|
171
|
-
avoid.push(rects[n.id]);
|
|
166
|
+
if (stubs.size)
|
|
172
167
|
el.querySelectorAll('[data-fig-gutter]').forEach((n) => {
|
|
173
168
|
const r = n.getBoundingClientRect();
|
|
174
|
-
|
|
169
|
+
extra.push({ x: (r.left - base.left) / k, y: (r.top - base.top) / k, w: r.width / k, h: r.height / k });
|
|
175
170
|
});
|
|
176
|
-
|
|
171
|
+
const avoid = avoidOf(nodes(layout).flatMap((n) => (rects[n.id] ? [rects[n.id]] : [])), extra, stubs.size > 0);
|
|
177
172
|
const bands = {};
|
|
178
173
|
el.querySelectorAll('[data-fig-copy]').forEach((n) => {
|
|
179
174
|
const r = n.getBoundingClientRect();
|
|
@@ -208,7 +203,7 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
208
203
|
stub: stubs.get(ids[i]).map(labelPillW),
|
|
209
204
|
bands: [bandOf(ids[i], e.from, 0), bandOf(ids[i], e.to, 1)],
|
|
210
205
|
}),
|
|
211
|
-
...(
|
|
206
|
+
...(!tl && e.label != null && { labelW: labelPillW(str(e.label)) }),
|
|
212
207
|
})), rects, tips, avoid, pillArea, lanePlan ? { bands: Object.values(laneBands), boxes: nodes(layout).flatMap((n) => (rects[n.id] ? [rects[n.id]] : [])) } : undefined);
|
|
213
208
|
setRouted(next);
|
|
214
209
|
if (lanePlan) {
|
|
@@ -285,7 +280,17 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
285
280
|
const e = edges[ids.indexOf(r.id)];
|
|
286
281
|
if (r.stub)
|
|
287
282
|
return r.stub.pts.map((pts, j) => ({ id: r.id, from: e.from, to: e.to, curve: r.curve, pts, label: labels[`${r.id}:${j}`] }));
|
|
288
|
-
return [
|
|
283
|
+
return [
|
|
284
|
+
{
|
|
285
|
+
id: r.id,
|
|
286
|
+
from: e.from,
|
|
287
|
+
to: e.to,
|
|
288
|
+
curve: r.curve,
|
|
289
|
+
label: labels[r.id],
|
|
290
|
+
...(tl && { behind: true }),
|
|
291
|
+
...(r.elbow && { elbow: r.elbow }),
|
|
292
|
+
},
|
|
293
|
+
];
|
|
289
294
|
}),
|
|
290
295
|
...(rail?.rows ?? []).flatMap((row) => {
|
|
291
296
|
const r = row.kind === 'message' ? labels[`rail:${row.n}`] : undefined;
|
|
@@ -458,7 +463,7 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
458
463
|
const inside = it.labelInside;
|
|
459
464
|
const outside = (_jsx("span", { "data-fig-outside": "", style: { position: 'absolute', left: '100%', marginLeft: 6, whiteSpace: 'nowrap', fontSize: 13, fontWeight: 500, color: v('fg') }, children: label }));
|
|
460
465
|
const top = LANE_PAD + it.row * (TL_BAR_H + TL_ROW_GAP);
|
|
461
|
-
return (_jsxs("div", { "data-fig": n.id, "data-diamond": it.milestone || undefined, onMouseEnter: () => setHover(n.id), onMouseLeave: () => setHover(null), style: {
|
|
466
|
+
return (_jsxs("div", { "data-fig": n.id, title: n.source, "data-diamond": it.milestone || undefined, onMouseEnter: () => setHover(n.id), onMouseLeave: () => setHover(null), style: {
|
|
462
467
|
position: 'absolute',
|
|
463
468
|
left: it.x,
|
|
464
469
|
top: it.milestone ? top + (TL_BAR_H - it.w) / 2 : top,
|
|
@@ -648,13 +653,15 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
648
653
|
const store = item.shape === 'store';
|
|
649
654
|
const card = carded.has(item.id);
|
|
650
655
|
const dl = diamond && !card ? diamondLines(item, nodeWidth(item, false) + 70) : null;
|
|
656
|
+
const w = diamond || card || item.width != null || str(item.label) ? nodeWidth(item, card, cap) + (diamond ? 70 : 0) : undefined;
|
|
657
|
+
const lines = w != null && !diamond && str(item.label) ? labelLines(item, w) : null;
|
|
651
658
|
const bt = item.tone && TONES[item.tone];
|
|
652
|
-
return (_jsxs("div", { "data-fig": item.id, "data-diamond": diamond || undefined, onMouseEnter: () => setHover(item.id), onMouseLeave: () => setHover(null), style: {
|
|
659
|
+
return (_jsxs("div", { "data-fig": item.id, title: item.source, "data-diamond": diamond || undefined, onMouseEnter: () => setHover(item.id), onMouseLeave: () => setHover(null), style: {
|
|
653
660
|
position: 'relative',
|
|
654
661
|
isolation: 'isolate',
|
|
655
662
|
minWidth: 100,
|
|
656
|
-
maxWidth:
|
|
657
|
-
width:
|
|
663
|
+
maxWidth: w == null ? cap : undefined,
|
|
664
|
+
width: w,
|
|
658
665
|
minHeight: dl ? Math.max(dl.h, (minHeight(item.id) ?? 0) + 24) : minHeight(item.id),
|
|
659
666
|
boxSizing: 'border-box',
|
|
660
667
|
display: 'flex',
|
|
@@ -707,7 +714,7 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
707
714
|
border: `1.5px solid ${bt ?? v('accent')}`,
|
|
708
715
|
background: `radial-gradient(circle, ${bt ?? v('accent')} 0 4px, transparent 4.5px)`,
|
|
709
716
|
}),
|
|
710
|
-
} })), _jsx("div", { children: item.label }), dl
|
|
717
|
+
} })), lines ? (lines.map((l, i) => (_jsx("div", { style: { whiteSpace: 'nowrap', lineHeight: `${LABEL_LINE}px` }, children: l }, i)))) : (_jsx("div", { style: { lineHeight: `${LABEL_LINE}px` }, children: item.label })), dl
|
|
711
718
|
? dl.subs.map((l, i) => (_jsx("div", { style: { fontSize: 12, fontWeight: 400, color: v('muted'), lineHeight: '15px', whiteSpace: 'nowrap' }, children: l.text }, i)))
|
|
712
719
|
: item.sub != null && _jsx("div", { style: { fontSize: 12, fontWeight: 400, color: v('muted'), marginTop: 2 }, children: item.sub }), card && (_jsxs("div", { style: {
|
|
713
720
|
marginTop: 8,
|
|
@@ -780,11 +787,10 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
780
787
|
margin: '0 auto',
|
|
781
788
|
transform: fit.scale < 1 ? `scale(${fit.scale})` : undefined,
|
|
782
789
|
transformOrigin: 'top left',
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
: 4,
|
|
790
|
+
paddingTop: arcRoom(routed, 'above') || 4,
|
|
791
|
+
paddingBottom: arcRoom(routed, 'below') || 4,
|
|
792
|
+
paddingLeft: arcRoom(routed, 'left') || 4,
|
|
793
|
+
paddingRight: arcRoom(routed, 'right') || 4,
|
|
788
794
|
}, children: [renderItem(layout, 0), _jsxs("svg", { style: {
|
|
789
795
|
position: 'absolute',
|
|
790
796
|
inset: 0,
|
|
@@ -797,11 +803,12 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
797
803
|
const on = litEdges.has(r.id);
|
|
798
804
|
const tone = hopTone(r.id);
|
|
799
805
|
const hidden = !on && edges[ids.indexOf(r.id)].quiet;
|
|
806
|
+
const tip = edgeTip(r.id, edges[ids.indexOf(r.id)].source, allBeats);
|
|
800
807
|
const look = (d, key, guides = false) => (_jsx("path", { ref: guides
|
|
801
808
|
? (p) => {
|
|
802
809
|
paths.current[r.id] = p;
|
|
803
810
|
}
|
|
804
|
-
: undefined, d: d, fill: "none", stroke: on ? (tone ?? v('accent')) : v('muted'), strokeWidth: on ? EDGE_ON : EDGE_OFF, strokeOpacity: hidden ? 0 : focus && !on ? 0.35 : 1, markerEnd: hidden ? undefined : `url(#fig-arrow-${on ? 'on' : 'off'})`, style: { transition: 'stroke .25s, stroke-opacity .25s' } }, key));
|
|
811
|
+
: undefined, d: d, fill: "none", stroke: on ? (tone ?? v('accent')) : v('muted'), strokeWidth: on ? EDGE_ON : EDGE_OFF, strokeOpacity: hidden ? 0 : focus && !on ? 0.35 : 1, markerEnd: hidden ? undefined : `url(#fig-arrow-${on ? 'on' : 'off'})`, style: { transition: 'stroke .25s, stroke-opacity .25s', pointerEvents: tip ? 'stroke' : undefined }, children: tip && _jsx("title", { children: tip }) }, key));
|
|
805
812
|
const guide = (_jsx("path", { ref: (p) => {
|
|
806
813
|
paths.current[r.id] = p;
|
|
807
814
|
}, d: r.d, fill: "none", stroke: "none" }, r.id));
|
|
@@ -899,7 +906,8 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
899
906
|
const ly = y + RAIL.row / 2;
|
|
900
907
|
const tone = state === 'now' && row.tone ? TONES[row.tone] : undefined;
|
|
901
908
|
const g = row.group != null ? rail.groups[row.group] : null;
|
|
902
|
-
|
|
909
|
+
const tip = edgeTip(row.edge, edges[ids.indexOf(row.edge)].source, allBeats);
|
|
910
|
+
return (_jsxs("g", { "data-rail-row": row.n, "data-state": state, opacity: state === 'next' && !on ? 0.45 : 1, style: { cursor: 'pointer' }, onClick: () => goTo(row.step, row.beat), onMouseEnter: () => setHoverEdge(row.edge), onMouseLeave: () => setHoverEdge(null), children: [tip && _jsx("title", { children: tip }), g && g.rows[0] === i && (_jsx("rect", { x: groupBox(rail, g).x, y: y + groupBox(rail, g).y, width: groupBox(rail, g).w, height: groupBox(rail, g).h, rx: 6, fill: `color-mix(in srgb, ${v('accent')} 8%, ${v('bg')})`, stroke: v('accent'), strokeDasharray: "3 3" })), _jsx("path", { d: `M ${x1} ${ly} H ${x2}`, fill: "none", stroke: on ? (tone ?? v('accent')) : v('muted'), strokeWidth: on ? EDGE_ON : EDGE_OFF, strokeDasharray: row.async ? '4 3' : undefined, markerEnd: "url(#flowfig-rail-arrow)" }), row.async &&
|
|
903
911
|
(() => {
|
|
904
912
|
const tagW = ASYNC_TAG_W;
|
|
905
913
|
const tx = !row.pill ? (x1 + x2) / 2 - tagW / 2 : x2 > x1 ? row.pill.x - tagW - 4 : row.pill.x + row.pill.w + 4;
|
package/dist/load.d.ts
CHANGED
|
@@ -8,3 +8,5 @@ export declare function svgWithSpec(props: FlowProps, opts?: SvgOptions, svg?: s
|
|
|
8
8
|
export declare function sortFindings(findings: Finding[], strict: boolean): Finding[];
|
|
9
9
|
/** One line per finding, the counts line (unless `withCounts` is false), and the `figure:` line. No color: the CLI adds it. */
|
|
10
10
|
export declare function reportLines(props: FlowProps, findings: Finding[], withCounts?: boolean): string[];
|
|
11
|
+
export declare function summaryLines(props: FlowProps): string[];
|
|
12
|
+
export declare function verifyLines(props: FlowProps, figure: string, root?: string): string[];
|
package/dist/load.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
|
-
import { counts } from './model.js';
|
|
2
|
+
import { counts, toBeat } from './model.js';
|
|
3
|
+
import { coverageLine, owners, unsureLines, verifyReport } from './verify.js';
|
|
3
4
|
import { toSvg } from './svg.js';
|
|
4
5
|
const SPEC_OPEN = '<metadata id="figure-spec"><![CDATA[';
|
|
5
6
|
const SPEC_CLOSE = ']]></metadata>';
|
|
@@ -54,3 +55,25 @@ export function reportLines(props, findings, withCounts = true) {
|
|
|
54
55
|
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')}`);
|
|
55
56
|
return out;
|
|
56
57
|
}
|
|
58
|
+
const text = (x) => (typeof x === 'string' ? x : '');
|
|
59
|
+
export function summaryLines(props) {
|
|
60
|
+
const edges = props.edges.map((e) => {
|
|
61
|
+
const label = text(e.label);
|
|
62
|
+
return `${e.from} -> ${e.to}${label ? `: ${label}` : ''}`;
|
|
63
|
+
});
|
|
64
|
+
const steps = (props.steps ?? []).map((s) => {
|
|
65
|
+
const hops = s.flow.reduce((k, b) => k + toBeat(b).hops.length, 0);
|
|
66
|
+
return `step "${text(s.label)}": ${hops} ${hops === 1 ? 'hop' : 'hops'}`;
|
|
67
|
+
});
|
|
68
|
+
return [...edges, ...steps];
|
|
69
|
+
}
|
|
70
|
+
export function verifyLines(props, figure, root = process.cwd()) {
|
|
71
|
+
if (!owners(props).some(([, source, via]) => source != null || via != null))
|
|
72
|
+
return [];
|
|
73
|
+
const { findings, coverage } = verifyReport(props, { root });
|
|
74
|
+
return [
|
|
75
|
+
...findings.map((f) => `${f.severity.padEnd(8)} ${f.rule.padEnd(18)} ${figure}: ${f.message}`),
|
|
76
|
+
coverageLine(figure, coverage),
|
|
77
|
+
...unsureLines(coverage),
|
|
78
|
+
];
|
|
79
|
+
}
|
package/dist/mcp.d.ts
CHANGED
|
@@ -13,6 +13,10 @@ export declare const TOOLS: ({
|
|
|
13
13
|
inputSchema: {
|
|
14
14
|
type: string;
|
|
15
15
|
properties: {
|
|
16
|
+
topic: {
|
|
17
|
+
type: string;
|
|
18
|
+
enum: string[];
|
|
19
|
+
};
|
|
16
20
|
paths?: undefined;
|
|
17
21
|
root?: undefined;
|
|
18
22
|
strict?: undefined;
|
|
@@ -72,6 +76,7 @@ export declare const TOOLS: ({
|
|
|
72
76
|
type: string;
|
|
73
77
|
description: string;
|
|
74
78
|
};
|
|
79
|
+
topic?: undefined;
|
|
75
80
|
out: {
|
|
76
81
|
type: string;
|
|
77
82
|
description: string;
|
|
@@ -89,6 +94,7 @@ export declare const TOOLS: ({
|
|
|
89
94
|
inputSchema: {
|
|
90
95
|
type: string;
|
|
91
96
|
properties: {
|
|
97
|
+
topic?: undefined;
|
|
92
98
|
paths: {
|
|
93
99
|
type: string;
|
|
94
100
|
items: {
|
|
@@ -114,6 +120,7 @@ export declare const TOOLS: ({
|
|
|
114
120
|
inputSchema: {
|
|
115
121
|
type: string;
|
|
116
122
|
properties: {
|
|
123
|
+
topic?: undefined;
|
|
117
124
|
paths?: undefined;
|
|
118
125
|
root?: undefined;
|
|
119
126
|
strict?: undefined;
|
package/dist/mcp.js
CHANGED
|
@@ -2,8 +2,8 @@ import { mkdirSync, writeFileSync } from 'node:fs';
|
|
|
2
2
|
import { dirname } from 'node:path';
|
|
3
3
|
import { createInterface } from 'node:readline';
|
|
4
4
|
import { diff, formatDiff } from './diff.js';
|
|
5
|
-
import { GUIDE } from './guide.js';
|
|
6
|
-
import { loadSpec, reportLines, sortFindings, svgWithSpec } from './load.js';
|
|
5
|
+
import { GUIDE, TOPICS } from './guide.js';
|
|
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
9
|
import { coverageLine, unsureLines, verifyReport } from './verify.js';
|
|
@@ -19,7 +19,11 @@ const checkProps = {
|
|
|
19
19
|
minText: { type: 'number', description: 'The smallest text in px at that width. Default 10.' },
|
|
20
20
|
};
|
|
21
21
|
export const TOOLS = [
|
|
22
|
-
{
|
|
22
|
+
{
|
|
23
|
+
name: 'docs',
|
|
24
|
+
description: 'The flowfig guide. Read it before the first render. A topic gives the detail for one kind of figure.',
|
|
25
|
+
inputSchema: { type: 'object', properties: { topic: { type: 'string', enum: Object.keys(TOPICS) } } },
|
|
26
|
+
},
|
|
23
27
|
{
|
|
24
28
|
name: 'check',
|
|
25
29
|
description: 'List the faults of a figure: unknown ids, text overflow, crossings, label overlap, small text, low contrast. Fix every error before render.',
|
|
@@ -84,8 +88,14 @@ const types = (args, tool, want) => {
|
|
|
84
88
|
}
|
|
85
89
|
};
|
|
86
90
|
function run(name, args) {
|
|
87
|
-
if (name === 'docs')
|
|
88
|
-
|
|
91
|
+
if (name === 'docs') {
|
|
92
|
+
const topic = args.topic;
|
|
93
|
+
if (topic == null)
|
|
94
|
+
return ok(GUIDE);
|
|
95
|
+
if (typeof topic !== 'string' || !Object.hasOwn(TOPICS, topic))
|
|
96
|
+
throw new Bad(`docs: topic must be one of ${Object.keys(TOPICS).join(', ')}`);
|
|
97
|
+
return ok(TOPICS[topic]);
|
|
98
|
+
}
|
|
89
99
|
if (name === 'check' || name === 'render') {
|
|
90
100
|
need(args, name, name === 'render' ? ['spec', 'out'] : ['spec']);
|
|
91
101
|
types(args, name, { width: 'number', minText: 'number', out: 'string', strict: 'boolean' });
|
|
@@ -106,7 +116,7 @@ function run(name, args) {
|
|
|
106
116
|
catch (e) {
|
|
107
117
|
throw new Fault(`${out}: ${e.message}`);
|
|
108
118
|
}
|
|
109
|
-
return ok([...lines, `${out} — ${(svg.length / 1024).toFixed(1)} kB`].join('\n'));
|
|
119
|
+
return ok([...lines, ...summaryLines(props), ...verifyLines(props, out), `${out} — ${(svg.length / 1024).toFixed(1)} kB`].join('\n'));
|
|
110
120
|
}
|
|
111
121
|
if (name === 'verify') {
|
|
112
122
|
need(args, name, ['paths']);
|
package/dist/model.d.ts
CHANGED
|
@@ -169,6 +169,7 @@ export type Beat = Omit<FigBeat, 'edges'> & {
|
|
|
169
169
|
}[];
|
|
170
170
|
};
|
|
171
171
|
export declare const toBeat: (b: FigHop | FigHop[] | FigBeat) => Beat;
|
|
172
|
+
export declare const edgeTip: (id: string, source: string | undefined, beats: Beat[]) => string | undefined;
|
|
172
173
|
export declare const isRows: (c: FigContent) => c is FigRow[];
|
|
173
174
|
export declare const edgeId: (e: FigEdge) => string;
|
|
174
175
|
export declare const isGroup: (x: FigNode | FigGroup) => x is FigGroup;
|
|
@@ -232,11 +233,17 @@ export declare const isLanesLayout: (g: FigGroup) => boolean;
|
|
|
232
233
|
export declare const validAt: (at: unknown) => at is number;
|
|
233
234
|
/** The time column of each box: first use in the steps, then layout order. `at` wins. */
|
|
234
235
|
export declare function laneColumns(fig: FlowProps): Map<string, number>;
|
|
236
|
+
export declare const LABEL_LINE = 18;
|
|
235
237
|
export declare const FRAME_SIDE = 18, NODE_MIN_W = 100, NODE_MAX_W = 190;
|
|
236
238
|
export declare const LANE_BLOCK_GAP = 40;
|
|
237
239
|
export declare const STUB = 12, STUB_CLEAR = 8, STUB_ROOM = 30;
|
|
238
240
|
/** The width of a box as the SVG draws it, before a diamond adds its 70 px. */
|
|
239
|
-
export declare function nodeWidth(item: FigNode, carded: boolean): number;
|
|
241
|
+
export declare function nodeWidth(item: FigNode, carded: boolean, cap?: number): number;
|
|
242
|
+
export declare function labelLines(item: FigNode, w: number): string[];
|
|
243
|
+
export declare function itemWidth(item: FigNode | FigGroup, carded: {
|
|
244
|
+
has(id: string): boolean;
|
|
245
|
+
}, edges: FigEdge[], cap?: number): number;
|
|
246
|
+
export declare function fitCap(fig: FlowProps, width: number, padding?: number): number;
|
|
240
247
|
export declare const diamondRoom: (w: number, h: number, far: number) => number;
|
|
241
248
|
/** The lines of a diamond with no card. The diamond grows 15 px for each sub line that wraps. */
|
|
242
249
|
export declare function diamondLines(item: FigNode, w: number): {
|
|
@@ -343,5 +350,4 @@ export declare const counts: (p: FlowProps) => {
|
|
|
343
350
|
steps: number;
|
|
344
351
|
messages: number;
|
|
345
352
|
};
|
|
346
|
-
/** A row's default gap grows to hold its widest edge label, plus 8 px each side. */
|
|
347
353
|
export declare function groupGap(g: FigGroup, edges: FigEdge[]): number;
|
package/dist/model.js
CHANGED
|
@@ -8,6 +8,14 @@ export const toBeat = (b) => {
|
|
|
8
8
|
hops: hops.map((h) => (typeof h === 'string' ? { edge: h, back: false } : { back: false, ...h })),
|
|
9
9
|
};
|
|
10
10
|
};
|
|
11
|
+
export const edgeTip = (id, source, beats) => {
|
|
12
|
+
const all = new Set(source ? [source] : []);
|
|
13
|
+
for (const b of beats)
|
|
14
|
+
for (const h of b.hops)
|
|
15
|
+
if (h.edge === id && h.source)
|
|
16
|
+
all.add(h.source);
|
|
17
|
+
return all.size ? [...all].join('\n') : undefined;
|
|
18
|
+
};
|
|
11
19
|
// A React node can be an object, so test for the `text` key.
|
|
12
20
|
export const isRows = (c) => Array.isArray(c) && c.every((r) => r != null && typeof r === 'object' && 'text' in r);
|
|
13
21
|
export const edgeId = (e) => e.id ?? `${e.from}->${e.to}`;
|
|
@@ -86,18 +94,47 @@ export function laneColumns(fig) {
|
|
|
86
94
|
cols.set(n.id, n.at);
|
|
87
95
|
return cols;
|
|
88
96
|
}
|
|
97
|
+
export const LABEL_LINE = 18;
|
|
89
98
|
export const FRAME_SIDE = 18, NODE_MIN_W = 100, NODE_MAX_W = 190;
|
|
90
99
|
export const LANE_BLOCK_GAP = 40;
|
|
91
100
|
export const STUB = 12, STUB_CLEAR = 8, STUB_ROOM = 30;
|
|
92
101
|
/** The width of a box as the SVG draws it, before a diamond adds its 70 px. */
|
|
93
|
-
export function nodeWidth(item, carded) {
|
|
102
|
+
export function nodeWidth(item, carded, cap = NODE_MAX_W) {
|
|
94
103
|
if (item.width != null)
|
|
95
104
|
return item.width;
|
|
96
105
|
if (carded)
|
|
97
106
|
return CARD_WIDTH;
|
|
98
107
|
const label = textWidth(str(item.label), 14) + 32;
|
|
99
108
|
const sub = textWidth(str(item.sub), 12) + 32;
|
|
100
|
-
return Math.min(NODE_MAX_W, Math.max(NODE_MIN_W, label, sub));
|
|
109
|
+
return Math.min(item.shape === 'decision' ? NODE_MAX_W : cap, Math.max(NODE_MIN_W, label, sub));
|
|
110
|
+
}
|
|
111
|
+
export function labelLines(item, w) {
|
|
112
|
+
const label = str(item.label);
|
|
113
|
+
if (item.shape === 'decision' || textWidth(label, 14) <= w - 32 + 0.5)
|
|
114
|
+
return [label];
|
|
115
|
+
const lines = wrap(label, w - 32, 14);
|
|
116
|
+
return lines.length === 2 && lines.every((l) => textWidth(l, 14) <= w - 32) ? lines : [label];
|
|
117
|
+
}
|
|
118
|
+
export function itemWidth(item, carded, edges, cap = NODE_MAX_W) {
|
|
119
|
+
if (!isGroup(item))
|
|
120
|
+
return nodeWidth(item, carded.has(item.id), cap) + (item.shape === 'decision' ? 70 : 0);
|
|
121
|
+
const ws = item.children.map((c) => itemWidth(c, carded, edges, cap));
|
|
122
|
+
const inner = item.direction === 'column' ? Math.max(...ws) : ws.reduce((a, b) => a + b, 0) + groupGap(item, edges) * (ws.length - 1);
|
|
123
|
+
return inner + (item.label != null ? FRAME_SIDE * 2 : 0);
|
|
124
|
+
}
|
|
125
|
+
export function fitCap(fig, width, padding = 24) {
|
|
126
|
+
if ((fig.lanes || fig.timeline) && isLanesLayout(fig.layout))
|
|
127
|
+
return NODE_MAX_W;
|
|
128
|
+
const carded = new Set((fig.steps ?? []).flatMap((s) => s.flow.flatMap((b) => Object.keys(toBeat(b).show ?? {}))));
|
|
129
|
+
const fits = (n, cap) => {
|
|
130
|
+
const w = nodeWidth(n, carded.has(n.id), cap);
|
|
131
|
+
return Math.max(textWidth(str(n.sub), 12), ...labelLines(n, w).map((l) => textWidth(l, 14))) + 32 <= w + 0.5;
|
|
132
|
+
};
|
|
133
|
+
const keep = nodes(fig.layout).filter((n) => fits(n, NODE_MAX_W));
|
|
134
|
+
let cap = NODE_MAX_W;
|
|
135
|
+
while (cap > 110 && itemWidth(fig.layout, carded, fig.edges, cap) + padding * 2 > width && keep.every((n) => fits(n, cap - 20)))
|
|
136
|
+
cap -= 20;
|
|
137
|
+
return cap;
|
|
101
138
|
}
|
|
102
139
|
export const diamondRoom = (w, h, far) => w * Math.max(0, 1 - far / (h / 2)) - 8;
|
|
103
140
|
/** The lines of a diamond with no card. The diamond grows 15 px for each sub line that wraps. */
|
|
@@ -415,16 +452,22 @@ export const counts = (p) => {
|
|
|
415
452
|
};
|
|
416
453
|
};
|
|
417
454
|
const idsIn = (c) => (isGroup(c) ? [...(c.id ? [c.id] : []), ...c.children.flatMap(idsIn)] : [c.id]);
|
|
418
|
-
/** A row's default gap grows to hold its widest edge label, plus 8 px each side. */
|
|
419
455
|
export function groupGap(g, edges) {
|
|
420
|
-
if (g.gap != null)
|
|
421
|
-
return g.gap;
|
|
422
|
-
if (g.direction === 'column')
|
|
423
|
-
return 28;
|
|
424
456
|
const side = new Map();
|
|
425
457
|
g.children.forEach((c, i) => idsIn(c).forEach((id) => side.set(id, i)));
|
|
426
|
-
const
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
458
|
+
const spans = edges.filter((e) => e.label != null && side.has(e.from) && side.has(e.to) && side.get(e.from) !== side.get(e.to) && !(g.gap != null && e.around));
|
|
459
|
+
if (g.gap != null && !spans.length)
|
|
460
|
+
return g.gap;
|
|
461
|
+
const need = Math.max(0, ...spans.map((e) => labelPillW(str(e.label)) + 16));
|
|
462
|
+
if (g.gap != null && g.direction !== 'column')
|
|
463
|
+
return Math.max(g.gap, need - 16);
|
|
464
|
+
let auto;
|
|
465
|
+
if (g.direction === 'column') {
|
|
466
|
+
const ends = spans.map((e) => [side.get(e.from), side.get(e.to)].sort((p, q) => p - q));
|
|
467
|
+
const most = Math.max(0, ...g.children.map((_, k) => ends.filter(([lo, hi]) => lo <= k && k < hi).length));
|
|
468
|
+
auto = most > 1 ? Math.min(most, 3) * 22 + 16 : 28;
|
|
469
|
+
}
|
|
470
|
+
else
|
|
471
|
+
auto = Math.max(56, need);
|
|
472
|
+
return Math.max(g.gap ?? 0, auto);
|
|
430
473
|
}
|
package/dist/scene.d.ts
CHANGED
|
@@ -33,6 +33,8 @@ export type Scene = {
|
|
|
33
33
|
edges: SceneEdge[];
|
|
34
34
|
/** The smallest reading text in px, before any scale. */
|
|
35
35
|
minFont: number;
|
|
36
|
+
/** The drawn map area. An edge label stays inside it. */
|
|
37
|
+
area?: Rect;
|
|
36
38
|
/** The lane bands of a swimlanes figure. A stub pill stays inside one. */
|
|
37
39
|
lanes?: {
|
|
38
40
|
id: string;
|