flowfig 0.4.0 → 0.6.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/dist/model.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { ReactNode } from 'react';
2
+ import type { Rect } from './geometry.ts';
2
3
  /** A box in the figure. A box with a `show` in some step becomes a content card: it grows to fit the most any step puts in it. */
3
4
  export type FigNode = {
4
5
  /** Name that edges, steps and beats use to point at this box. Must be unique in the figure. */
@@ -217,6 +218,12 @@ export declare const playheadItem: <T extends {
217
218
  }>(items: T[], beats: {
218
219
  focus?: string[];
219
220
  }[], i: number) => T | undefined;
221
+ /** The item the playhead rests on when the loop starts: the playhead item of the first beat of the first step. */
222
+ export declare const loopStartItem: <T extends {
223
+ id: string;
224
+ }>(items: T[], steps: {
225
+ flow: FigStep['flow'];
226
+ }[]) => T | undefined;
220
227
  /** The box of a timeline label that sits beside its bar, in the bar's coordinates. */
221
228
  export declare const outsideLabelRect: (bar: {
222
229
  x: number;
@@ -248,6 +255,96 @@ export declare const validAt: (at: unknown) => at is number;
248
255
  /** The time column of each box in a lanes figure: its first appearance in the steps (from, then to; a back hop to, then from),
249
256
  * then the boxes no step touches in layout order. `at` overrides. Two boxes may share a column. */
250
257
  export declare function laneColumns(fig: FlowProps): Map<string, number>;
258
+ /** The side of an SVG frame, and the width limits of a box with no card. The lane split measures with them. */
259
+ export declare const FRAME_SIDE = 18, NODE_MIN_W = 100, NODE_MAX_W = 190;
260
+ /** The gap between two blocks of wrapped lanes. */
261
+ export declare const LANE_BLOCK_GAP = 40;
262
+ /** The length of a stub line between a box and its pill, the clear space past a pill, and the room a lane grows by for a pill. */
263
+ export declare const STUB = 12, STUB_CLEAR = 8, STUB_ROOM = 30;
264
+ /** The width of a box as the SVG draws it, before a diamond adds its 70 px. */
265
+ export declare function nodeWidth(item: FigNode, carded: boolean): number;
266
+ /** The room a diamond of size w by h gives a text line whose far edge sits `far` px from the center: the width of the outline there, less 8 px. */
267
+ export declare const diamondRoom: (w: number, h: number, far: number) => number;
268
+ /** The lines of a diamond with no card. The sub wraps to the room the outline gives its last line; the diamond grows taller by 15 px
269
+ * for each line until the lines fit. `dy` is the baseline of each line below the center, as the SVG draws it: the label sits 2 px
270
+ * above the center over a one-line sub, and 5 px below it with no sub. `far` is the distance from the center to the far edge of the text. */
271
+ export declare function diamondLines(item: FigNode, w: number): {
272
+ h: number;
273
+ label: {
274
+ dy: number;
275
+ far: number;
276
+ };
277
+ subs: {
278
+ text: string;
279
+ dy: number;
280
+ far: number;
281
+ }[];
282
+ };
283
+ /** The lane copies (`laneIndex@block`) that hold a stub end with no place clear of every edge path. A renderer grows each by
284
+ * STUB_ROOM and routes again: the pill then fits under or over its box, inside the band. Both renderers call it with their routes. */
285
+ export declare function tightCopies(lanes: FigGroup[], ends: Map<string, [number, number]>, edges: FlowProps['edges'], routed: {
286
+ id: string;
287
+ stub?: {
288
+ tight: [boolean, boolean];
289
+ };
290
+ }[]): Set<string>;
291
+ /** The width of the label gutter of lanes and of a timeline: the widest track label plus the frame sides. */
292
+ export declare const laneGutter: (lanes: FigGroup[]) => number;
293
+ /** How a lanes figure wraps. Both renderers lay out with it, so the player shows the blocks and stubs of the SVG. */
294
+ export type LanePlan = {
295
+ /** The time column of each box. */
296
+ cols: Map<string, number>;
297
+ /** The first column of each block, then the column count: block k holds columns starts[k] to starts[k + 1] - 1. */
298
+ starts: number[];
299
+ /** The most columns in one block. */
300
+ per: number;
301
+ /** The blocks that hold a box, in order. A block with no box is not drawn. A figure with no box keeps block 0. */
302
+ blocks: number[];
303
+ /** The width of the label gutter, the same in every block. */
304
+ gutter: number;
305
+ /** The space after each column: the gap to the next column of its block, or the right margin of the band for the last one. */
306
+ gaps: number[];
307
+ /** The room before the first column of each block, after the gutter. */
308
+ lead: number[];
309
+ /** The block of the source and of the target of each edge, by edge id. A lane end names the copy of the lane in that block. */
310
+ ends: Map<string, [number, number]>;
311
+ /** The edges whose ends sit in two blocks, by edge id: the pill texts at the source and at the target, then a shorter source
312
+ * text without the edge label, if the edge has one. */
313
+ stubs: Map<string, string[]>;
314
+ /** Edges between two boxes of one lane and one block, with a box of that lane between them and no `around` of their own.
315
+ * A straight route would cross that box, so the renderers route them around below. */
316
+ around: Set<string>;
317
+ /** The lane copies (`laneIndex@block`) that grow by STUB_ROOM at the bottom: a stub pill there has no room beside its box. */
318
+ tall: Set<string>;
319
+ /** Edges with a lane end that has no block on the side of the other end. They use the nearest block; `check` reports them. */
320
+ lost: string[];
321
+ };
322
+ /** The block of a time column, from the block starts of a plan. */
323
+ export declare const laneBlock: (starts: number[], c: number) => number;
324
+ /** The pill texts of a cross-block edge: "label → Target" at the source, "from Source" at the target, and "→ Target", a
325
+ * shorter source text for a pill that has no place for the long one. */
326
+ export declare const stubTexts: (label: unknown, from: unknown, to: unknown) => string[];
327
+ /**
328
+ * The wrap of a lanes figure. If all the time columns do not fit `width` with text at `minText` or more, the lanes wrap like a
329
+ * line of text: block 1 holds columns 0..n-1, block 2 holds n..2n-1. n is the largest count that fits; it is 1 at least.
330
+ * The text test is the one `check` runs: the smallest text (11 px, or 10.5 px with a mono card row) times width / figure width.
331
+ * If the wrap cannot make the text readable, the lanes do not wrap: the smallest font is under `minText`, or `floor` (the least
332
+ * width of the rest of the figure, such as the rail) is already too wide.
333
+ * The blocks share the columns as evenly as they can, and a block holds 2 columns at least: if 2 do not fit, the lanes keep
334
+ * one block. An edge between two blocks becomes two stubs with pills. A pill takes no room of its own, so n depends on the boxes.
335
+ */
336
+ export declare function lanePlan(fig: FlowProps, { width, minText, padding, floor }?: {
337
+ floor?: number | undefined;
338
+ minText?: number | undefined;
339
+ padding?: number | undefined;
340
+ width?: number | undefined;
341
+ }): LanePlan;
342
+ /**
343
+ * The rect id that an edge end routes to in wrapped lanes. A box keeps its id. A lane has one band per block, so a lane end names
344
+ * the band in the block from the plan (`band` finds it). A stub end at a lane is a point inside that band: the source at the band
345
+ * end, past the room for its pill, and the target at the first column of the block. The function adds the rect it names to `rects`.
346
+ */
347
+ export declare function laneEnd(plan: LanePlan, edge: string, id: string, start: boolean, band: (lane: string, block: number) => Rect | undefined, rects: Record<string, Rect>): string;
251
348
  /** Timeline constants, shared by both renderers: the bar height, the gap between rows, the least bar width, the milestone width,
252
349
  * the axis strip height and the axis width of the SVG. */
253
350
  export declare const TL_BAR_H = 28, TL_ROW_GAP = 8, TL_MIN_BAR = 24, TL_DIAMOND = 14, TL_AXIS_H = 28, TL_AXIS_W = 640;
package/dist/model.js CHANGED
@@ -1,4 +1,4 @@
1
- import { textWidth } from './text.js';
1
+ import { textWidth, wrap } from './text.js';
2
2
  export const toBeat = (b) => {
3
3
  const isBeat = typeof b === 'object' && !Array.isArray(b) && !('edge' in b);
4
4
  const { edges, ...rest } = isBeat ? b : { edges: b };
@@ -62,6 +62,8 @@ export const playheadItem = (items, beats, i) => {
62
62
  }
63
63
  return undefined;
64
64
  };
65
+ /** The item the playhead rests on when the loop starts: the playhead item of the first beat of the first step. */
66
+ export const loopStartItem = (items, steps) => playheadItem(items, (steps[0]?.flow ?? []).slice(0, 1).map(toBeat), 0);
65
67
  /** The box of a timeline label that sits beside its bar, in the bar's coordinates. */
66
68
  export const outsideLabelRect = (bar, label) => ({
67
69
  x: bar.x + bar.w + 6,
@@ -110,6 +112,254 @@ export function laneColumns(fig) {
110
112
  cols.set(n.id, n.at);
111
113
  return cols;
112
114
  }
115
+ /** The side of an SVG frame, and the width limits of a box with no card. The lane split measures with them. */
116
+ export const FRAME_SIDE = 18, NODE_MIN_W = 100, NODE_MAX_W = 190;
117
+ /** The gap between two blocks of wrapped lanes. */
118
+ export const LANE_BLOCK_GAP = 40;
119
+ /** The length of a stub line between a box and its pill, the clear space past a pill, and the room a lane grows by for a pill. */
120
+ export const STUB = 12, STUB_CLEAR = 8, STUB_ROOM = 30;
121
+ /** The width of a box as the SVG draws it, before a diamond adds its 70 px. */
122
+ export function nodeWidth(item, carded) {
123
+ if (item.width != null)
124
+ return item.width;
125
+ if (carded)
126
+ return CARD_WIDTH;
127
+ const label = textWidth(str(item.label), 14) + 32;
128
+ const sub = textWidth(str(item.sub), 12) + 32;
129
+ return Math.min(NODE_MAX_W, Math.max(NODE_MIN_W, label, sub));
130
+ }
131
+ /** The room a diamond of size w by h gives a text line whose far edge sits `far` px from the center: the width of the outline there, less 8 px. */
132
+ export const diamondRoom = (w, h, far) => w * Math.max(0, 1 - far / (h / 2)) - 8;
133
+ /** The lines of a diamond with no card. The sub wraps to the room the outline gives its last line; the diamond grows taller by 15 px
134
+ * for each line until the lines fit. `dy` is the baseline of each line below the center, as the SVG draws it: the label sits 2 px
135
+ * above the center over a one-line sub, and 5 px below it with no sub. `far` is the distance from the center to the far edge of the text. */
136
+ export function diamondLines(item, w) {
137
+ const sub = item.sub ? str(item.sub) : '';
138
+ for (let n = sub ? 1 : 0;; n++) {
139
+ const h = 62 + 15 * n;
140
+ const dy0 = n ? -2 - 7.5 * (n - 1) : 5;
141
+ const at = (dy, size) => Math.max(Math.abs(dy - 0.75 * size), dy + 0.25 * size);
142
+ const last = n ? at(dy0 + 15 * n, 12) : 0;
143
+ const lines = n ? wrap(sub, diamondRoom(w, h, last), 12) : [];
144
+ if (lines.length <= n || n >= 6)
145
+ return {
146
+ h,
147
+ label: { dy: dy0, far: at(dy0, 14) },
148
+ subs: lines.map((text, k) => ({ text, dy: dy0 + 15 * (k + 1), far: at(dy0 + 15 * (k + 1), 12) })),
149
+ };
150
+ }
151
+ }
152
+ /** The lane copies (`laneIndex@block`) that hold a stub end with no place clear of every edge path. A renderer grows each by
153
+ * STUB_ROOM and routes again: the pill then fits under or over its box, inside the band. Both renderers call it with their routes. */
154
+ export function tightCopies(lanes, ends, edges, routed) {
155
+ const laneOf = (id) => lanes.findIndex((l) => l.id === id || l.children.some((b) => b.id === id));
156
+ const out = new Set();
157
+ for (const r of routed) {
158
+ const e = edges.find((x) => edgeId(x) === r.id);
159
+ const [fb, tb] = ends.get(r.id) ?? [];
160
+ if (!e || !r.stub)
161
+ continue;
162
+ if (r.stub.tight[0] && fb != null)
163
+ out.add(`${laneOf(e.from)}@${fb}`);
164
+ if (r.stub.tight[1] && tb != null)
165
+ out.add(`${laneOf(e.to)}@${tb}`);
166
+ }
167
+ return out;
168
+ }
169
+ /** The width of the label gutter of lanes and of a timeline: the widest track label plus the frame sides. */
170
+ export const laneGutter = (lanes) => Math.max(0, ...lanes.map((l) => textWidth(str(l.label).toUpperCase(), 12))) + FRAME_SIDE * 2;
171
+ /** The block of a time column, from the block starts of a plan. */
172
+ export const laneBlock = (starts, c) => {
173
+ let k = 0;
174
+ while (k < starts.length - 2 && c >= starts[k + 1])
175
+ k++;
176
+ return k;
177
+ };
178
+ /** The pill texts of a cross-block edge: "label → Target" at the source, "from Source" at the target, and "→ Target", a
179
+ * shorter source text for a pill that has no place for the long one. */
180
+ export const stubTexts = (label, from, to) => [
181
+ `${str(label) ? str(label) + ' ' : ''}→ ${str(to)}`,
182
+ `from ${str(from)}`,
183
+ ...(str(label) ? [`→ ${str(to)}`] : []),
184
+ ];
185
+ /**
186
+ * The wrap of a lanes figure. If all the time columns do not fit `width` with text at `minText` or more, the lanes wrap like a
187
+ * line of text: block 1 holds columns 0..n-1, block 2 holds n..2n-1. n is the largest count that fits; it is 1 at least.
188
+ * The text test is the one `check` runs: the smallest text (11 px, or 10.5 px with a mono card row) times width / figure width.
189
+ * If the wrap cannot make the text readable, the lanes do not wrap: the smallest font is under `minText`, or `floor` (the least
190
+ * width of the rest of the figure, such as the rail) is already too wide.
191
+ * The blocks share the columns as evenly as they can, and a block holds 2 columns at least: if 2 do not fit, the lanes keep
192
+ * one block. An edge between two blocks becomes two stubs with pills. A pill takes no room of its own, so n depends on the boxes.
193
+ */
194
+ export function lanePlan(fig, { width = 830, minText = 10, padding = 24, floor = 0 } = {}) {
195
+ const cols = laneColumns(fig);
196
+ const n = Math.max(-1, ...cols.values()) + 1;
197
+ const shown = (fig.steps ?? []).flatMap((s) => s.flow.flatMap((b) => Object.entries(toBeat(b).show ?? {})));
198
+ const carded = new Set(shown.map(([id]) => id));
199
+ const mono = shown.some(([, c]) => isRows(c) && c.some((r) => r.mono));
200
+ const colW = Array.from({ length: n }, () => 0);
201
+ const byId = new Map(nodes(fig.layout).map((b) => [b.id, b]));
202
+ for (const b of byId.values()) {
203
+ const c = cols.get(b.id);
204
+ colW[c] = Math.max(colW[c], nodeWidth(b, carded.has(b.id)) + (b.shape === 'decision' ? 70 : 0));
205
+ }
206
+ const font = mono ? 10.5 : 11;
207
+ const limit = (width * font) / minText;
208
+ const room = limit - padding * 2;
209
+ const lanes = fig.layout.children;
210
+ const laneOf = new Map(lanes.filter((l) => l.id != null).map((l) => [l.id, l]));
211
+ const gutter = laneGutter(lanes);
212
+ const plan = (count) => {
213
+ // count blocks share the n columns evenly; the first n % count blocks take one column more.
214
+ const starts = [0];
215
+ for (let k = 0; k < count; k++)
216
+ starts.push(starts[k] + Math.floor(n / count) + (k < n % count ? 1 : 0));
217
+ const colBlock = (c) => laneBlock(starts, c);
218
+ const blockOf = (id) => colBlock(cols.get(id));
219
+ const held = [...new Set([...byId.keys()].map(blockOf))].sort((a, b) => a - b);
220
+ const blocks = held.length ? held : [0];
221
+ const wrapped = blocks.length > 1;
222
+ // The blocks that show a lane: a wrapped block shows only the lanes with a box in it.
223
+ const showing = (l) => wrapped ? [...new Set(l.children.map((b) => blockOf(b.id)))].sort((a, b) => a - b) : blocks;
224
+ const stubs = new Map();
225
+ const ends = new Map();
226
+ const lost = [];
227
+ fig.edges.forEach((e) => {
228
+ const id = edgeId(e);
229
+ const [fl, tl] = [laneOf.get(e.from), laneOf.get(e.to)];
230
+ if (!(byId.has(e.from) || fl) || !(byId.has(e.to) || tl))
231
+ return;
232
+ // A lane source takes the last block at or before the target that shows it; a lane target the first at or after the source.
233
+ let miss = false;
234
+ const near = (l, k, after) => {
235
+ const ks = showing(l);
236
+ const hit = after ? ks.find((b) => b >= k) : ks.findLast((b) => b <= k);
237
+ if (hit != null)
238
+ return hit;
239
+ miss = true;
240
+ return after ? ks.at(-1) : ks[0];
241
+ };
242
+ let fb, tb;
243
+ if (!fl)
244
+ fb = blockOf(e.from);
245
+ if (!tl)
246
+ tb = blockOf(e.to);
247
+ if (fl && tl)
248
+ fb = showing(fl)[0];
249
+ if (fl && !tl)
250
+ fb = near(fl, tb, false);
251
+ if (tl)
252
+ tb = near(tl, fb, true);
253
+ if (fb == null || tb == null)
254
+ return void (wrapped && lost.push(id));
255
+ if (miss)
256
+ lost.push(id);
257
+ ends.set(id, [fb, tb]);
258
+ if (fb === tb)
259
+ return;
260
+ const texts = stubTexts(e.label, (byId.get(e.from) ?? fl).label, (byId.get(e.to) ?? tl).label);
261
+ stubs.set(id, texts);
262
+ });
263
+ // A pill takes no room of its own: the route puts it in free space (route() in geometry.ts), so n depends on the boxes only.
264
+ // A label on an edge between two next columns of one block sits in the gap between them. A label too wide for the gap would
265
+ // cover a box, so that gap grows to the label plus 8 px. A label that fits keeps the gap of 0.4.0.
266
+ const label = Array.from({ length: n }, () => 0);
267
+ for (const e of fig.edges) {
268
+ const [a, b] = [cols.get(e.from), cols.get(e.to)];
269
+ if (e.label == null || a == null || b == null || Math.abs(a - b) !== 1 || colBlock(a) !== colBlock(b))
270
+ continue;
271
+ const w = labelPillW(str(e.label));
272
+ if (w - 2 > LANE_GAP)
273
+ label[Math.min(a, b)] = Math.max(label[Math.min(a, b)], w + 8);
274
+ }
275
+ const gaps = colW.map((_, c) => (c === starts[colBlock(c) + 1] - 1 ? FRAME_SIDE : Math.max(LANE_GAP, label[c])));
276
+ const per = Math.max(...starts.slice(1).map((e, k) => e - starts[k]));
277
+ const lead = Array.from({ length: count }, () => 0);
278
+ // A pill takes free room only: a gap next to a stub end grows for its pill while the block still fits the width. So the
279
+ // block count never depends on a pill. A pill with no room finds a free place in route() (geometry.ts).
280
+ const want = (text) => STUB + labelPillW(text) + STUB_CLEAR;
281
+ const fit = blocks.every((k) => widthOf(starts[k], starts[k + 1], gaps, 0) <= room);
282
+ const out = new Map(), inn = new Map();
283
+ const roomOut = new Set(), roomIn = new Set();
284
+ for (const [id, texts] of stubs) {
285
+ const e = fig.edges.find((x) => edgeId(x) === id);
286
+ const [a, b] = [cols.get(e.from), cols.get(e.to)];
287
+ if (a != null)
288
+ out.set(a, Math.max(out.get(a) ?? 0, want(texts[0])));
289
+ if (b != null)
290
+ inn.set(b, Math.max(inn.get(b) ?? 0, want(texts[1])));
291
+ }
292
+ for (const k of blocks) {
293
+ const [c0, end] = [starts[k], starts[k + 1]];
294
+ let free = room - widthOf(c0, end, gaps, 0);
295
+ const grow = (extra, apply) => {
296
+ if (extra > 0 && extra <= free) {
297
+ free -= extra;
298
+ apply();
299
+ }
300
+ };
301
+ grow(inn.get(c0) ?? 0, () => (lead[k] = inn.get(c0)));
302
+ if (!inn.has(c0) || lead[k])
303
+ roomIn.add(c0);
304
+ for (let c = c0; c < end; c++) {
305
+ const need = (out.get(c) ?? 0) + (c < end - 1 ? (inn.get(c + 1) ?? 0) : 0);
306
+ grow(need - gaps[c], () => (gaps[c] = need));
307
+ if (gaps[c] >= need)
308
+ roomOut.add(c).add(-1 - (c + 1)); // -1 - c marks room before column c
309
+ }
310
+ }
311
+ // A stub end with no room beside its box makes its lane taller in that block: the pill then fits under the box, inside the band.
312
+ const laneIndex = (id) => lanes.findIndex((l) => l.id === id || l.children.some((b) => b.id === id));
313
+ const tall = new Set();
314
+ for (const [id] of stubs) {
315
+ const e = fig.edges.find((x) => edgeId(x) === id);
316
+ const [a, b] = [cols.get(e.from), cols.get(e.to)];
317
+ const [fb, tb] = ends.get(id);
318
+ if (a == null || !roomOut.has(a))
319
+ tall.add(`${laneIndex(e.from)}@${fb}`);
320
+ if (b == null || !(b === starts[tb] ? roomIn.has(b) : roomOut.has(-1 - b)))
321
+ tall.add(`${laneIndex(e.to)}@${tb}`);
322
+ }
323
+ const laneOfBox = new Map(lanes.flatMap((l) => l.children.map((b) => [b.id, l])));
324
+ const around = new Set(fig.edges
325
+ .filter((e) => {
326
+ const [a, b, l] = [cols.get(e.from), cols.get(e.to), laneOfBox.get(e.from)];
327
+ if (e.around || a == null || b == null || !l || l !== laneOfBox.get(e.to) || colBlock(a) !== colBlock(b))
328
+ return false;
329
+ return l.children.some((x) => cols.get(x.id) > Math.min(a, b) && cols.get(x.id) < Math.max(a, b));
330
+ })
331
+ .map(edgeId));
332
+ return [{ cols, starts, per, blocks, gutter, gaps, lead, ends, stubs, around, tall, lost }, fit];
333
+ };
334
+ const widthOf = (c, end, gaps, lead) => gutter + lead + colW.slice(c, end).reduce((a, w) => a + w, 0) + gaps.slice(c, end).reduce((a, g) => a + g, 0);
335
+ // The wrap only narrows the lanes. If the text stays too small at any width, keep one block, and check reports small-text.
336
+ if (font < minText || floor > limit)
337
+ return plan(1)[0];
338
+ // A block has 2 columns at least: one column per block reads as a list, not as lanes. If 2 do not fit, keep one block.
339
+ // The fit uses the boxes and the label gaps only, before a pill takes free room.
340
+ for (let count = 1; count <= Math.max(1, Math.floor(n / 2)); count++) {
341
+ const [p, fit] = plan(count);
342
+ if (fit)
343
+ return p;
344
+ }
345
+ return plan(1)[0];
346
+ }
347
+ /**
348
+ * The rect id that an edge end routes to in wrapped lanes. A box keeps its id. A lane has one band per block, so a lane end names
349
+ * the band in the block from the plan (`band` finds it). A stub end at a lane is a point inside that band: the source at the band
350
+ * end, past the room for its pill, and the target at the first column of the block. The function adds the rect it names to `rects`.
351
+ */
352
+ export function laneEnd(plan, edge, id, start, band, rects) {
353
+ const k = plan.ends.get(edge)?.[start ? 0 : 1];
354
+ const b = k == null ? undefined : band(id, k);
355
+ if (plan.blocks.length < 2 || plan.cols.has(id) || !b)
356
+ return id;
357
+ const texts = plan.stubs.get(edge);
358
+ const key = texts ? `${edge}@${start ? 's' : 't'}` : `${id}@${k}`;
359
+ const x = start ? b.x + b.w - (STUB + labelPillW(texts?.[0] ?? '') + STUB_CLEAR) : b.x + plan.gutter + plan.lead[k];
360
+ rects[key] = texts ? { x, y: b.y, w: 0, h: b.h } : b;
361
+ return key;
362
+ }
113
363
  /** Timeline constants, shared by both renderers: the bar height, the gap between rows, the least bar width, the milestone width,
114
364
  * the axis strip height and the axis width of the SVG. */
115
365
  export const TL_BAR_H = 28, TL_ROW_GAP = 8, TL_MIN_BAR = 24, TL_DIAMOND = 14, TL_AXIS_H = 28, TL_AXIS_W = 640;
package/dist/scene.d.ts CHANGED
@@ -14,21 +14,30 @@ export type SceneBox = {
14
14
  };
15
15
  /** `step` marks a rail row: rows of two steps never show at the same time, so their labels cannot overlap. */
16
16
  /** `behind` marks an edge drawn under the boxes (a timeline dependency): `check` skips its crossing test. */
17
+ /** `pts` are the corners of an edge drawn as straight runs (a cross-block lanes edge): `check` tests them in place of `curve`. */
17
18
  export type SceneEdge = {
18
19
  id: string;
19
20
  from: string;
20
21
  to: string;
21
22
  curve: [Pt, Pt, Pt, Pt];
23
+ pts?: Pt[];
22
24
  label?: Rect;
23
25
  step?: number;
24
26
  behind?: true;
27
+ /** The corners of a timeline elbow. `check` tests a `behind` edge against the boxes only when it has them. */
28
+ elbow?: Pt[];
25
29
  };
26
30
  /** `minFont` is the smallest reading text in px, before any scale. Group frames are not boxes. */
31
+ /** `lanes` are the lane bands of a swimlanes figure, one per lane and block, named by the lane label. A stub pill stays inside one. */
27
32
  export type Scene = {
28
33
  width: number;
29
34
  boxes: SceneBox[];
30
35
  edges: SceneEdge[];
31
36
  minFont: number;
37
+ lanes?: {
38
+ id: string;
39
+ rect: Rect;
40
+ }[];
32
41
  };
33
42
  /** One fault that `flowfig check` found. `rule` names the check, for example `unknown-id`, `text-overflow`, `edge-crosses-box`, `label-overlap`, `small-text`, `low-contrast`. `ids` are the boxes or edges it names. */
34
43
  export type Finding = {
package/dist/svg.d.ts CHANGED
@@ -15,6 +15,9 @@ export type SvgOptions = {
15
15
  padding?: number;
16
16
  /** Colors, in place of `FlowProps.theme`. Default: `FlowProps.theme`. */
17
17
  theme?: FigTheme;
18
+ /** Lanes: the page width and the smallest text that decide when the time columns wrap into blocks. Default: 830 and 10, as in `check`. */
19
+ width?: number;
20
+ minText?: number;
18
21
  };
19
22
  /**
20
23
  * The SVG string, and the scene it drew. The scene lets `flowfig check` test the same layout the SVG uses.