@knowvah/dot-engine 2.6.1 → 2.6.3

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.
Files changed (48) hide show
  1. package/dist/common/htmltable-lex.d.ts +38 -0
  2. package/dist/common/htmltable-lex.d.ts.map +1 -1
  3. package/dist/common/htmltable-parse-attrs.d.ts +41 -0
  4. package/dist/common/htmltable-parse-attrs.d.ts.map +1 -0
  5. package/dist/common/htmltable-parse.d.ts +3 -0
  6. package/dist/common/htmltable-parse.d.ts.map +1 -1
  7. package/dist/common/htmltable-pos.d.ts +17 -0
  8. package/dist/common/htmltable-pos.d.ts.map +1 -1
  9. package/dist/common/htmltable.d.ts +13 -1
  10. package/dist/common/htmltable.d.ts.map +1 -1
  11. package/dist/common/make-label.d.ts.map +1 -1
  12. package/dist/common/pointset.d.ts +35 -0
  13. package/dist/common/pointset.d.ts.map +1 -0
  14. package/dist/common/spline-midpoint.d.ts +8 -0
  15. package/dist/common/spline-midpoint.d.ts.map +1 -1
  16. package/dist/index.js +639 -315
  17. package/dist/index.js.map +4 -4
  18. package/dist/label/node.d.ts +7 -1
  19. package/dist/label/node.d.ts.map +1 -1
  20. package/dist/layout/circo/position-helpers.d.ts +15 -3
  21. package/dist/layout/circo/position-helpers.d.ts.map +1 -1
  22. package/dist/layout/circo/position.d.ts +17 -1
  23. package/dist/layout/circo/position.d.ts.map +1 -1
  24. package/dist/layout/dot/ortho-adapter.d.ts.map +1 -1
  25. package/dist/layout/neato/multispline.d.ts.map +1 -1
  26. package/dist/layout/pack/poly-pack.d.ts +2 -18
  27. package/dist/layout/pack/poly-pack.d.ts.map +1 -1
  28. package/dist/layout/pack/poly-place.d.ts.map +1 -1
  29. package/dist/pathplan/shortest.d.ts.map +1 -1
  30. package/dist/render.js +792 -468
  31. package/dist/render.js.map +4 -4
  32. package/package.json +2 -2
  33. package/src/common/htmltable-lex.ts +265 -5
  34. package/src/common/htmltable-parse-attrs.ts +166 -0
  35. package/src/common/htmltable-parse.ts +262 -242
  36. package/src/common/htmltable-pos.ts +37 -38
  37. package/src/common/htmltable.ts +139 -45
  38. package/src/common/make-label.ts +19 -3
  39. package/src/common/pointset.ts +73 -0
  40. package/src/common/spline-midpoint.ts +30 -9
  41. package/src/label/node.ts +8 -2
  42. package/src/layout/circo/position-helpers.ts +28 -17
  43. package/src/layout/circo/position.ts +28 -7
  44. package/src/layout/dot/ortho-adapter.ts +20 -9
  45. package/src/layout/neato/multispline.ts +17 -5
  46. package/src/layout/pack/poly-pack.ts +5 -31
  47. package/src/layout/pack/poly-place.ts +42 -47
  48. package/src/pathplan/shortest.ts +15 -2
@@ -12,12 +12,18 @@
12
12
  import type { Node } from '../../model/node.js';
13
13
  import type { Block } from './blocks.js';
14
14
  import { isCoalesced } from './blocks.js';
15
+ import { fma } from '../../common/fma.js';
16
+ import { libmHypot } from '../../common/libm-hypot.js';
15
17
 
16
- /** 2-D rotation of (x,y) by (cosR, sinR). */
18
+ /**
19
+ * 2-D rotation of (x,y) by (cosR, sinR). clang contracts both lines into
20
+ * fmadd: X = x*cosR + (-sinR*y), Y = x*sinR + cosR*y (objdump of
21
+ * `_applyDelta`).
22
+ */
17
23
  export function rotatePoint(
18
24
  x: number, y: number, cosR: number, sinR: number,
19
25
  ): [number, number] {
20
- return [x * cosR - y * sinR, x * sinR + y * cosR];
26
+ return [fma(x, cosR, -sinR * y), fma(x, sinR, cosR * y)];
21
27
  }
22
28
 
