@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@knowvah/dot-engine",
3
- "version": "2.6.1",
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.28.0",
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.add(st.x, st.y);
83
+ ps.addPS(st.x, st.y);
100
84
  if (st.x === st.x2) return;
101
85
  if (d >= 0) { st.y += st.sy; d -= st.ax; }
102
86
  st.x += st.sx;
@@ -108,7 +92,7 @@ function lineXDominant(st: LineState, ps: PointSet): void {
108
92
  function lineYDominant(st: LineState, ps: PointSet): void {
109
93
  let d = st.ax - (st.ay >> 1);
110
94
  for (;;) {
111
- ps.add(st.x, st.y);
95
+ ps.addPS(st.x, st.y);
112
96
  if (st.y === st.y2) return;
113
97
  if (d >= 0) { st.x += st.sx; d -= st.ay; }
114
98
  st.y += st.sy;
@@ -136,7 +120,7 @@ function fillLine(p: Point, q: Point, ps: PointSet): void {
136
120
  /** Mark all cells in the inclusive cell-rect [LL, UR]. */
137
121
  function fillRect(LL: Point, UR: Point, ps: PointSet): void {
138
122
  for (let x = LL.x; x <= UR.x; x++) {
139
- for (let y = LL.y; y <= UR.y; y++) ps.add(x, y);
123
+ for (let y = LL.y; y <= UR.y; y++) ps.addPS(x, y);
140
124
  }
141
125
  }
142
126
 
@@ -208,7 +192,7 @@ function genBox(bb0: Box, ssize: number, margin: number, center: Point): GInfo {
208
192
  const UR = roundedCell({ x: center.x + bbW + margin, y: center.y + bbH + margin }, ssize);
209
193
  fillRect(LL, UR, ps);
210
194
  return {
211
- cells: ps.points(),
195
+ cells: ps.pointsOf(),
212
196
  perim: grid(bb0.ur.x - bb0.ll.x + 2 * margin, ssize)
213
197
  + grid(bb0.ur.y - bb0.ll.y + 2 * margin, ssize),
214
198
  index: 0,
@@ -266,7 +250,7 @@ function genPoly(
266
250
  };
267
251
  for (const n of g.nodes.values()) coverNode(n, ctx);
268
252
  return {
269
- cells: ps.points(),
253
+ cells: ps.pointsOf(),
270
254
  perim: grid(bb.ur.x - bb.ll.x + 2 * pinfo.margin, ssize)
271
255
  + grid(bb.ur.y - bb.ll.y + 2 * pinfo.margin, ssize),
272
256
  index: 0,
@@ -289,7 +273,7 @@ interface PlaceCtx {
289
273
 
290
274
  function fits(x: number, y: number, info: GInfo, ctx: PlaceCtx): boolean {
291
275
  for (const c of info.cells) {
292
- if (ctx.ps.has({ x: c.x + x, y: c.y + y })) return false;
276
+ if (ctx.ps.isInPS(c.x + x, c.y + y)) return false;
293
277
  }
294
278
  const bb = ctx.bbs[info.index]!;
295
279
  const LL = { x: cround(bb.ll.x), y: cround(bb.ll.y) };
@@ -297,30 +281,44 @@ function fits(x: number, y: number, info: GInfo, ctx: PlaceCtx): boolean {
297
281
  // is widened to double (objdump of libgvc `_fits`). Matters only once
298
282
  // computeStep has saturated at INT_MAX.
299
283
  ctx.places[info.index] = { x: Math.imul(ctx.step, x) - LL.x, y: Math.imul(ctx.step, y) - LL.y };
300
- for (const c of info.cells) ctx.ps.add(c.x + x, c.y + y);
284
+ for (const c of info.cells) ctx.ps.addPS(c.x + x, c.y + y);
301
285
  return true;
302
286
  }
303
287
 
304
- /** Ring candidates for wide graphs (W >= H). @see pack.c:placeGraph */
305
- function* ringWide(bnd: number): Generator<[number, number]> {
306
- let x = 0;
307
- let y = -bnd;
308
- for (; x < bnd; x++) yield [x, y];
309
- for (; y < bnd; y++) yield [x, y];
310
- for (; x > -bnd; x--) yield [x, y];
311
- for (; y > -bnd; y--) yield [x, y];
312
- for (; x < 0; x++) yield [x, y];
288
+ /** Ring cursor, shared by the legs of one ring as C shares its x and y. */
289
+ interface Cursor {
290
+ x: number;
291
+ y: number;
292
+ }
293
+
294
+ /**
295
+ * One straight leg of a ring: step (dx, dy) from the cursor until the moving
296
+ * coordinate reaches `end`, trying each cell. Every leg starts on the
297
+ * `end` side's integer lattice with a unit step, so `!== end` stops where C's
298
+ * `<`/`>` bound does.
299
+ * @see pack.c:placeGraph (one of the five loops per ring)
300
+ */
301
+ function leg(c: Cursor, dx: number, dy: number, end: number, info: GInfo, ctx: PlaceCtx): boolean {
302
+ for (; (dx !== 0 ? c.x : c.y) !== end; c.x += dx, c.y += dy) {
303
+ if (fits(c.x, c.y, info, ctx)) return true;
304
+ }
305
+ return false;
313
306
  }
314
307
 
315
- /** Ring candidates for tall graphs (W < H). @see pack.c:placeGraph */
316
- function* ringTall(bnd: number): Generator<[number, number]> {
317
- let y = 0;
318
- let x = -bnd;
319
- for (; y > -bnd; y--) yield [x, y];
320
- for (; x < bnd; x++) yield [x, y];
321
- for (; y < bnd; y++) yield [x, y];
322
- for (; x > -bnd; x--) yield [x, y];
323
- for (; y > 0; y--) yield [x, y];
308
+ /** Ring `bnd` for wide graphs (W >= H). @see pack.c:placeGraph */
309
+ function ringWide(bnd: number, info: GInfo, ctx: PlaceCtx): boolean {
310
+ const c = { x: 0, y: -bnd };
311
+ return leg(c, 1, 0, bnd, info, ctx) || leg(c, 0, 1, bnd, info, ctx)
312
+ || leg(c, -1, 0, -bnd, info, ctx) || leg(c, 0, -1, -bnd, info, ctx)
313
+ || leg(c, 1, 0, 0, info, ctx);
314
+ }
315
+
316
+ /** Ring `bnd` for tall graphs (W < H). @see pack.c:placeGraph */
317
+ function ringTall(bnd: number, info: GInfo, ctx: PlaceCtx): boolean {
318
+ const c = { x: -bnd, y: 0 };
319
+ return leg(c, 0, -1, -bnd, info, ctx) || leg(c, 1, 0, bnd, info, ctx)
320
+ || leg(c, 0, 1, bnd, info, ctx) || leg(c, -1, 0, -bnd, info, ctx)
321
+ || leg(c, 0, -1, 0, info, ctx);
324
322
  }
325
323
 
326
324
  /**
@@ -331,7 +329,7 @@ function* ringTall(bnd: number): Generator<[number, number]> {
331
329
  */
332
330
  function placeFixed(info: GInfo, ctx: PlaceCtx, center: Point): void {
333
331
  ctx.places[info.index] = { x: -center.x, y: -center.y };
334
- for (const c of info.cells) ctx.ps.add(c.x, c.y);
332
+ for (const c of info.cells) ctx.ps.addPS(c.x, c.y);
335
333
  }
336
334
 
337
335
  /**
@@ -349,10 +347,7 @@ function placeGraph(i: number, info: GInfo, ctx: PlaceCtx): void {
349
347
  if (fits(0, 0, info, ctx)) return;
350
348
  const wide = Math.ceil(bb.ur.x - bb.ll.x) >= Math.ceil(bb.ur.y - bb.ll.y);
351
349
  for (let bnd = 1; ; bnd++) {
352
- const ring = wide ? ringWide(bnd) : ringTall(bnd);
353
- for (const [x, y] of ring) {
354
- if (fits(x, y, info, ctx)) return;
355
- }
350
+ if (wide ? ringWide(bnd, info, ctx) : ringTall(bnd, info, ctx)) return;
356
351
  }
357
352
  }
358
353