@knowvah/dot-engine 2.6.2 → 2.6.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.
Files changed (46) 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/spline-midpoint.d.ts +8 -0
  13. package/dist/common/spline-midpoint.d.ts.map +1 -1
  14. package/dist/index.js +621 -323
  15. package/dist/index.js.map +4 -4
  16. package/dist/label/node.d.ts +7 -1
  17. package/dist/label/node.d.ts.map +1 -1
  18. package/dist/layout/circo/position-helpers.d.ts +15 -3
  19. package/dist/layout/circo/position-helpers.d.ts.map +1 -1
  20. package/dist/layout/circo/position.d.ts +17 -1
  21. package/dist/layout/circo/position.d.ts.map +1 -1
  22. package/dist/layout/dot/ortho-adapter.d.ts.map +1 -1
  23. package/dist/layout/dot/splines-flat-labeled.d.ts.map +1 -1
  24. package/dist/layout/neato/multispline.d.ts.map +1 -1
  25. package/dist/layout/pack/poly-pack.d.ts +2 -18
  26. package/dist/layout/pack/poly-pack.d.ts.map +1 -1
  27. package/dist/pathplan/shortest.d.ts.map +1 -1
  28. package/dist/render.js +774 -476
  29. package/dist/render.js.map +4 -4
  30. package/package.json +1 -1
  31. package/src/common/htmltable-lex.ts +265 -5
  32. package/src/common/htmltable-parse-attrs.ts +166 -0
  33. package/src/common/htmltable-parse.ts +262 -242
  34. package/src/common/htmltable-pos.ts +37 -38
  35. package/src/common/htmltable.ts +139 -45
  36. package/src/common/make-label.ts +19 -3
  37. package/src/common/spline-midpoint.ts +30 -9
  38. package/src/label/node.ts +8 -2
  39. package/src/layout/circo/position-helpers.ts +28 -17
  40. package/src/layout/circo/position.ts +28 -7
  41. package/src/layout/dot/__fixtures__/2796-flat-labeled-lost.dot +161 -0
  42. package/src/layout/dot/ortho-adapter.ts +20 -9
  43. package/src/layout/dot/splines-flat-labeled.ts +3 -1
  44. package/src/layout/neato/multispline.ts +17 -5
  45. package/src/layout/pack/poly-pack.ts +5 -31
  46. package/src/pathplan/shortest.ts +15 -2
