flowfig 0.7.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/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 pills = edges
427
- .filter((e) => e.label != null && side.has(e.from) && side.has(e.to) && side.get(e.from) !== side.get(e.to))
428
- .map((e) => labelPillW(str(e.label)) + 16);
429
- return Math.max(56, ...pills);
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;
package/dist/source.d.ts CHANGED
@@ -12,6 +12,6 @@ export type Link = {
12
12
  symbol?: string;
13
13
  };
14
14
  /** Every box, edge and hop with its raw `source`, well-formed or not. The one owner list that `links` and `checkSpec` share. */
15
- export declare function owners(fig: FlowProps): [string, string | undefined][];
15
+ export declare function owners(fig: FlowProps): [string, string | undefined, string | undefined][];
16
16
  /** Every well-formed link in the figure, with the box, edge or hop that carries it. `bad-source` covers the rest. */
17
17
  export declare function links(fig: FlowProps): Link[];
package/dist/source.js CHANGED
@@ -15,9 +15,9 @@ export const headingSlug = (text) => text
15
15
  /** Every box, edge and hop with its raw `source`, well-formed or not. The one owner list that `links` and `checkSpec` share. */
16
16
  export function owners(fig) {
17
17
  return [
18
- ...nodes(fig.layout).map((n) => [`box "${n.id}"`, n.source]),
19
- ...fig.edges.map((e) => [`edge "${edgeId(e)}"`, e.source]),
20
- ...(fig.steps ?? []).flatMap((s) => s.flow.flatMap((b) => toBeat(b).hops.map((h) => [`hop on "${h.edge}"`, h.source]))),
18
+ ...nodes(fig.layout).map((n) => [`box "${n.id}"`, n.source, undefined]),
19
+ ...fig.edges.map((e) => [`edge "${edgeId(e)}"`, e.source, e.via]),
20
+ ...(fig.steps ?? []).flatMap((s) => s.flow.flatMap((b) => toBeat(b).hops.map((h) => [`hop on "${h.edge}"`, h.source, h.via]))),
21
21
  ];
22
22
  }
23
23
  /** Every well-formed link in the figure, with the box, edge or hop that carries it. `bad-source` covers the rest. */
package/dist/svg.js CHANGED
@@ -1,14 +1,14 @@
1
1
  // Wrapping is approximate: text is measured by character class, not by a browser.
2
- import { route } from './geometry.js';
2
+ import { arcRoom, avoidOf, route } from './geometry.js';
3
3
  import { foldedLabel, groupBox, layoutRail, railState, RAIL } from './rail.js';
4
4
  import { textWidth, wrap } from './text.js';
5
5
  import { checkRendered, planFor } from './check.js';
6
6
  export { parseSource, links } from './source.js';
7
7
  export { diff, formatDiff } from './diff.js';
8
- import { ASYNC_TAG_W, BASE_RATE, CARD_LINE, CARD_PAD, CARD_WIDTH, DARK, EDGE_OFF, EDGE_ON, LIGHT, TONES, toneFill, toneTint, labelPillW, ON_ACCENT, str, edgeId, groupGap, isGroup, isRows, LANE_PAD, LANE_ROW_GAP, LANE_BLOCK_GAP, FRAME_SIDE, laneGutter as gutterOf, STUB_ROOM, laneBlock, laneEnd, tightCopies, nodeWidth, diamondLines, diamondRoom, isLanesLayout, timelineBeats, timelineLayout, TL_AXIS_H, TL_AXIS_W, TL_BAR_H, TL_ROW_GAP, toBeat, beatMs, STEP_HOLD_MS, playheadItem, loopStartItem, dateLabelRaised, labelSpan, outsideLabelRect, } from './model.js';
8
+ import { ASYNC_TAG_W, BASE_RATE, CARD_LINE, CARD_PAD, CARD_WIDTH, DARK, EDGE_OFF, EDGE_ON, LIGHT, TONES, toneFill, toneTint, labelPillW, ON_ACCENT, str, edgeId, groupGap, isGroup, isRows, LANE_PAD, LANE_ROW_GAP, LANE_BLOCK_GAP, FRAME_SIDE, laneGutter as gutterOf, STUB_ROOM, laneBlock, laneEnd, tightCopies, LABEL_LINE, itemWidth, labelLines, fitCap, diamondLines, diamondRoom, isLanesLayout, timelineBeats, timelineLayout, TL_AXIS_H, TL_AXIS_W, TL_BAR_H, TL_ROW_GAP, toBeat, beatMs, STEP_HOLD_MS, playheadItem, loopStartItem, dateLabelRaised, labelSpan, outsideLabelRect, edgeTip, } from './model.js';
9
9
  export { LIGHT, DARK } from './model.js';
