@knowvah/dot-engine 2.6.0 → 2.6.1

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.0",
3
+ "version": "2.6.1",
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",
@@ -0,0 +1,58 @@
1
+ // SPDX-License-Identifier: EPL-2.0
2
+ //
3
+ // Which precomputed arrow ops to draw for each bezier of an edge.
4
+ //
5
+ // C draws arrows at render time inside emit_edge_graphics' per-bezier loop:
6
+ // every bezier whose sflag/eflag is set gets its own arrow_gen call. The port
7
+ // computes arrows at clip time (arrowhead-geometry ADR-2) and keeps one op list
8
+ // per edge end (`e.info.tailArrowOps` / `headArrowOps`) — enough while at most
9
+ // one bezier carries each flag, which is every spline except neato's
10
+ // multispline fallback (neatosplines.c:683-701: makeMultiSpline fails partway,
11
+ // then makeSpline appends a second flagged bezier). For that case each bezier
12
+ // also carries its own ops (`Bezier.sArrowOps` / `eArrowOps`, set by
13
+ // clipAndInstall), and these helpers hand the emitters one list per bezier.
14
+ // With at most one flagged bezier per end, the edge-level ops stay the source:
15
+ // dot's swap, flat-edge and compound passes rewrite only those.
16
+ //
17
+ // @see lib/common/emit.c:emit_edge_graphics (2669-2675)
18
+
19
+ import type { Edge } from '../model/edge.js';
20
+ import type { Bezier } from '../model/geom.js';
21
+ import type { ArrowDrawOp } from './arrows-types.js';
22
+
23
+ /** Edge end: `tail` pairs with a bezier's sflag/sp, `head` with eflag/ep. */
24
+ export type ArrowEnd = 'head' | 'tail';
25
+
26
+ const flagOf = (bz: Bezier, end: ArrowEnd): number => (end === 'head' ? bz.eflag : bz.sflag);
27
+ const ownOps = (bz: Bezier, end: ArrowEnd): ArrowDrawOp[] | undefined =>
28
+ (end === 'head' ? bz.eArrowOps : bz.sArrowOps);
29
+ const edgeOps = (e: Edge, end: ArrowEnd): ArrowDrawOp[] | undefined =>
30
+ (end === 'head' ? e.info.headArrowOps : e.info.tailArrowOps);
31
+
32
+ function flaggedBeziers(e: Edge, end: ArrowEnd): Bezier[] {
33
+ const list = (e.info.spl?.list ?? []) as (Bezier | undefined)[];
34
+ return list.filter((bz): bz is Bezier => bz !== undefined && flagOf(bz, end) !== 0);
35
+ }
36
+
37
+ /** Two or more beziers carry `end`'s flag: C draws an arrow for each. */
38
+ function multiFlagged(e: Edge, end: ArrowEnd): boolean {
39
+ return flaggedBeziers(e, end).length > 1;
40
+ }
41
+
42
+ /**
43
+ * The ops C's per-bezier loop draws right after `bz` for `end`, when the edge
44
+ * has several beziers flagged at that end; undefined otherwise (the caller's
45
+ * edge-level path applies).
46
+ */
47
+ export function bezierEndOps(e: Edge, bz: Bezier, end: ArrowEnd): ArrowDrawOp[] | undefined {
48
+ if (flagOf(bz, end) === 0 || !multiFlagged(e, end)) return undefined;
49
+ return ownOps(bz, end) ?? edgeOps(e, end);
50
+ }
51
+
52
+ /** Every arrow to draw for `end`, one op list per arrow, in bezier order. */
53
+ export function endArrowOps(e: Edge, end: ArrowEnd): ArrowDrawOp[][] {
54
+ const lists = multiFlagged(e, end)
55
+ ? flaggedBeziers(e, end).map((bz) => ownOps(bz, end) ?? edgeOps(e, end))
56
+ : [edgeOps(e, end)];
57
+ return lists.filter((ops): ops is ArrowDrawOp[] => ops !== undefined && ops.length > 0);
58
+ }
@@ -16,7 +16,7 @@
16
16
  */
17
17
 
18
18
  import type { Graph } from '../model/graph.js';
19
- import type { Point, Box } from '../model/geom.js';
19
+ import type { Point, Box, Bezier } from '../model/geom.js';
20
20
  import { ccwrotatepf } from '../model/geom.js';
21
21
  import type { ArrowDrawOp } from './arrows-types.js';
