@knowvah/dot-engine 1.9.0 → 2.0.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/README.md +232 -30
- package/dist/api/builder.d.ts +3 -0
- package/dist/api/builder.d.ts.map +1 -1
- package/dist/api/edge-ops.d.ts +7 -0
- package/dist/api/edge-ops.d.ts.map +1 -1
- package/dist/api/geometry.d.ts +5 -2
- package/dist/api/geometry.d.ts.map +1 -1
- package/dist/api.js +159 -31
- package/dist/api.js.map +3 -3
- package/dist/async/collect.d.ts +49 -0
- package/dist/async/collect.d.ts.map +1 -0
- package/dist/async/fonts.d.ts +20 -0
- package/dist/async/fonts.d.ts.map +1 -0
- package/dist/async/render-async.d.ts +91 -0
- package/dist/async/render-async.d.ts.map +1 -0
- package/dist/async/render-into.d.ts +38 -0
- package/dist/async/render-into.d.ts.map +1 -0
- package/dist/async/sanitize.d.ts +28 -0
- package/dist/async/sanitize.d.ts.map +1 -0
- package/dist/common/htmltable-types.d.ts +3 -3
- package/dist/common/htmltable-types.d.ts.map +1 -1
- package/dist/common/make-label.d.ts.map +1 -1
- package/dist/common/poly-shapes.d.ts.map +1 -1
- package/dist/common/textmeasure-factory.d.ts +2 -0
- package/dist/common/textmeasure-factory.d.ts.map +1 -1
- package/dist/common/utils-inputscale.d.ts +19 -0
- package/dist/common/utils-inputscale.d.ts.map +1 -0
- package/dist/errors.d.ts +61 -5
- package/dist/errors.d.ts.map +1 -1
- package/dist/gvc/context.d.ts +36 -4
- package/dist/gvc/context.d.ts.map +1 -1
- package/dist/gvc/device.d.ts +5 -2
- package/dist/gvc/device.d.ts.map +1 -1
- package/dist/gvc/image-resolver.d.ts +16 -16
- package/dist/gvc/image-resolver.d.ts.map +1 -1
- package/dist/gvc/job.d.ts +1 -9
- package/dist/gvc/job.d.ts.map +1 -1
- package/dist/gvc/usershape.d.ts +2 -12
- package/dist/gvc/usershape.d.ts.map +1 -1
- package/dist/index.d.ts +27 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7560 -5446
- package/dist/index.js.map +4 -4
- package/dist/label/index.d.ts.map +1 -1
- package/dist/label/node.d.ts +0 -6
- package/dist/label/node.d.ts.map +1 -1
- package/dist/label/rectangle.d.ts +1 -7
- package/dist/label/rectangle.d.ts.map +1 -1
- package/dist/layout/circo/circular.d.ts +8 -5
- package/dist/layout/circo/circular.d.ts.map +1 -1
- package/dist/layout/dot/pack-components.d.ts +0 -19
- package/dist/layout/dot/pack-components.d.ts.map +1 -1
- package/dist/layout/dot/position.d.ts +7 -2
- package/dist/layout/dot/position.d.ts.map +1 -1
- package/dist/layout/fdp/derive.d.ts.map +1 -1
- package/dist/layout/fdp/index.d.ts.map +1 -1
- package/dist/layout/fdp/init.d.ts.map +1 -1
- package/dist/layout/fdp/layout.d.ts.map +1 -1
- package/dist/layout/fdp/ports.d.ts +0 -10
- package/dist/layout/fdp/ports.d.ts.map +1 -1
- package/dist/layout/fdp/xlayout.d.ts +0 -16
- package/dist/layout/fdp/xlayout.d.ts.map +1 -1
- package/dist/layout/neato/adjust-info.d.ts +117 -0
- package/dist/layout/neato/adjust-info.d.ts.map +1 -0
- package/dist/layout/neato/cdt-surface.d.ts.map +1 -1
- package/dist/layout/neato/constraint-adjust.d.ts +40 -0
- package/dist/layout/neato/constraint-adjust.d.ts.map +1 -0
- package/dist/layout/neato/edge-len.d.ts +20 -0
- package/dist/layout/neato/edge-len.d.ts.map +1 -0
- package/dist/layout/neato/fdp-adjust.d.ts +32 -4
- package/dist/layout/neato/fdp-adjust.d.ts.map +1 -1
- package/dist/layout/neato/index.d.ts +9 -6
- package/dist/layout/neato/index.d.ts.map +1 -1
- package/dist/layout/neato/init.d.ts +4 -11
- package/dist/layout/neato/init.d.ts.map +1 -1
- package/dist/layout/neato/kk-paths.d.ts +50 -0
- package/dist/layout/neato/kk-paths.d.ts.map +1 -0
- package/dist/layout/neato/kk-solve.d.ts +14 -0
- package/dist/layout/neato/kk-solve.d.ts.map +1 -0
- package/dist/layout/neato/kk.d.ts +44 -0
- package/dist/layout/neato/kk.d.ts.map +1 -0
- package/dist/layout/neato/multispline-router.d.ts.map +1 -1
- package/dist/layout/neato/poly.d.ts +50 -0
- package/dist/layout/neato/poly.d.ts.map +1 -0
- package/dist/layout/neato/sgd-dijkstra.d.ts +20 -0
- package/dist/layout/neato/sgd-dijkstra.d.ts.map +1 -0
- package/dist/layout/neato/sgd.d.ts +14 -9
- package/dist/layout/neato/sgd.d.ts.map +1 -1
- package/dist/layout/neato/start.d.ts +51 -0
- package/dist/layout/neato/start.d.ts.map +1 -0
- package/dist/layout/neato/vpsc-adjust.d.ts +21 -0
- package/dist/layout/neato/vpsc-adjust.d.ts.map +1 -0
- package/dist/layout/sfdp/index.d.ts.map +1 -1
- package/dist/layout/sfdp/init.d.ts +0 -9
- package/dist/layout/sfdp/init.d.ts.map +1 -1
- package/dist/layout/sfdp/spring-driver.d.ts.map +1 -1
- package/dist/layout/twopi/circle.d.ts +0 -7
- package/dist/layout/twopi/circle.d.ts.map +1 -1
- package/dist/ortho/ortho-parallel.d.ts.map +1 -1
- package/dist/ortho/trap-query.d.ts.map +1 -1
- package/dist/parser/index.d.ts +9 -5
- package/dist/parser/index.d.ts.map +1 -1
- package/dist/render/index.d.ts +2 -0
- package/dist/render/index.d.ts.map +1 -1
- package/dist/render/public.d.ts +9 -3
- package/dist/render/public.d.ts.map +1 -1
- package/dist/render/xdot-public.d.ts +7 -2
- package/dist/render/xdot-public.d.ts.map +1 -1
- package/dist/render.js +7355 -5476
- package/dist/render.js.map +4 -4
- package/dist/util/xml.d.ts.map +1 -1
- package/dist/vpsc/Solver.d.ts +1 -0
- package/dist/vpsc/Solver.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/api/builder.ts +73 -5
- package/src/api/edge-ops.ts +19 -0
- package/src/api/geometry.ts +22 -5
- package/src/async/collect.ts +204 -0
- package/src/async/fonts.ts +61 -0
- package/src/async/render-async.ts +205 -0
- package/src/async/render-into.ts +115 -0
- package/src/async/sanitize.ts +150 -0
- package/src/common/htmltable-types.ts +4 -5
- package/src/common/make-label.ts +10 -1
- package/src/common/poly-shapes.ts +4 -1
- package/src/common/textmeasure-factory.ts +11 -0
- package/src/common/utils-inputscale.ts +31 -0
- package/src/errors.ts +188 -5
- package/src/gvc/context.ts +96 -17
- package/src/gvc/device.ts +11 -6
- package/src/gvc/image-resolver.ts +25 -2
- package/src/gvc/job.ts +3 -1
- package/src/gvc/usershape.ts +6 -0
- package/src/index.ts +59 -43
- package/src/label/index.ts +6 -2
- package/src/label/node.ts +2 -1
- package/src/label/rectangle.ts +4 -2
- package/src/layout/circo/circular.ts +9 -6
- package/src/layout/dot/pack-components.ts +6 -2
- package/src/layout/dot/position.ts +21 -3
- package/src/layout/fdp/derive.ts +7 -6
- package/src/layout/fdp/index.ts +45 -5
- package/src/layout/fdp/init.ts +7 -6
- package/src/layout/fdp/layout.ts +2 -1
- package/src/layout/fdp/ports.ts +3 -2
- package/src/layout/fdp/xlayout.ts +3 -1
- package/src/layout/neato/adjust-info.ts +332 -0
- package/src/layout/neato/cdt-surface.ts +50 -30
- package/src/layout/neato/constraint-adjust.ts +466 -0
- package/src/layout/neato/edge-len.ts +36 -0
- package/src/layout/neato/fdp-adjust.ts +117 -12
- package/src/layout/neato/index.ts +36 -54
- package/src/layout/neato/init.ts +21 -36
- package/src/layout/neato/kk-paths.ts +146 -0
- package/src/layout/neato/kk-solve.ts +64 -0
- package/src/layout/neato/kk.ts +322 -0
- package/src/layout/neato/multispline-router.ts +4 -3
- package/src/layout/neato/poly.ts +493 -0
- package/src/layout/neato/sgd-dijkstra.ts +125 -0
- package/src/layout/neato/sgd.ts +63 -40
- package/src/layout/neato/start.ts +222 -0
- package/src/layout/neato/vpsc-adjust.ts +93 -0
- package/src/layout/sfdp/index.ts +4 -5
- package/src/layout/sfdp/init.ts +40 -4
- package/src/layout/sfdp/spring-driver.ts +30 -1
- package/src/layout/twopi/circle.ts +2 -1
- package/src/ortho/ortho-parallel.ts +2 -1
- package/src/ortho/trap-query.ts +2 -1
- package/src/parser/index.ts +11 -11
- package/src/render/index.ts +5 -0
- package/src/render/public.ts +24 -24
- package/src/render/svg.ts +1 -1
- package/src/render/xdot-public.ts +19 -18
- package/src/util/xml.ts +24 -30
- package/src/vpsc/Solver.ts +5 -3
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Node polygons for neato's overlap counting (`countOverlap` -> `polyOverlap`).
|
|
5
|
+
* A faithful port of lib/neatogen/poly.c. Units are inches, as in C.
|
|
6
|
+
*
|
|
7
|
+
* C's `makePoly` / `makeAddPoly` fill a caller-owned `Poly` and return a
|
|
8
|
+
* non-zero status on failure; here they return the `Poly`, or `null` where C
|
|
9
|
+
* returns 1. `breakPoly` only frees `verts` (garbage collected here), so it
|
|
10
|
+
* is not ported. The C functions are split only where the repo's complexity
|
|
11
|
+
* limits force it (per-shape vertex builders, `edgesIntersect` advance rule,
|
|
12
|
+
* `inPoly` per-edge crossing); evaluation order is unchanged.
|
|
13
|
+
*
|
|
14
|
+
* @see lib/neatogen/poly.c
|
|
15
|
+
* @see lib/neatogen/poly.h
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { Node } from '../../model/node.js';
|
|
19
|
+
import type { Box, Point } from '../../model/geom.js';
|
|
20
|
+
import { ShapeKind } from '../../common/types.js';
|
|
21
|
+
import type { PolygonT } from '../../common/types.js';
|
|
22
|
+
|
|
23
|
+
/** @see lib/neatogen/poly.c:BOX */
|
|
24
|
+
export const POLY_BOX = 1;
|
|
25
|
+
/** @see lib/neatogen/poly.c:CIRCLE */
|
|
26
|
+
export const POLY_CIRCLE = 2;
|
|
27
|
+
|
|
28
|
+
/** @see lib/common/const.h:DFLT_SAMPLE */
|
|
29
|
+
const DFLT_SAMPLE = 20;
|
|
30
|
+
/** @see lib/common/geom.h:POINTS_PER_INCH */
|
|
31
|
+
const POINTS_PER_INCH = 72;
|
|
32
|
+
/** Minimum samplepoints accepted by genRound. @see lib/neatogen/poly.c:genRound */
|
|
33
|
+
const MIN_SAMPLES = 3;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Polygon of a node, relative to the node centre. `nverts` is `verts.length`.
|
|
37
|
+
* @see lib/neatogen/poly.h:Poly
|
|
38
|
+
*/
|
|
39
|
+
export interface Poly {
|
|
40
|
+
origin: Point;
|
|
41
|
+
corner: Point;
|
|
42
|
+
verts: Point[];
|
|
43
|
+
kind: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** @see lib/common/geom.h:PS2INCH */
|
|
47
|
+
function ps2inch(a: number): number {
|
|
48
|
+
return a / POINTS_PER_INCH;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** @see lib/neatogen/poly.c:ISBOX */
|
|
52
|
+
function isBoxKind(p: Poly): boolean {
|
|
53
|
+
return (p.kind & POLY_BOX) !== 0;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** @see lib/neatogen/poly.c:ISCIRCLE */
|
|
57
|
+
function isCircleKind(p: Poly): boolean {
|
|
58
|
+
return (p.kind & POLY_CIRCLE) !== 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Bounding box of `verts` as [origin, corner].
|
|
63
|
+
* @see lib/neatogen/poly.c:bbox
|
|
64
|
+
*/
|
|
65
|
+
function bbox(verts: Point[]): { origin: Point; corner: Point } {
|
|
66
|
+
let xMin = verts[0]!.x;
|
|
67
|
+
let xMax = xMin;
|
|
68
|
+
let yMin = verts[0]!.y;
|
|
69
|
+
let yMax = yMin;
|
|
70
|
+
for (let i = 1; i < verts.length; i++) {
|
|
71
|
+
const v = verts[i]!;
|
|
72
|
+
xMin = Math.min(xMin, v.x);
|
|
73
|
+
yMin = Math.min(yMin, v.y);
|
|
74
|
+
xMax = Math.max(xMax, v.x);
|
|
75
|
+
yMax = Math.max(yMax, v.y);
|
|
76
|
+
}
|
|
77
|
+
return { origin: { x: xMin, y: yMin }, corner: { x: xMax, y: yMax } };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Scale every vertex in place.
|
|
82
|
+
* @see lib/neatogen/poly.c:inflatePts
|
|
83
|
+
*/
|
|
84
|
+
function inflatePts(verts: Point[], xmargin: number, ymargin: number): void {
|
|
85
|
+
for (const v of verts) {
|
|
86
|
+
v.x *= xmargin;
|
|
87
|
+
v.y *= ymargin;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* True when the 4 vertices form an axis-parallel rectangle (exact compares).
|
|
93
|
+
* @see lib/neatogen/poly.c:isBox
|
|
94
|
+
*/
|
|
95
|
+
function isBox(verts: Point[]): boolean {
|
|
96
|
+
if (verts.length !== 4) return false;
|
|
97
|
+
const [v0, v1, v2, v3] = verts as [Point, Point, Point, Point];
|
|
98
|
+
if (v0.y === v1.y) {
|
|
99
|
+
return v2.y === v3.y && v0.x === v3.x && v1.x === v2.x;
|
|
100
|
+
}
|
|
101
|
+
return v0.x === v1.x && v2.x === v3.x && v0.y === v3.y && v1.y === v2.y;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** @see lib/neatogen/poly.c:makeScaledTransPoint */
|
|
105
|
+
function makeScaledTransPoint(x: number, y: number, dx: number, dy: number): Point {
|
|
106
|
+
return { x: ps2inch(x) + dx, y: ps2inch(y) + dy };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** @see lib/neatogen/poly.c:makeScaledPoint */
|
|
110
|
+
function makeScaledPoint(x: number, y: number): Point {
|
|
111
|
+
return { x: ps2inch(x), y: ps2inch(y) };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** C `atoi`: leading integer, 0 when there is none. */
|
|
115
|
+
function atoi(s: string | undefined): number {
|
|
116
|
+
const v = s === undefined ? 0 : parseInt(s, 10);
|
|
117
|
+
return Number.isNaN(v) ? 0 : v;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Sample an ellipse (`samplepoints` points, default 20) of the node's
|
|
122
|
+
* half-size plus the additive margin.
|
|
123
|
+
* @see lib/neatogen/poly.c:genRound
|
|
124
|
+
*/
|
|
125
|
+
function genRound(n: Node, xm: number, ym: number): Point[] {
|
|
126
|
+
const isides = atoi(n.attrs.get('samplepoints'));
|
|
127
|
+
const sides = isides < MIN_SAMPLES ? DFLT_SAMPLE : isides;
|
|
128
|
+
const verts: Point[] = [];
|
|
129
|
+
for (let i = 0; i < sides; i++) {
|
|
130
|
+
verts.push({
|
|
131
|
+
x: (n.info.width / 2.0 + xm) * Math.cos((i / sides) * Math.PI * 2.0),
|
|
132
|
+
y: (n.info.height / 2.0 + ym) * Math.sin((i / sides) * Math.PI * 2.0),
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
return verts;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** `ND_shape(n)->name`; empty for an unbound shape. */
|
|
139
|
+
function shapeName(n: Node): string {
|
|
140
|
+
return (n.info.shape as { name?: string } | undefined)?.name ?? '';
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** `shapeOf(n)`. @see lib/common/shapes.c:shapeOf */
|
|
144
|
+
function shapeOf(n: Node): ShapeKind {
|
|
145
|
+
const s = n.info.shape as { kind?: ShapeKind } | undefined;
|
|
146
|
+
return s?.kind ?? ShapeKind.SH_UNSET;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** `ND_shape_info(n)` of a polygon shape; vertices are always set post-init. */
|
|
150
|
+
function polygonOf(n: Node): PolygonT & { vertices: Point[] } {
|
|
151
|
+
const poly = n.info.shape_info as PolygonT;
|
|
152
|
+
if (poly.vertices === null) {
|
|
153
|
+
throw new Error(`poly: node ${n.name} has no polygon vertices`);
|
|
154
|
+
}
|
|
155
|
+
return poly as PolygonT & { vertices: Point[] };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** `((field_t*)ND_shape_info(n))->b`. */
|
|
159
|
+
function recordBox(n: Node): Box {
|
|
160
|
+
return (n.info.shape_info as { b: Box }).b;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Cluster-node box, half-size `ND_width/2 + margin`, CCW from the UR. */
|
|
164
|
+
function clusterVerts(n: Node, xmargin: number, ymargin: number): Point[] {
|
|
165
|
+
const bx = n.info.width / 2.0 + xmargin;
|
|
166
|
+
const by = n.info.height / 2.0 + ymargin;
|
|
167
|
+
return [
|
|
168
|
+
{ x: bx, y: by },
|
|
169
|
+
{ x: -bx, y: by },
|
|
170
|
+
{ x: -bx, y: -by },
|
|
171
|
+
{ x: bx, y: -by },
|
|
172
|
+
];
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Polygon kind, shared by makePoly and makeAddPoly: BOX for "box" or a
|
|
177
|
+
* box-shaped "polygon", CIRCLE for a regular shape with fewer than 3 sides.
|
|
178
|
+
* `verts` are the first `sides` vertices (C passes `sides` as the count, so
|
|
179
|
+
* extra periphery rings are ignored); points or inches are equivalent because
|
|
180
|
+
* isBox compares for exact equality.
|
|
181
|
+
* @see lib/neatogen/poly.c:makeAddPoly
|
|
182
|
+
*/
|
|
183
|
+
function polyKind(n: Node, poly: PolygonT, verts: Point[]): number {
|
|
184
|
+
const name = shapeName(n);
|
|
185
|
+
if (name === 'box') return POLY_BOX;
|
|
186
|
+
if (name === 'polygon' && isBox(verts)) return POLY_BOX;
|
|
187
|
+
if (poly.sides < 3 && poly.regular) return POLY_CIRCLE;
|
|
188
|
+
return 0;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Additive margin for a real polygon. BOX relies on the vertices being CCW
|
|
193
|
+
* starting from the UR; other polygons grow each vertex radially.
|
|
194
|
+
* @see lib/neatogen/poly.c:makeAddPoly
|
|
195
|
+
*/
|
|
196
|
+
function addPolyVerts(poly: PolygonT & { vertices: Point[] }, box: boolean, m: Point): Point[] {
|
|
197
|
+
const pv = poly.vertices;
|
|
198
|
+
if (box) {
|
|
199
|
+
return [
|
|
200
|
+
{ x: ps2inch(pv[0]!.x) + m.x, y: ps2inch(pv[0]!.y) + m.y },
|
|
201
|
+
{ x: ps2inch(pv[1]!.x) - m.x, y: ps2inch(pv[1]!.y) + m.y },
|
|
202
|
+
{ x: ps2inch(pv[2]!.x) - m.x, y: ps2inch(pv[2]!.y) - m.y },
|
|
203
|
+
{ x: ps2inch(pv[3]!.x) + m.x, y: ps2inch(pv[3]!.y) - m.y },
|
|
204
|
+
];
|
|
205
|
+
}
|
|
206
|
+
const verts: Point[] = [];
|
|
207
|
+
for (let i = 0; i < poly.sides; i++) {
|
|
208
|
+
const h = Math.hypot(pv[i]!.x, pv[i]!.y);
|
|
209
|
+
verts.push({
|
|
210
|
+
x: ps2inch(pv[i]!.x * (1.0 + m.x / h)),
|
|
211
|
+
y: ps2inch(pv[i]!.y * (1.0 + m.y / h)),
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
return verts;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** SH_POLY case of makeAddPoly. */
|
|
218
|
+
function addPolyShape(n: Node, m: Point): { verts: Point[]; kind: number } {
|
|
219
|
+
const poly = polygonOf(n);
|
|
220
|
+
const kind = polyKind(n, poly, poly.vertices.slice(0, poly.sides));
|
|
221
|
+
if (poly.sides < 3) return { verts: genRound(n, m.x, m.y), kind };
|
|
222
|
+
return { verts: addPolyVerts(poly, kind === POLY_BOX, m), kind };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** SH_RECORD case of makeAddPoly. */
|
|
226
|
+
function addPolyRecord(n: Node, xmargin: number, ymargin: number): Point[] {
|
|
227
|
+
const b = recordBox(n);
|
|
228
|
+
return [
|
|
229
|
+
makeScaledTransPoint(b.ll.x, b.ll.y, -xmargin, -ymargin),
|
|
230
|
+
makeScaledTransPoint(b.ur.x, b.ll.y, xmargin, -ymargin),
|
|
231
|
+
makeScaledTransPoint(b.ur.x, b.ur.y, xmargin, ymargin),
|
|
232
|
+
makeScaledTransPoint(b.ll.x, b.ur.y, -xmargin, ymargin),
|
|
233
|
+
];
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Assemble a Poly from its vertices; origin/corner come from `bbox`. */
|
|
237
|
+
function finishPoly(verts: Point[], kind: number): Poly {
|
|
238
|
+
const { origin, corner } = bbox(verts);
|
|
239
|
+
return { origin, corner, verts, kind };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Polygon of node `n` with an additive margin (inches) on each side.
|
|
244
|
+
* Returns null (C returns 1) for an unsupported shape type.
|
|
245
|
+
* @see lib/neatogen/poly.c:makeAddPoly
|
|
246
|
+
*/
|
|
247
|
+
export function makeAddPoly(n: Node, xmargin: number, ymargin: number): Poly | null {
|
|
248
|
+
if (n.info.clust !== undefined) {
|
|
249
|
+
return finishPoly(clusterVerts(n, xmargin, ymargin), POLY_BOX);
|
|
250
|
+
}
|
|
251
|
+
switch (shapeOf(n)) {
|
|
252
|
+
case ShapeKind.SH_POLY: {
|
|
253
|
+
const { verts, kind } = addPolyShape(n, { x: xmargin, y: ymargin });
|
|
254
|
+
return finishPoly(verts, kind);
|
|
255
|
+
}
|
|
256
|
+
case ShapeKind.SH_RECORD:
|
|
257
|
+
return finishPoly(addPolyRecord(n, xmargin, ymargin), POLY_BOX);
|
|
258
|
+
case ShapeKind.SH_POINT:
|
|
259
|
+
return finishPoly(genRound(n, xmargin, ymargin), POLY_CIRCLE);
|
|
260
|
+
default:
|
|
261
|
+
console.error(`Error: makeAddPoly: unknown shape type ${shapeName(n)}\n`);
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** Polygon vertices in inches, or sampled ellipse when `sides < 3`. */
|
|
267
|
+
function polyVerts(n: Node, poly: PolygonT & { vertices: Point[] }): Point[] {
|
|
268
|
+
if (poly.sides < 3) return genRound(n, 0, 0);
|
|
269
|
+
return poly.vertices.slice(0, poly.sides).map((v) => makeScaledPoint(v.x, v.y));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** SH_RECORD case of makePoly. */
|
|
273
|
+
function recordVerts(n: Node): Point[] {
|
|
274
|
+
const b = recordBox(n);
|
|
275
|
+
return [
|
|
276
|
+
makeScaledPoint(b.ll.x, b.ll.y),
|
|
277
|
+
makeScaledPoint(b.ur.x, b.ll.y),
|
|
278
|
+
makeScaledPoint(b.ur.x, b.ur.y),
|
|
279
|
+
makeScaledPoint(b.ll.x, b.ur.y),
|
|
280
|
+
];
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Vertices and kind per shape, before the multiplicative margin. */
|
|
284
|
+
function baseShape(n: Node): { verts: Point[]; kind: number } | null {
|
|
285
|
+
if (n.info.clust !== undefined) {
|
|
286
|
+
return { verts: clusterVerts(n, 0, 0), kind: POLY_BOX };
|
|
287
|
+
}
|
|
288
|
+
switch (shapeOf(n)) {
|
|
289
|
+
case ShapeKind.SH_POLY: {
|
|
290
|
+
const poly = polygonOf(n);
|
|
291
|
+
const verts = polyVerts(n, poly);
|
|
292
|
+
return { verts, kind: polyKind(n, poly, verts) };
|
|
293
|
+
}
|
|
294
|
+
case ShapeKind.SH_RECORD:
|
|
295
|
+
return { verts: recordVerts(n), kind: POLY_BOX };
|
|
296
|
+
case ShapeKind.SH_POINT:
|
|
297
|
+
return { verts: genRound(n, 0, 0), kind: POLY_CIRCLE };
|
|
298
|
+
default:
|
|
299
|
+
console.error(`Error: makePoly: unknown shape type ${shapeName(n)}\n`);
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Polygon of node `n` scaled by (xmargin, ymargin) about the node centre.
|
|
306
|
+
* Returns null (C returns 1) for an unsupported shape type.
|
|
307
|
+
* @see lib/neatogen/poly.c:makePoly
|
|
308
|
+
*/
|
|
309
|
+
export function makePoly(n: Node, xmargin: number, ymargin: number): Poly | null {
|
|
310
|
+
const base = baseShape(n);
|
|
311
|
+
if (base === null) return null;
|
|
312
|
+
if (xmargin !== 1.0 || ymargin !== 1.0) {
|
|
313
|
+
inflatePts(base.verts, xmargin, ymargin);
|
|
314
|
+
}
|
|
315
|
+
return finishPoly(base.verts, base.kind);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** @see lib/neatogen/poly.c:pintersect */
|
|
319
|
+
function pintersect(originp: Point, cornerp: Point, originq: Point, cornerq: Point): boolean {
|
|
320
|
+
return (
|
|
321
|
+
originp.x <= cornerq.x &&
|
|
322
|
+
originq.x <= cornerp.x &&
|
|
323
|
+
originp.y <= cornerq.y &&
|
|
324
|
+
originq.y <= cornerp.y
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** @see lib/neatogen/geometry.c:area_2 */
|
|
329
|
+
function area2(a: Point, b: Point, c: Point): number {
|
|
330
|
+
return (a.y - b.y) * (c.x - b.x) - (c.y - b.y) * (a.x - b.x);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** @see lib/neatogen/geometry.c:leftOf */
|
|
334
|
+
function leftOf(a: Point, b: Point, c: Point): boolean {
|
|
335
|
+
return area2(a, b, c) > 0;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Segment ab versus cd (parallel segments report false).
|
|
340
|
+
* @see lib/neatogen/geometry.c:intersection
|
|
341
|
+
*/
|
|
342
|
+
function intersection(a: Point, b: Point, c: Point, d: Point): boolean {
|
|
343
|
+
const denom = a.x * (d.y - c.y) + b.x * (c.y - d.y) + d.x * (b.y - a.y) + c.x * (a.y - b.y);
|
|
344
|
+
if (denom === 0.0) return false;
|
|
345
|
+
const s = (a.x * (d.y - c.y) + c.x * (a.y - d.y) + d.x * (c.y - a.y)) / denom;
|
|
346
|
+
const t = -(a.x * (c.y - b.y) + b.x * (a.y - c.y) + c.x * (b.y - a.y)) / denom;
|
|
347
|
+
return 0.0 <= s && s <= 1.0 && 0.0 <= t && t <= 1.0;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** Cursor state of the `edgesIntersect` walk (`a`, `b`, `aa`, `ba`). */
|
|
351
|
+
interface EdgeWalk {
|
|
352
|
+
a: number;
|
|
353
|
+
b: number;
|
|
354
|
+
aa: number;
|
|
355
|
+
ba: number;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Advance rules of `edgesIntersect`; `advance(A,B,N)` is `B++, A=(A+1)%N`.
|
|
360
|
+
* @see lib/neatogen/poly.c:edgesIntersect
|
|
361
|
+
*/
|
|
362
|
+
function advanceRule(
|
|
363
|
+
w: EdgeWalk,
|
|
364
|
+
turn: { cross: number; bHA: boolean; aHB: boolean },
|
|
365
|
+
n: number,
|
|
366
|
+
m: number,
|
|
367
|
+
): void {
|
|
368
|
+
const advanceA = (): void => {
|
|
369
|
+
w.aa++;
|
|
370
|
+
w.a = (w.a + 1) % n;
|
|
371
|
+
};
|
|
372
|
+
const advanceB = (): void => {
|
|
373
|
+
w.ba++;
|
|
374
|
+
w.b = (w.b + 1) % m;
|
|
375
|
+
};
|
|
376
|
+
if (turn.cross === 0 && !turn.bHA && !turn.aHB) advanceA();
|
|
377
|
+
else if (turn.cross >= 0) {
|
|
378
|
+
if (turn.bHA) advanceA();
|
|
379
|
+
else advanceB();
|
|
380
|
+
} else if (turn.aHB) advanceB();
|
|
381
|
+
else advanceA();
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* True when the boundaries of convex-walked polygons P and Q cross.
|
|
386
|
+
* @see lib/neatogen/poly.c:edgesIntersect
|
|
387
|
+
*/
|
|
388
|
+
function edgesIntersect(P: Point[], Q: Point[], n: number, m: number): boolean {
|
|
389
|
+
const w: EdgeWalk = { a: 0, b: 0, aa: 0, ba: 0 };
|
|
390
|
+
do {
|
|
391
|
+
const a1 = (w.a + n - 1) % n;
|
|
392
|
+
const b1 = (w.b + m - 1) % m;
|
|
393
|
+
const pa = P[w.a]!;
|
|
394
|
+
const pa1 = P[a1]!;
|
|
395
|
+
const qb = Q[w.b]!;
|
|
396
|
+
const qb1 = Q[b1]!;
|
|
397
|
+
const A = { x: pa.x - pa1.x, y: pa.y - pa1.y };
|
|
398
|
+
const B = { x: qb.x - qb1.x, y: qb.y - qb1.y };
|
|
399
|
+
const cross = area2({ x: 0, y: 0 }, A, B);
|
|
400
|
+
const bHA = leftOf(pa1, pa, qb);
|
|
401
|
+
const aHB = leftOf(qb1, qb, pa);
|
|
402
|
+
if (intersection(pa1, pa, qb1, qb)) return true;
|
|
403
|
+
advanceRule(w, { cross, bHA, aHB }, n, m);
|
|
404
|
+
} while ((w.aa < n || w.ba < m) && w.aa < 2 * n && w.ba < 2 * m);
|
|
405
|
+
return false;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/** Marker: the query point lies on the polygon boundary. */
|
|
409
|
+
const ON_BOUNDARY = -1;
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Crossing of an edge that straddles the x-axis with the +x ray from the
|
|
413
|
+
* origin: ON_BOUNDARY, 0, 0.5 (through a vertex) or 1.
|
|
414
|
+
* @see lib/neatogen/poly.c:inPoly
|
|
415
|
+
*/
|
|
416
|
+
function straddleCrossing(t: Point, t1: Point): number {
|
|
417
|
+
const x = (t.x * t1.y - t1.x * t.y) / (t1.y - t.y);
|
|
418
|
+
if (x === 0) return ON_BOUNDARY;
|
|
419
|
+
if (x <= 0) return 0;
|
|
420
|
+
return t.y === 0 || t1.y === 0 ? 0.5 : 1.0;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Ray-crossing contribution of edge (t1 -> t) with the query at the origin,
|
|
425
|
+
* or ON_BOUNDARY when the origin is on the edge.
|
|
426
|
+
* @see lib/neatogen/poly.c:inPoly
|
|
427
|
+
*/
|
|
428
|
+
function edgeCrossing(t: Point, t1: Point): number {
|
|
429
|
+
// horizontal edge: test whether the point is on it
|
|
430
|
+
if (t.y === 0 && t1.y === 0) {
|
|
431
|
+
return t.x * t1.x < 0 ? ON_BOUNDARY : 0;
|
|
432
|
+
}
|
|
433
|
+
const straddles = (t.y >= 0 && t1.y <= 0) || (t1.y >= 0 && t.y <= 0);
|
|
434
|
+
return straddles ? straddleCrossing(t, t1) : 0;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* True when q is inside polygon `vertex` (CCW order assumed).
|
|
439
|
+
* @see lib/neatogen/poly.c:inPoly
|
|
440
|
+
*/
|
|
441
|
+
function inPoly(vertex: Point[], n: number, q: Point): boolean {
|
|
442
|
+
const tp3 = vertex.map((v) => ({ x: v.x - q.x, y: v.y - q.y }));
|
|
443
|
+
let crossings = 0;
|
|
444
|
+
for (let i = 0; i < n; i++) {
|
|
445
|
+
const c = edgeCrossing(tp3[i]!, tp3[(i + n - 1) % n]!);
|
|
446
|
+
if (c === ON_BOUNDARY) return true;
|
|
447
|
+
crossings += c;
|
|
448
|
+
}
|
|
449
|
+
return Math.trunc(crossings) % 2 === 1;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/** @see lib/neatogen/poly.c:inBox */
|
|
453
|
+
function inBox(p: Point, originPoint: Point, corner: Point): boolean {
|
|
454
|
+
return p.x <= corner.x && p.x >= originPoint.x && p.y <= corner.y && p.y >= originPoint.y;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/** @see lib/neatogen/poly.c:transCopy */
|
|
458
|
+
function transCopy(inp: Point[], off: Point): Point[] {
|
|
459
|
+
return inp.map((v) => ({ x: v.x + off.x, y: v.y + off.y }));
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* True when polygon `pp` placed at `p` overlaps `qp` placed at `q`.
|
|
464
|
+
* Bounding boxes first, then the BOX/CIRCLE shortcuts, then the exact
|
|
465
|
+
* polygon test.
|
|
466
|
+
* @see lib/neatogen/poly.c:polyOverlap
|
|
467
|
+
*/
|
|
468
|
+
export function polyOverlap(p: Point, pp: Poly, q: Point, qp: Poly): boolean {
|
|
469
|
+
// translate bounding boxes
|
|
470
|
+
const op = { x: p.x + pp.origin.x, y: p.y + pp.origin.y };
|
|
471
|
+
const cp = { x: p.x + pp.corner.x, y: p.y + pp.corner.y };
|
|
472
|
+
const oq = { x: q.x + qp.origin.x, y: q.y + qp.origin.y };
|
|
473
|
+
const cq = { x: q.x + qp.corner.x, y: q.y + qp.corner.y };
|
|
474
|
+
|
|
475
|
+
// if bounding boxes don't overlap, done
|
|
476
|
+
if (!pintersect(op, cp, oq, cq)) return false;
|
|
477
|
+
|
|
478
|
+
if (isBoxKind(pp) && isBoxKind(qp)) return true;
|
|
479
|
+
if (isCircleKind(pp) && isCircleKind(qp)) {
|
|
480
|
+
const d = pp.corner.x - pp.origin.x + qp.corner.x - qp.origin.x;
|
|
481
|
+
const dx = p.x - q.x;
|
|
482
|
+
const dy = p.y - q.y;
|
|
483
|
+
return dx * dx + dy * dy <= (d * d) / 4.0;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const tp1 = transCopy(pp.verts, p);
|
|
487
|
+
const tp2 = transCopy(qp.verts, q);
|
|
488
|
+
return (
|
|
489
|
+
edgesIntersect(tp1, tp2, pp.verts.length, qp.verts.length) ||
|
|
490
|
+
(inBox(tp1[0]!, oq, cq) && inPoly(tp2, qp.verts.length, tp1[0]!)) ||
|
|
491
|
+
(inBox(tp2[0]!, op, cp) && inPoly(tp1, pp.verts.length, tp2[0]!))
|
|
492
|
+
);
|
|
493
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EPL-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Single-source shortest paths for the SGD engine, ported with C's indexed
|
|
4
|
+
* binary heap. The heap's tie-breaking order decides the order terms are
|
|
5
|
+
* emitted, and sgd's Fisher-Yates shuffle consumes that order, so the heap
|
|
6
|
+
* (including its 1-based left/right/parent arithmetic over a 0-based array)
|
|
7
|
+
* is reproduced exactly rather than replaced by a lazy-deletion heap.
|
|
8
|
+
*
|
|
9
|
+
* @see lib/neatogen/dijkstra.c
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { TermSgd } from './dijkstra.js';
|
|
13
|
+
import type { GraphSgd } from './sgd.js';
|
|
14
|
+
|
|
15
|
+
/** C FLT_MAX. */
|
|
16
|
+
const FLT_MAX = 3.4028234663852886e38;
|
|
17
|
+
|
|
18
|
+
/** @see lib/neatogen/dijkstra.c:heap */
|
|
19
|
+
interface Heap {
|
|
20
|
+
data: Int32Array;
|
|
21
|
+
heapSize: number;
|
|
22
|
+
index: Int32Array;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// C's left/right/parent are 1-based formulas applied to a 0-based array.
|
|
26
|
+
const left = (i: number): number => 2 * i;
|
|
27
|
+
const right = (i: number): number => 2 * i + 1;
|
|
28
|
+
const parent = (i: number): number => Math.trunc(i / 2);
|
|
29
|
+
|
|
30
|
+
/** @see lib/neatogen/dijkstra.c:greaterPriority_f */
|
|
31
|
+
const greaterPriority = (h: Heap, i: number, j: number, dist: Float32Array): boolean =>
|
|
32
|
+
dist[h.data[i]] < dist[h.data[j]];
|
|
33
|
+
|
|
34
|
+
/** @see lib/neatogen/dijkstra.c:exchange */
|
|
35
|
+
function exchange(h: Heap, i: number, j: number): void {
|
|
36
|
+
const t = h.data[i];
|
|
37
|
+
h.data[i] = h.data[j];
|
|
38
|
+
h.data[j] = t;
|
|
39
|
+
h.index[h.data[i]] = i;
|
|
40
|
+
h.index[h.data[j]] = j;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** @see lib/neatogen/dijkstra.c:heapify_f */
|
|
44
|
+
function heapify(h: Heap, start: number, dist: Float32Array): void {
|
|
45
|
+
let i = start;
|
|
46
|
+
for (;;) {
|
|
47
|
+
const l = left(i);
|
|
48
|
+
const r = right(i);
|
|
49
|
+
let largest = l < h.heapSize && greaterPriority(h, l, i, dist) ? l : i;
|
|
50
|
+
if (r < h.heapSize && greaterPriority(h, r, largest, dist)) largest = r;
|
|
51
|
+
if (largest === i) break;
|
|
52
|
+
exchange(h, largest, i);
|
|
53
|
+
i = largest;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** @see lib/neatogen/dijkstra.c:initHeap_f */
|
|
58
|
+
function initHeap(startVertex: number, dist: Float32Array, n: number): Heap {
|
|
59
|
+
const h: Heap = {
|
|
60
|
+
data: new Int32Array(Math.max(n - 1, 0)),
|
|
61
|
+
heapSize: n - 1,
|
|
62
|
+
index: new Int32Array(n),
|
|
63
|
+
};
|
|
64
|
+
let count = 0;
|
|
65
|
+
for (let i = 0; i < n; i++) {
|
|
66
|
+
if (i === startVertex) continue;
|
|
67
|
+
h.data[count] = i;
|
|
68
|
+
h.index[i] = count++;
|
|
69
|
+
}
|
|
70
|
+
for (let j = Math.trunc((n - 1) / 2); j >= 0; j--) heapify(h, j, dist);
|
|
71
|
+
return h;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** @see lib/neatogen/dijkstra.c:extractMax_f; returns -1 when empty. */
|
|
75
|
+
function extractMax(h: Heap, dist: Float32Array): number {
|
|
76
|
+
if (h.heapSize === 0) return -1;
|
|
77
|
+
const max = h.data[0];
|
|
78
|
+
h.data[0] = h.data[h.heapSize - 1];
|
|
79
|
+
h.index[h.data[0]] = 0;
|
|
80
|
+
h.heapSize--;
|
|
81
|
+
heapify(h, 0, dist);
|
|
82
|
+
return max;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** @see lib/neatogen/dijkstra.c:increaseKey_f */
|
|
86
|
+
function increaseKey(h: Heap, v: number, newDist: number, dist: Float32Array): void {
|
|
87
|
+
if (dist[v] <= newDist) return;
|
|
88
|
+
dist[v] = newDist;
|
|
89
|
+
let i = h.index[v];
|
|
90
|
+
while (i > 0 && dist[h.data[parent(i)]] > newDist) {
|
|
91
|
+
h.data[i] = h.data[parent(i)];
|
|
92
|
+
h.index[h.data[i]] = i;
|
|
93
|
+
i = parent(i);
|
|
94
|
+
}
|
|
95
|
+
h.data[i] = v;
|
|
96
|
+
h.index[v] = i;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Single-source shortest paths that also builds terms as it goes. Writes
|
|
101
|
+
* terms for reachable pinned nodes and nodes below `source`; returns how many.
|
|
102
|
+
* `terms` is appended to from index 0 (the caller passes a fresh chunk).
|
|
103
|
+
*
|
|
104
|
+
* @see lib/neatogen/dijkstra.c:dijkstra_sgd
|
|
105
|
+
*/
|
|
106
|
+
export function dijkstraSgd(graph: GraphSgd, source: number, terms: TermSgd[]): number {
|
|
107
|
+
const dists = new Float32Array(graph.n).fill(FLT_MAX);
|
|
108
|
+
dists[source] = 0;
|
|
109
|
+
for (let i = graph.sources[source]; i < graph.sources[source + 1]; i++) {
|
|
110
|
+
dists[graph.targets[i]] = graph.weights[i];
|
|
111
|
+
}
|
|
112
|
+
const h = initHeap(source, dists, graph.n);
|
|
113
|
+
let offset = 0;
|
|
114
|
+
for (let closest = extractMax(h, dists); closest >= 0; closest = extractMax(h, dists)) {
|
|
115
|
+
const d = dists[closest];
|
|
116
|
+
if (d === FLT_MAX) break;
|
|
117
|
+
if (graph.pinneds[closest] || closest < source) {
|
|
118
|
+
terms[offset++] = { i: source, j: closest, d, w: Math.fround(1 / Math.fround(d * d)) };
|
|
119
|
+
}
|
|
120
|
+
for (let i = graph.sources[closest]; i < graph.sources[closest + 1]; i++) {
|
|
121
|
+
increaseKey(h, graph.targets[i], Math.fround(d + graph.weights[i]), dists);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return offset;
|
|
125
|
+
}
|