@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.
- package/dist/common/htmltable-lex.d.ts +38 -0
- package/dist/common/htmltable-lex.d.ts.map +1 -1
- package/dist/common/htmltable-parse-attrs.d.ts +41 -0
- package/dist/common/htmltable-parse-attrs.d.ts.map +1 -0
- package/dist/common/htmltable-parse.d.ts +3 -0
- package/dist/common/htmltable-parse.d.ts.map +1 -1
- package/dist/common/htmltable-pos.d.ts +17 -0
- package/dist/common/htmltable-pos.d.ts.map +1 -1
- package/dist/common/htmltable.d.ts +13 -1
- package/dist/common/htmltable.d.ts.map +1 -1
- package/dist/common/make-label.d.ts.map +1 -1
- package/dist/common/pointset.d.ts +35 -0
- package/dist/common/pointset.d.ts.map +1 -0
- package/dist/common/spline-midpoint.d.ts +8 -0
- package/dist/common/spline-midpoint.d.ts.map +1 -1
- package/dist/index.js +639 -315
- package/dist/index.js.map +4 -4
- package/dist/label/node.d.ts +7 -1
- package/dist/label/node.d.ts.map +1 -1
- package/dist/layout/circo/position-helpers.d.ts +15 -3
- package/dist/layout/circo/position-helpers.d.ts.map +1 -1
- package/dist/layout/circo/position.d.ts +17 -1
- package/dist/layout/circo/position.d.ts.map +1 -1
- package/dist/layout/dot/ortho-adapter.d.ts.map +1 -1
- package/dist/layout/neato/multispline.d.ts.map +1 -1
- package/dist/layout/pack/poly-pack.d.ts +2 -18
- package/dist/layout/pack/poly-pack.d.ts.map +1 -1
- package/dist/layout/pack/poly-place.d.ts.map +1 -1
- package/dist/pathplan/shortest.d.ts.map +1 -1
- package/dist/render.js +792 -468
- package/dist/render.js.map +4 -4
- package/package.json +2 -2
- package/src/common/htmltable-lex.ts +265 -5
- package/src/common/htmltable-parse-attrs.ts +166 -0
- package/src/common/htmltable-parse.ts +262 -242
- package/src/common/htmltable-pos.ts +37 -38
- package/src/common/htmltable.ts +139 -45
- package/src/common/make-label.ts +19 -3
- package/src/common/pointset.ts +73 -0
- package/src/common/spline-midpoint.ts +30 -9
- package/src/label/node.ts +8 -2
- package/src/layout/circo/position-helpers.ts +28 -17
- package/src/layout/circo/position.ts +28 -7
- package/src/layout/dot/ortho-adapter.ts +20 -9
- package/src/layout/neato/multispline.ts +17 -5
- package/src/layout/pack/poly-pack.ts +5 -31
- package/src/layout/pack/poly-place.ts +42 -47
- 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
|
-
/**
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
|
73
|
-
if (
|
|
74
|
-
return
|
|
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
|
-
/**
|
|
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
|
|
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.
|
|
67
|
-
|
|
68
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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 (
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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)
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
|
316
|
-
function
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
package/src/pathplan/shortest.ts
CHANGED
|
@@ -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
|
-
|
|
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))
|
|
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]);
|