10
10
  const LINE = CARD_LINE, CARD_SIDE = 8, ROW_GAP = 4;
11
- const LABEL_LINE = 18, SUB_LINE = 15;
11
+ const SUB_LINE = 15;
12
12
  const FRAME_TOP = 37, FRAME_BOTTOM = 18;
13
13
  // The card-on fills approximate the player's 8 % accent tint.
14
14
  const vars = (t, cardOn) => `--accent:${t.accent}; --fg:${t.fg}; --muted:${t.muted}; --bg:${t.bg}; --surface:${t.surface}; --border:${t.border}; --card-on:${cardOn}; --tint:color-mix(in srgb, var(--accent) 10%, var(--surface));`;
@@ -42,20 +42,18 @@ function layoutCard(c, width) {
42
42
  function size(item, s) {
43
43
  if (!isGroup(item)) {
44
44
  const contents = s.cards.get(item.id);
45
- const w = nodeWidth(item, contents != null) + (item.shape === 'decision' ? 70 : 0);
45
+ const w = itemWidth(item, s.cards, s.fig.edges, s.cap);
46
46
  const top = item.shape === 'store' ? 24 : 10;
47
47
  const card = contents ? 8 + s.cardH.get(item.id) : 0;
48
- const h = Math.max(top + LABEL_LINE + (item.sub ? SUB_LINE : 0) + card + 10, s.minH(item.id));
48
+ const label = labelLines(item, w).length * LABEL_LINE;
49
+ const h = Math.max(top + label + (item.sub ? SUB_LINE : 0) + card + 10, s.minH(item.id));
49
50
  if (item.shape !== 'decision')
50
51
  return { w, h };
51
52
  return { w, h: contents ? h + 24 : Math.max(diamondLines(item, w).h, s.minH(item.id) + 24) };
52
53
  }
53
54
  const kids = item.children.map((c) => size(c, s));
54
- const gap = s.gap(item);
55
- const along = kids.reduce((n, k) => n + (item.direction === 'column' ? k.h : k.w), 0) + gap * (kids.length - 1);
56
- const across = Math.max(...kids.map((k) => (item.direction === 'column' ? k.w : k.h)));
57
- const inner = item.direction === 'column' ? { w: across, h: along } : { w: along, h: across };
58
- return item.label != null ? { w: inner.w + FRAME_SIDE * 2, h: inner.h + FRAME_TOP + FRAME_BOTTOM } : inner;
55
+ const h = item.direction === 'column' ? kids.reduce((n, k) => n + k.h, 0) + s.gap(item) * (kids.length - 1) : Math.max(...kids.map((k) => k.h));
56
+ return { w: itemWidth(item, s.cards, s.fig.edges, s.cap), h: h + (item.label != null ? FRAME_TOP + FRAME_BOTTOM : 0) };
59
57
  }
60
58
  function placeLanes(fig, x, y, s, out) {
61
59
  const lanes = fig.layout.children;
@@ -186,7 +184,16 @@ export function render(fig, opts = {}) {
186
184
  const cardH = new Map();
187
185
  const lanes = !tl && fig.lanes && isLanesLayout(fig.layout);
188
186
  const plan = lanes ? planFor(fig, opts) : null;
189
- const sizes = { cards, cardH, minH, gap: (g) => groupGap(g, fig.edges), fig, plan, tall: new Set(plan?.tall) };
187
+ const sizes = {
188
+ cards,
189
+ cardH,
190
+ minH,
191
+ gap: (g) => groupGap(g, fig.edges),
192
+ fig,
193
+ plan,
194
+ tall: new Set(plan?.tall),
195
+ cap: fitCap(fig, opts.width ?? 830, pad),
196
+ };
190
197
  for (const [id, contents] of cards) {
191
198
  const width = CARD_WIDTH;
192
199
  const widths = [width];
@@ -227,13 +234,10 @@ export function render(fig, opts = {}) {
227
234
  around: e.around ?? (plan?.around.has(ids[i]) ? 'below' : undefined),
228
235
  ...(tl && { sides: ['r', 'l'], elbow: true }),
229
236
  ...(stubs.has(ids[i]) && { stub: stubs.get(ids[i]).map(labelPillW), bands: [bandOf(ids[i], e.from, 0), bandOf(ids[i], e.to, 1)] }),
230
- ...((stubs.size || lanes) && e.label != null && { labelW: labelPillW(str(e.label)) }),
231
- })), rects, tips, stubs.size
232
- ? placed.flatMap((p) => (!isGroup(p.item) ? [p] : p.lane ? [{ x: p.x, y: p.y, w: gutter, h: p.h }] : []))
233
- : [
234
- ...placed.filter((p) => p.tl && !p.tl.labelInside).map((p) => outsideLabelRect(p, str(p.item.label))),
235
- ...placed.filter((p) => p.tl).map((p) => ({ x: p.x, y: p.y, w: p.w, h: p.h, box: true })),
236
- ], stubs.size ? { x: placed[0].x + gutter, y: placed[0].y, w: placed[0].w - gutter, h: placed[0].h } : undefined, lanes ? { bands: placed.filter((p) => p.lane), boxes: placed.filter((p) => !isGroup(p.item)) } : undefined);
237
+ ...(!tl && e.label != null && { labelW: labelPillW(str(e.label)) }),
238
+ })), rects, tips, avoidOf(placed.filter((p) => !isGroup(p.item)), stubs.size
239
+ ? placed.filter((p) => p.lane).map((p) => ({ x: p.x, y: p.y, w: gutter, h: p.h }))
240
+ : placed.filter((p) => p.tl && !p.tl.labelInside).map((p) => outsideLabelRect(p, str(p.item.label))), stubs.size > 0), stubs.size ? { x: placed[0].x + gutter, y: placed[0].y, w: placed[0].w - gutter, h: placed[0].h } : undefined, lanes ? { bands: placed.filter((p) => p.lane), boxes: placed.filter((p) => !isGroup(p.item)) } : undefined, pad);
237
241
  };
