@knowvah/dot-engine 1.1.1 → 1.2.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/README.md +32 -17
- package/dist/api.js +24 -0
- package/dist/api.js.map +2 -2
- package/dist/common/arrows.d.ts.map +1 -1
- package/dist/common/edge-label-init.d.ts.map +1 -1
- package/dist/common/htmltable-emit.d.ts +0 -8
- package/dist/common/htmltable-emit.d.ts.map +1 -1
- package/dist/common/ps-fontalias.d.ts +22 -9
- package/dist/common/ps-fontalias.d.ts.map +1 -1
- package/dist/common/splines-clip.d.ts.map +1 -1
- package/dist/gvc/device.d.ts.map +1 -1
- package/dist/gvc/job.d.ts +4 -0
- package/dist/gvc/job.d.ts.map +1 -1
- package/dist/index.js +1981 -1533
- package/dist/index.js.map +4 -4
- package/dist/layout/dot/index.d.ts +1 -1
- package/dist/layout/dot/index.d.ts.map +1 -1
- package/dist/layout/sfdp/init.d.ts.map +1 -1
- package/dist/model/graph.d.ts +24 -0
- package/dist/model/graph.d.ts.map +1 -1
- package/dist/parser/builder.d.ts +32 -0
- package/dist/parser/builder.d.ts.map +1 -1
- package/dist/render/dot/agwrite.d.ts +186 -0
- package/dist/render/dot/agwrite.d.ts.map +1 -0
- package/dist/render/dot/attrs.d.ts +220 -0
- package/dist/render/dot/attrs.d.ts.map +1 -0
- package/dist/render/dot/edge-draw.d.ts +60 -0
- package/dist/render/dot/edge-draw.d.ts.map +1 -0
- package/dist/render/dot/types.d.ts +28 -0
- package/dist/render/dot/types.d.ts.map +1 -0
- package/dist/render/dot/xdot-ops.d.ts +206 -0
- package/dist/render/dot/xdot-ops.d.ts.map +1 -0
- package/dist/render/dot.d.ts +58 -277
- package/dist/render/dot.d.ts.map +1 -1
- package/dist/render/map.d.ts +14 -1
- package/dist/render/map.d.ts.map +1 -1
- package/dist/render/svg-graph.d.ts.map +1 -1
- package/dist/render.js +1942 -1494
- package/dist/render.js.map +4 -4
- package/package.json +1 -1
- package/src/common/arrows.ts +17 -4
- package/src/common/edge-label-init.ts +9 -2
- package/src/common/htmltable-emit.ts +36 -8
- package/src/common/poly-gencode.ts +8 -8
- package/src/common/ps-fontalias.ts +80 -49
- package/src/common/splines-clip.ts +36 -10
- package/src/gvc/device.ts +18 -6
- package/src/gvc/job.ts +4 -0
- package/src/layout/dot/index.ts +37 -5
- package/src/layout/sfdp/init.ts +11 -0
- package/src/model/graph.ts +26 -0
- package/src/parser/builder.ts +90 -5
- package/src/render/dot/agwrite.ts +506 -0
- package/src/render/dot/attrs.ts +437 -0
- package/src/render/dot/edge-draw.ts +203 -0
- package/src/render/dot/types.ts +32 -0
- package/src/render/dot/xdot-ops.ts +432 -0
- package/src/render/dot.ts +114 -1131
- package/src/render/map.ts +18 -1
- package/src/render/svg-graph.ts +4 -0
- package/src/render/svg-helpers.ts +1 -1
package/src/render/dot.ts
CHANGED
|
@@ -17,9 +17,7 @@ import type { Point } from '../model/geom.js';
|
|
|
17
17
|
import type { TextSpan } from '../common/emit-types.js';
|
|
18
18
|
import type { ArrowDrawOp } from '../common/arrows-types.js';
|
|
19
19
|
import type { GVColor } from '../common/color.js';
|
|
20
|
-
import { colorxlate } from '../common/color.js';
|
|
21
20
|
import { resolveRenderColor } from './color-resolve.js';
|
|
22
|
-
import { getGradientPoints } from './svg-gradient.js';
|
|
23
21
|
import { edgeIsTapered } from './svg-tapered-edge.js';
|
|
24
22
|
import { taper, taperfun } from '../common/taper.js';
|
|
25
23
|
import { orthoRoundedRadius } from './svg-helpers.js';
|
|
@@ -29,651 +27,32 @@ import { parseGraphPad } from '../gvc/viewport.js';
|
|
|
29
27
|
import { parseSegs } from '../common/multicolor.js';
|
|
30
28
|
import { splitSplineByColor } from './svg-edge-split.js';
|
|
31
29
|
import { buildOffsetLists, advanceTmpList } from '../common/edge-offset.js';
|
|
32
|
-
import { IGNORED } from '../layout/dot/rank.js';
|
|
33
30
|
import { edgeHasDrawableContent } from './svg.js';
|
|
34
31
|
import type { Bezier } from '../model/geom.js';
|
|
35
32
|
import type { RendererPlugin } from '../gvc/context.js';
|
|
33
|
+
import { EdgeDrawBase } from './dot/edge-draw.js';
|
|
34
|
+
export * from './dot/types.js';
|
|
35
|
+
export type { XdotDraws, SerCtx } from './dot/types.js';
|
|
36
|
+
import type { XdotDraws } from './dot/types.js';
|
|
36
37
|
import { PenType, FillType } from '../gvc/context.js';
|
|
37
|
-
import type { RenderJob
|
|
38
|
-
import { EmitState
|
|
38
|
+
import type { RenderJob } from '../gvc/job.js';
|
|
39
|
+
import { EmitState } from '../gvc/job.js';
|
|
39
40
|
import { renderEdgeLabels } from '../gvc/edge-labels.js';
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
import
|
|
41
|
+
export * from './dot/xdot-ops.js';
|
|
42
|
+
export * from './dot/attrs.js';
|
|
43
|
+
import {
|
|
44
|
+
NON_LINE_STYLES, XDOT_VERSION, agcanonEscape, escBackslash, gfmt5, linearGradientOp,
|
|
45
|
+
lpStr, makeXbufs, radialGradientOp, trimFixed3, utf8Len, xdotFillColor, xdotFont,
|
|
46
|
+
xdotId, xdotNum, xdotPenColor, xdotPoint, xdotPoints, xdotStrOp,
|
|
47
|
+
} from './dot/xdot-ops.js';
|
|
48
|
+
import { isDirected } from './dot/attrs.js';
|
|
43
49
|
|
|
44
|
-
// ---------------------------------------------------------------------------
|
|
45
|
-
// Constants
|
|
46
|
-
// ---------------------------------------------------------------------------
|
|
47
|
-
|
|
48
|
-
const XDOT_VERSION = '1.7';
|
|
49
|
-
|
|
50
|
-
/** Max magnitude cap — matches maxnegnum in C source. */
|
|
51
|
-
const MAX_NEGNUM = 999999999999999.99;
|
|
52
|
-
|
|
53
|
-
/** Near-zero suppression threshold (same as gvprintdouble). */
|
|
54
|
-
const NEAR_ZERO = 0.005;
|
|
55
|
-
|
|
56
|
-
/** Indices 8/9 alias 1; 10/11 alias 5. NUM_XBUFS covers 0–7. */
|
|
57
|
-
const NUM_XBUFS = 8;
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Style tokens that never become an xdot `S` op: `filled`/`bold`/`setlinewidth`
|
|
61
|
-
* are filtered by xdot_style itself; the polygon/fill styles are consumed by the
|
|
62
|
-
* shape/fill code before gvrender_set_style, so they never reach the render
|
|
63
|
-
* style. Only line styles (solid/dashed/dotted + unknown) emit an `S` op.
|
|
64
|
-
* @see plugin/core/gvrender_core_dot.c:184 xdot_style · lib/common/shapes.c
|
|
65
|
-
*/
|
|
66
|
-
const NON_LINE_STYLES: ReadonlySet<string> = new Set([
|
|
67
|
-
'filled', 'bold', 'rounded', 'diagonals', 'striped', 'wedged',
|
|
68
|
-
'invis', 'invisible', 'radial',
|
|
69
|
-
]);
|
|
70
|
-
|
|
71
|
-
// ---------------------------------------------------------------------------
|
|
72
|
-
// printNum — @see lib/gvc/gvdevice.c:gvprintnum
|
|
73
|
-
// ---------------------------------------------------------------------------
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Strip trailing zeros (and the decimal point) from a toFixed(3) string,
|
|
77
|
-
* then collapse a leading "0." or "-0." prefix.
|
|
78
|
-
*/
|
|
79
|
-
function trimAndStrip(s: string): string {
|
|
80
|
-
const dot = s.indexOf('.');
|
|
81
|
-
if (dot < 0) return s;
|
|
82
|
-
let end = s.length;
|
|
83
|
-
while (end > dot + 1 && s[end - 1] === '0') end--;
|
|
84
|
-
if (s[end - 1] === '.') end--;
|
|
85
|
-
const t = s.slice(0, end);
|
|
86
|
-
if (t.startsWith('0.')) return t.slice(1);
|
|
87
|
-
if (t.startsWith('-0.')) return '-' + t.slice(2);
|
|
88
|
-
return t;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Convert a double to compact string form for DOT/XDOT output.
|
|
93
|
-
*
|
|
94
|
-
* Rules (porting gvprintnum from lib/gvc/gvdevice.c):
|
|
95
|
-
* - |n| > MAX_NEGNUM → clamped to ±MAX_NEGNUM
|
|
96
|
-
* - |n| < NEAR_ZERO → "0" (suppresses -0)
|
|
97
|
-
* - 3 decimal places, trailing zeros and point stripped
|
|
98
|
-
* - Leading "0." collapsed to "." (e.g. 0.5 → ".5")
|
|
99
|
-
*
|
|
100
|
-
* Exported for json.ts (T30) and map.ts (T31).
|
|
101
|
-
*
|
|
102
|
-
* @see lib/gvc/gvdevice.c:gvprintnum
|
|
103
|
-
*/
|
|
104
|
-
export function printNum(n: number): string {
|
|
105
|
-
if (Math.abs(n) > MAX_NEGNUM) {
|
|
106
|
-
return n < 0 ? String(-MAX_NEGNUM) : String(MAX_NEGNUM);
|
|
107
|
-
}
|
|
108
|
-
if (Math.abs(n) < NEAR_ZERO) return '0';
|
|
109
|
-
return trimAndStrip(n.toFixed(3));
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// ---------------------------------------------------------------------------
|
|
113
|
-
// XDot buffer management
|
|
114
|
-
// ---------------------------------------------------------------------------
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Create a 12-slot xbufs array with the canonical aliasing.
|
|
118
|
-
*
|
|
119
|
-
* Indices 8–9 (NDraw, EDraw) alias index 1 (CDraw).
|
|
120
|
-
* Indices 10–11 (NLabel, ELabel) alias index 5 (CLabel).
|
|
121
|
-
*
|
|
122
|
-
* @see plugin/core/gvrender_core_dot.c:xbufs
|
|
123
|
-
*/
|
|
124
|
-
export function makeXbufs(): string[][] {
|
|
125
|
-
const bufs: string[][] = Array.from({ length: NUM_XBUFS + 4 }, () => []);
|
|
126
|
-
bufs[EmitState.NDraw] = bufs[EmitState.CDraw]!;
|
|
127
|
-
bufs[EmitState.EDraw] = bufs[EmitState.CDraw]!;
|
|
128
|
-
bufs[EmitState.NLabel] = bufs[EmitState.CLabel]!;
|
|
129
|
-
bufs[EmitState.ELabel] = bufs[EmitState.CLabel]!;
|
|
130
|
-
return bufs;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// ---------------------------------------------------------------------------
|
|
134
|
-
// XDOT op helpers — @see plugin/core/gvrender_core_dot.c
|
|
135
|
-
// ---------------------------------------------------------------------------
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Format one xdot draw-op number: 2 decimals, trailing zeros and point trimmed
|
|
139
|
-
* — mirroring xdot_fmt_num ("%.02f" + agxbuf_trim_zeros). Distinct from
|
|
140
|
-
* `printNum` (used for the DOT `pos`/`bb`/`width`/`height` attributes), which
|
|
141
|
-
* keeps more precision; xdot's DRAW ops are emitted at 2 dp by the C engine.
|
|
142
|
-
* @see plugin/core/gvrender_core_dot.c:126 xdot_fmt_num
|
|
143
|
-
*/
|
|
144
|
-
export function xdotNum(v: number): string {
|
|
145
|
-
// Round half-to-even like C's printf %.02f (FE_TONEAREST); JS toFixed rounds
|
|
146
|
-
// half-away-from-zero, which diverges at exact .xx5 ties (2323.125 → native
|
|
147
|
-
// 2323.12, not 2323.13). @see lib/gvc/gvdevice.c gvprintdouble
|
|
148
|
-
let s = toFixed2HalfEven(v);
|
|
149
|
-
if (s.indexOf('.') >= 0) {
|
|
150
|
-
let end = s.length;
|
|
151
|
-
while (end > 0 && s[end - 1] === '0') end--;
|
|
152
|
-
if (s[end - 1] === '.') end--;
|
|
153
|
-
s = s.slice(0, end);
|
|
154
|
-
}
|
|
155
|
-
return s === '-0' ? '0' : s;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Format a single xdot point "x y ". xdot is y-up: `Y_invert` defaults false, so
|
|
160
|
-
* `yDir(y, yOff)` returns `y` unchanged for xdot (only `-Ty` plain/dot invert).
|
|
161
|
-
* The layout coordinate passes through with NO inversion — unlike the SVG path.
|
|
162
|
-
* @see lib/common/output.c:36 yDir · plugin/core/gvrender_core_dot.c:132 xdot_point
|
|
163
|
-
*/
|
|
164
|
-
export function xdotPoint(p: Point): string {
|
|
165
|
-
return xdotNum(p.x) + ' ' + xdotNum(p.y) + ' ';
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/** Format N points preceded by opcode and count: "<c> <n> x0 y0 x1 y1 …". */
|
|
169
|
-
export function xdotPoints(c: string, pts: Point[]): string {
|
|
170
|
-
let s = c + ' ' + String(pts.length) + ' ';
|
|
171
|
-
for (const p of pts) s += xdotPoint(p);
|
|
172
|
-
return s;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* UTF-8 byte length of a string — the value C's `xdot_str` writes as the length
|
|
177
|
-
* prefix (`strlen(s)` over the UTF-8 bytes), NOT the JS UTF-16 code-unit count.
|
|
178
|
-
* A label like `ÿ` (U+00FF) is 2 UTF-8 bytes, so its `T`/`F` op prefix is 2.
|
|
179
|
-
* @see plugin/core/gvrender_core_dot.c:83 xdot_str_xbuf (`%zu`, strlen)
|
|
180
|
-
*/
|
|
181
|
-
export function utf8Len(s: string): number {
|
|
182
|
-
let n = 0;
|
|
183
|
-
for (let i = 0; i < s.length; i++) {
|
|
184
|
-
const c = s.charCodeAt(i);
|
|
185
|
-
if (c < 0x80) n += 1;
|
|
186
|
-
else if (c < 0x800) n += 2;
|
|
187
|
-
else if (c >= 0xd800 && c <= 0xdbff) { n += 4; i++; } // surrogate pair → 4 bytes
|
|
188
|
-
else n += 3;
|
|
189
|
-
}
|
|
190
|
-
return n;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/** Clamp a normalized [0,1] float channel to a 0-255 byte (round-to-nearest). */
|
|
194
|
-
function chanByte(v: number): number {
|
|
195
|
-
return Math.round(Math.max(0, Math.min(1, v)) * 255);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* Resolve a GVColor to RGBA bytes — the value the C xdot callbacks read from
|
|
200
|
-
* `job->obj->pencolor.u.rgba` (already resolved by gvrender_set_pencolor). A
|
|
201
|
-
* plain `rgba` passes through; named/hex/HSV specs run through colorxlate; a
|
|
202
|
-
* `none` color is fully transparent black (callers gate on PEN_NONE).
|
|
203
|
-
*/
|
|
204
|
-
export function gvColorRgba(c: GVColor): [number, number, number, number] {
|
|
205
|
-
if (c.type === 'rgba') return [chanByte(c.r), chanByte(c.g), chanByte(c.b), chanByte(c.a)];
|
|
206
|
-
// A `none`/transparent paint emits graphviz's "transparent" bytes (ff ff fe 00),
|
|
207
|
-
// matching native's `#fffffe00` — not fully-zero black. @see colxlate.c transparent
|
|
208
|
-
if (c.type === 'none') return [0xff, 0xff, 0xfe, 0x00];
|
|
209
|
-
const out: GVColor = { type: 'rgba', r: 0, g: 0, b: 0, a: 0 };
|
|
210
|
-
colorxlate(c.type === 'string' ? c.s : '', out, 'rgba');
|
|
211
|
-
return out.type === 'rgba'
|
|
212
|
-
? [chanByte(out.r), chanByte(out.g), chanByte(out.b), chanByte(out.a)]
|
|
213
|
-
: [0, 0, 0, 255];
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* Bare xdot color body (no `c `/`C ` prefix): the CONSTANT length prefix
|
|
218
|
-
* (7 for `#rrggbb`, 9 for `#rrggbbaa`) plus `-#hex`, the alpha byte present only
|
|
219
|
-
* when not fully opaque. Used both by the color ops and by gradient color stops.
|
|
220
|
-
* @see plugin/core/gvrender_core_dot.c:99 xdot_str_color_xbuf
|
|
221
|
-
*/
|
|
222
|
-
export function xdotColorBody(rgba: [number, number, number, number]): string {
|
|
223
|
-
const hx = (n: number): string => n.toString(16).padStart(2, '0');
|
|
224
|
-
const body = '#' + hx(rgba[0]) + hx(rgba[1]) + hx(rgba[2]);
|
|
225
|
-
return rgba[3] === 0xff ? '7 -' + body : '9 -' + body + hx(rgba[3]);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* Format an xdot color op ("c "/"C ") from RGBA bytes.
|
|
230
|
-
* @see plugin/core/gvrender_core_dot.c:99 xdot_str_color_xbuf
|
|
231
|
-
*/
|
|
232
|
-
export function xdotColorOp(prefix: 'c ' | 'C ', rgba: [number, number, number, number]): string {
|
|
233
|
-
return prefix + xdotColorBody(rgba) + ' ';
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* Build the linear-gradient `C len -[x0 y0 x1 y1 2 <stops>]` fill op. Endpoints
|
|
238
|
-
* come from getGradientPoints (the same geometry the SVG gradient uses); stops
|
|
239
|
-
* are (frac,fill)/(frac,stop) when frac>0, else (0,fill)/(1,stop). Shared by
|
|
240
|
-
* node/cluster gradient fills and the graph-background gradient.
|
|
241
|
-
* @see plugin/core/gvrender_core_dot.c:544-598 xdot_gradient_fillcolor
|
|
242
|
-
*/
|
|
243
|
-
export function linearGradientOp(
|
|
244
|
-
pts: Point[],
|
|
245
|
-
fillColor: GVColor,
|
|
246
|
-
stopColor: GVColor,
|
|
247
|
-
frac: number,
|
|
248
|
-
angleDeg: number,
|
|
249
|
-
): string {
|
|
250
|
-
// isRHS=true: native y-up coords, matching C's get_gradient_points(A,G,n,angle,2)
|
|
251
|
-
// for the xdot device path (the SVG path uses isRHS=false + a container flip).
|
|
252
|
-
const { g0, g1 } = getGradientPoints(pts, (angleDeg * Math.PI) / 180, false, true);
|
|
253
|
-
const inner =
|
|
254
|
-
'[' + xdotNum(g0.x) + ' ' + xdotNum(g0.y) + ' ' + xdotNum(g1.x) + ' ' + xdotNum(g1.y) +
|
|
255
|
-
' 2 ' + gradientStops(fillColor, stopColor, frac) + ']';
|
|
256
|
-
return 'C ' + String(utf8Len(inner)) + ' -' + inner + ' ';
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
/** The `<frac> <colorbody>` stop pairs for a gradient (frac>0 vs the 0/1 form). */
|
|
260
|
-
function gradientStops(fillColor: GVColor, stopColor: GVColor, frac: number): string {
|
|
261
|
-
const fill = gvColorRgba(fillColor);
|
|
262
|
-
const stop = gvColorRgba(stopColor);
|
|
263
|
-
const stops: Array<[number, [number, number, number, number]]> =
|
|
264
|
-
frac > 0 ? [[frac, fill], [frac, stop]] : [[0, fill], [1, stop]];
|
|
265
|
-
return stops.map(([f, c]) => trimFixed3(f) + ' ' + xdotColorBody(c)).join(' ');
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
/**
|
|
269
|
-
* Build the radial-gradient `C len -(c1x c1y r1 c2x c2y r2 2 <stops>)` fill op.
|
|
270
|
-
* Reuses getGradientPoints (radial) for the center/radii, un-negating its SVG
|
|
271
|
-
* y. r1 = outerR/4, r2 = outerR; c2 is the center, c1 the center offset by r1
|
|
272
|
-
* along the gradient angle (== center when angle 0).
|
|
273
|
-
* @see plugin/core/gvrender_core_dot.c:562-585 xdot_gradient_fillcolor (radial)
|
|
274
|
-
*/
|
|
275
|
-
export function radialGradientOp(
|
|
276
|
-
pts: Point[],
|
|
277
|
-
fillColor: GVColor,
|
|
278
|
-
stopColor: GVColor,
|
|
279
|
-
frac: number,
|
|
280
|
-
angleDeg: number,
|
|
281
|
-
): string {
|
|
282
|
-
const rad = (angleDeg * Math.PI) / 180;
|
|
283
|
-
// isRHS=true: native y-up coords, matching C's get_gradient_points(A,G,n,0,3).
|
|
284
|
-
const gp = getGradientPoints(pts, rad, true, true);
|
|
285
|
-
const cx = gp.g0.x;
|
|
286
|
-
const cy = gp.g0.y;
|
|
287
|
-
const r1 = gp.g1.x;
|
|
288
|
-
const r2 = gp.g1.y;
|
|
289
|
-
const c1x = angleDeg === 0 ? cx : cx + r1 * Math.cos(rad);
|
|
290
|
-
const c1y = angleDeg === 0 ? cy : cy + r1 * Math.sin(rad);
|
|
291
|
-
const inner =
|
|
292
|
-
'(' + xdotNum(c1x) + ' ' + xdotNum(c1y) + ' ' + xdotNum(r1) + ' ' +
|
|
293
|
-
xdotNum(cx) + ' ' + xdotNum(cy) + ' ' + xdotNum(r2) + ' 2 ' +
|
|
294
|
-
gradientStops(fillColor, stopColor, frac) + ')';
|
|
295
|
-
return 'C ' + String(utf8Len(inner)) + ' -' + inner + ' ';
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
/** Pen ("c ") color op from a resolved GVColor. */
|
|
299
|
-
export function xdotPenColor(c: GVColor): string {
|
|
300
|
-
return xdotColorOp('c ', gvColorRgba(c));
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
/** Fill ("C ") color op from a resolved GVColor. */
|
|
304
|
-
export function xdotFillColor(c: GVColor): string {
|
|
305
|
-
return xdotColorOp('C ', gvColorRgba(c));
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
/**
|
|
309
|
-
* Build the xdot "F size len -name " font op. Mirrors xdot_textspan's `F` +
|
|
310
|
-
* `xdot_str(job, "", font->name)` — the length prefix is the byte length of the
|
|
311
|
-
* face name. @see plugin/core/gvrender_core_dot.c:498 xdot_textspan
|
|
312
|
-
*/
|
|
313
|
-
export function xdotFont(size: number, name: string): string {
|
|
314
|
-
return 'F ' + xdotNum(size > 0 ? size : 0) + ' ' + String(utf8Len(name)) + ' -' + name + ' ';
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
/**
|
|
318
|
-
* Build an xdot length-prefixed string op ("S "/"" prefix): "<pfx><len> -<s> ".
|
|
319
|
-
* @see plugin/core/gvrender_core_dot.c:83 xdot_str_xbuf
|
|
320
|
-
*/
|
|
321
|
-
function xdotStrOp(prefix: string, s: string): string {
|
|
322
|
-
return prefix + String(utf8Len(s)) + ' -' + s + ' ';
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
/**
|
|
326
|
-
* Quote a DOT identifier unless it is a bare id or numeral, mirroring agwrite's
|
|
327
|
-
* agcanonStr so the serialized graph reparses (the comparator reparses both
|
|
328
|
-
* sides). Only `"` is escaped (→ `\"`); a `\` is left as-is — it is already the
|
|
329
|
-
* start of a stored escape like `\n`/`\l` that agcanonStr keeps verbatim, so
|
|
330
|
-
* doubling it (`\\n`) would change the name (`a\n(b\n"c")` must stay `a\n…`, not
|
|
331
|
-
* `a\\n…`). Over-quoting a value native leaves bare is harmless: both parse to
|
|
332
|
-
* the same name. @see lib/cgraph/write.c:_agstrcanon (escapes '"', keeps '\')
|
|
333
|
-
*/
|
|
334
|
-
export function xdotId(s: string): string {
|
|
335
|
-
if (/^[A-Za-z_][A-Za-z_0-9]*$/.test(s)) return s;
|
|
336
|
-
if (/^-?(\.[0-9]+|[0-9]+(\.[0-9]*)?)$/.test(s)) return s;
|
|
337
|
-
return '"' + s.replace(/"/g, '\\"') + '"';
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
/**
|
|
341
|
-
* Format a number as C's `%.5g` — 5 significant figures, trailing zeros and
|
|
342
|
-
* point trimmed, switching to `e±NN` (min 2 exponent digits) when the exponent
|
|
343
|
-
* is < -4 or ≥ 5. Native writes the DOT `pos`/`bb`/`width`/`height` attributes
|
|
344
|
-
* with `%.5g` (output.c:71/294/302), so large coordinates round to 5 sig figs
|
|
345
|
-
* (`2219962` → `2.2201e+06`); `printNum`'s fixed 3 dp keeps too much precision.
|
|
346
|
-
*
|
|
347
|
-
* Uses {@link printfSig} rather than `Number.prototype.toPrecision(5)` so
|
|
348
|
-
* exact-halfway values (e.g. 1399.25) round half-to-even like C's snprintf,
|
|
349
|
-
* not half-away-from-zero like `toPrecision`. @see docs proven case
|
|
350
|
-
* graphs-b786 (circo): pos coordinate 1399.25 → native "1399.2", `toPrecision`
|
|
351
|
-
* gave "1399.3".
|
|
352
|
-
* @see lib/common/output.c:71 (agxbprint "%.5g")
|
|
353
|
-
*/
|
|
354
|
-
export function gfmt5(v: number): string {
|
|
355
|
-
if (!Number.isFinite(v)) return String(v);
|
|
356
|
-
if (v === 0) return '0';
|
|
357
|
-
let s = printfSig(v, 5);
|
|
358
|
-
const e = s.indexOf('e');
|
|
359
|
-
if (e >= 0) {
|
|
360
|
-
let mant = s.slice(0, e);
|
|
361
|
-
if (mant.indexOf('.') >= 0) mant = mant.replace(/0+$/, '').replace(/\.$/, '');
|
|
362
|
-
const ei = parseInt(s.slice(e + 1), 10);
|
|
363
|
-
return mant + 'e' + (ei < 0 ? '-' : '+') + String(Math.abs(ei)).padStart(2, '0');
|
|
364
|
-
}
|
|
365
|
-
if (s.indexOf('.') >= 0) s = s.replace(/0+$/, '').replace(/\.$/, '');
|
|
366
|
-
return s;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
/**
|
|
370
|
-
* Format a number as C's `%.2f` — fixed 2 decimals, half-to-even on exact ties.
|
|
371
|
-
* `attach_attrs` writes the graph-label size attributes `lwidth`/`lheight` in
|
|
372
|
-
* INCHES with `%.2f`, unlike every other computed attribute (which uses `%.5g`).
|
|
373
|
-
* @see lib/common/output.c:244-247 (agxbprint "%.2f", PS2INCH)
|
|
374
|
-
*/
|
|
375
|
-
function gfmt2(v: number): string {
|
|
376
|
-
return printfFixed(v, 2);
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
/**
|
|
380
|
-
* Format a label position as the `x,y` pair every computed `*_lp` attribute
|
|
381
|
-
* uses: both coordinates at `%.5g`. C passes y through `yDir()`, which is the
|
|
382
|
-
* identity unless `Y_invert` — and `Y_invert` is only set by `-Ty` (plain/dot),
|
|
383
|
-
* never for xdot — so no inversion is applied here, exactly as the existing
|
|
384
|
-
* `pos`/`bb` emission does.
|
|
385
|
-
* @see lib/common/output.c:35 yDir · lib/common/output.c:241 (agxbprint "%.5g,%.5g")
|
|
386
|
-
*/
|
|
387
|
-
function lpStr(p: Point): string {
|
|
388
|
-
return gfmt5(p.x) + ',' + gfmt5(p.y);
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
/**
|
|
392
|
-
* C's `attach_attrs` edge loop skips IGNORED edges and edges with no spline
|
|
393
|
-
* (`ED_spl(e) == NULL`) via `continue`, so such an edge is attached NEITHER
|
|
394
|
-
* `pos` NOR any of `lp`/`xlp`/`head_lp`/`tail_lp` — even when it carries a
|
|
395
|
-
* label. All five attributes therefore share this one gate.
|
|
396
|
-
* @see lib/common/output.c:349-353
|
|
397
|
-
*/
|
|
398
|
-
function edgeAttrsAttached(e: Edge): boolean {
|
|
399
|
-
return e.info.edge_type !== IGNORED && e.info.spl != null;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
/**
|
|
403
|
-
* Append the leaf-field rectangles of a record node, mirroring the recursion in
|
|
404
|
-
* `set_record_rects`: a field with sub-fields contributes nothing itself, while
|
|
405
|
-
* a LEAF field (`n_flds == 0`) contributes its box translated by the node centre
|
|
406
|
-
* as `llx,lly,urx,ury` at `%.5g`. C emits each field followed by a space and
|
|
407
|
-
* then pops the trailing one — joining with a single space is equivalent.
|
|
408
|
-
* @see lib/common/output.c:215 set_record_rects
|
|
409
|
-
*/
|
|
410
|
-
function appendRecordRects(n: Node, f: FieldT, out: string[]): void {
|
|
411
|
-
const c = n.info.coord;
|
|
412
|
-
if (f.n_flds === 0) {
|
|
413
|
-
out.push(
|
|
414
|
-
gfmt5(f.b.ll.x + c.x) + ',' + gfmt5(f.b.ll.y + c.y) + ',' +
|
|
415
|
-
gfmt5(f.b.ur.x + c.x) + ',' + gfmt5(f.b.ur.y + c.y),
|
|
416
|
-
);
|
|
417
|
-
}
|
|
418
|
-
for (let i = 0; i < f.n_flds; i++) appendRecordRects(n, f.fld![i], out);
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
/**
|
|
422
|
-
* Echo an attribute that `attach_attrs_and_arrows` did NOT overwrite.
|
|
423
|
-
*
|
|
424
|
-
* Every computed attribute is `agset` behind a gate (`rects` only for a
|
|
425
|
-
* `record` shape; an edge's `lp` only for a routed edge that HAS a label;
|
|
426
|
-
* a graph's `lp` only when `GD_label(g)` exists...). When the gate FAILS, C
|
|
427
|
-
* simply does not write — so the slot keeps whatever the INPUT file parsed
|
|
428
|
-
* into it, and `agwrite` (which serializes the whole attribute table, not
|
|
429
|
-
* just the fields layout computed) prints that stale value verbatim. This is
|
|
430
|
-
* highly visible under patchwork, which forces every shape to `box` and routes
|
|
431
|
-
* no edges: on a re-fed dot output (most of the corpus) native echoes back the
|
|
432
|
-
* `rects` / `lp` a *previous* dot run wrote, in that run's coordinate space.
|
|
433
|
-
*
|
|
434
|
-
* cgraph interns attribute strings, so a value equal to the declared (empty)
|
|
435
|
-
* default is the SAME pointer as the default and write.c's
|
|
436
|
-
* `data->str[sym->id] != sym->defval` test drops it — hence an empty value is
|
|
437
|
-
* never printed.
|
|
438
|
-
* @see lib/cgraph/write.c:427 write_nondefault_attrs · lib/common/output.c:270
|
|
439
|
-
*/
|
|
440
|
-
function echoAttr(attrs: Map<string, string>, key: string): string[] {
|
|
441
|
-
const v = attrs.get(key);
|
|
442
|
-
if (v === undefined || v.length === 0) return [];
|
|
443
|
-
return [key + '="' + agcanonEscape(v) + '"'];
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
/** `echoAttr`'s result as a space-prefixed suffix for the string-built node block. */
|
|
447
|
-
function pfx(parts: string[]): string {
|
|
448
|
-
return parts.length === 0 ? '' : ' ' + parts.join(' ');
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
/**
|
|
452
|
-
* The `lp`/`lwidth`/`lheight` triple for a graph or cluster, in C's order.
|
|
453
|
-
* `rec_attach_bb` attaches them to the root graph AND recursively to every
|
|
454
|
-
* cluster, but ONLY when the graph carries a label with non-empty text
|
|
455
|
-
* (`GD_label(g) && GD_label(g)->text[0]`) — an absent or empty-text label emits
|
|
456
|
-
* none of the three. `lp` is the label centre in points (`%.5g`); `lwidth` and
|
|
457
|
-
* `lheight` are the label's `dimen` converted to inches and written `%.2f`.
|
|
458
|
-
* When the gate fails, the input's own values survive and are echoed: patchwork
|
|
459
|
-
* and osage never build a cluster label object, so a re-fed dot file's cluster
|
|
460
|
-
* `lp` comes straight back out.
|
|
461
|
-
* @see lib/common/output.c:239-248 rec_attach_bb
|
|
462
|
-
*/
|
|
463
|
-
function graphLabelAttrs(g: Graph): string[] {
|
|
464
|
-
const label = g.info.label as TextlabelT | undefined;
|
|
465
|
-
if (!label || label.text.length === 0) {
|
|
466
|
-
return [
|
|
467
|
-
...echoAttr(g.attrs, 'lp'),
|
|
468
|
-
...echoAttr(g.attrs, 'lwidth'),
|
|
469
|
-
...echoAttr(g.attrs, 'lheight'),
|
|
470
|
-
];
|
|
471
|
-
}
|
|
472
|
-
return [
|
|
473
|
-
'lp="' + lpStr(label.pos) + '"',
|
|
474
|
-
'lwidth=' + gfmt2(label.dimen.x / POINTS_PER_INCH),
|
|
475
|
-
'lheight=' + gfmt2(label.dimen.y / POINTS_PER_INCH),
|
|
476
|
-
];
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
// ---------------------------------------------------------------------------
|
|
480
|
-
// DOT attribute helpers
|
|
481
|
-
// ---------------------------------------------------------------------------
|
|
482
|
-
|
|
483
|
-
/** Format node attributes for DOT output. */
|
|
484
|
-
export function formatNodeAttrs(n: Node): string {
|
|
485
|
-
const info = n.info;
|
|
486
|
-
const x = printNum(info.coord.x);
|
|
487
|
-
const y = printNum(info.coord.y);
|
|
488
|
-
const w = printNum(info.width);
|
|
489
|
-
const h = printNum(info.height);
|
|
490
|
-
const parts: string[] = [`pos="${x},${y}"`, `width=${w}`, `height=${h}`];
|
|
491
|
-
n.attrs.forEach((v, k) => {
|
|
492
|
-
if (k !== 'pos' && k !== 'width' && k !== 'height') {
|
|
493
|
-
parts.push(`${k}="${v}"`);
|
|
494
|
-
}
|
|
495
|
-
});
|
|
496
|
-
return parts.join(' ');
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
/**
|
|
500
|
-
* Format edge spline points for the DOT `pos` attribute. Per bezier: the start
|
|
501
|
-
* endpoint `s,sp` when `sflag` set, then the end endpoint `e,ep` when `eflag`
|
|
502
|
-
* set, then `bez.size` control points — all at `%.5g`, exactly as native's
|
|
503
|
-
* spline serialization. @see lib/common/output.c:357-372
|
|
504
|
-
*/
|
|
505
|
-
export function formatEdgePos(e: Edge): string {
|
|
506
|
-
const spl = e.info.spl;
|
|
507
|
-
if (!spl || spl.list.length === 0) return '';
|
|
508
|
-
// Native's pos loop skips IGNORED edges (output.c:350) — concentrate merges
|
|
509
|
-
// an edge into its opposite and marks the absorbed one IGNORED; it is still
|
|
510
|
-
// drawn (has _draw_) but carries no `pos`. @see lib/common/output.c:349-353
|
|
511
|
-
if (e.info.edge_type === IGNORED) return '';
|
|
512
|
-
const parts: string[] = [];
|
|
513
|
-
for (const bez of spl.list) {
|
|
514
|
-
if (bez.sflag) parts.push('s,' + gfmt5(bez.sp.x) + ',' + gfmt5(bez.sp.y));
|
|
515
|
-
if (bez.eflag) parts.push('e,' + gfmt5(bez.ep.x) + ',' + gfmt5(bez.ep.y));
|
|
516
|
-
const pts = bez.list.slice(0, bez.size);
|
|
517
|
-
for (const p of pts) parts.push(gfmt5(p.x) + ',' + gfmt5(p.y));
|
|
518
|
-
}
|
|
519
|
-
return 'pos="' + parts.join(' ') + '"';
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
/** Return the edge connector token. */
|
|
523
|
-
export function edgeConnector(directed: boolean): string {
|
|
524
|
-
return directed ? '->' : '--';
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
/** Return true if the graph is directed or strict-directed. */
|
|
528
|
-
export function isDirected(g: Graph): boolean {
|
|
529
|
-
return g.kind === 'directed' || g.kind === 'strict-directed';
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
// ---------------------------------------------------------------------------
|
|
533
|
-
// Shared graph header helper
|
|
534
|
-
// ---------------------------------------------------------------------------
|
|
535
|
-
|
|
536
|
-
/** Emit the opening digraph/graph line and bb attribute. */
|
|
537
|
-
export function dotBeginGraphHeader(g: Graph, job: RenderJob): void {
|
|
538
|
-
const kw = isDirected(g) ? 'digraph' : 'graph';
|
|
539
|
-
job.write(kw + ' ' + g.name + ' {\n');
|
|
540
|
-
const bb = g.info.bb;
|
|
541
|
-
const bbStr = printNum(bb.ll.x) + ',' + printNum(bb.ll.y) + ','
|
|
542
|
-
+ printNum(bb.ur.x) + ',' + printNum(bb.ur.y);
|
|
543
|
-
job.write('\tgraph [bb="' + bbStr + '"];\n');
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
// ---------------------------------------------------------------------------
|
|
547
|
-
// DotRenderer
|
|
548
|
-
// ---------------------------------------------------------------------------
|
|
549
|
-
|
|
550
|
-
/**
|
|
551
|
-
* DOT format renderer — writes graphviz DOT language with layout attributes.
|
|
552
|
-
*
|
|
553
|
-
* @see plugin/core/gvrender_core_dot.c:dot_engine
|
|
554
|
-
*/
|
|
555
|
-
export class DotRenderer implements RendererPlugin {
|
|
556
|
-
readonly type = 'dot';
|
|
557
|
-
/** @see plugin/core/gvplugin_core.c registration table */
|
|
558
|
-
readonly quality = 0;
|
|
559
|
-
|
|
560
|
-
beginGraph(g: Graph, job: RenderJob): void {
|
|
561
|
-
dotBeginGraphHeader(g, job);
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
endGraph(_g: Graph, job: RenderJob): void {
|
|
565
|
-
job.write('}\n');
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
beginNode(_n: Node, _job: RenderJob): void { /* no-op */ }
|
|
569
|
-
|
|
570
|
-
endNode(n: Node, job: RenderJob): void {
|
|
571
|
-
const attrs = formatNodeAttrs(n);
|
|
572
|
-
job.write('\t' + n.name + ' [' + attrs + '];\n');
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
beginEdge(_e: Edge, _job: RenderJob): void { /* no-op */ }
|
|
576
|
-
|
|
577
|
-
endEdge(e: Edge, job: RenderJob): void {
|
|
578
|
-
const directed = isDirected(e.tail.root);
|
|
579
|
-
const conn = edgeConnector(directed);
|
|
580
|
-
const posAttr = formatEdgePos(e);
|
|
581
|
-
const attrList = posAttr ? ' [' + posAttr + ']' : '';
|
|
582
|
-
job.write('\t' + e.tail.name + ' ' + conn + ' ' + e.head.name + attrList + ';\n');
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
textspan(_pos: Point, _span: TextSpan, _job: RenderJob): void { /* no-op */ }
|
|
586
|
-
ellipse(_c: Point, _rx: number, _ry: number, _f: boolean, _j: RenderJob): void { /* no-op */ }
|
|
587
|
-
polygon(_pts: Point[], _filled: boolean, _job: RenderJob): void { /* no-op */ }
|
|
588
|
-
bezier(_pts: Point[], _filled: boolean, _job: RenderJob): void { /* no-op */ }
|
|
589
|
-
polyline(_pts: Point[], _job: RenderJob): void { /* no-op */ }
|
|
590
|
-
}
|
|
591
50
|
|
|
592
51
|
// ---------------------------------------------------------------------------
|
|
593
52
|
// XdotRenderer
|
|
594
53
|
// ---------------------------------------------------------------------------
|
|
595
54
|
|
|
596
|
-
/** Accumulated xdot draw strings for one model object (agset side-table). */
|
|
597
|
-
export interface XdotDraws {
|
|
598
|
-
draw?: string;
|
|
599
|
-
ldraw?: string;
|
|
600
|
-
hdraw?: string;
|
|
601
|
-
tdraw?: string;
|
|
602
|
-
hldraw?: string;
|
|
603
|
-
tldraw?: string;
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
/** Mutable state threaded through the recursive agwrite serializer. */
|
|
607
|
-
interface SerCtx {
|
|
608
|
-
out: string[];
|
|
609
|
-
/** subgraph → preorder number (write.c:subgdfs). */
|
|
610
|
-
preorder: Map<Graph, number>;
|
|
611
|
-
/** node → preorder of the subgraph it was last written in (node_last_written). */
|
|
612
|
-
nodeLW: Map<Node, number>;
|
|
613
|
-
/** edge → preorder of the subgraph it was last written in (edge_last_written). */
|
|
614
|
-
edgeLW: Map<Edge, number>;
|
|
615
|
-
/** objects whose attributes have already been emitted (AGATTRWF/attrs_written). */
|
|
616
|
-
attrsWritten: Set<Node | Edge>;
|
|
617
|
-
/** current indentation depth. */
|
|
618
|
-
level: number;
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
/**
|
|
622
|
-
* Escape backslashes in a LABEL draw string — C's put_escaping_backslashes,
|
|
623
|
-
* applied to the `_ldraw_`/`_hldraw_`/`_tldraw_` buffers (not the shape draws)
|
|
624
|
-
* before agset. A literal `\` in a label's text (e.g. `WXYZ\nabc`) becomes `\\`
|
|
625
|
-
* so it survives the DOT string round-trip; the T-op byte-length prefix stays on
|
|
626
|
-
* the UNescaped text. @see plugin/core/gvrender_core_dot.c:218 put_escaping_backslashes
|
|
627
|
-
*/
|
|
628
|
-
function escBackslash(s: string): string {
|
|
629
|
-
return s.replace(/\\/g, '\\\\');
|
|
630
|
-
}
|
|
631
55
|
|
|
632
|
-
/** True if s[i..] starts an escape sequence agcanonStr keeps verbatim: a `\`
|
|
633
|
-
* followed by one of E G H L N T l n r \ ". @see lib/cgraph/write.c:is_escape */
|
|
634
|
-
function isEscapeSeq(s: string, i: number): boolean {
|
|
635
|
-
if (s[i] !== '\\') return false;
|
|
636
|
-
const n = s[i + 1];
|
|
637
|
-
return n === 'E' || n === 'G' || n === 'H' || n === 'L' || n === 'N' || n === 'T'
|
|
638
|
-
|| n === 'l' || n === 'n' || n === 'r' || n === '\\' || n === '"';
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
/**
|
|
642
|
-
* Escape a value for a DOT attribute exactly as agwrite's agcanonStr does: a `"`
|
|
643
|
-
* becomes `\"` ONLY when it is not already part of an escape sequence, so an
|
|
644
|
-
* existing `\"` or `\\` in the value is passed through verbatim rather than
|
|
645
|
-
* double-escaped. For an already-backslash-doubled value (node/edge/graph labels,
|
|
646
|
-
* post put_escaping_backslashes) this is identical to a naive `"`→`\"` — every
|
|
647
|
-
* `"` follows a doubled `\\`, so none are ever part_of_escape. For raw cluster
|
|
648
|
-
* labels (agset, no put_escaping) it preserves the source `\"`/`\\` unchanged.
|
|
649
|
-
* @see lib/cgraph/write.c:_agstrcanon (135-167)
|
|
650
|
-
*/
|
|
651
|
-
function agcanonEscape(s: string): string {
|
|
652
|
-
let out = '';
|
|
653
|
-
let partOfEscape = false;
|
|
654
|
-
for (let i = 0; i < s.length; i++) {
|
|
655
|
-
const c = s[i]!;
|
|
656
|
-
if (c === '"' && !partOfEscape) {
|
|
657
|
-
out += '\\';
|
|
658
|
-
} else if (!partOfEscape && isEscapeSeq(s, i)) {
|
|
659
|
-
partOfEscape = true;
|
|
660
|
-
} else {
|
|
661
|
-
partOfEscape = false;
|
|
662
|
-
}
|
|
663
|
-
out += c;
|
|
664
|
-
}
|
|
665
|
-
return out;
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
/** Trim a "%.3f" fixed string like C's agxbuf_trim_zeros (trailing 0s + dot). */
|
|
669
|
-
function trimFixed3(v: number): string {
|
|
670
|
-
let s = v.toFixed(3);
|
|
671
|
-
if (s.indexOf('.') < 0) return s;
|
|
672
|
-
let end = s.length;
|
|
673
|
-
while (end > 0 && s[end - 1] === '0') end--;
|
|
674
|
-
if (s[end - 1] === '.') end--;
|
|
675
|
-
return s.slice(0, end);
|
|
676
|
-
}
|
|
677
56
|
|
|
678
57
|
/**
|
|
679
58
|
* XDOT format renderer — mirrors plugin/core/gvrender_core_dot.c's xdot engine.
|
|
@@ -690,29 +69,39 @@ function trimFixed3(v: number): string {
|
|
|
690
69
|
*
|
|
691
70
|
* @see plugin/core/gvrender_core_dot.c xdot_begin_graph / xdot_end_graph
|
|
692
71
|
*/
|
|
693
|
-
export class XdotRenderer implements RendererPlugin {
|
|
694
|
-
readonly type = 'xdot';
|
|
72
|
+
export class XdotRenderer extends EdgeDrawBase implements RendererPlugin {
|
|
73
|
+
readonly type: string = 'xdot';
|
|
695
74
|
/** @see plugin/core/gvplugin_core.c registration table */
|
|
696
75
|
readonly quality = 0;
|
|
697
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Whether this format attaches the xdot draw attributes before agwrite.
|
|
79
|
+
* Mirrors C's `job->render.id` switch: FORMAT_DOT runs `attach_attrs(g)` then
|
|
80
|
+
* `agwrite`, while FORMAT_XDOT runs `attach_attrs_and_arrows` +
|
|
81
|
+
* `xdot_begin_graph`/`xdot_end_graph` and *then* the same `agwrite`. The
|
|
82
|
+
* serializer below is that shared `agwrite`; this flag is the only difference.
|
|
83
|
+
* @see plugin/core/gvrender_core_dot.c:404,418,475,482
|
|
84
|
+
*/
|
|
85
|
+
protected override readonly emitDraws: boolean = true;
|
|
86
|
+
|
|
87
|
+
/** Draw strings for `obj`, or undefined in a format that emits none (`-Tdot`).
|
|
88
|
+
* Single gate so the xdot path cannot drift from the dot path. */
|
|
89
|
+
protected drawsOf(obj: Node | Edge | Graph): XdotDraws | undefined {
|
|
90
|
+
return this.emitDraws ? this.draws.get(obj) : undefined;
|
|
91
|
+
}
|
|
92
|
+
|
|
698
93
|
/** Per-render draw buffers. Indices 8/9 alias 1; 10/11 alias 5. */
|
|
699
94
|
private bufs: string[][] = makeXbufs();
|
|
700
|
-
/** setlinewidth state per emit_state. @see gvrender_core_dot.c penwidth[] */
|
|
701
|
-
private penwidth: number[] = new Array(12).fill(1);
|
|
702
95
|
/** text-flag state per emit_state. @see gvrender_core_dot.c textflags[] */
|
|
703
96
|
private textflags: number[] = new Array(12).fill(0);
|
|
704
97
|
/** Accumulated draw strings keyed by model object (C: agset on the object). */
|
|
705
98
|
private draws = new Map<Node | Edge | Graph, XdotDraws>();
|
|
706
|
-
/** Clusters in render order — the graphs `endCluster` was actually called for
|
|
707
|
-
* (from `renderClusters`/`GD_clust`), which is NOT always reachable via
|
|
708
|
-
* `g.subgraphs`, so it is tracked here rather than re-walked. */
|
|
709
|
-
private clusters: Graph[] = [];
|
|
710
99
|
/** edge_in_box gate for the current edge: emit_edge draws nothing (spline OR
|
|
711
100
|
* label) for an edge whose content is outside job->clip. Mirrors svg.ts's
|
|
712
101
|
* edgeGroupOpen. @see lib/common/emit.c:emit_edge (3039) */
|
|
713
102
|
private edgeDrawable = true;
|
|
714
103
|
|
|
715
|
-
beginGraph(
|
|
104
|
+
beginGraph(_g: Graph, _job: RenderJob): void {
|
|
716
105
|
this.bufs = makeXbufs();
|
|
717
106
|
this.penwidth = new Array(12).fill(1);
|
|
718
107
|
this.textflags = new Array(12).fill(0);
|
|
@@ -735,7 +124,7 @@ export class XdotRenderer implements RendererPlugin {
|
|
|
735
124
|
// @see gvrender_core_dot.c:427 xdot_end_graph
|
|
736
125
|
const gd = this.flush(EmitState.GDraw);
|
|
737
126
|
const gl = this.flush(EmitState.GLabel);
|
|
738
|
-
if (gd || gl) {
|
|
127
|
+
if (this.emitDraws && (gd || gl)) {
|
|
739
128
|
const set = this.drawsFor(g);
|
|
740
129
|
if (gd) set.draw = gd;
|
|
741
130
|
if (gl) set.ldraw = escBackslash(gl);
|
|
@@ -767,22 +156,23 @@ export class XdotRenderer implements RendererPlugin {
|
|
|
767
156
|
];
|
|
768
157
|
const buf = this.bufs[EmitState.GDraw]!;
|
|
769
158
|
buf.push(xdotPenColor(resolveRenderColor('transparent')));
|
|
770
|
-
|
|
771
|
-
// linear one emits the bracketed fill, a radial one is deferred to the base.
|
|
772
|
-
const stop = fillSpec.includes(':') ? findStopColor(fillSpec) : null;
|
|
773
|
-
if (stop !== null) {
|
|
774
|
-
const angle = Number(g.attrs.get('gradientangle') ?? 0) || 0;
|
|
775
|
-
const radial = parseStyleFlags(g.attrs.get('style')).radial;
|
|
776
|
-
const op = radial ? radialGradientOp : linearGradientOp;
|
|
777
|
-
buf.push(
|
|
778
|
-
op(corners, resolveRenderColor(stop.fillColor), resolveRenderColor(stop.stopColor), stop.frac, angle),
|
|
779
|
-
);
|
|
780
|
-
} else {
|
|
781
|
-
buf.push(xdotFillColor(resolveRenderColor(fillSpec)));
|
|
782
|
-
}
|
|
159
|
+
buf.push(this.backgroundFillOp(g, fillSpec, corners));
|
|
783
160
|
buf.push(xdotPoints('P', corners));
|
|
784
161
|
}
|
|
785
162
|
|
|
163
|
+
/** The canvas fill op: a two-colour `bgcolor` is a gradient (emit_background →
|
|
164
|
+
* findStopColor), linear or radial per style; anything else is a flat fill.
|
|
165
|
+
* @see lib/common/emit.c:1476 emit_background */
|
|
166
|
+
private backgroundFillOp(g: Graph, fillSpec: string, corners: Point[]): string {
|
|
167
|
+
const stop = fillSpec.includes(':') ? findStopColor(fillSpec) : null;
|
|
168
|
+
if (stop === null) return xdotFillColor(resolveRenderColor(fillSpec));
|
|
169
|
+
const angle = Number(g.attrs.get('gradientangle') ?? 0) || 0;
|
|
170
|
+
const op = parseStyleFlags(g.attrs.get('style')).radial ? radialGradientOp : linearGradientOp;
|
|
171
|
+
return op(
|
|
172
|
+
corners, resolveRenderColor(stop.fillColor), resolveRenderColor(stop.stopColor), stop.frac, angle,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
786
176
|
beginNode(_n: Node, _job: RenderJob): void { /* no-op */ }
|
|
787
177
|
|
|
788
178
|
endNode(n: Node, _job: RenderJob): void {
|
|
@@ -837,32 +227,12 @@ export class XdotRenderer implements RendererPlugin {
|
|
|
837
227
|
} else if (edgeIsTapered(e)) {
|
|
838
228
|
this.emitTaperedSpline(e, spl.list[0], edraw, job);
|
|
839
229
|
} else if (numc > 0) {
|
|
840
|
-
this.emitParallelSpline(
|
|
230
|
+
const c = this.emitParallelSpline(
|
|
231
|
+
spl.list as (Bezier | undefined)[], colorAttr, numc, edraw, job);
|
|
232
|
+
tailArrowColor = c.tailColor;
|
|
233
|
+
headArrowColor = c.headColor;
|
|
841
234
|
} else {
|
|
842
|
-
|
|
843
|
-
// as polylines (L), else the single bezier. @see emit.c:2583 / svg-helpers
|
|
844
|
-
const radius = orthoRoundedRadius(e, job);
|
|
845
|
-
const obj = job.obj;
|
|
846
|
-
const origStyle = obj !== null ? [...obj.rawStyle] : [];
|
|
847
|
-
const multi = spl.list.length > 1;
|
|
848
|
-
for (const bez of spl.list) {
|
|
849
|
-
const pts = bez.list.slice(0, bez.size);
|
|
850
|
-
const polys = radius !== null && bez.size >= 4 ? orthoRoundedPolylines(pts, radius) : [];
|
|
851
|
-
if (polys.length > 0) {
|
|
852
|
-
for (const poly of polys) edraw.push(this.styleOp(job), this.penOp(job), xdotPoints('L', poly));
|
|
853
|
-
} else {
|
|
854
|
-
edraw.push(this.styleOp(job), this.penOp(job), xdotPoints('B', pts));
|
|
855
|
-
}
|
|
856
|
-
// arrow_gen (drawn to TDRAW/HDRAW below) resets the job style to
|
|
857
|
-
// defaultlinestyle ("solid") as a side effect. For a multi-bezier spline
|
|
858
|
-
// the NEXT segment's xdot_style re-emits the current rawstyle every call,
|
|
859
|
-
// so a solid edge picks up a bare `S 5 -solid`; C restores the edge's own
|
|
860
|
-
// styles afterward only when it has explicit ones. @see emit.c:2668-2677
|
|
861
|
-
if (obj !== null && multi && (bez.sflag || bez.eflag)) {
|
|
862
|
-
obj.rawStyle = origStyle.length > 0 ? origStyle : ['solid'];
|
|
863
|
-
}
|
|
864
|
-
}
|
|
865
|
-
if (obj !== null) obj.rawStyle = origStyle;
|
|
235
|
+
this.emitPlainSpline(e, spl.list, edraw, job);
|
|
866
236
|
}
|
|
867
237
|
// Arrows: y-up ops already computed for the shared render path. C sets the
|
|
868
238
|
// default line style ("solid") + penwidth before each arrow primitive.
|
|
@@ -875,122 +245,12 @@ export class XdotRenderer implements RendererPlugin {
|
|
|
875
245
|
* -tapered` (the style), a transparent pen, the edge-color fill, then the
|
|
876
246
|
* polygon vertices from `taper()` (y-up). @see lib/common/emit.c:2422
|
|
877
247
|
*/
|
|
878
|
-
private emitTaperedSpline(e: Edge, bz: Bezier | undefined, edraw: string[], job: RenderJob): void {
|
|
879
|
-
if (bz === undefined) return;
|
|
880
|
-
const radfunc = taperfun(e.attrs.get('dir'), isDirected(e.tail.root));
|
|
881
|
-
const verts = taper(bz, radfunc, job.obj?.penWidth ?? 1);
|
|
882
|
-
const edgeColor = job.obj?.penColor ?? { type: 'string', s: 'black' };
|
|
883
|
-
edraw.push(this.styleOp(job));
|
|
884
|
-
edraw.push(xdotPenColor(resolveRenderColor('transparent')));
|
|
885
|
-
edraw.push(xdotFillColor(edgeColor));
|
|
886
|
-
edraw.push(xdotPoints('P', verts));
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
/**
|
|
890
|
-
* Emit a split-along-length `;` multicolor edge spline: split each routed
|
|
891
|
-
* bezier along its arc length into one sub-curve per color segment, drawn
|
|
892
|
-
* under that segment's pen. Reuses the same split geometry
|
|
893
|
-
* (splitSplineByColor) as the SVG path. @see lib/common/emit.c:1975 multicolor
|
|
894
|
-
*/
|
|
895
|
-
private emitSplitSpline(
|
|
896
|
-
bzList: (Bezier | undefined)[],
|
|
897
|
-
colorAttr: string,
|
|
898
|
-
edraw: string[],
|
|
899
|
-
job: RenderJob,
|
|
900
|
-
): { firstColor: string; endColor: string } {
|
|
901
|
-
const segs = parseSegs(colorAttr).segs;
|
|
902
|
-
const firstColor = segs[0]?.color ?? 'black';
|
|
903
|
-
let endColor = firstColor;
|
|
904
|
-
for (const bz of bzList) {
|
|
905
|
-
if (bz === undefined || bz.size < 4) continue;
|
|
906
|
-
const split = splitSplineByColor(bz.list.slice(0, bz.size), segs);
|
|
907
|
-
endColor = split.endColor;
|
|
908
|
-
for (const c of split.curves) {
|
|
909
|
-
edraw.push(this.styleOp(job), xdotPenColor(resolveRenderColor(c.color)), xdotPoints('B', c.points));
|
|
910
|
-
}
|
|
911
|
-
}
|
|
912
|
-
return { firstColor, endColor };
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
/**
|
|
916
|
-
* Emit the parallel-multicolor edge spline: one offset Bézier per color,
|
|
917
|
-
* offset SEP=2.0 perpendicular per pass — reusing the same offset geometry
|
|
918
|
-
* (buildOffsetLists/advanceTmpList) as the SVG parallel-edge path.
|
|
919
|
-
* @see lib/common/emit.c:2443 (parallel multicolor) / svg-parallel-edge.ts
|
|
920
|
-
*/
|
|
921
|
-
private emitParallelSpline(
|
|
922
|
-
bzList: (Bezier | undefined)[],
|
|
923
|
-
colorAttr: string,
|
|
924
|
-
numc: number,
|
|
925
|
-
edraw: string[],
|
|
926
|
-
job: RenderJob,
|
|
927
|
-
): void {
|
|
928
|
-
const segData = bzList.map((bz) =>
|
|
929
|
-
bz !== undefined && bz.size >= 4
|
|
930
|
-
? buildOffsetLists(bz.list, (2 + numc) / 2)
|
|
931
|
-
: { offlist: [] as Point[], tmplist: [] as Point[] },
|
|
932
|
-
);
|
|
933
|
-
const colors = parseSegs(colorAttr).segs.map((s) => s.color ?? 'black');
|
|
934
|
-
for (const color of colors) {
|
|
935
|
-
const pen = xdotPenColor(resolveRenderColor(color));
|
|
936
|
-
for (const sd of segData) {
|
|
937
|
-
if (sd.offlist.length === 0) continue;
|
|
938
|
-
advanceTmpList(sd.tmplist, sd.offlist);
|
|
939
|
-
edraw.push(this.styleOp(job), pen, xdotPoints('B', sd.tmplist));
|
|
940
|
-
}
|
|
941
|
-
}
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
/** Emit one arrow's primitive ops into `buf` (pen/fill from the edge color).
|
|
945
|
-
* arrow_gen sets the default line style ("solid") and the edge penwidth
|
|
946
|
-
* before each primitive, so a non-default penwidth emits `S setlinewidth(N)`
|
|
947
|
-
* once (tracked per HDRAW/TDRAW state). @see arrows.c:arrow_gen */
|
|
948
|
-
private emitArrows(
|
|
949
|
-
buf: string[], ops: ArrowDrawOp[] | undefined, job: RenderJob, state: EmitState,
|
|
950
|
-
penOverride?: string,
|
|
951
|
-
): void {
|
|
952
|
-
if (ops === undefined) return;
|
|
953
|
-
const pen = penOverride !== undefined
|
|
954
|
-
? resolveRenderColor(penOverride)
|
|
955
|
-
: job.obj?.penColor ?? { type: 'string', s: 'black' };
|
|
956
|
-
const pw = job.obj?.penWidth ?? 1;
|
|
957
|
-
for (const op of ops) {
|
|
958
|
-
if (Math.abs(pw - this.penwidth[state]!) >= 0.0005) {
|
|
959
|
-
this.penwidth[state] = pw;
|
|
960
|
-
buf.push(xdotStrOp('S ', 'setlinewidth(' + trimFixed3(pw) + ')'));
|
|
961
|
-
}
|
|
962
|
-
buf.push(xdotStrOp('S ', 'solid'));
|
|
963
|
-
buf.push(xdotPenColor(pen));
|
|
964
|
-
switch (op.kind) {
|
|
965
|
-
case 'polygon':
|
|
966
|
-
if (op.filled) buf.push(xdotFillColor(pen));
|
|
967
|
-
buf.push(xdotPoints(op.filled ? 'P' : 'p', op.points));
|
|
968
|
-
break;
|
|
969
|
-
case 'ellipse':
|
|
970
|
-
if (op.filled) buf.push(xdotFillColor(pen));
|
|
971
|
-
buf.push(
|
|
972
|
-
(op.filled ? 'E ' : 'e ') + xdotPoint(op.center) +
|
|
973
|
-
xdotNum(op.rx) + ' ' + xdotNum(op.ry) + ' ',
|
|
974
|
-
);
|
|
975
|
-
break;
|
|
976
|
-
case 'polyline':
|
|
977
|
-
buf.push(xdotPoints('L', op.points));
|
|
978
|
-
break;
|
|
979
|
-
case 'bezier':
|
|
980
|
-
buf.push(xdotPoints('B', op.points));
|
|
981
|
-
break;
|
|
982
|
-
}
|
|
983
|
-
}
|
|
984
|
-
}
|
|
985
248
|
|
|
986
249
|
endEdge(e: Edge, job: RenderJob): void {
|
|
987
250
|
// edge_in_box gate (set in beginEdge): a content-less / off-box edge draws
|
|
988
251
|
// nothing, matching native's emit_edge early return. @see emit.c:emit_edge
|
|
989
252
|
if (!this.edgeDrawable) {
|
|
990
|
-
this.
|
|
991
|
-
this.resetState(EmitState.HDraw, EmitState.TDraw);
|
|
992
|
-
this.penwidth[EmitState.HLabel] = 1;
|
|
993
|
-
this.penwidth[EmitState.TLabel] = 1;
|
|
253
|
+
this.resetEdgeState();
|
|
994
254
|
return;
|
|
995
255
|
}
|
|
996
256
|
// Emit the edge's labels (center/xlabel/head/tail) — the port draws these
|
|
@@ -998,27 +258,42 @@ export class XdotRenderer implements RendererPlugin {
|
|
|
998
258
|
// itself, mirroring emit_edge's emit_edge_label. gvrenderTextspan routes
|
|
999
259
|
// each span to the edge's ELABEL buffer → _ldraw_. @see emit.c:3010
|
|
1000
260
|
renderEdgeLabels(e, this, job);
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
261
|
+
this.attachEdgeDraws(e);
|
|
262
|
+
this.resetEdgeState();
|
|
263
|
+
this.textflags[EmitState.HLabel] = 0;
|
|
264
|
+
this.textflags[EmitState.TLabel] = 0;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Flush the six edge buffers and attach the non-empty ones. Label buffers get
|
|
268
|
+
* put_escaping_backslashes; shape buffers do not.
|
|
269
|
+
* @see plugin/core/gvrender_core_dot.c:218 */
|
|
270
|
+
private attachEdgeDraws(e: Edge): void {
|
|
271
|
+
// (buffer, target field, is-a-label) in C's flush order. Label buffers get
|
|
272
|
+
// put_escaping_backslashes; shape buffers do not.
|
|
273
|
+
const spec: [EmitState, keyof XdotDraws, boolean][] = [
|
|
274
|
+
[EmitState.EDraw, 'draw', false],
|
|
275
|
+
[EmitState.HDraw, 'hdraw', false],
|
|
276
|
+
[EmitState.TDraw, 'tdraw', false],
|
|
277
|
+
[EmitState.ELabel, 'ldraw', true],
|
|
278
|
+
[EmitState.HLabel, 'hldraw', true],
|
|
279
|
+
[EmitState.TLabel, 'tldraw', true],
|
|
280
|
+
];
|
|
281
|
+
const flushed = spec.map(([state, field, isLabel]) =>
|
|
282
|
+
[field, this.flush(state), isLabel] as const);
|
|
283
|
+
if (!flushed.some(([, s]) => s)) return;
|
|
284
|
+
const set = this.drawsFor(e);
|
|
285
|
+
for (const [field, s, isLabel] of flushed) {
|
|
286
|
+
if (s) set[field] = isLabel ? escBackslash(s) : s;
|
|
1015
287
|
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** Per-edge emit-state reset, run on both the drawable and the early-return
|
|
291
|
+
* path — identical in C, so it is one helper here rather than two copies. */
|
|
292
|
+
private resetEdgeState(): void {
|
|
1016
293
|
this.resetState(EmitState.EDraw, EmitState.ELabel);
|
|
1017
294
|
this.resetState(EmitState.HDraw, EmitState.TDraw);
|
|
1018
295
|
this.penwidth[EmitState.HLabel] = 1;
|
|
1019
296
|
this.penwidth[EmitState.TLabel] = 1;
|
|
1020
|
-
this.textflags[EmitState.HLabel] = 0;
|
|
1021
|
-
this.textflags[EmitState.TLabel] = 0;
|
|
1022
297
|
}
|
|
1023
298
|
|
|
1024
299
|
beginCluster(_sg: Graph, _job: RenderJob): void { /* no-op */ }
|
|
@@ -1090,7 +365,7 @@ export class XdotRenderer implements RendererPlugin {
|
|
|
1090
365
|
// --- graphics-state ops ------------------------------------------------
|
|
1091
366
|
|
|
1092
367
|
/** Pen color op from the resolved graphics state (default black). */
|
|
1093
|
-
|
|
368
|
+
protected penOp(job: RenderJob): string {
|
|
1094
369
|
return xdotPenColor(job.obj?.penColor ?? { type: 'string', s: 'black' });
|
|
1095
370
|
}
|
|
1096
371
|
|
|
@@ -1114,7 +389,7 @@ export class XdotRenderer implements RendererPlugin {
|
|
|
1114
389
|
|
|
1115
390
|
/** Style ops (`S`): setlinewidth on a penwidth change, plus dash/dot pen.
|
|
1116
391
|
* @see gvrender_core_dot.c:161 xdot_style */
|
|
1117
|
-
|
|
392
|
+
protected styleOp(job: RenderJob): string {
|
|
1118
393
|
const obj = job.obj;
|
|
1119
394
|
if (obj === null) return '';
|
|
1120
395
|
let s = '';
|
|
@@ -1178,331 +453,38 @@ export class XdotRenderer implements RendererPlugin {
|
|
|
1178
453
|
|
|
1179
454
|
// --- serialization (agwrite-at-end) ------------------------------------
|
|
1180
455
|
|
|
1181
|
-
/**
|
|
1182
|
-
* Emit `key="value"`, escaping the value the way agwrite's agcanonStr does: a
|
|
1183
|
-
* `"` becomes `\"` unless it is already part of an escape sequence (see
|
|
1184
|
-
* agcanonEscape). A draw string carries label text that may contain a bare `"`
|
|
1185
|
-
* (would close the attribute early) or a source `\"`/`\\` (must not be
|
|
1186
|
-
* double-escaped). The byte-length prefix stays on the UNescaped text (the
|
|
1187
|
-
* parser un-escapes before parseXDot re-reads it), matching native exactly.
|
|
1188
|
-
* @see lib/cgraph/write.c:_agstrcanon (135-167)
|
|
1189
|
-
*/
|
|
1190
|
-
private drawAttr(key: string, value: string): string {
|
|
1191
|
-
return key + '="' + agcanonEscape(value) + '"';
|
|
1192
|
-
}
|
|
1193
|
-
|
|
1194
|
-
/** `llx,lly,urx,ury` from a graph's layout bb. */
|
|
1195
|
-
private bbStr(g: Graph): string {
|
|
1196
|
-
const bb = g.info.bb;
|
|
1197
|
-
return gfmt5(bb.ll.x) + ',' + gfmt5(bb.ll.y) + ',' +
|
|
1198
|
-
gfmt5(bb.ur.x) + ',' + gfmt5(bb.ur.y);
|
|
1199
|
-
}
|
|
1200
|
-
|
|
1201
|
-
/**
|
|
1202
|
-
* Serialize the whole laid-out graph to xdot DOT text — a faithful port of
|
|
1203
|
-
* cgraph's agwrite (lib/cgraph/write.c). Recurses the subgraph tree
|
|
1204
|
-
* (write_subgs/write_body), scoping each node/edge to the subgraph(s) it
|
|
1205
|
-
* belongs to via preorder numbers (write_node_test/write_edge_test), and
|
|
1206
|
-
* re-emits an object bare (no attrs) on any scope after the first
|
|
1207
|
-
* (attrs_written). This reproduces native's per-subgraph edge re-declarations
|
|
1208
|
-
* — e.g. an edge in a rank=same subgraph is drawn once, then re-declared bare.
|
|
1209
|
-
* @see lib/cgraph/write.c:agwrite
|
|
1210
|
-
*/
|
|
1211
|
-
private serialize(g: Graph): string {
|
|
1212
|
-
const ctx: SerCtx = {
|
|
1213
|
-
out: [],
|
|
1214
|
-
preorder: new Map<Graph, number>(),
|
|
1215
|
-
nodeLW: new Map<Node, number>(),
|
|
1216
|
-
edgeLW: new Map<Edge, number>(),
|
|
1217
|
-
attrsWritten: new Set<Node | Edge>(),
|
|
1218
|
-
level: 0,
|
|
1219
|
-
};
|
|
1220
|
-
this.subgdfs(g, 1, ctx.preorder);
|
|
1221
|
-
this.writeHdr(g, true, ctx);
|
|
1222
|
-
this.writeBody(g, ctx);
|
|
1223
|
-
this.writeTrl(ctx);
|
|
1224
|
-
return ctx.out.join('');
|
|
1225
|
-
}
|
|
1226
|
-
|
|
1227
|
-
/** Preorder-number the subgraph tree. @see write.c:subgdfs */
|
|
1228
|
-
private subgdfs(g: Graph, ix: number, preorder: Map<Graph, number>): number {
|
|
1229
|
-
let ix0 = ix;
|
|
1230
|
-
preorder.set(g, ix0);
|
|
1231
|
-
for (const sub of g.subgraphs.values()) ix0 = this.subgdfs(sub, ix0, preorder);
|
|
1232
|
-
return ix0 + 1;
|
|
1233
|
-
}
|
|
1234
|
-
|
|
1235
|
-
/** A subgraph is anonymous when its name is empty or a `%N` local name. */
|
|
1236
|
-
private isAnonymous(g: Graph): boolean {
|
|
1237
|
-
return g.name.length === 0 || g.name.charCodeAt(0) === 0x25 /* % */;
|
|
1238
|
-
}
|
|
1239
|
-
|
|
1240
|
-
/** A graph carries a `bb` attribute when it is the root or has a layout bb
|
|
1241
|
-
* (clusters, and any subgraph output.c computed a box for). Native seeds `bb`
|
|
1242
|
-
* on every graph via safe_dcl, so the value differs (set vs empty "") between
|
|
1243
|
-
* a boxed graph and an unboxed child — the driver that makes an anon subgraph
|
|
1244
|
-
* under the root or a cluster "relevant". @see lib/common/output.c:safe_dcl */
|
|
1245
|
-
private hasBb(g: Graph): boolean {
|
|
1246
|
-
return g === g.root || this.clusters.includes(g);
|
|
1247
|
-
}
|
|
1248
|
-
|
|
1249
|
-
/** Anonymous subgraph with no own node/edge defaults and no graph attrs
|
|
1250
|
-
* differing from its parent → inlined into the parent. Native compares every
|
|
1251
|
-
* graph attr over the root attr dict; the load-bearing ones are `bb` (set on
|
|
1252
|
-
* the parent, empty on the child) and `rank`. @see write.c:irrelevant_subgraph */
|
|
1253
|
-
private irrelevantSubgraph(g: Graph): boolean {
|
|
1254
|
-
if (!this.isAnonymous(g)) return false;
|
|
1255
|
-
if (this.clusters.includes(g)) return false;
|
|
1256
|
-
if (g.nodeDefaults.size > 0 || g.edgeDefaults.size > 0) return false;
|
|
1257
|
-
if (g.parent) {
|
|
1258
|
-
if (this.hasBb(g) !== this.hasBb(g.parent)) return false;
|
|
1259
|
-
for (const [k, v] of g.attrs) {
|
|
1260
|
-
if (g.parent.attrs.get(k) !== v) return false;
|
|
1261
|
-
}
|
|
1262
|
-
} else if (g.attrs.size > 0) {
|
|
1263
|
-
return false;
|
|
1264
|
-
}
|
|
1265
|
-
return true;
|
|
1266
|
-
}
|
|
1267
|
-
|
|
1268
|
-
/** Non-draw graph attrs a subgraph emits (rank for rank=same; clusters use
|
|
1269
|
-
* clusterAttrs). Only comparator-relevant fields need be exact.
|
|
1270
|
-
*
|
|
1271
|
-
* `rec_attach_bb` walks the ROOT and then `GD_clust` recursively, so a
|
|
1272
|
-
* subgraph the layout did not box is attached NEITHER `bb` NOR the label
|
|
1273
|
-
* triple — both keep the INPUT's values, which agwrite echoes. circo and
|
|
1274
|
-
* twopi lay out no clusters at all (`GD_clust` empty — mirrored here by
|
|
1275
|
-
* `this.clusters`, filled from endCluster), so on a re-fed dot output their
|
|
1276
|
-
* `cluster0` comes back carrying the *previous* run's `bb` and `lp`.
|
|
1277
|
-
* @see lib/common/output.c:249 rec_attach_bb */
|
|
1278
|
-
private subgGraphAttrs(sg: Graph): string {
|
|
1279
|
-
if (this.clusters.includes(sg)) return this.clusterAttrs(sg);
|
|
1280
|
-
const parts: string[] = [];
|
|
1281
|
-
const rank = sg.attrs.get('rank');
|
|
1282
|
-
if (rank !== undefined) parts.push('rank=' + xdotId(rank));
|
|
1283
|
-
parts.push(...echoAttr(sg.attrs, 'bb'));
|
|
1284
|
-
parts.push(...graphLabelAttrs(sg));
|
|
1285
|
-
return parts.join(' ');
|
|
1286
|
-
}
|
|
1287
|
-
|
|
1288
|
-
private indent(ctx: SerCtx): string {
|
|
1289
|
-
return '\t'.repeat(ctx.level);
|
|
1290
|
-
}
|
|
1291
|
-
|
|
1292
|
-
/** @see write.c:write_hdr */
|
|
1293
|
-
private writeHdr(g: Graph, top: boolean, ctx: SerCtx): void {
|
|
1294
|
-
if (top) {
|
|
1295
|
-
const strict = g.kind === 'strict-directed' || g.kind === 'strict-undirected' ? 'strict ' : '';
|
|
1296
|
-
const kw = isDirected(g) ? 'digraph' : 'graph';
|
|
1297
|
-
const nm = g.name.length > 0 && !this.isAnonymous(g) ? xdotId(g.name) + ' ' : '';
|
|
1298
|
-
ctx.out.push(strict + kw + ' ' + nm + '{\n');
|
|
1299
|
-
ctx.level++;
|
|
1300
|
-
ctx.out.push(this.indent(ctx) + 'graph [' + this.graphAttrs(g) + '];\n');
|
|
1301
|
-
ctx.out.push(this.indent(ctx) + 'node [label="\\N"];\n');
|
|
1302
|
-
} else {
|
|
1303
|
-
const nm = this.isAnonymous(g) ? '' : 'subgraph ' + xdotId(g.name) + ' ';
|
|
1304
|
-
ctx.out.push(this.indent(ctx) + nm + '{\n');
|
|
1305
|
-
ctx.level++;
|
|
1306
|
-
const ga = this.subgGraphAttrs(g);
|
|
1307
|
-
if (ga) ctx.out.push(this.indent(ctx) + 'graph [' + ga + '];\n');
|
|
1308
|
-
}
|
|
1309
|
-
}
|
|
1310
|
-
|
|
1311
|
-
/** @see write.c:write_trl */
|
|
1312
|
-
private writeTrl(ctx: SerCtx): void {
|
|
1313
|
-
ctx.level--;
|
|
1314
|
-
ctx.out.push(this.indent(ctx) + '}\n');
|
|
1315
|
-
}
|
|
1316
|
-
|
|
1317
|
-
/** @see write.c:write_subgs */
|
|
1318
|
-
private writeSubgs(g: Graph, ctx: SerCtx): void {
|
|
1319
|
-
for (const sub of g.subgraphs.values()) {
|
|
1320
|
-
if (this.irrelevantSubgraph(sub)) {
|
|
1321
|
-
this.writeSubgs(sub, ctx);
|
|
1322
|
-
} else {
|
|
1323
|
-
this.writeHdr(sub, false, ctx);
|
|
1324
|
-
this.writeBody(sub, ctx);
|
|
1325
|
-
this.writeTrl(ctx);
|
|
1326
|
-
}
|
|
1327
|
-
}
|
|
1328
|
-
}
|
|
1329
|
-
|
|
1330
|
-
/** @see write.c:write_body — subgraphs, then this scope's nodes and edges. */
|
|
1331
|
-
private writeBody(g: Graph, ctx: SerCtx): void {
|
|
1332
|
-
this.writeSubgs(g, ctx);
|
|
1333
|
-
for (const n of g.nodes.values()) {
|
|
1334
|
-
if (this.writeNodeTest(g, n, ctx)) this.writeNode(g, n, ctx);
|
|
1335
|
-
let prev: Node = n;
|
|
1336
|
-
for (const e of n.outEdges(g)) {
|
|
1337
|
-
if (prev !== e.head && this.writeNodeTest(g, e.head, ctx)) {
|
|
1338
|
-
this.writeNode(g, e.head, ctx);
|
|
1339
|
-
prev = e.head;
|
|
1340
|
-
}
|
|
1341
|
-
if (this.writeEdgeTest(g, e, ctx)) this.writeEdge(g, e, ctx);
|
|
1342
|
-
}
|
|
1343
|
-
}
|
|
1344
|
-
}
|
|
1345
|
-
|
|
1346
|
-
/** @see write.c:write_node_test — every xdot node carries pos/size, so it is
|
|
1347
|
-
* never "default"; write it in the first scope that has not yet emitted it.
|
|
1348
|
-
* Cluster nodes (fdp compound proxies, ND_clustnode) are never declared —
|
|
1349
|
-
* C's write_plain/writenodeandport suppress them. @see lib/common/output.c:146 */
|
|
1350
|
-
private writeNodeTest(g: Graph, n: Node, ctx: SerCtx): boolean {
|
|
1351
|
-
if (n.info.clustnode) return false;
|
|
1352
|
-
return (ctx.nodeLW.get(n) ?? 0) < ctx.preorder.get(g)!;
|
|
1353
|
-
}
|
|
1354
|
-
|
|
1355
|
-
/** Emitted node name: a cluster node's synthetic `__i:<cluster>` id is written
|
|
1356
|
-
* as the cluster name it stands for. @see lib/common/output.c:114 */
|
|
1357
|
-
private emitNodeName(n: Node): string {
|
|
1358
|
-
if (n.info.clustnode) {
|
|
1359
|
-
const i = n.name.indexOf(':');
|
|
1360
|
-
if (i >= 0) return n.name.slice(i + 1);
|
|
1361
|
-
}
|
|
1362
|
-
return n.name;
|
|
1363
|
-
}
|
|
1364
|
-
|
|
1365
|
-
/** @see write.c:write_edge_test */
|
|
1366
|
-
private writeEdgeTest(g: Graph, e: Edge, ctx: SerCtx): boolean {
|
|
1367
|
-
return (ctx.edgeLW.get(e) ?? 0) < ctx.preorder.get(g)!;
|
|
1368
|
-
}
|
|
1369
|
-
|
|
1370
|
-
/** @see write.c:write_node */
|
|
1371
|
-
private writeNode(g: Graph, n: Node, ctx: SerCtx): void {
|
|
1372
|
-
let s = this.indent(ctx) + xdotId(this.emitNodeName(n));
|
|
1373
|
-
if (!ctx.attrsWritten.has(n)) {
|
|
1374
|
-
s += ' [' + this.nodeAttrs(n) + ']';
|
|
1375
|
-
ctx.attrsWritten.add(n);
|
|
1376
|
-
}
|
|
1377
|
-
ctx.out.push(s + ';\n');
|
|
1378
|
-
ctx.nodeLW.set(n, ctx.preorder.get(g)!);
|
|
1379
|
-
}
|
|
1380
|
-
|
|
1381
|
-
/** @see write.c:write_edge — attrs only on first emission, bare thereafter. */
|
|
1382
|
-
private writeEdge(g: Graph, e: Edge, ctx: SerCtx): void {
|
|
1383
|
-
const conn = edgeConnector(isDirected(g));
|
|
1384
|
-
let s = this.indent(ctx) + xdotId(this.emitNodeName(e.tail)) + ' ' + conn + ' ' +
|
|
1385
|
-
xdotId(this.emitNodeName(e.head));
|
|
1386
|
-
if (!ctx.attrsWritten.has(e)) {
|
|
1387
|
-
const attrs = this.edgeAttrStr(e);
|
|
1388
|
-
if (attrs) s += ' [' + attrs + ']';
|
|
1389
|
-
ctx.attrsWritten.add(e);
|
|
1390
|
-
}
|
|
1391
|
-
ctx.out.push(s + ';\n');
|
|
1392
|
-
ctx.edgeLW.set(e, ctx.preorder.get(g)!);
|
|
1393
|
-
}
|
|
1394
|
-
|
|
1395
|
-
/** Root-graph attribute block: `_draw_`, `_ldraw_`, `bb`, `xdotversion`. */
|
|
1396
|
-
private graphAttrs(g: Graph): string {
|
|
1397
|
-
const d = this.draws.get(g);
|
|
1398
|
-
const parts: string[] = [];
|
|
1399
|
-
if (d?.draw) parts.push(this.drawAttr('_draw_', d.draw));
|
|
1400
|
-
if (d?.ldraw) parts.push(this.drawAttr('_ldraw_', d.ldraw));
|
|
1401
|
-
parts.push('bb="' + this.bbStr(g) + '"');
|
|
1402
|
-
parts.push(...graphLabelAttrs(g));
|
|
1403
|
-
parts.push('xdotversion="' + XDOT_VERSION + '"');
|
|
1404
|
-
return parts.join(' ');
|
|
1405
|
-
}
|
|
1406
|
-
|
|
1407
|
-
/** Cluster attribute block. C's xdot_end_cluster ALWAYS agsets `_draw_` when
|
|
1408
|
-
* the graph has clusters (even empty, e.g. peripheries=0), so emit it
|
|
1409
|
-
* unconditionally; `_ldraw_` only when the cluster has a label.
|
|
1410
|
-
* @see plugin/core/gvrender_core_dot.c:284 xdot_end_cluster */
|
|
1411
|
-
private clusterAttrs(sg: Graph): string {
|
|
1412
|
-
const d = this.draws.get(sg);
|
|
1413
|
-
const parts: string[] = [this.drawAttr('_draw_', d?.draw ?? '')];
|
|
1414
|
-
if (d?.ldraw) parts.push(this.drawAttr('_ldraw_', d.ldraw));
|
|
1415
|
-
if (sg.info.bb) parts.push('bb="' + this.bbStr(sg) + '"');
|
|
1416
|
-
// rec_attach_bb recurses into GD_clust, so a labelled cluster carries the
|
|
1417
|
-
// same lp/lwidth/lheight triple as the root. @see lib/common/output.c:249
|
|
1418
|
-
parts.push(...graphLabelAttrs(sg));
|
|
1419
|
-
return parts.join(' ');
|
|
1420
|
-
}
|
|
1421
|
-
|
|
1422
|
-
/** Node attribute block: pos/width/height plus `_draw_`/`_ldraw_`. */
|
|
1423
|
-
private nodeAttrs(n: Node): string {
|
|
1424
|
-
const info = n.info;
|
|
1425
|
-
// attach_attrs derives the emitted size from ND_lw+ND_rw / ND_ht, NOT
|
|
1426
|
-
// ND_width/ND_height (output.c:307-308). They coincide except where an
|
|
1427
|
-
// engine leaves them divergent — patchwork's finishNode lets poly_init
|
|
1428
|
-
// clobber ND_width/height while the tile survives in lw/rw/ht.
|
|
1429
|
-
let s = 'pos="' + gfmt5(info.coord.x) + ',' + gfmt5(info.coord.y) + '"' +
|
|
1430
|
-
' width=' + gfmt5((info.lw + info.rw) / 72) + ' height=' + gfmt5(info.ht / 72);
|
|
1431
|
-
// xlp — only when the node HAS an xlabel AND the xlabel placer actually set
|
|
1432
|
-
// its position (`ND_xlabel(n)->set`). An xlabel that was never placed is
|
|
1433
|
-
// omitted entirely — so an input `xlp` survives and is echoed.
|
|
1434
|
-
// @see lib/common/output.c:309-313
|
|
1435
|
-
const xlabel = info.xlabel as TextlabelT | undefined;
|
|
1436
|
-
if (xlabel && xlabel.set) s += ' xlp="' + lpStr(xlabel.pos) + '"';
|
|
1437
|
-
else s += pfx(echoAttr(n.attrs, 'xlp'));
|
|
1438
|
-
// rects — record field boxes. C gates on the SHAPE NAME being exactly
|
|
1439
|
-
// "record" (`strcmp(ND_shape(n)->name, "record") == 0`), so `Mrecord` and
|
|
1440
|
-
// HTML-table nodes get NO rects; confirmed against the native oracle. When
|
|
1441
|
-
// the shape is not a record the input's `rects` is never overwritten and is
|
|
1442
|
-
// echoed — patchwork forces every node to `box`, so a re-fed dot output
|
|
1443
|
-
// returns its old record rects untouched. @see lib/common/output.c:314-317
|
|
1444
|
-
const shape = info.shape as ShapeDesc | undefined;
|
|
1445
|
-
if (shape?.name === 'record') {
|
|
1446
|
-
const rects: string[] = [];
|
|
1447
|
-
appendRecordRects(n, info.shape_info as FieldT, rects);
|
|
1448
|
-
s += ' rects="' + rects.join(' ') + '"';
|
|
1449
|
-
} else {
|
|
1450
|
-
s += pfx(echoAttr(n.attrs, 'rects'));
|
|
1451
|
-
}
|
|
1452
|
-
const d = this.draws.get(n);
|
|
1453
|
-
if (d?.draw) s += ' ' + this.drawAttr('_draw_', d.draw);
|
|
1454
|
-
if (d?.ldraw) s += ' ' + this.drawAttr('_ldraw_', d.ldraw);
|
|
1455
|
-
return s;
|
|
1456
|
-
}
|
|
1457
|
-
|
|
1458
|
-
/** The `[...]` attribute body for an edge (draw ops + spline `pos`), or ''. */
|
|
1459
|
-
private edgeAttrStr(e: Edge): string {
|
|
1460
|
-
const d = this.draws.get(e);
|
|
1461
|
-
const parts: string[] = [];
|
|
1462
|
-
if (d?.draw) parts.push(this.drawAttr('_draw_', d.draw));
|
|
1463
|
-
if (d?.ldraw) parts.push(this.drawAttr('_ldraw_', d.ldraw));
|
|
1464
|
-
if (d?.hdraw) parts.push(this.drawAttr('_hdraw_', d.hdraw));
|
|
1465
|
-
if (d?.tdraw) parts.push(this.drawAttr('_tdraw_', d.tdraw));
|
|
1466
|
-
if (d?.hldraw) parts.push(this.drawAttr('_hldraw_', d.hldraw));
|
|
1467
|
-
if (d?.tldraw) parts.push(this.drawAttr('_tldraw_', d.tldraw));
|
|
1468
|
-
const pos = formatEdgePos(e);
|
|
1469
|
-
if (pos) {
|
|
1470
|
-
parts.push(pos);
|
|
1471
|
-
} else {
|
|
1472
|
-
// C's attach_attrs only agsets `pos` when the edge HAS a spline
|
|
1473
|
-
// (output.c:348); an engine that never routes (patchwork) leaves the
|
|
1474
|
-
// INPUT's own pos attribute intact and write.c emits it verbatim.
|
|
1475
|
-
parts.push(...echoAttr(e.attrs, 'pos'));
|
|
1476
|
-
}
|
|
1477
|
-
// The label-position attributes live inside the SAME loop that writes `pos`
|
|
1478
|
-
// (output.c:377-396), so they are attached only for a routed, non-IGNORED
|
|
1479
|
-
// edge. Note the asymmetry C encodes: `lp`/`head_lp`/`tail_lp` are emitted
|
|
1480
|
-
// whenever the label EXISTS, but `xlp` additionally requires `->set` — an
|
|
1481
|
-
// unplaced xlabel is omitted. Order mirrors C: lp, xlp, head_lp, tail_lp.
|
|
1482
|
-
// Each gate is independent, and each FAILED gate leaves the input's own
|
|
1483
|
-
// value in the slot for write.c to echo: an unrouted edge (patchwork routes
|
|
1484
|
-
// none) returns the `lp` a previous dot run wrote.
|
|
1485
|
-
const attached = edgeAttrsAttached(e);
|
|
1486
|
-
const info = e.info;
|
|
1487
|
-
if (attached && info.label) parts.push('lp="' + lpStr(info.label.pos) + '"');
|
|
1488
|
-
else parts.push(...echoAttr(e.attrs, 'lp'));
|
|
1489
|
-
if (attached && info.xlabel && info.xlabel.set) {
|
|
1490
|
-
parts.push('xlp="' + lpStr(info.xlabel.pos) + '"');
|
|
1491
|
-
} else parts.push(...echoAttr(e.attrs, 'xlp'));
|
|
1492
|
-
if (attached && info.head_label) {
|
|
1493
|
-
parts.push('head_lp="' + lpStr(info.head_label.pos) + '"');
|
|
1494
|
-
} else parts.push(...echoAttr(e.attrs, 'head_lp'));
|
|
1495
|
-
if (attached && info.tail_label) {
|
|
1496
|
-
parts.push('tail_lp="' + lpStr(info.tail_label.pos) + '"');
|
|
1497
|
-
} else parts.push(...echoAttr(e.attrs, 'tail_lp'));
|
|
1498
|
-
return parts.join(' ');
|
|
1499
|
-
}
|
|
1500
456
|
}
|
|
1501
457
|
|
|
1502
458
|
// ---------------------------------------------------------------------------
|
|
1503
459
|
// Factory functions
|
|
1504
460
|
// ---------------------------------------------------------------------------
|
|
1505
461
|
|
|
462
|
+
/**
|
|
463
|
+
* DOT format renderer (`-Tdot`) — the same engine as {@link XdotRenderer} with
|
|
464
|
+
* the xdot draw-attribute step switched off, mirroring C's one renderer with a
|
|
465
|
+
* `job->render.id` switch: FORMAT_DOT is `attach_attrs(g)` then `agwrite(g)`,
|
|
466
|
+
* FORMAT_XDOT is `attach_attrs_and_arrows` + the `xdot_*` draw attributes then
|
|
467
|
+
* *the same* `agwrite(g)`. Sharing the serializer is what makes `-Tdot` emit the
|
|
468
|
+
* subgraph tree, per-scope node/edge scoping, and the `node [label="\N"]`
|
|
469
|
+
* default line, none of which a streaming emitter can produce (the whole graph
|
|
470
|
+
* must be walked before the first byte).
|
|
471
|
+
*
|
|
472
|
+
* The drawing callbacks are overridden to no-ops: `-Tdot` renders no draw ops,
|
|
473
|
+
* so accumulating them would be dead work.
|
|
474
|
+
*
|
|
475
|
+
* @see plugin/core/gvrender_core_dot.c:398 dot_begin_graph / :464 dot_end_graph
|
|
476
|
+
*/
|
|
477
|
+
export class DotRenderer extends XdotRenderer {
|
|
478
|
+
override readonly type: string = 'dot';
|
|
479
|
+
protected override readonly emitDraws: boolean = false;
|
|
480
|
+
|
|
481
|
+
override textspan(_pos: Point, _span: TextSpan, _job: RenderJob): void { /* no-op */ }
|
|
482
|
+
override ellipse(_c: Point, _rx: number, _ry: number, _f: boolean, _j: RenderJob): void { /* no-op */ }
|
|
483
|
+
override polygon(_pts: Point[], _filled: boolean, _job: RenderJob): void { /* no-op */ }
|
|
484
|
+
override bezier(_pts: Point[], _filled: boolean, _job: RenderJob): void { /* no-op */ }
|
|
485
|
+
override polyline(_pts: Point[], _job: RenderJob): void { /* no-op */ }
|
|
486
|
+
}
|
|
487
|
+
|
|
1506
488
|
/** @see plugin/core/gvrender_core_dot.c FORMAT_DOT */
|
|
1507
489
|
export function createDotRenderer(): RendererPlugin {
|
|
1508
490
|
return new DotRenderer();
|
|
@@ -1512,3 +494,4 @@ export function createDotRenderer(): RendererPlugin {
|
|
|
1512
494
|
export function createXdotRenderer(): RendererPlugin {
|
|
1513
495
|
return new XdotRenderer();
|
|
1514
496
|
}
|
|
497
|
+
|