23
29
  /** Read psi from a node's alg field (set during Pass 4). */
@@ -27,7 +33,7 @@ export function nodePsi(n: Node): number {
27
33
 
28
34
  /** getRotation: single-node block with preset parent_pos. */
29
35
  export function rotFromParentPos(sn: Block, theta: number): number {
30
- let r = theta + Math.PI - sn.parentPos;
36
+ let r = theta + (Math.PI - sn.parentPos); // C: theta += M_PI - parent_pos
31
37
  if (r < 0) r += 2 * Math.PI;
32
38
  return r;
33
39
  }
@@ -35,16 +41,19 @@ export function rotFromParentPos(sn: Block, theta: number): number {
35
41
  /** Find the node in sn closest to origin after translation by (x,y). */
36
42
  export function closestNode(sn: Block, x: number, y: number, skip: Node): Node {
37
43
  let best = skip;
38
- let bestDist = Math.hypot(skip.info.pos![0]! + x, skip.info.pos![1]! + y);
44
+ let bestDist = libmHypot(skip.info.pos![0]! + x, skip.info.pos![1]! + y);
39
45
  for (const dn of sn.subGraph.nodes) {
40
46
  if (dn.orig === skip) continue;
41
- const d = Math.hypot(dn.orig.info.pos![0]! + x, dn.orig.info.pos![1]! + y);
47
+ const d = libmHypot(dn.orig.info.pos![0]! + x, dn.orig.info.pos![1]! + y);
42
48
  if (d < bestDist) { bestDist = d; best = dn.orig; }
43
49
  }
44
50
  return best;
45
51
  }
46
52
 
47
- /** getRotation: COALESCED block — off-center trigonometric correction. */
53
+ /**
54
+ * getRotation: COALESCED block whose neighbor lies right of -r — off-center
55
+ * trigonometric correction. C: `theta += M_PI / 2.0 - phi - asin(...)`.
56
+ */
48
57
  export function rotCoalesced(
49
58
  sn: Block, x: number, y: number, nbr: Node, theta: number,
50
59
  ): number {
@@ -52,26 +61,28 @@ export function rotCoalesced(
52
61
  const r = sn.radius - rho;
53
62
  const nx = nbr.info.pos![0]!;
54
63
  const ny = nbr.info.pos![1]!;
55
- if (-r >= nx) {
56
- const phi = Math.atan2(ny, nx);
57
- return theta + Math.PI - phi - nodePsi(nbr);
58
- }
59
- const R = Math.hypot(x, y);
64
+ const R = libmHypot(x, y);
60
65
  const phi = Math.atan2(ny, nx + r);
61
66
  const l = r - rho / Math.cos(phi);
62
- return theta + Math.PI / 2 - phi - Math.asin((l / R) * Math.cos(phi));
67
+ return theta + ((Math.PI / 2 - phi) - Math.asin((l / R) * Math.cos(phi)));
63
68
  }
64
69
 
65
- /** getRotation: general multi-node non-coalesced block. */
70
+ /**
71
+ * getRotation when the neighbor is not the node closest to the parent: the
72
+ * coalesced correction when `COALESCED(sn) && -r < n_x`, else the
73
+ * origin-centred rotation `theta += M_PI - phi - PSI`, wrapped past 2*M_PI.
74
+ * @see lib/circogen/circpos.c:getRotation
75
+ */
66
76
  export function rotGeneral(
67
77
  sn: Block, x: number, y: number, nbr: Node, theta: number,
68
78
  ): number {
69
79
  if (closestNode(sn, x, y, nbr) === nbr) return 0;
70
- if (isCoalesced(sn)) return rotCoalesced(sn, x, y, nbr, theta);
80
+ const r = sn.radius - sn.rad0;
81
+ if (isCoalesced(sn) && -r < nbr.info.pos![0]!) return rotCoalesced(sn, x, y, nbr, theta);
71
82
  const phi = Math.atan2(nbr.info.pos![1]!, nbr.info.pos![0]!);
72
- let r = theta + Math.PI - phi - nodePsi(nbr);
73
- if (r > 2 * Math.PI) r -= 2 * Math.PI;
74
- return r;
83
+ let t = theta + ((Math.PI - phi) - nodePsi(nbr));
84
+ if (t > 2 * Math.PI) t -= 2 * Math.PI;
85
+ return t;
75
86
  }
76
87
 
77
88
  /**
@@ -15,6 +15,7 @@ import type { Block, SubGraph, CircState } from './blocks.js';
15
15
  import { blkParent, setCoalesced, FLAGS_ISPARENT } from './blocks.js';
16
16
  import { applyDelta, getRotation } from './position-helpers.js';
17
17
  import { layoutBlock } from './blockpath.js';
18
+ import { fma } from '../../common/fma.js';
18
19
 
19
20
  export type PosState = {
20
21
  radius: number; subtreeR: number; nodeAngle: number;
@@ -48,10 +49,14 @@ export function getInfo(pi: PosInfo, stp: PosState, minDist: number): number {
48
49
  return maxR;
49
50
  }
50
51
 
51
- /** @see lib/circogen/circpos.c:setInfo */
52
+ /**
53
+ * The numerator is one clang-contracted fmadd: p0.diameter * p1.minRadius
54
+ * added to the rounded p1.diameter * p0.minRadius (objdump of `_doBlock`).
55
+ * @see lib/circogen/circpos.c:setInfo
56
+ */
52
57
  export function setInfo(p0: PosInfo, p1: PosInfo, delta: number): void {
53
58
  const t = Math.max(
54
- (p0.diameter * p1.minRadius + p1.diameter * p0.minRadius)
59
+ fma(p0.diameter, p1.minRadius, p1.diameter * p0.minRadius)
55
60
  / (2 * delta * p0.minRadius * p1.minRadius), 1,
56
61
  );
57
62
  p0.scale = Math.max(p0.scale, t);
@@ -63,9 +68,25 @@ export function placeChild(child: Block, ca: number, ctx: PlaceCtx): number {
63
68
  const dx = ctx.childRadius * Math.cos(ca);
64
69
  const dy = ctx.childRadius * Math.sin(ca);
65
70
  applyDelta(child, dx, dy, getRotation(child, dx, dy, ca));
66
- return ctx.length === 1
67
- ? ca + ctx.incAngle + ctx.mindistAngle
68
- : ca + ctx.incAngle + ctx.mindistAngle / 2;
71
+ return nextChildAngle(ca, ctx.incAngle, ctx.mindistAngle, ctx.length);
72
+ }
73
+
74
+ /**
75
+ * The angle after placing a child. C's `childAngle += incidentAngle +
76
+ * mindistAngle` (or `+ mindistAngle / 2`) adds the right-hand sum last.
77
+ * @see lib/circogen/circpos.c:positionChildren
78
+ */
79
+ export function nextChildAngle(ca: number, inc: number, mda: number, length: number): number {
80
+ return length === 1 ? ca + (inc + mda) : ca + (inc + mda / 2);
81
+ }
82
+
83
+ /**
84
+ * Circumference left over on a length-1 block's child circle: C's
85
+ * `2 * M_PI * childRadius - info->diameter`, contracted to one fnmsub.
86
+ * @see lib/circogen/circpos.c:positionChildren
87
+ */
88
+ export function leftoverArc(childRadius: number, diameter: number): number {
89
+ return fma(2 * Math.PI, childRadius, -diameter);
69
90
  }
70
91
 
71
92
  /** Advance child angle for length===1 blocks; updates firstAngle/lastAngle. */
@@ -81,7 +102,7 @@ export function advanceAngleL1(
81
102
  /** Advance child angle for multi-node blocks. */
82
103
  export function advanceAngleLN(ca: number, inc: number, mda: number, total: number, theta: number): number {
83
104
  if (total === 1) return theta;
84
- return ca + inc + mda / 2;
105
+ return ca + (inc + mda / 2); // C: childAngle += incidentAngle + mindistAngle / 2
85
106
  }
86
107
 
87
108
  /** Write psi midpoint angle to node alg when conditions are met. */
@@ -101,7 +122,7 @@ export function positionChildren(pi: PosInfo, stp: PosState, length: number, min
101
122
  if (length === 1) {
102
123
  cr = Math.max(cr, pi.diameter / (2 * Math.PI));
103
124
  // C redistributes leftover circumference into min_dist.
104
- const d = 2 * Math.PI * cr - pi.diameter;
125
+ const d = leftoverArc(cr, pi.diameter);
105
126
  if (d > 0) md += d / pi.childCount;
106
127
  }
107
128
  stp.subtreeR = Math.max(stp.subtreeR, cr + pi.maxRadius);
@@ -16,6 +16,7 @@ import type { Edge } from '../../model/edge.js';
16
16
  import { buildOutEdgeIndex } from '../../model/node.js';
17
17
  import { dotRoot } from './mincross-utils.js';
18
18
  import { nodesInSeq } from './decomp.js';
19
+ import { PointSet } from '../../common/pointset.js';
19
20
  import { clipAndInstall } from '../../common/splines-clip.js';
20
21
  import { buildDotSinfo } from './self-loop.js';
21
22
  import { orthoEdges } from '../../ortho/index.js';
@@ -49,11 +50,27 @@ function buildNodes(nodeArr: Node[]): OrthoNodeArr {
49
50
  }));
50
51
  }
51
52
 
53
+ /**
54
+ * Concentrate dedup: true when the unordered (tail, head) pair is already in
55
+ * `ps`; otherwise records it. C normalises AGSEQ(tail)/AGSEQ(head) to
56
+ * (min, max) and makes one isInPS/addPS pair on it.
57
+ * @see lib/ortho/ortho.c:1221-1229 (orthoEdges Concentrate PS)
58
+ */
59
+ function isConcentrateDup(ps: PointSet, e: Edge): boolean {
60
+ const ti = e.tail.id;
61
+ const hi = e.head.id;
62
+ const lo = ti <= hi ? ti : hi;
63
+ const up = ti <= hi ? hi : ti;
64
+ if (ps.isInPS(lo, up)) return true;
65
+ ps.addPS(lo, up);
66
+ return false;
67
+ }
68
+
52
69
  /**
53
70
  * Gather edges to route, mirroring C's ortho gather loop exactly:
54
71
  * iterate nodes in agfstnode (creation/seq) order, then each node's out-edges
55
72
  * in agfstout order. When `concentrate` is set, dedup by the UNORDERED node
56
- * pair via a point-set — the first edge between a pair (in either direction)
73
+ * pair via a `PointSet` — the first edge between a pair (in either direction)
57
74
  * is routed; any later edge sharing that pair (the reverse of a 2-cycle, or a
58
75
  * parallel multi-edge) is skipped. This is ortho's own concentrate mechanism,
59
76
  * distinct from class2's `edge_type == IGNORED` marking, and the keep-first
@@ -70,19 +87,13 @@ function buildEdges(
70
87
  const nodeIndex = new Map<Node, number>();
71
88
  nodeArr.forEach((n, i) => nodeIndex.set(n, i));
72
89
  const outIndex = buildOutEdgeIndex(g);
73
- const seen = new Set<string>();
90
+ const ps = concentrate ? new PointSet() : null;
74
91
  const result: TaggedOrthoEdge[] = [];
75
92
  for (const n of nodeArr) {
76
93
  const oes = outIndex.get(n);
77
94
  if (oes === undefined) continue;
78
95
  for (const e of oes) {
79
- if (concentrate) {
80
- const ti = e.tail.id;
81
- const hi = e.head.id;
82
- const key = ti <= hi ? `${ti},${hi}` : `${hi},${ti}`;
83
- if (seen.has(key)) continue;
84
- seen.add(key);
85
- }
96
+ if (ps !== null && isConcentrateDup(ps, e)) continue;
86
97
  result.push({
87
98
  tail: orthoNodes[nodeIndex.get(e.tail)!],
88
99
  head: orthoNodes[nodeIndex.get(e.head)!],
@@ -333,6 +333,18 @@ function chainOf(e: Edge): Edge[] {
333
333
  return list;
334
334
  }
335
335
 
336
+ /**
337
+ * agwarningf at each genroute failure site (Graphviz prefixes "Warning: ");
338
+ * returns genroute's failure code 1.
339
+ * @see multispline.c:genroute
340
+ */
341
+ function warnNoControlPoints(e: Edge): number {
342
+ console.warn(
343
+ `Warning: Could not create control points for multiple spline for edge (${e.tail.name},${e.head.name})`,
344
+ );
345
+ return 1;
346
+ }
347
+
336
348
  /**
337
349
  * Generate splines for e and cohorts through the corridor polygon.
338
350
  * Edges run from polygon index 0 to t. Returns 0 on success.
@@ -350,7 +362,7 @@ function genroute(
350
362
  const head = e.head;
351
363
 
352
364
  const pl = shortestPath({ ps: trip.ps }, eps);
353
- if (pl === null) return 1;
365
+ if (pl === null) return warnNoControlPoints(e);
354
366
 
355
367
  if (pl.length === 2) {
356
368
  // C passes doPolyline (0/1) AS the edge type — so the polyline case
@@ -365,7 +377,7 @@ function genroute(
365
377
 
366
378
  if (mult === 1 || concentrate) {
367
379
  const spl = routeMember(trip.ps, t, pl, doPolyline);
368
- if (spl === null) return 1;
380
+ if (spl === null) return warnNoControlPoints(e);
369
381
  finishEdge(e, spl, e.head !== head);
370
382
  return 0;
371
383
  }
@@ -374,7 +386,7 @@ function genroute(
374
386
  const cpts: Point[][] = [];
375
387
  for (let i = 0; i + 2 < pl.length; i++) {
376
388
  const cp = mkCtrlPts(t, mult + 1, pl[i]!, pl[i + 1]!, pl[i + 2]!, trip);
377
- if (cp === null) return 1;
389
+ if (cp === null) return warnNoControlPoints(e);
378
390
  cpts.push(cp);
379
391
  }
380
392
 
@@ -390,10 +402,10 @@ function genroute(
390
402
  poly[pn - j] = cpts[j - 1]![i + 1]!;
391
403
  }
392
404
  const mmpl = shortestPath({ ps: poly }, eps);
393
- if (mmpl === null) return 1;
405
+ if (mmpl === null) return warnNoControlPoints(cur);
394
406
 
395
407
  const spl = routeMember(poly, pl.length - 1, mmpl, doPolyline);
396
- if (spl === null) return 1;
408
+ if (spl === null) return warnNoControlPoints(cur);
397
409
  finishEdge(cur, spl, cur.head !== head);
398
410
 
399
411
  cur = cur.info.to_virt;
@@ -17,6 +17,7 @@ import type { PackInfo } from './types.js';
17
17
  import { spiralSearch } from './spiral-search.js';
18
18
  import { gvQsort } from '../../util/bsd-qsort.js';
19
19
  import { cround } from '../../common/arith.js';
20
+ import { PointSet } from '../../common/pointset.js';
20
21
  import { d2i } from '../../util/math.js';
21
22
 
22
23
  /** Max average polyomino size constant. @see lib/pack/pack.c:C */
@@ -29,33 +30,6 @@ export interface GInfo {
29
30
  index: number;
30
31
  }
31
32
 
32
- /** A set of occupied grid cells (string-keyed for O(1) lookup). */
33
- export type PointSet = Set<string>;
34
-
35
- /**
36
- * Encode a grid point as a canonical string key.
37
- *
38
- * C keys its PointSet dict on the `pointf` cell itself (exact double compare,
39
- * no rounding). Every cell coordinate reaching here is an exact integer grid
40
- * index produced by genBox's `cround(cval(...))`, so the rounding mode is
41
- * unobservable; `cround` is used only so no `Math.round` survives in a layout
42
- * coordinate path.
43
- * @see lib/common/pointset.h:45 (insertPS/inPS take pointf)
44
- */
45
- export function psKey(x: number, y: number): string {
46
- return `${cround(x)},${cround(y)}`;
47
- }
48
-
49
- /** Check if a cell is in the point set. @see lib/pack/pack.c:inPS */
50
- export function inPS(ps: PointSet, p: Point): boolean {
51
- return ps.has(psKey(p.x, p.y));
52
- }
53
-
54
- /** Insert a cell into the point set. @see lib/pack/pack.c:insertPS */
55
- export function insertPS(ps: PointSet, p: Point): void {
56
- ps.add(psKey(p.x, p.y));
57
- }
58
-
59
33
  /**
60
34
  * Compute the polyomino grid step size (positive root of quadratic).
61
35
  *
@@ -167,7 +141,7 @@ export interface PlaceCtx {
167
141
  export function hasCollision(cells: Point[], dx: number, dy: number, ps: PointSet): boolean {
168
142
  for (let i = 0; i < cells.length; i++) {
169
143
  const cell = cells[i];
170
- if (cell !== undefined && inPS(ps, { x: cell.x + dx, y: cell.y + dy })) return true;
144
+ if (cell !== undefined && ps.inPS({ x: cell.x + dx, y: cell.y + dy })) return true;
171
145
  }
172
146
  return false;
173
147
  }
@@ -175,12 +149,12 @@ export function hasCollision(cells: Point[], dx: number, dy: number, ps: PointSe
175
149
  /**
176
150
  * Mark all shifted cells as occupied in the point set.
177
151
  *
178
- * @see lib/pack/pack.c:fits (cell insertion loop)
152
+ * @see lib/pack/pack.c:fits (cell insertion loop; `addPS`)
179
153
  */
180
154
  export function markCells(cells: Point[], dx: number, dy: number, ps: PointSet): void {
181
155
  for (let i = 0; i < cells.length; i++) {
182
156
  const cell = cells[i];
183
- if (cell !== undefined) insertPS(ps, { x: cell.x + dx, y: cell.y + dy });
157
+ if (cell !== undefined) ps.addPS(cell.x + dx, cell.y + dy);
184
158
  }
185
159
  }
186
160
 
@@ -268,7 +242,7 @@ export function polyRects(ng: number, bbs: Box[], pinfo: PackInfo): Point[] | nu
268
242
  // perimeter, so the tie order of equal-perimeter components is decided by libc
269
243
  // qsort, not insertion order. Match it. @see util/bsd-qsort.ts
270
244
  const sorted = gvQsort([...infos], cmpByGPerimeter);
271
- const ctx: PlaceCtx = { ps: new Set(), step, bbs };
245
+ const ctx: PlaceCtx = { ps: new PointSet(), step, bbs };
272
246
  const places: Point[] = new Array<Point>(ng).fill({ x: 0, y: 0 });
273
247
  for (let i = 0; i < sorted.length; i++) {
274
248
  const info = sorted[i];
@@ -21,6 +21,7 @@ import { PackMode } from './types.js';
21
21
  import { cround } from '../../common/arith.js';
22
22
  import { d2i } from '../../util/math.js';
23
23
  import { gvQsort } from '../../util/bsd-qsort.js';
24
+ import { PointSet } from '../../common/pointset.js';
24
25
 
25
26
  /** Max. avg. polyomino size. @see lib/pack/pack.c:C */
26
27
  const C_AVG = 100;
@@ -40,23 +41,6 @@ function cell(p: Point, s: number): Point {
40
41
  return { x: cval(p.x, s), y: cval(p.y, s) };
41
42
  }
42
43
 
43
- /** Integer point set. @see lib/common/pointset.c */
44
- class PointSet {
45
- private readonly m = new Map<string, Point>();
46
- add(x: number, y: number): void {
47
- this.m.set(`${x},${y}`, { x, y });
48
- }
49
- has(p: Point): boolean {
50
- return this.m.has(`${p.x},${p.y}`);
51
- }
52
- points(): Point[] {
53
- return [...this.m.values()];
54
- }
55
- get size(): number {
56
- return this.m.size;
57
- }
58
- }
59
-
60
44
  /** Polyomino info for one graph. @see lib/pack/pack.c:ginfo */
61
45
  interface GInfo {
62
46
  perim: number;
@@ -96,7 +80,7 @@ interface LineState {
96
80
  function lineXDominant(st: LineState, ps: PointSet): void {
97
81
  let d = st.ay - (st.ax >> 1);
98
82
  for (;;) {
99
- ps.add(st.x, st.y);
83
+ ps.addPS(st.x, st.y);
100
84
  if (st.x === st.x2) return;
101
85
  if (d >= 0) { st.y += st.sy; d -= st.ax; }
102
86
  st.x += st.sx;
@@ -108,7 +92,7 @@ function lineXDominant(st: LineState, ps: PointSet): void {
108
92
  function lineYDominant(st: LineState, ps: PointSet): void {
109
93
  let d = st.ax - (st.ay >> 1);
110
94
  for (;;) {
111
- ps.add(st.x, st.y);
95
+ ps.addPS(st.x, st.y);
112
96
  if (st.y === st.y2) return;
113
97
  if (d >= 0) { st.x += st.sx; d -= st.ay; }
114
98
  st.y += st.sy;
@@ -136,7 +120,7 @@ function fillLine(p: Point, q: Point, ps: PointSet): void {
136
120
  /** Mark all cells in the inclusive cell-rect [LL, UR]. */
137
121
  function fillRect(LL: Point, UR: Point, ps: PointSet): void {
138
122
  for (let x = LL.x; x <= UR.x; x++) {
139
- for (let y = LL.y; y <= UR.y; y++) ps.add(x, y);
123
+ for (let y = LL.y; y <= UR.y; y++) ps.addPS(x, y);
140
124
  }
141
125
  }
142
126
 
@@ -208,7 +192,7 @@ function genBox(bb0: Box, ssize: number, margin: number, center: Point): GInfo {
208
192
  const UR = roundedCell({ x: center.x + bbW + margin, y: center.y + bbH + margin }, ssize);
209
193
  fillRect(LL, UR, ps);
210
194
  return {
211
- cells: ps.points(),
195
+ cells: ps.pointsOf(),
212
196
  perim: grid(bb0.ur.x - bb0.ll.x + 2 * margin, ssize)
213
197
  + grid(bb0.ur.y - bb0.ll.y + 2 * margin, ssize),
214
198
  index: 0,
@@ -266,7 +250,7 @@ function genPoly(
266
250
  };
267
251
  for (const n of g.nodes.values()) coverNode(n, ctx);
268
252
  return {
269
- cells: ps.points(),
253
+ cells: ps.pointsOf(),
270
254
  perim: grid(bb.ur.x - bb.ll.x + 2 * pinfo.margin, ssize)
271
255
  + grid(bb.ur.y - bb.ll.y + 2 * pinfo.margin, ssize),
272
256
  index: 0,
@@ -289,7 +273,7 @@ interface PlaceCtx {
289
273
 
290
274
  function fits(x: number, y: number, info: GInfo, ctx: PlaceCtx): boolean {
291
275
  for (const c of info.cells) {
292
- if (ctx.ps.has({ x: c.x + x, y: c.y + y })) return false;
276
+ if (ctx.ps.isInPS(c.x + x, c.y + y)) return false;
293
277
  }
294
278
  const bb = ctx.bbs[info.index]!;
295
279
  const LL = { x: cround(bb.ll.x), y: cround(bb.ll.y) };
@@ -297,30 +281,44 @@ function fits(x: number, y: number, info: GInfo, ctx: PlaceCtx): boolean {
297
281
  // is widened to double (objdump of libgvc `_fits`). Matters only once
298
282
  // computeStep has saturated at INT_MAX.
299
283
  ctx.places[info.index] = { x: Math.imul(ctx.step, x) - LL.x, y: Math.imul(ctx.step, y) - LL.y };
300
- for (const c of info.cells) ctx.ps.add(c.x + x, c.y + y);
284
+ for (const c of info.cells) ctx.ps.addPS(c.x + x, c.y + y);
301
285
  return true;
302
286
  }
303
287
 
304
- /** Ring candidates for wide graphs (W >= H). @see pack.c:placeGraph */
305
- function* ringWide(bnd: number): Generator<[number, number]> {
306
- let x = 0;
307
- let y = -bnd;
308
- for (; x < bnd; x++) yield [x, y];
309
- for (; y < bnd; y++) yield [x, y];
310
- for (; x > -bnd; x--) yield [x, y];
311
- for (; y > -bnd; y--) yield [x, y];
312
- for (; x < 0; x++) yield [x, y];
288
+ /** Ring cursor, shared by the legs of one ring as C shares its x and y. */
289
+ interface Cursor {
290
+ x: number;
291
+ y: number;
292
+ }
293
+
294
+ /**
295
+ * One straight leg of a ring: step (dx, dy) from the cursor until the moving
296
+ * coordinate reaches `end`, trying each cell. Every leg starts on the
297
+ * `end` side's integer lattice with a unit step, so `!== end` stops where C's
298
+ * `<`/`>` bound does.
299
+ * @see pack.c:placeGraph (one of the five loops per ring)
300
+ */
301
+ function leg(c: Cursor, dx: number, dy: number, end: number, info: GInfo, ctx: PlaceCtx): boolean {
302
+ for (; (dx !== 0 ? c.x : c.y) !== end; c.x += dx, c.y += dy) {
303
+ if (fits(c.x, c.y, info, ctx)) return true;
304
+ }
305
+ return false;
313
306
  }
314
307
 
315
- /** Ring candidates for tall graphs (W < H). @see pack.c:placeGraph */
316
- function* ringTall(bnd: number): Generator<[number, number]> {
317
- let y = 0;
318
- let x = -bnd;
319
- for (; y > -bnd; y--) yield [x, y];
320
- for (; x < bnd; x++) yield [x, y];
321
- for (; y < bnd; y++) yield [x, y];
322
- for (; x > -bnd; x--) yield [x, y];
323
- for (; y > 0; y--) yield [x, y];
308
+ /** Ring `bnd` for wide graphs (W >= H). @see pack.c:placeGraph */
309
+ function ringWide(bnd: number, info: GInfo, ctx: PlaceCtx): boolean {
310
+ const c = { x: 0, y: -bnd };
311
+ return leg(c, 1, 0, bnd, info, ctx) || leg(c, 0, 1, bnd, info, ctx)
312
+ || leg(c, -1, 0, -bnd, info, ctx) || leg(c, 0, -1, -bnd, info, ctx)
313
+ || leg(c, 1, 0, 0, info, ctx);
314
+ }
315
+
316
+ /** Ring `bnd` for tall graphs (W < H). @see pack.c:placeGraph */
317
+ function ringTall(bnd: number, info: GInfo, ctx: PlaceCtx): boolean {
318
+ const c = { x: -bnd, y: 0 };
319
+ return leg(c, 0, -1, -bnd, info, ctx) || leg(c, 1, 0, bnd, info, ctx)
320
+ || leg(c, 0, 1, bnd, info, ctx) || leg(c, -1, 0, -bnd, info, ctx)
321
+ || leg(c, 0, -1, 0, info, ctx);
324
322
  }
325
323
 
326
324
  /**
@@ -331,7 +329,7 @@ function* ringTall(bnd: number): Generator<[number, number]> {
331
329
  */
332
330
  function placeFixed(info: GInfo, ctx: PlaceCtx, center: Point): void {
333
331
  ctx.places[info.index] = { x: -center.x, y: -center.y };
334
- for (const c of info.cells) ctx.ps.add(c.x, c.y);
332
+ for (const c of info.cells) ctx.ps.addPS(c.x, c.y);
335
333
  }
336
334
 
337
335
  /**
@@ -349,10 +347,7 @@ function placeGraph(i: number, info: GInfo, ctx: PlaceCtx): void {
349
347
  if (fits(0, 0, info, ctx)) return;
350
348
  const wide = Math.ceil(bb.ur.x - bb.ll.x) >= Math.ceil(bb.ur.y - bb.ll.y);
351
349
  for (let bnd = 1; ; bnd++) {
352
- const ring = wide ? ringWide(bnd) : ringTall(bnd);
353
- for (const [x, y] of ring) {
354
- if (fits(x, y, info, ctx)) return;
355
- }
350
+ if (wide ? ringWide(bnd, info, ctx) : ringTall(bnd, info, ctx)) return;
356
351
  }
357
352
  }
358
353
 
@@ -250,9 +250,22 @@ class ShortestHelper {
250
250
  ShortestHelper.connectAll(tris);
251
251
  const ftrii = ShortestHelper.findTri(tris, eps[0]);
252
252
  const ltrii = ShortestHelper.findTri(tris, eps[1]);
253
- if (ftrii === NO_TRI || ltrii === NO_TRI) return null;
253
+ // C finds the first triangle, fails if none, and only then looks for the
254
+ // last one (shortest.c:178-204), so the two messages are exclusive.
255
+ if (ftrii === NO_TRI) {
256
+ console.warn('source point not in any triangle');
257
+ return null;
258
+ }
259
+ if (ltrii === NO_TRI) {
260
+ console.warn('destination point not in any triangle');
261
+ return null;
262
+ }
254
263
  if (ftrii === ltrii) return [eps[0], eps[1]];
255
- if (!ShortestHelper.markPath(tris, ftrii, ltrii)) return [eps[0], eps[1]];
264
+ if (!ShortestHelper.markPath(tris, ftrii, ltrii)) {
265
+ console.warn('cannot find triangle path');
266
+ // C: a straight line is better than failing
267
+ return [eps[0], eps[1]];
268
+ }
256
269
  const epnls: [PNL, PNL] = [{ pp: eps[0], link: null }, { pp: eps[1], link: null }];
257
270
  const dq = ShortestHelper.initDeque(pnlps.length);
258
271
  ShortestHelper.add2dq(dq, DQ_FRONT, epnls[0]);