22
22
  import { mapArrowOpPoints } from './arrows-shapes-util.js';
@@ -39,9 +39,12 @@ const IGNORED_EDGE = 6;
39
39
  let Rankdir = 0;
40
40
  let Offset: Point = { x: 0, y: 0 };
41
41
 
42
+ /** The bezier fields map_edge reads/writes. */
43
+ type MappedBezier = Pick<Bezier, 'list' | 'sflag' | 'eflag' | 'sp' | 'ep' | 'sArrowOps' | 'eArrowOps'>;
44
+
42
45
  /** The edge-info shape map_edge reads/writes (spline, labels, arrow ops). */
43
46
  interface EdgeArrowInfo {
44
- spl?: { list: Array<{ list: Point[]; sflag: number; eflag: number; sp: Point; ep: Point }> };
47
+ spl?: { list: MappedBezier[] };
45
48
  label?: unknown;
46
49
  xlabel?: unknown;
47
50
  head_label?: unknown;
@@ -116,17 +119,6 @@ function mapArrowOps(ops: ArrowDrawOp[] | undefined): ArrowDrawOp[] | undefined
116
119
  });
117
120
  }
118
121
 
119
- /** The edge-info shape map_edge reads/writes (spline, labels, arrow ops). */
120
- interface EdgeArrowInfo {
121
- spl?: { list: Array<{ list: Point[]; sflag: number; eflag: number; sp: Point; ep: Point }> };
122
- label?: unknown;
123
- xlabel?: unknown;
124
- head_label?: unknown;
125
- tail_label?: unknown;
126
- headArrowOps?: ArrowDrawOp[];
127
- tailArrowOps?: ArrowDrawOp[];
128
- }
129
-
130
122
  /**
131
123
  * Map spline control points, label positions, and pre-computed arrow draw-ops
132
124
  * for one edge. The C port regenerates arrowheads at render time from the
@@ -145,6 +137,10 @@ function mapEdge(e: { info: EdgeArrowInfo }): void {
145
137
  // Rotate pre-computed arrowhead draw-ops (TS-only: C computes at render time).
146
138
  e.info.headArrowOps = mapArrowOps(e.info.headArrowOps);
147
139
  e.info.tailArrowOps = mapArrowOps(e.info.tailArrowOps);
140
+ for (const bz of spl.list) {
141
+ if (bz.sArrowOps) bz.sArrowOps = mapArrowOps(bz.sArrowOps);
142
+ if (bz.eArrowOps) bz.eArrowOps = mapArrowOps(bz.eArrowOps);
143
+ }
148
144
  }
149
145
 
150
146
  // ---------------------------------------------------------------------------
@@ -258,20 +258,20 @@ class SplineClipHelper {
258
258
  if (sflag || eflag) {
259
259
  SplineClipHelper.arrowOrthoClip(
260
260
  orig, ps, bounds.start, bounds.end, newspl, sflag, eflag);
261
- if (sflag) SplineClipHelper.stashArrow(orig, newspl.sp, ps[bounds.start]!, true, startEnd);
262
- if (eflag) SplineClipHelper.stashArrow(orig, newspl.ep, ps[bounds.end + 3]!, false, endEnd);
261
+ if (sflag) SplineClipHelper.stashArrow(orig, newspl, newspl.sp, ps[bounds.start]!, true, startEnd);
262
+ if (eflag) SplineClipHelper.stashArrow(orig, newspl, newspl.ep, ps[bounds.end + 3]!, false, endEnd);
263
263
  }
264
264
  return;
265
265
  }
266
266
  if (sflag) {
267
267
  bounds.start = SplineClipHelper.arrowStartClip(
268
268
  orig, ps, bounds.start, bounds.end, newspl, sflag, startEnd);
269
- SplineClipHelper.stashArrow(orig, newspl.sp, ps[bounds.start]!, true, startEnd);
269
+ SplineClipHelper.stashArrow(orig, newspl, newspl.sp, ps[bounds.start]!, true, startEnd);
270
270
  }
271
271
  if (eflag) {
272
272
  bounds.end = SplineClipHelper.arrowEndClip(
273
273
  orig, ps, bounds.start, bounds.end, newspl, eflag, endEnd);
274
- SplineClipHelper.stashArrow(orig, newspl.ep, ps[bounds.end + 3]!, false, endEnd);
274
+ SplineClipHelper.stashArrow(orig, newspl, newspl.ep, ps[bounds.end + 3]!, false, endEnd);
275
275
  }
276
276
  }
277
277
 
@@ -336,15 +336,19 @@ class SplineClipHelper {
336
336
  }
337
337
  }
338
338
 
339
- /** Record an arrow polygon on the edge for svgArrowPolygons. */
339
+ /**
340
+ * Record an arrow on the edge end and on the bezier being installed. The
341
+ * edge slot holds the latest arrow; the bezier keeps its own, since C draws
342
+ * one per flagged bezier (see common/bezier-arrows.ts).
343
+ */
340
344
  static stashArrow(
341
- e: Edge, tip: Point, base: Point, isTail: boolean, attrEnd?: 'head' | 'tail',
345
+ e: Edge, bz: Bezier, tip: Point, base: Point, isTail: boolean, attrEnd?: 'head' | 'tail',
342
346
  ): void {
343
347
  const dir = { x: base.x - tip.x, y: base.y - tip.y };
344
348
  const end = attrEnd ?? (isTail ? 'tail' : 'head');
345
349
  const ops = arrowDrawOpsForEnd(e, end, tip, dir, SplineClipHelper.renderPenwidth(e));
346
- if (isTail) e.info.tailArrowOps = ops;
347
- else e.info.headArrowOps = ops;
350
+ if (isTail) { e.info.tailArrowOps = ops; bz.sArrowOps = ops; }
351
+ else { e.info.headArrowOps = ops; bz.eArrowOps = ops; }
348
352
  }
