@knowvah/dot-engine 2.6.1 → 2.6.2
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/pointset.d.ts +35 -0
- package/dist/common/pointset.d.ts.map +1 -0
- package/dist/index.js +73 -44
- package/dist/index.js.map +3 -3
- package/dist/layout/pack/poly-place.d.ts.map +1 -1
- package/dist/render.js +73 -44
- package/dist/render.js.map +3 -3
- package/package.json +2 -2
- package/src/common/pointset.ts +73 -0
- package/src/layout/pack/poly-place.ts +42 -47
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@knowvah/dot-engine",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.2",
|
|
4
4
|
"description": "Faithful TypeScript port of Graphviz (dot, neato, fdp, sfdp, circo, twopi, osage, patchwork) — no C: no native binary, no WASM. Oracle-verified against the C implementation, runs in the browser.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"graphviz",
|
|
@@ -100,7 +100,7 @@
|
|
|
100
100
|
},
|
|
101
101
|
"devDependencies": {
|
|
102
102
|
"@fontsource/jetbrains-mono": "^5.3.0",
|
|
103
|
-
"@hpcc-js/wasm-graphviz": "1.
|
|
103
|
+
"@hpcc-js/wasm-graphviz": "1.29.2",
|
|
104
104
|
"@knowvah/theme": "0.3.0",
|
|
105
105
|
"@knowvah/vitepress-plugin-dot": "^3.0.1",
|
|
106
106
|
"@semantic-release/commit-analyzer": "^13.0.1",
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Set of points, compared exactly on their double coordinates.
|
|
5
|
+
*
|
|
6
|
+
* C keeps the points in an ordered cdt dictionary (`Dtoset`) under `cmppair`:
|
|
7
|
+
* x first, then y, plain double `<`/`>`, so -0 and 0 are one key. Here the
|
|
8
|
+
* dictionary is a two-level `Map` keyed on the coordinates themselves. Map
|
|
9
|
+
* keys use SameValueZero, which also makes -0 and 0 one key. A lookup
|
|
10
|
+
* allocates nothing; packing calls it once per cell per candidate position.
|
|
11
|
+
*
|
|
12
|
+
* @see lib/common/pointset.c
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { Point } from '../model/geom.js';
|
|
16
|
+
|
|
17
|
+
/** @see lib/common/pointset.c:cmppair */
|
|
18
|
+
function cmpPair(a: Point, b: Point): number {
|
|
19
|
+
if (a.x > b.x) return 1;
|
|
20
|
+
if (a.x < b.x) return -1;
|
|
21
|
+
if (a.y > b.y) return 1;
|
|
22
|
+
if (a.y < b.y) return -1;
|
|
23
|
+
return 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** @see lib/common/pointset.h:PointSet */
|
|
27
|
+
export class PointSet {
|
|
28
|
+
private readonly byX = new Map<number, Map<number, Point>>();
|
|
29
|
+
private n = 0;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Insert (x, y). A duplicate keeps the point inserted first, as `dtinsert`
|
|
33
|
+
* returns the existing object and the new one is freed.
|
|
34
|
+
* @see lib/common/pointset.c:addPS
|
|
35
|
+
*/
|
|
36
|
+
addPS(x: number, y: number): void {
|
|
37
|
+
let col = this.byX.get(x);
|
|
38
|
+
if (col === undefined) {
|
|
39
|
+
col = new Map();
|
|
40
|
+
this.byX.set(x, col);
|
|
41
|
+
}
|
|
42
|
+
if (col.has(y)) return;
|
|
43
|
+
col.set(y, { x, y });
|
|
44
|
+
this.n++;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** @see lib/common/pointset.c:inPS */
|
|
48
|
+
inPS(pt: Point): boolean {
|
|
49
|
+
return this.byX.get(pt.x)?.has(pt.y) ?? false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** @see lib/common/pointset.c:isInPS */
|
|
53
|
+
isInPS(x: number, y: number): boolean {
|
|
54
|
+
return this.byX.get(x)?.has(y) ?? false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** @see lib/common/pointset.c:sizeOf */
|
|
58
|
+
sizeOf(): number {
|
|
59
|
+
return this.n;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The points in dictionary order (`dtflatten` walks the ordered set).
|
|
64
|
+
* @see lib/common/pointset.c:pointsOf
|
|
65
|
+
*/
|
|
66
|
+
pointsOf(): Point[] {
|
|
67
|
+
const pts: Point[] = [];
|
|
68
|
+
for (const col of this.byX.values()) {
|
|
69
|
+
for (const p of col.values()) pts.push({ x: p.x, y: p.y });
|
|
70
|
+
}
|
|
71
|
+
return pts.sort(cmpPair);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -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
|
|