@@ -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);
@@ -0,0 +1,161 @@
1
+ // Reduced (ddmin) from graphviz tests/2796.dot: one labeled flat edge whose
2
+ // routesplines fails. Native 16.1.0 reports the failure once.
3
+ digraph "troublesome" {
4
+ rankdir=LR
5
+ I -> 36
6
+ subgraph cluster_0 {
7
+ }
8
+ subgraph cluster_6 {
9
+ 57
10
+ 12
11
+ }
12
+ subgraph cluster_10 {
13
+ 26
14
+ }
15
+ subgraph cluster_14 {
16
+ }
17
+ subgraph cluster_17 {
18
+ }
19
+ subgraph cluster_18 {
20
+ }
21
+ subgraph cluster_21 {
22
+ 55
23
+ }
24
+ subgraph cluster_23 {
25
+ }
26
+ subgraph cluster_24 {
27
+ }
28
+ subgraph cluster_33 {
29
+ 39
30
+ 4
31
+ 1
32
+ }
33
+ subgraph cluster_35 {
34
+ }
35
+ subgraph cluster_37 {
36
+ }
37
+ subgraph cluster_38 {
38
+ }
39
+ subgraph cluster_39 {
40
+ }
41
+ subgraph cluster_40 {
42
+ 46
43
+ }
44
+ subgraph cluster_41 {
45
+ }
46
+ subgraph cluster_42 {
47
+ }
48
+ 1 -> 48 [label="p1"]
49
+ 2 -> 48 [label="!p0 & !p1"]
50
+ 3 -> 26 [label="p0 & p2"]
51
+ 4 -> 1 [label="p0 & p1 & p2"]
52
+ 4 -> 48 [label="!p1"]
53
+ 12 -> 57 [label="p0 & p1"]
54
+ 13 -> 28 [label="p0 & p1 & p2"]
55
+ 15 -> 16 [label="!p0 & p1 & p2"]
56
+ 16 -> 26 [label="p0 & p2"]
57
+ 17 -> 3 [label="!p0 & !p2"]
58
+ 17 -> 21 [label="p0 & !p2"]
59
+ 20 -> 48 [label="!p0 & p1"]
60
+ 20 -> 50 [label="p0 & !p1"]
61
+ 21 -> 3 [label="!p0 & p1 & !p2"]
62
+ 21 -> 12 [label="p0 & p1 & p2"]
63
+ 21 -> 48 [label="!p0 & !p1"]
64
+ 21 -> 57 [label="p0 & !p1"]
65
+ 22 -> 48 [label="!p0"]
66
+ 22 -> 52 [label="p0 & p2"]
67
+ 23 -> 14 [label="p0 & p1 & p2"]
68
+ 23 -> 21 [label="p0 & p1 & !p2"]
69
+ 23 -> 30 [label="p0 & !p1 & !p2"]
70
+ 23 -> 48 [label="(!p0 & !p1) | (!p0 & p2)"]
71
+ 23 -> 52 [label="p0 & !p1 & p2"]
72
+ 24 -> 3 [label="!p0 & p1 & !p2"]
73
+ 24 -> 15 [label="p0 & p1 & !p2"]
74
+ 24 -> 52 [label="p0 & !p1 & p2"]
75
+ 25 -> 18 [label="!p0 & p1 & !p2"]
76
+ 25 -> 34 [label="!p0 & p1 & p2"]
77
+ 26 -> 48 [label="p1"]
78
+ 27 -> 48 [label="!p0 & !p2"]
79
+ 27 -> 57 [label="p0 & !p2"]
80
+ 28 -> 29 [label="p0 & !p1 & !p2"]
81
+ 28 -> 48 [label="!p0 | p1"]
82
+ 29 -> 48 [label="!p0 | !p1"]
83
+ 30 -> 48 [label="!p0"]
84
+ 32 -> 30 [label="p0 & !p1 & !p2"]
85
+ 32 -> 48 [label="!p0"]
86
+ 32 -> 52 [label="p0 & p2"]
87
+ 32 -> 55 [label="p0 & p1 & !p2"]
88
+ 33 -> 48 [label="!p0"]
89
+ 33 -> 52 [label="p0 & p2"]
90
+ 33 -> 55 [label="p0 & !p2"]
91
+ 34 -> 0 [label="p0 & !p2"]
92
+ 34 -> 2 [label="p0 & p2"]
93
+ 34 -> 48 [label="!p0"]
94
+ 35 -> 9 [label="p0 & !p1 & !p2"]
95
+ 35 -> 14 [label="p0 & !p1 & p2"]
96
+ 35 -> 55 [label="p0 & p1 & !p2"]
97
+ 36 -> 5 [label="!p0 & !p1 & !p2"]
98
+ 36 -> 6 [label="p0 & !p1 & p2"]
99
+ 36 -> 17 [label="p0 & !p1 & !p2"]
100
+ 36 -> 37 [label="p1 & !p2"]
101
+ 36 -> 38 [label="p1 & p2"]
102
+ 36 -> 44 [label="!p0 & !p1 & p2"]
103
+ 37 -> 10 [label="!p0 & !p1 & !p2"]
104
+ 37 -> 23 [label="p0 & !p1 & !p2"]
105
+ 38 -> 24 [label="p0 & !p1 & !p2"]
106
+ 38 -> 35 [label="p0 & !p1 & p2"]
107
+ 38 -> 37 [label="!p0 & p1 & !p2"]
108
+ 38 -> 39 [label="p0 & p1 & p2"]
109
+ 38 -> 44 [label="!p0 & !p1 & p2"]
110
+ 38 -> 53 [label="p0 & p1 & !p2"]
111
+ 39 -> 5 [label="!p0 & !p1 & !p2"]
112
+ 39 -> 24 [label="p0 & !p1 & !p2"]
113
+ 39 -> 35 [label="p0 & !p1 & p2"]
114
+ 39 -> 43 [label="p0 & p1 & p2"]
115
+ 39 -> 44 [label="!p0 & !p1 & p2"]
116
+ 39 -> 48 [label="!p0 & p1"]
117
+ 39 -> 54 [label="p0 & p1 & !p2"]
118
+ 40 -> 10 [label="!p0 & !p1 & !p2"]
119
+ 41 -> 5 [label="!p0 & !p1 & !p2"]
120
+ 42 -> 5 [label="!p0 & !p1 & !p2"]
121
+ 42 -> 44 [label="!p0 & !p1 & p2"]
122
+ 42 -> 48 [label="p1"]
123
+ 42 -> 51 [label="p0 & !p1 & p2"]
124
+ 43 -> 32 [label="p0 & !p1 & !p2"]
125
+ 43 -> 33 [label="p0 & !p1 & p2"]
126
+ 43 -> 48 [label="!p0"]
127
+ 43 -> 54 [label="p0 & p1 & !p2"]
128
+ 44 -> 19 [label="p0 & p2"]
129
+ 44 -> 20 [label="p0 & !p2"]
130
+ 44 -> 48 [label="!p0"]
131
+ 45 -> 28 [label="p0 & p2"]
132
+ 45 -> 29 [label="p0 & !p2"]
133
+ 45 -> 48 [label="!p0"]
134
+ 46 -> 3 [label="!p0 & p1 & !p2"]
135
+ 46 -> 48 [label="!p1 | (!p0 & p2)"]
136
+ 46 -> 51 [label="p0 & p1 & p2"]
137
+ 46 -> 56 [label="p0 & p1 & !p2"]
138
+ 47 -> 3 [label="!p0 & p1 & !p2"]
139
+ 47 -> 48 [label="!p1"]
140
+ 47 -> 51 [label="p0 & p1 & p2"]
141
+ 47 -> 56 [label="p0 & p1 & !p2"]
142
+ 49 -> 40 [label="!p0 & p1 & !p2"]
143
+ 49 -> 41 [label="!p0 & p1 & p2"]
144
+ 49 -> 42 [label="p0 & p1 & p2"]
145
+ 49 -> 48 [label="!p1"]
146
+ 51 -> 4 [label="p0 & !p1 & !p2"]
147
+ 51 -> 48 [label="p1 | (!p0 & p2)"]
148
+ 52 -> 48 [label="!p0"]
149
+ 52 -> 57 [label="p0 & p1"]
150
+ 53 -> 22 [label="p0 & !p1 & !p2"]
151
+ 53 -> 33 [label="p0 & !p1 & p2"]
152
+ 53 -> 39 [label="p0 & p1 & p2"]
153
+ 53 -> 40 [label="!p0 & p1 & !p2"]
154
+ 53 -> 41 [label="!p0 & p1 & p2"]
155
+ 53 -> 48 [label="!p0 & !p1"]
156
+ 54 -> 22 [label="p0 & !p1 & !p2"]
157
+ 54 -> 48 [label="!p0"]
158
+ 55 -> 48 [label="!p0"]
159
+ 55 -> 57 [label="p0 & !p1"]
160
+ 56 -> 3 [label="!p0 & p1 & !p2"]
161
+ }
@@ -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)!],
@@ -149,7 +149,9 @@ export function makeFlatLabeledEdge(g: Graph, e: Edge): boolean {
149
149
  // Degenerate route (routesplines pn=0): label set, no spline installed — C's
150
150
  // make_flat_labeled_edge returns the same way; emit's edge_in_box then decides
151
151
  // whether the (untranslated) label overlaps the clip. Still HANDLED → true.
152
- if (ps === null || ps.length === 0) return true;
152
+ // C's dot_splines_ visits the edge once, so mark it lost: the routeDotEdges
153
+ // backstop must not route (and report the failure) a second time.
154
+ if (ps === null || ps.length === 0) { e.info.lost = true; return true; }
153
155
  if (!tailIsLeft) ps.reverse();
154
156
  clipAndInstall(e, e.head, ps, ps.length, { ...buildDotSinfo(), ignoreSwap: true });
155
157
  return true;
@@ -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];
@@ -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]);