349
353
 
350
354
  static clipSide(n: Node, box: Box | null, ps: Point[], pn: number, tail: boolean): number {
@@ -290,6 +290,10 @@ export function swapBezier(b: Bezier): void {
290
290
  }
291
291
  const sf = b.sflag; b.sflag = b.eflag; b.eflag = sf;
292
292
  const sp = b.sp; b.sp = b.ep; b.ep = sp;
293
+ // TS-only per-bezier arrow ops stay paired with their flag.
294
+ if (b.sArrowOps !== undefined || b.eArrowOps !== undefined) {
295
+ const so = b.sArrowOps; b.sArrowOps = b.eArrowOps; b.eArrowOps = so;
296
+ }
293
297
  }
294
298
 
295
299
  /** Reverse the list of beziers in a spline and swap each bezier. */
@@ -313,6 +313,10 @@ export function shiftEdgePoints(e: Edge, dx: number, dy: number): void {
313
313
  ops?.map((op) => mapArrowOpPoints(op, (p) => ({ x: p.x + dx, y: p.y + dy })));
314
314
  e.info.headArrowOps = shiftOps(e.info.headArrowOps);
315
315
  e.info.tailArrowOps = shiftOps(e.info.tailArrowOps);
316
+ for (const bz of spl.list) {
317
+ if (bz.sArrowOps) bz.sArrowOps = shiftOps(bz.sArrowOps);
318
+ if (bz.eArrowOps) bz.eArrowOps = shiftOps(bz.eArrowOps);
319
+ }
316
320
  }
317
321
 