238
242
  let routed = go();
239
243
  for (let pass = 0; plan && pass < 3; pass++) {
@@ -260,7 +264,11 @@ export function render(fig, opts = {}) {
260
264
  const dl = dia ? diamondLines(p.item, p.w) : null;
261
265
  const room = (far) => diamondRoom(p.w, p.h, far);
262
266
  const flat = (p.item.shape === 'decision' ? p.w - 70 : p.w) - 16;
263
- const texts = [{ text: str(p.item.label), fontSize: 14, room: dl ? room(dl.label.far) : flat }];
267
+ const texts = labelLines(p.item, p.w).map((text) => ({
268
+ text,
269
+ fontSize: 14,
270
+ room: dl ? room(dl.label.far) : flat,
271
+ }));
264
272
  if (dl)
265
273
  for (const l of dl.subs)
266
274
  texts.push({ text: l.text, fontSize: 12, room: room(l.far) });
@@ -443,6 +451,7 @@ export function render(fig, opts = {}) {
443
451
  cls(p.lane && 'lane', anim(on, 'stroke: var(--accent)', 'stroke: var(--border)', 'n')) +
444
452
  `/><text x="${n2(p.x + FRAME_SIDE)}" y="${n2(p.lane ? p.y + p.h / 2 + 4 : p.y + 20)}" class="frame">${esc(str(item.label).toUpperCase())}</text>`);
445
453
  }
454
+ const tip = (inner) => (item.source ? `<g><title>${esc(item.source)}</title>${inner}</g>` : inner);
446
455
  const bt = item.tone && TONES[item.tone];
447
456
  const stroke = cls(boxAnim(item.id, true, bt));
448
457
  if (p.tl) {
@@ -455,7 +464,7 @@ export function render(fig, opts = {}) {
455
464
  ? `<polygon points="${n2(cx)},${n2(p.y)} ${n2(p.x + p.w)},${n2(cy)} ${n2(cx)},${n2(p.y + p.h)} ${n2(p.x)},${n2(cy)}" fill="${fill0}" stroke="${stroke0}"${stroke}/>`
456
465
  : `<rect x="${n2(p.x)}" y="${n2(p.y)}" width="${n2(p.w)}" height="${n2(p.h)}" rx="6" fill="${fill0}" stroke="${stroke0}"${stroke}/>`;
457
466
  const tx = inside ? p.x + 8 : p.x + p.w + 6;
458
- return shape + `<text x="${n2(tx)}" y="${n2(cy + 4.5)}" class="bar">${esc(label)}</text>`;
467
+ return tip(shape + `<text x="${n2(tx)}" y="${n2(cy + 4.5)}" class="bar">${esc(label)}</text>`);
459
468
  }
460
469
  const rim = item.shape === 'store' ? cls(boxAnim(item.id, false, bt)) : '';
461
470
  const fill0 = bt ? toneTint(bt, 'var(--bg)') : 'var(--bg)';
@@ -471,11 +480,14 @@ export function render(fig, opts = {}) {
471
480
  ? `<path d="M${n2(p.x)} ${n2(p.y + 12)} a ${n2(p.w / 2)} 12 0 0 1 ${n2(p.w)} 0 v ${n2(p.h - 24)} a ${n2(p.w / 2)} 12 0 0 1 ${n2(-p.w)} 0 z" fill="${fill0}" stroke="${stroke0}"${stroke}/>` +
472
481
  `<path d="M${n2(p.x)} ${n2(p.y + 12)} a ${n2(p.w / 2)} 12 0 0 0 ${n2(p.w)} 0" fill="none" stroke="${stroke0}"${rim}/>`
473
482
  : `<rect x="${n2(p.x)}" y="${n2(p.y)}" width="${n2(p.w)}" height="${n2(p.h)}" rx="10" fill="${fill0}" stroke="${stroke0}"${stroke}/>`;
474
- const label = `<text x="${n2(cx)}" y="${n2(labelY)}" class="label">${esc(str(item.label))}</text>`;
483
+ const lines = labelLines(item, p.w);
484
+ const extra = (lines.length - 1) * LABEL_LINE;
485
+ const firstY = item.shape === 'store' || contents ? labelY : labelY - extra / 2;
486
+ const label = lines.map((l, i) => `<text x="${n2(cx)}" y="${n2(firstY + i * LABEL_LINE)}" class="label">${esc(l)}</text>`).join('');
475
487
  const sub = dl
476
488
  ? dl.subs.map((l) => `<text x="${n2(cx)}" y="${n2(p.y + p.h / 2 + l.dy)}" class="sub">${esc(l.text)}</text>`).join('')
477
489
  : item.sub
478
- ? `<text x="${n2(cx)}" y="${n2(labelY + SUB_LINE)}" class="sub">${esc(str(item.sub))}</text>`
490
+ ? `<text x="${n2(cx)}" y="${n2(firstY + extra + SUB_LINE)}" class="sub">${esc(str(item.sub))}</text>`
479
491
  : '';
480
492
  const dot = bt ?? 'var(--accent)';
481
493
  const my = n2(p.y + p.h / 2);
@@ -484,7 +496,7 @@ export function render(fig, opts = {}) {
484
496
  : item.mark === 'end'
485
497
  ? `<circle cx="${n2(p.x + p.w + 12)}" cy="${my}" r="6.25" fill="none" stroke="${dot}" stroke-width="1.5"/><circle cx="${n2(p.x + p.w + 12)}" cy="${my}" r="4" fill="${dot}"/>`
486
498
  : '';
487
- return shape + label + sub + mark + (contents ? card(p, cardTop, contents) : '');
499
+ return tip(shape + label + sub + mark + (contents ? card(p, cardTop, contents) : ''));
488
500
  });
489
501
  function card(p, top, contents) {
490
502
  const id = p.item.id;
@@ -550,11 +562,13 @@ export function render(fig, opts = {}) {
550
562
  const lit = cls(toned
551
563
  ? frames(on.map((x, i) => (x ? `stroke: ${col[i]}; stroke-width: ${EDGE_ON}` : off)), 'e', off)
552
564
  : anim(on, `stroke: var(--accent); stroke-width: ${EDGE_ON}`, off, 'e'));
553
- const path = r.stub
565
+ const tip = edgeTip(r.id, e.source, beats.flat());
566
+ const path0 = r.stub
554
567
  ? r.stub.parts
555
568
  .map((d) => `<path d="${d}" fill="none" stroke="var(--muted)" stroke-width="${EDGE_OFF}" marker-end="url(#arrow)"${lit}/>`)
556
569
  .join('') + `<path id="p-${esc(r.id)}" d="${r.d}" fill="none" stroke="none"/>`
557
570
  : `<path id="p-${esc(r.id)}" d="${r.d}" fill="none" stroke="var(--muted)" stroke-width="${EDGE_OFF}" marker-end="url(#arrow)"${lit}/>`;
571
+ const path = tip ? `<g><title>${esc(tip)}</title>${path0}</g>` : path0;
558
572
  const pill = (x, y, lw, text) => `<rect x="${n2(x - lw / 2)}" y="${n2(y - 9)}" width="${n2(lw)}" height="18" rx="9" fill="var(--bg)" stroke="var(--border)"` +
559
573
  cls(toned
560
574
  ? frames(on.map((x, i) => x ? `fill: ${tone[i] ? toneFill(col[i]) : col[i]}; stroke: ${col[i]}` : 'fill: var(--bg); stroke: var(--border)'), 'l', 'fill: var(--bg); stroke: var(--border)')
@@ -668,18 +682,18 @@ export function render(fig, opts = {}) {
668
682
  }
669
683
  }
670
684
  const bounds = placed[0];
671
- const arcs = fig.edges.some((e, i) => (e.around || plan?.around.has(ids[i])) && !stubs.has(ids[i]));
685
+ const [arcT, arcB, arcL, arcR] = ['above', 'below', 'left', 'right'].map((w) => arcRoom(routed, w));
672
686
  const capLines = [...new Set(captions)].flatMap((c) => wrap(c, Math.max(560, bounds.w), 13.5).length);
673
- const mapW = Math.max(bounds.w + pad * 2, 560);
687
+ const mapW = Math.max(bounds.w + pad * 2 + arcL + arcR, 560);
674
688
  const rail = fig.rail ? layoutRail(fig, fig.rail === 'only' ? 560 : mapW) : null;
675
689
  const only = fig.rail === 'only' && rail != null;
676
690
  const capTop = rail ? 20 : 26;
677
691
  const capH = steps.length ? capTop + 4 + Math.max(0, ...capLines) * 20 : 0;
678
692
  const W = only ? Math.max(560, rail.width) : Math.max(mapW, rail?.width ?? 0);
679
- const mapH = only ? 0 : bounds.h + pad * 2 + (arcs ? 44 : 0);
693
+ const mapH = only ? 0 : bounds.h + pad * 2 + arcT + arcB;
680
694
  const top = only ? pad : mapH + RAIL.gap;
681
695
  const H = only ? pad + rail.height + capH : mapH + (rail ? RAIL.gap + rail.height : 0) + capH;
682
- const shift = (W - (bounds.w + pad * 2)) / 2;
696
+ const shift = (W - (bounds.w + pad * 2 + arcL + arcR)) / 2 + arcL;
683
697
  const railX = rail ? (W - rail.width) / 2 : 0;
684
698
  const labels = rail || synthetic
685
699
  ? []
@@ -758,7 +772,8 @@ export function render(fig, opts = {}) {
758
772
  ? `<rect x="${n2(row.pill.x)}" y="${n2(ly - 9)}" width="${n2(row.pill.w)}" height="18" rx="9" fill="var(--bg)" stroke="var(--border)"${cls(anim(now, `fill: ${row.tone ? toneFill(col) : col}; stroke: ${col}`, 'fill: var(--bg); stroke: var(--border)', row.tone ? 'l' + col : 'l'))}/>` +
759
773
  `<text x="${n2(row.pill.x + row.pill.w / 2)}" y="${n2(ly + 4)}"${cls('edgelabel', anim(now, `fill: ${ON_ACCENT}`, 'fill: var(--muted)', 'x'))}>${esc(row.text)}</text>`
760
774
  : '';
761
- parts.push(`<g${cls('railrow', moveRow(i))}><g${cls(anim(next, 'opacity: .45', 'opacity: 1', 'r'))}>` +
775
+ const tip = edgeTip(row.edge, fig.edges[ids.indexOf(row.edge)].source, beats.flat());
776
+ parts.push(`<g${cls('railrow', moveRow(i))}>${tip ? `<title>${esc(tip)}</title>` : ''}<g${cls(anim(next, 'opacity: .45', 'opacity: 1', 'r'))}>` +
762
777
  `<path d="M ${n2(x1)} ${n2(ly)} H ${n2(x2)}" fill="none" stroke="var(--muted)" stroke-width="${EDGE_OFF}"${row.async ? ' stroke-dasharray="4 3"' : ''} marker-end="url(#arrow)"${cls(lit)}/>` +
763
778
  tag +
764
779
  pill +
@@ -796,7 +811,7 @@ ${css.join('\n')}
796
811
  <rect width="100%" height="100%" fill="var(--bg)"/>
797
812
  ${only
798
813
  ? ''
799
- : `<g transform="translate(${n2(shift)} ${arcs ? 44 : 0})">
814
+ : `<g transform="translate(${n2(shift)} ${arcT})">
800
815
  ${axisSvg}
801
816
  ${tl ? boxes.filter((b, i) => b && isGroup(placed[i].item)).join('\n') : ''}${lineSvg && `\n${lineSvg}`}
802
817
  ${tl ? edgeSvg.join('\n') : ''}
@@ -814,6 +829,7 @@ ${said.join('\n')}
814
829
  fonts.push(12, 13);
815
830
  const scene = {
816
831
  width: n2(W),
832
+ ...(!only && { area: { x: -shift, y: -arcT, w: W, h: mapH } }),
817
833
  boxes: only ? [] : sceneBoxes,
818
834
  edges: [
819
835
  ...(only ? [] : routed).flatMap((r) => {
@@ -827,14 +843,15 @@ ${said.join('\n')}
827
843
  to: e.to,
828
844
  curve: r.curve,
829
845
  label: labelRects[r.id],
830
- ...(tl && { behind: true, elbow: r.elbow }),
846
+ ...(tl && { behind: true }),
847
+ ...(r.elbow && { elbow: r.elbow }),
831
848
  },
832
849
  ];
833
850
  }),
834
851
  ...(rail?.rows ?? []).flatMap((row, i) => {
835
852
  if (row.kind !== 'message')
836
853
  return [];
837
- const dx = only ? railX : railX - shift, dy = only ? top : top - (arcs ? 44 : 0);
854
+ const dx = only ? railX : railX - shift, dy = only ? top : top - arcT;
838
855
  const y = dy + railState(rail, row.step)[i].y + RAIL.row / 2;
839
856
  const [x1, x2] = [dx + rail.columns[row.from].x, dx + rail.columns[row.to].x];
840
857
  const p = { x: x1, y }, q = { x: x2, y };
@@ -0,0 +1,55 @@
1
+ import { type CodeFile, type Lang, type Read } from './code.ts';
2
+ export type Decl = {
3
+ info: CodeFile;
4
+ start: number;
5
+ end: number;
6
+ name: string;
7
+ kind: 'class' | 'interface';
8
+ dir?: string;
9
+ };
10
+ export type Bind = {
11
+ mod: string | null;
12
+ imported: string;
13
+ dir: boolean;
14
+ };
15
+ type Globals = {
16
+ types: Map<string, string[]>;
17
+ impls: Map<string, Set<string>>;
18
+ aliases: Map<string, {
19
+ to: string;
20
+ path: string;
21
+ }>;
22
+ usings: Set<string>;
23
+ projects: string[];
24
+ };
25
+ type Index = {
26
+ types: Map<string, Decl | null>;
27
+ binds: Map<string, Map<string, Bind>>;
28
+ repo: string[] | null;
29
+ globals: Map<Lang, Globals>;
30
+ };
31
+ type Cache = Map<string, CodeFile | null>;
32
+ export type Ctx = {
33
+ root: string;
34
+ read: Read;
35
+ cache: Cache;
36
+ ix: Index;
37
+ };
38
+ export declare function makeCtx(root: string, read?: Read, cache?: Cache): Ctx;
39
+ export declare const lastSeg: (s: string) => string;
40
+ export declare const file: (cx: Ctx, path: string) => CodeFile | null;
41
+ export declare function bindings(cx: Ctx, f: CodeFile): Map<string, Bind>;
42
+ export declare function goDirFiles(cx: Ctx, dir: string): CodeFile[];
43
+ export declare function global(cx: Ctx, lang: 'java' | 'cs' | 'rs'): Globals;
44
+ export declare function declIn(info: CodeFile | null, name: string): Decl | null;
45
+ export declare function findType(cx: Ctx, ctx: CodeFile, name: string | undefined, qual?: string | null): Decl | null;
46
+ export declare function ancestors(cx: Ctx, d: Decl): {
47
+ name: string;
48
+ d: Decl | null;
49
+ }[];
50
+ export declare function goMethods(cx: Ctx, d: Decl): Set<string>;
51
+ export declare const goIfaceMethods: (d: Decl) => string[];
52
+ export declare function defines(cx: Ctx, d: Decl, n: string): boolean;
53
+ export declare function rustUses(fi: CodeFile, n: string, tp: string): 'yes' | 'no' | 'outside';
54
+ export declare const rustMod: (tp: string) => string;
55
+ export {};