@knowvah/dot-engine 2.0.2 → 2.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@knowvah/dot-engine",
3
- "version": "2.0.2",
3
+ "version": "2.1.0",
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",
@@ -63,11 +63,14 @@ export interface AsyncSvgResult {
63
63
  fontIssues: FontIssue[];
64
64
  }
65
65
 
66
- /** `document.fonts` when a document exposes one (absent in Node and Workers). */
66
+ /**
67
+ * `document.fonts` in a page, else `self.fonts` (WorkerGlobalScope.fonts) in a
68
+ * Worker; undefined in Node. @see plans/async-api/DESIGN.md D5
69
+ */
67
70
  function defaultFontSet(): FontSetLike | undefined {
68
71
  const doc: unknown = Reflect.get(globalThis, 'document');
69
- if (typeof doc !== 'object' || doc === null) return undefined;
70
- const fonts: unknown = Reflect.get(doc, 'fonts');
72
+ const owner: unknown = typeof doc === 'object' && doc !== null ? doc : globalThis;
73
+ const fonts: unknown = Reflect.get(owner as object, 'fonts');
71
74
  if (typeof fonts !== 'object' || fonts === null) return undefined;
72
75
  return fonts as FontSetLike; // structural: FontFaceSet.load
73
76
  }
@@ -37,3 +37,12 @@
37
37
  export function cround(v: number): number {
38
38
  return v >= 0 ? Math.floor(v + 0.5) : Math.ceil(v - 0.5);
39
39
  }
40
+
41
+ /**
42
+ * Degrees to radians in C's operation order, `(deg)/180.0 * M_PI`.
43
+ * `deg * Math.PI / 180` rounds differently for some angles (e.g. 33°).
44
+ * @see lib/common/arith.h:49 (RADIANS)
45
+ */
46
+ export function RADIANS(deg: number): number {
47
+ return (deg / 180.0) * Math.PI;
48
+ }
@@ -22,6 +22,7 @@ import type { Point } from '../model/geom.js';
22
22
  import { STAR, CYLINDER } from './shapeData.js';
23
23
  import { starVertices } from './poly-vertices.js';
24
24
  import { fma } from './fma.js';
25
+ import { RADIANS } from './arith.js';
25
26
 
26
27
  /** Whitespace in points around labels / between peripheries. @see lib/common/const.h:GAP */
27
28
  export const GAP = 4;
