flowfig 0.6.0 → 0.7.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/draw.js CHANGED
@@ -1,4 +1,3 @@
1
- // `flowfig draw`: one command to the first figure. Claude Code reads the repo and renders; draw checks the result itself.
2
1
  import { spawnSync } from 'node:child_process';
3
2
  import { existsSync, mkdirSync, rmSync, statSync } from 'node:fs';
4
3
  import { dirname, resolve } from 'node:path';
@@ -10,7 +9,7 @@ import { verify } from './verify.js';
10
9
  const ALLOWED = 'Read,Glob,Grep,Bash(npx flowfig *)';
11
10
  const DISALLOWED = 'Edit,MultiEdit,NotebookEdit';
12
11
  const USAGE = 'usage: flowfig draw "<question>" [--out <path>] [--model <alias>] [--max-turns <n>] [--json] [--open]';
13
- /** A file name from the question: lower case, letters and digits, joined by -, at most 60 characters. */
12
+ /** A file name from the question: at most 60 characters. */
14
13
  export function slug(question) {
15
14
  const s = question
16
15
  .toLowerCase()
@@ -21,7 +20,6 @@ export function slug(question) {
21
20
  return s || 'figure';
22
21
  }
23
22
  const specPath = (out) => out.replace(/\.svg$/, '.json');
24
- /** The claude argv after the binary. The system prompt carries the agent text, the output path and the folder rule. */
25
23
  export function agentArgs(o) {
26
24
  const spec = specPath(o.out);
27
25
  const system = `${AGENT_TEXT.trimEnd()}\n\nWrite the spec to ${spec} with the Write tool. Then run exactly \`npx flowfig ${spec} ${o.out}\` as one command, alone on its line: no heredoc, no \`;\`, no \`&&\`, no pipe (the permission rule matches one plain command only). Then run \`npx flowfig verify ${o.out}\` the same way. Run every command from this folder, ${o.cwd}.`;
@@ -51,7 +49,6 @@ const fail = (message, code) => {
51
49
  console.error(message);
52
50
  return code;
53
51
  };
54
- /** Run `flowfig draw`. Prints, and returns the exit code. */
55
52
  export async function runDraw(argv) {
56
53
  const args = [...argv];
57
54
  const flag = (n) => args.includes(n) && args.splice(args.indexOf(n), 1).length > 0;
@@ -103,7 +100,7 @@ export async function runDraw(argv) {
103
100
  catch (e) {
104
101
  return fail(`draw: ${e.message}`, 2);
105
102
  }
106
- // A figure from an earlier run must not pass as this run's work: compare the mtime.
103
+ // Compare the mtime: a figure from an earlier run must not pass.
107
104
  const mtime = () => (existsSync(full) ? statSync(full).mtimeMs : undefined);
108
105
  const before = mtime();
109
106
  const run = spawnSync(bin, agentArgs({ question, out, cwd, maxTurns, model }), {
@@ -112,7 +109,7 @@ export async function runDraw(argv) {
112
109
  stdio: ['ignore', 'pipe', 'inherit'],
113
110
  maxBuffer: 64 * 1024 * 1024,
114
111
  });
115
- // The JSON is the last line that starts with {: a warning line may come before it.
112
+ // The JSON is the last line that starts with {; a warning can come before.
116
113
  const line = (run.stdout ?? '')
117
114
  .split('\n')
118
115
  .reverse()
@@ -177,7 +174,6 @@ export async function runDraw(argv) {
177
174
  }
178
175
  stop();
179
176
  let code = errors || stopped !== undefined ? 1 : 0;
180
- // The SVG is on disk also with findings, so the user can look at it.
181
177
  if (open) {
182
178
  try {
183
179
  const line = openedLine((await openSvg(full))[0]);
@@ -9,8 +9,7 @@ export type Pt = {
9
9
  x: number;
10
10
  y: number;
11
11
  };
12
- /** A cross-block edge of wrapped lanes: the two drawn stubs, their pills, and their points for `check`. `d` then holds both stubs,
13
- * so a packet runs the source stub and jumps to the target stub. */
12
+ /** A cross-lane edge as two stubs, their pills and their points. */
14
13
  export type Stub = {
15
14
  parts: [string, string];
16
15
  pills: [Rect, Rect];
@@ -18,7 +17,7 @@ export type Stub = {
18
17
  short?: true;
19
18
  tight: [boolean, boolean];
20
19
  };
21
- /** `elbow` holds the corners of an elbow path, so `check` can test its straight runs. */
20
+ /** `elbow` holds the corners of an elbow path for `check`. */
22
21
  export type Routed = {
23
22
  id: string;
24
23
  d: string;
@@ -27,11 +26,12 @@ export type Routed = {
27
26
  stub?: Stub;
28
27
  elbow?: Pt[];
29
28
  };
30
- /** A rect that a route keeps clear of. `box` marks a box: an elbow's horizontal runs keep clear of a box, not of a label. */
29
+ /** A rect that a route keeps clear of. `box` marks a box. */
31
30
  export type Avoid = Rect & {
32
31
  box?: boolean;
33
32
  };
34
33
  type Around = 'above' | 'below';
34
+ /** Routes edges between measured boxes as curved SVG paths. `elbow` draws right-angle paths; `stub` splits a cross-lane edge in two. */
35
35
  export declare function route(edges: {
36
36
  id: string;
37
37
  from: string;
@@ -42,12 +42,10 @@ export declare function route(edges: {
42
42
  stub?: number[];
43
43
  bands?: [Rect | undefined, Rect | undefined];
44
44
  labelW?: number;
45
- }[], rects: Record<string, Rect>, tips?: Set<string>, avoid?: Avoid[], // what an elbow's vertical run and a stub pill must not cross (the timeline's outside labels; the boxes and gutters of lanes)
46
- area?: Rect, // where a stub pill may sit: the lanes right of the gutter
47
- lanes?: {
45
+ }[], rects: Record<string, Rect>, tips?: Set<string>, avoid?: Avoid[], area?: Rect, lanes?: {
48
46
  bands: Rect[];
49
47
  boxes: Rect[];
50
48
  }): Routed[];
51
- /** True if the segment a-b crosses the segment c-d. The ends of a-b are trimmed by 1.5 px, so a touch at a box side does not count. */
49
+ /** True if segment a-b crosses c-d. The ends of a-b are trimmed by 1.5 px. */
52
50
  export declare function crosses(a: Pt, b: Pt, c: Pt, d: Pt): boolean;
53
51
  export {};
package/dist/geometry.js CHANGED
@@ -1,27 +1,14 @@
1
1
  import { STUB } from './model.js';
2
- /** The least gap between a label pill and a lane border. */
3
2
  const LABEL_CLEAR = 4;
4
3
  const cx = (r) => r.x + r.w / 2;
5
4
  const cy = (r) => r.y + r.h / 2;
6
- /** A point on the cubic Bézier at `t`, as `check` samples an edge. */
7
5
  const bezier = ([p0, p1, p2, p3], t) => {
8
6
  const u = 1 - t;
9
7
  const [a, b, c, d] = [u * u * u, 3 * u * u * t, 3 * u * t * t, t * t * t];
10
8
  return { x: a * p0.x + b * p1.x + c * p2.x + d * p3.x, y: a * p0.y + b * p1.y + c * p2.y + d * p3.y };
11
9
  };
12
- // Turns edges between measured boxes into curved SVG paths.
13
- // Boxes stacked on top of each other connect bottom->top, otherwise side->side.
14
- // Several edges leaving the same side of a box are spread out so they don't overlap,
15
- // except on `tips` boxes (diamonds), where they all meet at the point.
16
- // `elbow` draws a right-angle path, from a right end to a left end: its four curve points are the corners.
17
- // `sides` fixes the two sides an edge uses (a timeline uses right to left).
18
- // `around` makes an edge leave and enter from the top or bottom, arcing over whatever sits between.
19
- // `bands` (with `stub`) are the lane bands of the source and of the target: each pill stays inside its band, 4 px clear of the border.
20
- // `labelW` is the width of an edge's label pill: a stub pill keeps clear of it.
21
- // `stub` (wrapped lanes: the source pill width, the target pill width, and the width of a shorter source pill) draws a short stub from the source to a pill, and from a second pill into the target.
22
- export function route(edges, rects, tips = new Set(), avoid = [], // what an elbow's vertical run and a stub pill must not cross (the timeline's outside labels; the boxes and gutters of lanes)
23
- area, // where a stub pill may sit: the lanes right of the gutter
24
- lanes) {
10
+ /** Routes edges between measured boxes as curved SVG paths. `elbow` draws right-angle paths; `stub` splits a cross-lane edge in two. */
11
+ export function route(edges, rects, tips = new Set(), avoid = [], area, lanes) {
25
12
  const picks = [];
26
13
  for (const e of edges) {
27
14
  const a = rects[e.from], b = rects[e.to];
@@ -45,7 +32,6 @@ lanes) {
45
32
  : ['l', 'r'];
46
33
  picks.push({ ...e, a, b, sa, sb });
47
34
  }
48
- // For every (box, side), list the edge ends that sit there, sorted by where the other end is.
49
35
  const ends = new Map();
50
36
  for (const pick of picks) {
51
37
  for (const start of [true, false]) {
@@ -64,7 +50,7 @@ lanes) {
64
50
  const r = x.start ? x.pick.b : x.pick.a;
65
51
  return horiz ? cx(r) : cy(r);
66
52
  };
67
- list.sort((p, q) => other(p) - other(q)); // stable: ties keep declaration order
53
+ list.sort((p, q) => other(p) - other(q));
68
54
  list.forEach((x, i) => {
69
55
  const r = x.start ? x.pick.a : x.pick.b;
70
56
  const f = tip ? 0.5 : (i + 1) / (list.length + 1);
@@ -78,16 +64,12 @@ lanes) {
78
64
  anchor.set(x.pick.id + (x.start ? ':s' : ':e'), pt);
79
65
  });
80
66
  }
81
- // The four points travel with the path string: `flowfig check` samples the curve, and nothing has to parse `d`.
82
67
  const drawn = (id, curve, mid) => {
83
68
  const [s, c1, c2, e] = curve;
84
69
  return { id, d: `M ${s.x} ${s.y} C ${c1.x} ${c1.y}, ${c2.x} ${c2.y}, ${e.x} ${e.y}`, mid, curve };
85
70
  };
86
- // Stub pills are placed after the other edges. Each pill keeps clear of `avoid`, of the edge labels, of the pills and stubs
87
- // before it, and of every edge path (sampled as `check` samples it).
88
71
  const pills = [];
89
72
  const paths = [];
90
- // The same samples as segments: a stub line must not cross one.
91
73
  const runs = [];
92
74
  const track = (pts) => {
93
75
  paths.push(...pts);
@@ -101,11 +83,6 @@ lanes) {
101
83
  const one = (p) => {
102
84
  const s = anchor.get(p.id + ':s'), e = anchor.get(p.id + ':e');
103
85
  if (p.stub) {
104
- // The source pill goes right of the source and the target pill left of the target, at the nearest clear place: it slides
105
- // away from the box in 8 px steps, and up or down inside the band in 4 px steps. The stub then runs on a slant. The last
106
- // places are below and above the box. A place is clear if the pill and its stub keep clear of everything, and the pill stays
107
- // inside its band. If no place is clear, the source pill tries its shorter text. If no place is clear then, the first place
108
- // stays and `check` reports it.
109
86
  const [ow, iw, sw] = p.stub;
110
87
  const [sb, tb] = p.bands ?? [];
111
88
  const rows = (box, y0, band) => {
@@ -114,14 +91,11 @@ lanes) {
114
91
  : [box.y + box.h + 2, box.y - 20];
115
92
  return [y0, ...ys.filter((y) => y !== y0)];
116
93
  };
117
- // A place and its stub. Beside the box, the stub runs from the box side to the near end of the pill. Under or over the box,
118
- // it runs straight down or up between the box and the pill. `dir` is 1 for the source (box to pill), -1 for the target.
119
94
  const place = (box, from, dir, r, side, tip) => {
120
95
  let a, b;
121
96
  if (side)
122
97
  [a, b] = [from, { x: dir > 0 ? r.x : r.x + r.w, y: r.y + 9 }];
123
98
  else {
124
- // A diamond has its point at the middle: the stub meets the point.
125
99
  const x = tip ? cx(box) : Math.min(Math.max(r.x + r.w / 2, box.x + 8, r.x + 8), box.x + box.w - 8, r.x + r.w - 8);
126
100
  const under = r.y >= box.y + box.h;
127
101
  [a, b] = [
@@ -131,22 +105,49 @@ lanes) {
131
105
  }
132
106
  return dir > 0 ? [r, a, b] : [r, b, a];
133
107
  };
134
- const beside = (w, box, from, dir, band) => {
108
+ function* beside(w, box, from, dir, band) {
135
109
  const tip = tips.has(dir > 0 ? p.from : p.to);
136
- const out = [];
137
- for (let dx = STUB - 8 * Math.ceil((box.w + w) / 8); dx <= STUB + 240; dx += 8)
138
- for (const y of rows(box, from.y - 9, band)) {
139
- // Closer than STUB to the box side, a pill sits under or over the box, with room for the stub.
140
- const x = dir > 0 ? from.x + dx : from.x - dx - w;
141
- if (dx < STUB && !(y >= box.y + box.h + STUB / 2 || y + 18 <= box.y - STUB / 2))
142
- continue;
143
- if (dx < STUB && tip && !(x + 8 <= cx(box) && cx(box) <= x + w - 8))
144
- continue;
145
- out.push([...place(box, from, dir, { x, y, w, h: 18 }, dx >= STUB, tip), Math.abs(dx - STUB) + Math.abs(y + 9 - from.y)]);
110
+ const ys = rows(box, from.y - 9, band)
111
+ .map((y, i) => ({ y, d: Math.abs(y + 9 - from.y), i }))
112
+ .sort((m, q) => m.d - q.d || m.i - q.i);
113
+ const heap = [];
114
+ for (let dx = STUB - 8 * Math.ceil((box.w + w) / 8); dx <= STUB + 240; dx += 8) {
115
+ const x = dir > 0 ? from.x + dx : from.x - dx - w;
116
+ if (dx < STUB && tip && !(x + 8 <= cx(box) && cx(box) <= x + w - 8))
117
+ continue;
118
+ if (ys.length)
119
+ heap.push({ dx, x, k: 0, key: Math.abs(dx - STUB) + ys[0].d, i: heap.length });
120
+ }
121
+ // Equal keys must keep the dx order, then the row order, or a pill moves.
122
+ const less = (m, q) => m.key < q.key || (m.key === q.key && m.i < q.i);
123
+ heap.sort((m, q) => m.key - q.key || m.i - q.i);
124
+ while (heap.length) {
125
+ const h = heap[0];
126
+ const { y } = ys[h.k++];
127
+ if (h.k < ys.length)
128
+ h.key = Math.abs(h.dx - STUB) + ys[h.k].d;
129
+ else {
130
+ const last = heap.pop();
131
+ if (heap.length)
132
+ heap[0] = last;
146
133
  }
147
- return out.sort((m, q) => m[3] - q[3]).map(([r, a, b]) => [r, a, b]);
148
- };
149
- // A diamond with no room beside its point: the stub leaves the side point straight down or up, to a pill that spans its x.
134
+ for (let i = 0;;) {
135
+ const l = 2 * i + 1;
136
+ let m = i;
137
+ if (l < heap.length && less(heap[l], heap[m]))
138
+ m = l;
139
+ if (l + 1 < heap.length && less(heap[l + 1], heap[m]))
140
+ m = l + 1;
141
+ if (m === i)
142
+ break;
143
+ [heap[i], heap[m]] = [heap[m], heap[i]];
144
+ i = m;
145
+ }
146
+ if (h.dx < STUB && !(y >= box.y + box.h + STUB / 2 || y + 18 <= box.y - STUB / 2))
147
+ continue;
148
+ yield place(box, from, dir, { x: h.x, y, w, h: 18 }, h.dx >= STUB, tip);
149
+ }
150
+ }
150
151
  const along = (w, box, from, dir, band) => {
151
152
  if (!tips.has(dir > 0 ? p.from : p.to))
152
153
  return [];
@@ -162,49 +163,54 @@ lanes) {
162
163
  }
163
164
  return out.sort((m, q) => m[3] - q[3]).map(([r, a, b]) => [r, a, b]);
164
165
  };
165
- const outs = (w) => [
166
- ...beside(w, p.a, s, 1, sb),
167
- [
166
+ function* outs(w) {
167
+ yield* beside(w, p.a, s, 1, sb);
168
+ yield [
168
169
  { x: cx(p.a) - w / 2, y: p.a.y + p.a.h + STUB, w, h: 18 },
169
170
  { x: cx(p.a), y: p.a.y + p.a.h },
170
171
  { x: cx(p.a), y: p.a.y + p.a.h + STUB },
171
- ],
172
- [
172
+ ];
173
+ yield [
173
174
  { x: cx(p.a) - w / 2, y: p.a.y - STUB - 18, w, h: 18 },
174
175
  { x: cx(p.a), y: p.a.y },
175
176
  { x: cx(p.a), y: p.a.y - STUB },
176
- ],
177
- ...along(w, p.a, s, 1, sb),
178
- ];
179
- const ins = [
180
- ...beside(iw, p.b, e, -1, tb),
181
- [
177
+ ];
178
+ yield* along(w, p.a, s, 1, sb);
179
+ }
180
+ function* ins() {
181
+ yield* beside(iw, p.b, e, -1, tb);
182
+ yield [
182
183
  { x: cx(p.b) - iw / 2, y: p.b.y - STUB - 18, w: iw, h: 18 },
183
184
  { x: cx(p.b), y: p.b.y - STUB },
184
185
  { x: cx(p.b), y: p.b.y },
185
- ],
186
- [
186
+ ];
187
+ yield [
187
188
  { x: cx(p.b) - iw / 2, y: p.b.y + p.b.h + STUB, w: iw, h: 18 },
188
189
  { x: cx(p.b), y: p.b.y + p.b.h + STUB },
189
190
  { x: cx(p.b), y: p.b.y + p.b.h },
190
- ],
191
- ...along(iw, p.b, e, -1, tb),
192
- ];
191
+ ];
192
+ yield* along(iw, p.b, e, -1, tb);
193
+ }
193
194
  const seg = (a, b) => Array.from({ length: 9 }, (_, i) => ({ x: a.x + ((b.x - a.x) * i) / 8, y: a.y + ((b.y - a.y) * i) / 8 }));
194
- // A stub must not run through a box other than its own, nor through a pill or a label, nor cross an edge path.
195
195
  const free = (a, b, own) => seg(a, b).every((q) => ![...avoid, ...pills].some((r) => r !== own && q.x > r.x + 2 && q.x < r.x + r.w - 2 && q.y > r.y + 2 && q.y < r.y + r.h - 2));
196
196
  const whole = (a, b) => !runs.some(([c, d]) => crosses(a, b, c, d));
197
197
  const fits = (own, band, strict) => ([r, a, b]) => clear(r, band) && free(a, b, own) && (!strict || whole(a, b));
198
- // The first place whose stub crosses no edge path. If none, the first place that clears everything else: in a dense
199
- // figure no place may avoid every edge, and `check` then warns about the crossing.
200
- const first = (own, band, lists) => [true, false].flatMap((strict) => lists.map((l, i) => [l.find(fits(own, band, strict)), i, !strict])).find(([x]) => x);
201
- const hit = first(p.a, sb, [outs(ow), ...(sw == null ? [] : [outs(sw)])]);
198
+ const first = (own, band, lists) => {
199
+ for (const strict of [true, false]) {
200
+ const ok = fits(own, band, strict);
201
+ for (const [i, list] of lists.entries())
202
+ for (const c of list())
203
+ if (ok(c))
204
+ return [c, i, !strict];
205
+ }
206
+ };
207
+ const hit = first(p.a, sb, [() => outs(ow), ...(sw == null ? [] : [() => outs(sw)])]);
202
208
  const cut = hit?.[1] === 1;
203
- const [po, o1, o2] = hit?.[0] ?? outs(ow)[0];
209
+ const [po, o1, o2] = hit?.[0] ?? outs(ow).next().value;
204
210
  pills.push(po);
205
211
  track(seg(o1, o2));
206
212
  const hit2 = first(p.b, tb, [ins]);
207
- const [pi, i1, i2] = hit2?.[0] ?? ins[0];
213
+ const [pi, i1, i2] = hit2?.[0] ?? ins().next().value;
208
214
  pills.push(pi);
209
215
  track(seg(i1, i2));
210
216
  const line = (a, b) => `M ${a.x} ${a.y} L ${b.x} ${b.y}`;
@@ -227,10 +233,7 @@ lanes) {
227
233
  };
228
234
  }
229
235
  if (p.elbow) {
230
- // Under 16 px of gap, the elbow detours: out 8 px past the start, back to 8 px before the end, then forward into the target.
231
- // A gap of 0 px or less takes the same detour.
232
236
  if (e.x - s.x < 16 && s.y === e.y) {
233
- // One row: a straight line, since a detour would only double back.
234
237
  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] };
235
238
  }
236
239
  if (e.x - s.x < 16) {
@@ -243,26 +246,20 @@ lanes) {
243
246
  elbow: [s, { x: mx, y: s.y }, { x: mx, y: my }, { x: ex, y: my }, { x: ex, y: e.y }, e],
244
247
  };
245
248
  }
246
- // Over 16 px, the vertical run tries the midpoint, 8 px before the end, 8 px after the start, then any x in the gap.
247
- // A vertical run keeps clear of every label and box; a horizontal run keeps clear of every box but the two ends.
248
249
  const own = (r) => [p.a, p.b].some((q) => q.x === r.x && q.y === r.y && q.w === r.w && q.h === r.h);
249
250
  const all = avoid.filter((r) => !own(r));
250
251
  const boxes = all.filter((r) => r.box);
251
- // `m` is the margin: 2 px in the gap, 8 px on a detour, so a detour does not run along a box side.
252
252
  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);
253
253
  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);
254
254
  const mid = (s.x + e.x) / 2;
255
255
  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));
256
256
  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));
257
257
  if (mx == null && boxes.length) {
258
- // No x in the gap is clear: the elbow goes around. It runs right past the boxes in its way, along a clear row, back to
259
- // a clear x at or before 8 px before the target, then down or up and forward into the target. The shortest clear
260
- // detour wins; ties take the row nearest the middle.
261
258
  const right = Math.max(...boxes.map((r) => r.x + r.w)) + 12;
262
259
  const left = Math.min(...boxes.map((r) => r.x));
263
260
  const [top, bottom] = [Math.min(...boxes.map((r) => r.y)) - 12, Math.max(...boxes.map((r) => r.y + r.h)) + 12];
264
261
  let best;
265
- // ponytail: a grid search in 4 px steps, about 10^5 tries for a figure the size of the roadmap.
262
+ // ponytail: grid search in 4 px steps, about 10^5 tries on the roadmap.
266
263
  for (let x = s.x + 8; x <= right; x += 4) {
267
264
  if (!hClear(s.y, s.x, x))
268
265
  continue;
@@ -290,7 +287,6 @@ lanes) {
290
287
  };
291
288
  }
292
289
  }
293
- // If no path is clear, the midpoint stays and `check` reports the crossing.
294
290
  const x = mx ?? mid;
295
291
  const c1 = { x, y: s.y }, c2 = { x, y: e.y };
296
292
  return {
@@ -302,7 +298,7 @@ lanes) {
302
298
  };
303
299
  }
304
300
  if (p.around) {
305
- // ponytail: arcs 50px past the two ends' boxes; a taller box in between can still be crossed.
301
+ // ponytail: arcs 50 px past the box ends; a taller box between can be crossed.
306
302
  const y = p.around === 'above' ? Math.min(p.a.y, p.b.y) - 50 : Math.max(p.a.y + p.a.h, p.b.y + p.b.h) + 50;
307
303
  return drawn(p.id, [s, { x: s.x, y }, { x: e.x, y }, e], { x: (s.x + e.x) / 2, y: (s.y + 6 * y + e.y) / 8 });
308
304
  }
@@ -330,9 +326,6 @@ lanes) {
330
326
  done.set(p, one(p));
331
327
  return picks.map((p) => done.get(p));
332
328
  }
333
- /** The point of the path nearest its middle where a label pill of width `w` sits inside one band, LABEL_CLEAR from the border, clear of the boxes
334
- * and the earlier pills. If a short run leaves no such point, the pill keeps the x of a path point and moves up or down into the nearest
335
- * band, still on the path. If that fails too, the middle. Both renderers route through this, so they place the label alike. */
336
329
  function labelInBand(r, w, { bands, boxes }, pills) {
337
330
  const hit = (a, b, m) => a.x < b.x + b.w + m && b.x < a.x + a.w + m && a.y < b.y + b.h + m && b.y < a.y + a.h + m;
338
331
  const ts = Array.from({ length: 65 }, (_, i) => i / 64).sort((a, b) => Math.abs(a - 0.5) - Math.abs(b - 0.5));
@@ -356,7 +349,7 @@ function labelInBand(r, w, { bands, boxes }, pills) {
356
349
  }
357
350
  return r.mid;
358
351
  }
359
- /** True if the segment a-b crosses the segment c-d. The ends of a-b are trimmed by 1.5 px, so a touch at a box side does not count. */
352
+ /** True if segment a-b crosses c-d. The ends of a-b are trimmed by 1.5 px. */
360
353
  export function crosses(a, b, c, d) {
361
354
  const len = Math.hypot(b.x - a.x, b.y - a.y);
362
355
  if (len < 4)
package/dist/gif.d.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import type { Image } from './png.ts';
2
- /** One frame: `load` returns the pixels (called twice per frame); `delay` is in centiseconds.
3
- * `same` says the pixels equal the frame before: the encoder then never calls `load`. */
2
+ /** One frame. `load` returns the pixels, `delay` is in cs, `same` skips `load`. */
4
3
  export type GifFrame = {
5
4
  load: () => Image;
6
5
  delay: number;