flowfig 0.8.2 → 0.8.4

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 CHANGED
@@ -65,7 +65,7 @@ jobs:
65
65
  - uses: actions/setup-node@v4
66
66
  with:
67
67
  node-version: 22
68
- - uses: iamalvisng/flowfig@v0.8.2
68
+ - uses: iamalvisng/flowfig@v0.8.4
69
69
  with:
70
70
  figures: 'docs/**/*.svg' # default **/*.svg
71
71
  ```
@@ -478,7 +478,7 @@ jobs:
478
478
  - uses: actions/setup-node@v4
479
479
  with:
480
480
  node-version: 22
481
- - uses: iamalvisng/flowfig@v0.8.2
481
+ - uses: iamalvisng/flowfig@v0.8.4
482
482
  with:
483
483
  figures: 'docs/**/*.svg' # default **/*.svg
484
484
  ```
package/dist/code.d.ts CHANGED
@@ -18,7 +18,7 @@ export declare function matchClose(code: string, open: number): number;
18
18
  export declare function pyBody(code: string, start: number): Extent;
19
19
  export declare function outside(root: string, path: string): boolean;
20
20
  export declare function codeFile(root: string, path: string, read?: Read, cache?: Map<string, CodeFile | null>): CodeFile | null;
21
- export declare function locate(file: CodeFile, symbol: string): {
21
+ export declare const locate: (file: CodeFile, symbol: string) => {
22
22
  start: number;
23
23
  end: number;
24
24
  } | null;
package/dist/code.js CHANGED
@@ -511,7 +511,7 @@ export function codeFile(root, path, read = readFile, cache = new Map()) {
511
511
  cache.set(full, file);
512
512
  return file;
513
513
  }
514
- export function locate(file, symbol) {
514
+ function locateDefinition(file, symbol) {
515
515
  const { code, lang } = file;
516
516
  const parts = symbol.split('.');
517
517
  if (!parts.every((p) => /^[\p{L}_$][\p{L}\p{N}_$]*$/u.test(p)))
@@ -565,4 +565,33 @@ export function locate(file, symbol) {
565
565
  }
566
566
  return null;
567
567
  }
568
+ function enumMember(file, symbol) {
569
+ const { code, lang } = file;
570
+ const [owner, name, ...rest] = symbol.split('.');
571
+ if (!name || rest.length || lang === 'go' || lang === 'py')
572
+ return null;
573
+ const head = new RegExp(`\\benum\\s+${esc(owner)}${E}[^;{}]*\\{`, 'u').exec(code);
574
+ if (!head)
575
+ return null;
576
+ const open = head.index + head[0].length - 1;
577
+ const close = matchClose(code, open);
578
+ let from = open + 1;
579
+ for (let i = from, d = 0; i <= close; i++) {
580
+ const c = code[i];
581
+ if ('{(['.includes(c))
582
+ d++;
583
+ else if (')]'.includes(c) || (c === '}' && i < close))
584
+ d--;
585
+ else if (d > 0 || (c !== ',' && c !== ';' && i < close))
586
+ continue;
587
+ const seg = code.slice(from, i).replace(/^(\s|@\w+(\([^)]*\))?|#\[[^\]]*\]|\[[^\]]*\])+/, '');
588
+ if (new RegExp(`^${esc(name)}${E}`, 'u').test(seg))
589
+ return { start: i - seg.length, end: i };
590
+ if (c === ';')
591
+ return null;
592
+ from = i + 1;
593
+ }
594
+ return null;
595
+ }
596
+ export const locate = (file, symbol) => locateDefinition(file, symbol) ?? enumMember(file, symbol);
568
597
  export const isDefined = (file, symbol) => locate(file, symbol) != null;
package/dist/geometry.js CHANGED
@@ -1,4 +1,4 @@
1
- import { STUB } from './model.js';
1
+ import { EDGE_ON, STUB } from './model.js';
2
2
  export const avoidOf = (boxes, extra, lanes) => [
3
3
  ...extra,
4
4
  ...boxes.map((r) => (lanes ? r : { ...r, box: true })),
@@ -78,6 +78,8 @@ export function route(edges, rects, tips = new Set(), avoid = [], area, lanes, p
78
78
  if (around && blocked(around, arc)) {
79
79
  const fits = Array.from({ length: ARC_STEPS + 1 }, (_, k) => [around, outward(around, k)]);
80
80
  arc = fits.find(([way, at]) => !blocked(way, at))?.[1] ?? arc;
81
+ if (blocked(around, arc))
82
+ around = undefined;
81
83
  }
82
84
  if (!around && !e.sides && !e.stub && !e.elbow && past.length) {
83
85
  const ways = stacked ? ['left', 'right'] : ['above', 'below'];
@@ -177,6 +179,7 @@ export function route(edges, rects, tips = new Set(), avoid = [], area, lanes, p
177
179
  const all = Object.values(rects);
178
180
  const [x0, y0] = [Math.min(...all.map((r) => r.x)), Math.min(...all.map((r) => r.y))];
179
181
  const [x1, y1] = [Math.max(...all.map((r) => r.x + r.w)), Math.max(...all.map((r) => r.y + r.h))];
182
+ const room = (d) => (d > 0 ? d + EDGE_ON / 2 : 0);
180
183
  const outside = (way, { x, y }) => (way === 'above' ? y0 - y : way === 'below' ? y - y1 : way === 'left' ? x0 - x : x - x1);
181
184
  const roomOf = (way, at, curve, labelW) => Math.max(ARC - 6, lanes || !labelW ? outside(way, { x: at, y: at }) - 6 : outside(way, bezier(curve, 0.5)) + (upright(way) ? labelW / 2 : 9));
182
185
  const one = (p) => {
@@ -335,21 +338,24 @@ export function route(edges, rects, tips = new Set(), avoid = [], area, lanes, p
335
338
  if (e.x - s.x < 16 && s.y === e.y) {
336
339
  return { id: p.id, d: `M ${s.x} ${s.y} H ${e.x}`, mid: { x: (s.x + e.x) / 2, y: s.y }, curve: [s, s, e, e], elbow: [s, e] };
337
340
  }
341
+ const own = (r) => [p.a, p.b].some((q) => same(q, r));
342
+ const all = avoid.filter((r) => !own(r));
343
+ const boxes = all.filter((r) => r.box);
344
+ const vClear = (x, ya, yb, m = 2) => !all.some((r) => x > r.x - m && x < r.x + r.w + m && Math.max(ya, yb) > r.y && Math.min(ya, yb) < r.y + r.h);
345
+ const hClear = (y, xa, xb, list = boxes, m = 2) => !list.some((r) => y > r.y - m && y < r.y + r.h + m && Math.max(xa, xb) > r.x && Math.min(xa, xb) < r.x + r.w);
338
346
  if (e.x - s.x < 16) {
339
- const mx = s.x + 8, ex = e.x - 8, my = (s.y + e.y) / 2;
347
+ const mx = s.x + 8, ex = e.x - 8, mid = (s.y + e.y) / 2;
348
+ const ys = Array.from({ length: Math.floor((y1 - y0 + 24) / 4) + 1 }, (_, i) => y0 - 12 + i * 4).sort((u, w) => Math.abs(u - mid) - Math.abs(w - mid));
349
+ const my = ys.find((y) => vClear(mx, s.y, y) && hClear(y, ex, mx, all) && vClear(ex, y, e.y)) ?? mid;
340
350
  return {
341
351
  id: p.id,
342
352
  d: `M ${s.x} ${s.y} H ${mx} V ${my} H ${ex} V ${e.y} H ${e.x}`,
343
353
  mid: { x: (mx + ex) / 2, y: my },
344
354
  curve: [s, { x: mx, y: my }, { x: ex, y: my }, e],
345
355
  elbow: [s, { x: mx, y: s.y }, { x: mx, y: my }, { x: ex, y: my }, { x: ex, y: e.y }, e],
356
+ bleed: { above: room(y0 - my), below: room(my - y1) },
346
357
  };
347
358
  }
348
- const own = (r) => [p.a, p.b].some((q) => same(q, r));
349
- const all = avoid.filter((r) => !own(r));
350
- const boxes = all.filter((r) => r.box);
351
- const vClear = (x, ya, yb, m = 2) => !all.some((r) => x > r.x - m && x < r.x + r.w + m && Math.max(ya, yb) > r.y && Math.min(ya, yb) < r.y + r.h);
352
- const hClear = (y, xa, xb, list = boxes, m = 2) => !list.some((r) => y > r.y - m && y < r.y + r.h + m && Math.max(xa, xb) > r.x && Math.min(xa, xb) < r.x + r.w);
353
359
  const mid = (s.x + e.x) / 2;
354
360
  const gap = Array.from({ length: Math.floor((e.x - s.x - 16) / 4) + 1 }, (_, i) => s.x + 8 + i * 4).sort((u, w) => Math.abs(u - mid) - Math.abs(w - mid));
355
361
  const mx = [mid, e.x - 8, s.x + 8, ...gap].find((x) => vClear(x, s.y, e.y) && hClear(s.y, s.x, x) && hClear(e.y, x, e.x));
@@ -384,10 +390,10 @@ export function route(edges, rects, tips = new Set(), avoid = [], area, lanes, p
384
390
  curve: [s, { x, y }, { x: ex, y }, e],
385
391
  elbow: [s, { x, y: s.y }, { x, y }, { x: ex, y }, { x: ex, y: e.y }, e],
386
392
  bleed: {
387
- left: Math.max(0, x0 - Math.min(x, ex)),
388
- right: Math.max(0, Math.max(x, ex) - x1),
389
- above: Math.max(0, y0 - y),
390
- below: Math.max(0, y - y1),
393
+ left: room(x0 - Math.min(x, ex)),
394
+ right: room(Math.max(x, ex) - x1),
395
+ above: room(y0 - y),
396
+ below: room(y - y1),
391
397
  },
392
398
  };
393
399
  }
@@ -499,13 +505,16 @@ export function labelAlong(r, w, boxes, placed, fig, lines = []) {
499
505
  y: Math.max(fig.y + 9, Math.min(at.y, fig.y + fig.h - 9)),
500
506
  }
501
507
  : at;
502
- const free = (at, strict) => {
508
+ const free = (at, strict, room = 1) => {
503
509
  const pill = { x: at.x - w / 2, y: at.y - 9, w, h: 18 };
504
510
  return (inFig(at) &&
505
- !boxes.some((b) => overlaps(pill, b, 1)) &&
511
+ !boxes.some((b) => overlaps(pill, b, room)) &&
506
512
  !placed.some((b) => overlaps(pill, b, 1)) &&
507
513
  (!strict || !lines.some((q) => q.x > pill.x - 2 && q.x < pill.x + w + 2 && q.y > pill.y - 2 && q.y < pill.y + 20)));
508
514
  };
515
+ for (const at of spots)
516
+ if (free(at, true, 6))
517
+ return at;
509
518
  for (const strict of [true, false])
510
519
  for (const at of spots)
511
520
  if (free(at, strict))
package/dist/model.js CHANGED
@@ -452,20 +452,21 @@ export const counts = (p) => {
452
452
  };
453
453
  };
454
454
  const idsIn = (c) => (isGroup(c) ? [...(c.id ? [c.id] : []), ...c.children.flatMap(idsIn)] : [c.id]);
455
+ const LINE_CLEAR = 16;
455
456
  export function groupGap(g, edges) {
456
457
  const side = new Map();
457
458
  g.children.forEach((c, i) => idsIn(c).forEach((id) => side.set(id, i)));
458
459
  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
460
  if (g.gap != null && !spans.length)
460
461
  return g.gap;
461
- const need = Math.max(0, ...spans.map((e) => labelPillW(str(e.label)) + 16));
462
+ const need = Math.max(0, ...spans.map((e) => labelPillW(str(e.label)) + 2 * LINE_CLEAR));
462
463
  if (g.gap != null && g.direction !== 'column')
463
- return Math.max(g.gap, need - 16);
464
+ return Math.max(g.gap, need);
464
465
  let auto;
465
466
  if (g.direction === 'column') {
466
467
  const ends = spans.map((e) => [side.get(e.from), side.get(e.to)].sort((p, q) => p - q));
467
468
  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
+ auto = most ? Math.min(most, 3) * 22 + 2 * LINE_CLEAR - 4 : 28;
469
470
  }
470
471
  else
471
472
  auto = Math.max(56, need);
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "0.8.2";
1
+ export declare const VERSION = "0.8.4";
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // The release list bumps this with package.json.
2
- export const VERSION = '0.8.2';
2
+ export const VERSION = '0.8.4';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flowfig",
3
- "version": "0.8.2",
3
+ "version": "0.8.4",
4
4
  "description": "Your coding agent draws animated diagrams from your code, and flowfig checks each box and arrow against that code. CI fails when a diagram drifts from the code. The output is one animated SVG with no script, for a README, PR or issue. Works with Claude Code, Cursor, GitHub Copilot, Codex, Gemini CLI, Windsurf and Kiro.",
5
5
  "license": "MIT",
6
6
  "repository": {