@knowvah/dot-engine 2.6.0 → 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/bezier-arrows.d.ts +14 -0
- package/dist/common/bezier-arrows.d.ts.map +1 -0
- package/dist/common/pointset.d.ts +35 -0
- package/dist/common/pointset.d.ts.map +1 -0
- package/dist/common/postproc.d.ts.map +1 -1
- package/dist/common/splines-clip.d.ts.map +1 -1
- package/dist/index.js +156 -80
- package/dist/index.js.map +4 -4
- package/dist/layout/dot/splines.d.ts.map +1 -1
- package/dist/layout/pack/index.d.ts.map +1 -1
- package/dist/layout/pack/poly-place.d.ts.map +1 -1
- package/dist/model/geom.d.ts +9 -0
- package/dist/model/geom.d.ts.map +1 -1
- package/dist/render/dot.d.ts.map +1 -1
- package/dist/render/svg-edge-arrows.d.ts +30 -0
- package/dist/render/svg-edge-arrows.d.ts.map +1 -0
- package/dist/render/svg-helpers.d.ts.map +1 -1
- package/dist/render.js +156 -80
- package/dist/render.js.map +4 -4
- package/package.json +2 -2
- package/src/common/bezier-arrows.ts +58 -0
- package/src/common/pointset.ts +73 -0
- package/src/common/postproc.ts +9 -13
- package/src/common/splines-clip.ts +12 -8
- package/src/layout/dot/splines.ts +4 -0
- package/src/layout/pack/index.ts +4 -0
- package/src/layout/pack/poly-place.ts +42 -47
- package/src/model/geom.ts +10 -0
- package/src/render/dot.ts +6 -0
- package/src/render/svg-edge-arrows.ts +54 -0
- package/src/render/svg-helpers.ts +10 -44
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,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
|
+
}
|
|
@@ -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
|
+
}
|
package/src/common/postproc.ts
CHANGED
|
@@ -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:
|
|
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
|
-
/**
|
|
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. */
|
package/src/layout/pack/index.ts
CHANGED
|
@@ -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 {
|
|
@@ -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
|
|
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
|
-
|
|
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
|