@@ -277,7 +278,7 @@ export interface PolyGeom {
277
278
  /** Distort, skew, orient, and scale one unit vertex. @see shapes.c:poly_init */
278
279
  function transformUnitVertex(R: Point, g: PolyGeom, c: { skewdist: number; gdistortion: number; gskew: number }, bb: Point): Point {
279
280
  const D = { x: R.x * (c.skewdist + R.y * c.gdistortion) + R.y * c.gskew, y: R.y };
280
- const alpha = (g.orientation * Math.PI) / 180 + Math.atan2(D.y, D.x);
281
+ const alpha = RADIANS(g.orientation) + Math.atan2(D.y, D.x);
281
282
  // C uses libm hypot(D.x, D.y). V8's Math.hypot is a scaled algorithm that
282
283
  // returns a result 1 ULP off from libm hypot for in-range polygon vertices
283
284
  // (e.g. hypot(0.35355…, 0.35355…): libm = 0.5 exactly, Math.hypot = 0.5+1ULP),
@@ -6,7 +6,8 @@
6
6
  * 1. explicit override via setTextMeasurer (tests, host-faithful Node opt-in)
7
7
  * 2. GV_TEXT_MEASURER=estimate|lut — test/CI hook to force a specific measurer
8
8
  * 3. browser (document present) — CanvasTextMeasurer over the page canvas
9
- * (host-faithful: the same font the browser renders the SVG with)
9
+ * (host-faithful: the same font the browser renders the SVG with);
10
+ * Worker (no document) — the same over an OffscreenCanvas
10
11
  * 4. Node default — EstimateTextMeasurer (deterministic; matches
11
12
  * graphviz's own headless estimate_textspan_size)
12
13
  *
@@ -77,6 +78,21 @@ function adviseHostFaithful(): void {
77
78
  );
78
79
  }
79
80
 
81
+ /**
82
+ * A CanvasTextMeasurer over an OffscreenCanvas 2d context, for Workers (no
83
+ * `document`); undefined where OffscreenCanvas is missing or unusable (Node).
84
+ * @see plans/async-api/DESIGN.md D5
85
+ */
86
+ function offscreenMeasurer(): CanvasTextMeasurer | undefined {
87
+ if (typeof OffscreenCanvas === 'undefined') return undefined;
88
+ try {
89
+ const ctx2d = new OffscreenCanvas(0, 0).getContext('2d');
90
+ return ctx2d ? new CanvasTextMeasurer(ctx2d) : undefined;
91
+ } catch {
92
+ return undefined; // OffscreenCanvas present but no 2d context
93
+ }
94
+ }
95
+
80
96
  /** Resolve the text measurer for this render (see resolution order above). */
81
97
  export function createMeasurer(): TextMeasurer {
82
98
  if (override) return override;
@@ -88,6 +104,9 @@ export function createMeasurer(): TextMeasurer {
88
104
  const ctx2d = document.createElement('canvas').getContext('2d');
89
105
  if (ctx2d) return new CanvasTextMeasurer(ctx2d);
90
106
  } catch { /* canvas unavailable */ }
107
+ } else {
108
+ const offscreen = offscreenMeasurer(); // Worker: no document, same fonts API
109
+ if (offscreen) return offscreen;
91
110
  }
92
111
  adviseHostFaithful(); // Node, no host canvas → built-in deterministic metrics
93
112
  // Node default = EstimateTextMeasurer: the deterministic reference that matches
@@ -285,7 +285,7 @@ export class CanvasTextMeasurer implements TextMeasurer {
285
285
  */
286
286
  private readonly fonts = new Map<string, string>();
287
287
 
288
- constructor(private readonly ctx: CanvasRenderingContext2D) {}
288
+ constructor(private readonly ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D) {}
289
289
 
290
290
  measure(
291
291
  text: string,
@@ -16,6 +16,7 @@ import { VIRTUAL } from './fastgr.js';
16
16
  import { IGNORED } from './rank.js';
17
17
  import { lateDouble } from '../../common/nodeinit.js';
18
18
  import { nodesInSeq } from './decomp.js';
19
+ import { RADIANS } from '../../common/arith.js';
19
20
 
20
21
  // ---------------------------------------------------------------------------
21
22
  // TextLabel interface (minimal — full type deferred to Batch 5b)
@@ -286,7 +287,7 @@ function writePortlabelPos(
286
287
  distanceStr: string,
287
288
  ): void {
288
289
  const angleDeg = lateDouble(angleStr, PORT_LABEL_ANGLE, -180.0);
289
- const angle = Math.atan2(pf.y - pe.y, pf.x - pe.x) + (Math.PI / 180) * angleDeg;
290
+ const angle = Math.atan2(pf.y - pe.y, pf.x - pe.x) + RADIANS(angleDeg);
290
291
  const distMult = lateDouble(distanceStr || undefined, 1.0, 0.0);
291
292
  const dist = PORT_LABEL_DISTANCE * distMult;
292
293
  l.pos.x = pe.x + dist * Math.cos(angle);
@@ -14,6 +14,7 @@
14
14
  import type { Graph } from '../../model/graph.js';
15
15
  import type { Node } from '../../model/node.js';
16
16
  import { aggetGraph } from './fdp-model.js';
17
+ import { RADIANS } from '../../common/arith.js';
17
18
 
18
19
  /** mapbool for the "normalize" attr. @see lib/common/utils.c:mapbool */
19
20
  function mapbool(s: string): boolean {
@@ -34,7 +35,7 @@ function angleSet(g: Graph): number | null {
34
35
  if (a === undefined || a === '') return null;
35
36
  const ang = parseAngle(a);
36
37
  if (ang === null) return null;
37
- return (wrapDegrees(ang) / 180.0) * Math.PI; // RADIANS(a) = a/180.0*M_PI
38
+ return RADIANS(wrapDegrees(ang));
38
39
  }
39
40
 
40
41
  /** strtod (leading whitespace, sign) with the mapbool fallback of angleSet. */