318
322
  export function shiftOneGraph(g: Graph, dx: number, dy: number): void {
package/src/model/geom.ts CHANGED
@@ -8,6 +8,8 @@
8
8
  * @see lib/common/const.h
9
9
  */
10
10
 
11
+ import type { ArrowDrawOp } from '../common/arrows-types.js';
12
+
11
13
  // ---------------------------------------------------------------------------
12
14
  // Points
13
15
  // ---------------------------------------------------------------------------
@@ -56,6 +58,14 @@ export type Bezier = {
56
58
  sp: Point;
57
59
  /** @see lib/common/types.h: pointf ep */
58
60
  ep: Point;
61
+ /**
62
+ * TS-only: the arrow drawn at `sp` (paired with `sflag`). C regenerates it
63
+ * per bezier at render time (emit.c arrow_gen); the port computes it at clip
64
+ * time, so it is stored per bezier for splines with several flagged beziers.
65
+ */
66
+ sArrowOps?: ArrowDrawOp[];
67
+ /** TS-only: the arrow drawn at `ep` (paired with `eflag`); see `sArrowOps`. */
68
+ eArrowOps?: ArrowDrawOp[];
59
69
  };
60
70
 
61
71
  /**
package/src/render/dot.ts CHANGED
@@ -32,6 +32,7 @@ import { PenType, FillType } from '../gvc/context.js';
32
32
  import type { RenderJob } from '../gvc/job.js';
33
33
  import { EmitState } from '../gvc/job.js';
34
34
  import { renderEdgeLabels } from '../gvc/edge-labels.js';
35
+ import { endArrowOps } from '../common/bezier-arrows.js';
35
36
  export * from './dot/xdot-ops.js';
36
37
  export * from './dot/attrs.js';
37
38
  import {
@@ -226,6 +227,11 @@ export class XdotRenderer extends EdgeDrawBase implements RendererPlugin {
226
227
  headArrowColor = c.headColor;
227
228
  } else {
228
229
  this.emitPlainSpline(e, spl.list, edraw, job);
230
+ // The plain branch draws an arrow per flagged bezier (emit.c:2669-2675);
231
+ // the multicolor/tapered branches above draw one per end.
232
+ for (const ops of endArrowOps(e, 'tail')) this.emitArrows(this.bufs[EmitState.TDraw]!, ops, job, EmitState.TDraw);
233
+ for (const ops of endArrowOps(e, 'head')) this.emitArrows(this.bufs[EmitState.HDraw]!, ops, job, EmitState.HDraw);
234
+ return;
229
235
  }
230
236
  // Arrows: y-up ops already computed for the shared render path. C sets the
231
237
  // default line style ("solid") + penwidth before each arrow primitive.
@@ -0,0 +1,54 @@
1
+ // SPDX-License-Identifier: EPL-2.0
2
+ //
3
+ // SVG arrowheads for one edge, drawn inside svgEdgePath's per-bezier loop the
4
+ // way emit_edge_graphics calls arrow_gen after each bezier.
5
+ //
6
+ // @see lib/common/emit.c:emit_edge_graphics (2668-2676)
7
+
8
+ import type { Edge } from '../model/edge.js';
9
+ import type { Bezier } from '../model/geom.js';
10
+ import type { RenderJob } from '../gvc/job.js';
11
+ import { bezierEndOps } from '../common/bezier-arrows.js';
12
+ import { emitArrowOps } from './svg-arrow-ops.js';
13
+
14
+ /** The edge pen an arrow is drawn with. */
15
+ export interface ArrowPaint { job: RenderJob; penColor: string; opacity: string | null; pw: number }
16
+
17
+ /** Which ends have drawn their edge-level arrow. */
18
+ export interface ArrowsDone { tailDone: boolean; headDone: boolean }
19
+
20
+ /**
21
+ * One end's arrow after bezier `bz`. With several beziers flagged at that end
22
+ * each draws its own arrow (bezierEndOps); otherwise the edge-level ops are
23
+ * drawn once, at the first flagged bezier.
24
+ */
25
+ export function emitBezierEndArrow(
26
+ e: Edge, bz: Bezier, end: 'head' | 'tail', st: ArrowsDone, p: ArrowPaint,
27
+ ): void {
28
+ const tail = end === 'tail';
29
+ const flag = tail ? bz.sflag : bz.eflag;
30
+ const done = tail ? st.tailDone : st.headDone;
31
+ const edgeOps = tail ? e.info.tailArrowOps : e.info.headArrowOps;
32
+ const ops = bezierEndOps(e, bz, end)
33
+ ?? (flag !== 0 && !done && edgeOps?.length ? edgeOps : undefined);
34
+ if (ops === undefined) return;
35
+ emitArrowOps(ops, p.penColor, p.job, p.pw, p.opacity);
36
+ if (tail) st.tailDone = true;
37
+ else st.headDone = true;
38
+ }
39
+
40
+ /**
41
+ * Arrow ops whose bezier carried no s/eflag (a port-model seam: ops are stored
42
+ * per edge end, flags per bezier). C cannot reach this state — an arrow is only
43
+ * generated where a flag is set — so this emits at the old position (group
44
+ * end) purely to keep behavior identical if a router ever stashes ops without
45
+ * setting the flag.
46
+ */
47
+ export function emitRemainingArrows(e: Edge, st: ArrowsDone, p: ArrowPaint): void {
48
+ if (!st.tailDone && e.info.tailArrowOps?.length) {
49
+ emitArrowOps(e.info.tailArrowOps, p.penColor, p.job, p.pw, p.opacity);
50
+ }
51
+ if (!st.headDone && e.info.headArrowOps?.length) {
52
+ emitArrowOps(e.info.headArrowOps, p.penColor, p.job, p.pw, p.opacity);
53
+ }
54
+ }
@@ -66,6 +66,8 @@ import { escapeXml, escapeXmlText, escapeXmlTitle } from './xml-escape.js';
66
66
  export { escapeXml, escapeXmlText, escapeXmlTitle };
67
67
  import { svgNodeId, svgEdgeId, svgNodeClass, svgEdgeClass } from './svg-id.js';
68
68
  import { orthoRoundedPolylines } from './svg-edge-ortho-radius.js';
69
+ import { emitBezierEndArrow, emitRemainingArrows } from './svg-edge-arrows.js';
70
+ import type { ArrowPaint } from './svg-edge-arrows.js';
69
71
 
70
72
  // ---------------------------------------------------------------------------
71
73
  // Color helpers
@@ -442,6 +444,11 @@ export function svgEdgePath(e: Edge, job: RenderJob): void {
442
444
  if (spl === undefined || spl.size === 0) return;
443
445
  const obj = job.obj;
444
446
  const stroke = obj !== null ? paintStr(obj, false) : 'black';
447
+ const paint: ArrowPaint = {
448
+ job, penColor: stroke,
449
+ opacity: obj !== null ? colorOpacity(obj.penColor) : null,
450
+ pw: obj !== null ? obj.penWidth : 1.0,
451
+ };
445
452
  const st = { tailDone: false, headDone: false };
446
453
  for (let si = 0; si < spl.size; si++) {
447
454
  const bz = spl.list[si] as Bezier | undefined;
@@ -452,51 +459,10 @@ export function svgEdgePath(e: Edge, job: RenderJob): void {
452
459
  // for a multi-bezier concentrate spline the arrow interleaves BETWEEN
453
460
  // the paths (b69 g[84]: [path, arrow-polygon, path]).
454
461
  // @see lib/common/emit.c:2668-2676 (arrow_gen after gvrender_beziercurve)
455
- emitBezierArrows(e, bz, job, st);
456
- }
457
- emitRemainingArrows(e, job, st);
458
- }
459
-
460
- /** Per-bezier arrow emission (C's in-loop arrow_gen calls). */
461
- function emitBezierArrows(
462
- e: Edge, bz: Bezier, job: RenderJob,
463
- st: { tailDone: boolean; headDone: boolean },
464
- ): void {
465
- const obj = job.obj;
466
- const penColor = obj !== null ? paintStr(obj, false) : 'black';
467
- const opacity = obj !== null ? colorOpacity(obj.penColor) : null;
468
- const pw = obj !== null ? obj.penWidth : 1.0;
469
- if (bz.sflag !== 0 && !st.tailDone && e.info.tailArrowOps?.length) {
470
- emitArrowOps(e.info.tailArrowOps, penColor, job, pw, opacity);
471
- st.tailDone = true;
472
- }
473
- if (bz.eflag !== 0 && !st.headDone && e.info.headArrowOps?.length) {
474
- emitArrowOps(e.info.headArrowOps, penColor, job, pw, opacity);
475
- st.headDone = true;
476
- }
477
- }
478
-
479
- /**
480
- * Arrow ops whose bezier carried no s/eflag (a port-model seam: ops are stored
481
- * per edge end, flags per bezier). C cannot reach this state — an arrow is only
482
- * generated where a flag is set — so this emits at the old position (group
483
- * end) purely to keep behavior identical if a router ever stashes ops without
484
- * setting the flag.
485
- */
486
- function emitRemainingArrows(
487
- e: Edge, job: RenderJob,
488
- st: { tailDone: boolean; headDone: boolean },
489
- ): void {
490
- const obj = job.obj;
491
- const penColor = obj !== null ? paintStr(obj, false) : 'black';
492
- const opacity = obj !== null ? colorOpacity(obj.penColor) : null;
493
- const pw = obj !== null ? obj.penWidth : 1.0;
494
- if (!st.tailDone && e.info.tailArrowOps?.length) {
495
- emitArrowOps(e.info.tailArrowOps, penColor, job, pw, opacity);
496
- }
497
- if (!st.headDone && e.info.headArrowOps?.length) {
498
- emitArrowOps(e.info.headArrowOps, penColor, job, pw, opacity);
462
+ emitBezierEndArrow(e, bz, 'tail', st, paint);
463
+ emitBezierEndArrow(e, bz, 'head', st, paint);
499
464
  }
465
+ emitRemainingArrows(e, st, paint);
500
466
  }
501
467
 
502
468
  /** Emit one bezier as a `<path d="M…C…">`. Extracted from svgEdgePath so the