@knowvah/dot-engine 1.1.0 → 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 +48 -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 +3 -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
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Attribute helpers for the `-Tdot` writer — the `attach_attrs_and_arrows`
|
|
5
|
+
* side of output.c, plus the value-record and dict-default rules
|
|
6
|
+
* write_nondefault_attrs / write_dict compare against.
|
|
7
|
+
*
|
|
8
|
+
* @see lib/common/output.c:254 attach_attrs_and_arrows
|
|
9
|
+
* @see lib/cgraph/write.c:471 write_nondefault_attrs
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { Graph } from '../../model/graph.js';
|
|
13
|
+
import type { Node } from '../../model/node.js';
|
|
14
|
+
import type { Edge } from '../../model/edge.js';
|
|
15
|
+
import { IGNORED } from '../../layout/dot/rank.js';
|
|
16
|
+
import { agstrcanon } from '../map.js';
|
|
17
|
+
import { POINTS_PER_INCH } from '../../model/geom.js';
|
|
18
|
+
import type { TextlabelT, FieldT } from '../../common/types.js';
|
|
19
|
+
import { agcanonEscape, gfmt2, gfmt5, lpStr } from './xdot-ops.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* C's `attach_attrs` edge loop skips IGNORED edges and edges with no spline
|
|
23
|
+
* (`ED_spl(e) == NULL`) via `continue`, so such an edge is attached NEITHER
|
|
24
|
+
* `pos` NOR any of `lp`/`xlp`/`head_lp`/`tail_lp` — even when it carries a
|
|
25
|
+
* label. All five attributes therefore share this one gate.
|
|
26
|
+
* @see lib/common/output.c:349-353
|
|
27
|
+
*/
|
|
28
|
+
export function edgeAttrsAttached(e: Edge): boolean {
|
|
29
|
+
return e.info.edge_type !== IGNORED && e.info.spl != null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Append the leaf-field rectangles of a record node, mirroring the recursion in
|
|
34
|
+
* `set_record_rects`: a field with sub-fields contributes nothing itself, while
|
|
35
|
+
* a LEAF field (`n_flds == 0`) contributes its box translated by the node centre
|
|
36
|
+
* as `llx,lly,urx,ury` at `%.5g`. C emits each field followed by a space and
|
|
37
|
+
* then pops the trailing one — joining with a single space is equivalent.
|
|
38
|
+
* @see lib/common/output.c:215 set_record_rects
|
|
39
|
+
*/
|
|
40
|
+
export function appendRecordRects(n: Node, f: FieldT, out: string[]): void {
|
|
41
|
+
const c = n.info.coord;
|
|
42
|
+
if (f.n_flds === 0) {
|
|
43
|
+
out.push(
|
|
44
|
+
gfmt5(f.b.ll.x + c.x) + ',' + gfmt5(f.b.ll.y + c.y) + ',' +
|
|
45
|
+
gfmt5(f.b.ur.x + c.x) + ',' + gfmt5(f.b.ur.y + c.y),
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
for (let i = 0; i < f.n_flds; i++) appendRecordRects(n, f.fld![i], out);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Echo an attribute that `attach_attrs_and_arrows` did NOT overwrite.
|
|
53
|
+
*
|
|
54
|
+
* Every computed attribute is `agset` behind a gate (`rects` only for a
|
|
55
|
+
* `record` shape; an edge's `lp` only for a routed edge that HAS a label;
|
|
56
|
+
* a graph's `lp` only when `GD_label(g)` exists...). When the gate FAILS, C
|
|
57
|
+
* simply does not write — so the slot keeps whatever the INPUT file parsed
|
|
58
|
+
* into it, and `agwrite` (which serializes the whole attribute table, not
|
|
59
|
+
* just the fields layout computed) prints that stale value verbatim. This is
|
|
60
|
+
* highly visible under patchwork, which forces every shape to `box` and routes
|
|
61
|
+
* no edges: on a re-fed dot output (most of the corpus) native echoes back the
|
|
62
|
+
* `rects` / `lp` a *previous* dot run wrote, in that run's coordinate space.
|
|
63
|
+
*
|
|
64
|
+
* cgraph interns attribute strings, so a value equal to the declared (empty)
|
|
65
|
+
* default is the SAME pointer as the default and write.c's
|
|
66
|
+
* `data->str[sym->id] != sym->defval` test drops it — hence an empty value is
|
|
67
|
+
* never printed.
|
|
68
|
+
* @see lib/cgraph/write.c:427 write_nondefault_attrs · lib/common/output.c:270
|
|
69
|
+
*/
|
|
70
|
+
/** Names the serializer computes for a node; never echoed from input. */
|
|
71
|
+
export const COMPUTED_NODE_ATTRS = new Set(['pos', 'width', 'height', 'xlp', 'rects', '_draw_', '_ldraw_']);
|
|
72
|
+
|
|
73
|
+
/** Names the serializer computes for an edge. `tailport`/`headport` are excluded
|
|
74
|
+
* too: write_nondefault_attrs skips those symbols because they are written as
|
|
75
|
+
* `:port` syntax on the endpoints instead. @see lib/cgraph/write.c:487-492 */
|
|
76
|
+
export const COMPUTED_EDGE_ATTRS = new Set([
|
|
77
|
+
'pos', 'lp', 'xlp', 'head_lp', 'tail_lp', 'tailport', 'headport',
|
|
78
|
+
'_draw_', '_ldraw_', '_hdraw_', '_tdraw_', '_hldraw_', '_tldraw_',
|
|
79
|
+
]);
|
|
80
|
+
|
|
81
|
+
/** dot's synthesized AGNODE `label` default. @see lib/common/const.h NODENAME_ESC */
|
|
82
|
+
export const NODENAME_ESC = '\\N';
|
|
83
|
+
|
|
84
|
+
/** Effective edge defaults at `scope`, inner overriding outer — mirrors the
|
|
85
|
+
* builder's snapshotEdgeDefaults walk (builder.ts:294). Edges have no stored
|
|
86
|
+
* snapshot: the builder copies inherited defaults straight into `edge.attrs`,
|
|
87
|
+
* so the only way to tell a defaulted value from an explicit one is to compare
|
|
88
|
+
* against this walk. That merge also means `edge.attrs` already IS the cgraph
|
|
89
|
+
* value record `objInputParts` expects — the node side has to build one
|
|
90
|
+
* (`nodeRecord`) because node defaults are kept in a separate snapshot. */
|
|
91
|
+
export function effectiveEdgeDefaults(scope: Graph): Map<string, string> {
|
|
92
|
+
const eff = new Map<string, string>();
|
|
93
|
+
for (let g: Graph | null = scope; g !== null; g = g.parent) {
|
|
94
|
+
for (const [k, v] of g.edgeDefaults) if (!eff.has(k)) eff.set(k, v);
|
|
95
|
+
}
|
|
96
|
+
return eff;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Effective node defaults at `scope`, inner overriding outer — the dict
|
|
101
|
+
* write_node compares each node against (`d`, threaded from write_body,
|
|
102
|
+
* write.c:537-545). This is the scope the node is WRITTEN in, which is not
|
|
103
|
+
* necessarily where it was created.
|
|
104
|
+
*
|
|
105
|
+
* Includes dot's synthesized `label` default: `graph_init` installs
|
|
106
|
+
* NODENAME_ESC on the root when the input declares none (input.c:737-739), and
|
|
107
|
+
* it is part of the dict C compares against. `nodeRecord` seeds the same value
|
|
108
|
+
* under the same condition so the two cancel — omitting it from either side
|
|
109
|
+
* would print `label="\N"` (or `label=""`) on every node in the corpus.
|
|
110
|
+
* @see lib/cgraph/write.c:537-545 write_node · lib/common/input.c:737-739
|
|
111
|
+
*/
|
|
112
|
+
export function effectiveNodeDefaults(scope: Graph): Map<string, string> {
|
|
113
|
+
const eff = new Map<string, string>();
|
|
114
|
+
for (let g: Graph | null = scope; g !== null; g = g.parent) {
|
|
115
|
+
for (const [k, v] of g.nodeDefaults) if (!eff.has(k)) eff.set(k, v);
|
|
116
|
+
}
|
|
117
|
+
if (!eff.has('label')) eff.set('label', NODENAME_ESC);
|
|
118
|
+
return eff;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* One node's cgraph value RECORD: the node-dict defaults in effect where the
|
|
123
|
+
* node was CREATED, overridden by its own explicit attributes.
|
|
124
|
+
*
|
|
125
|
+
* `addattr` seeds a node's slot from the default at declaration time and an
|
|
126
|
+
* explicit `n [k=v]` overwrites it; neither is disturbed by a LATER
|
|
127
|
+
* `node [k=…]` in the same scope, which only moves the dict symbol. That gap is
|
|
128
|
+
* the whole mechanism: `{node[shape=house]; A; node[shape=invhouse]; B}` leaves
|
|
129
|
+
* A's record at `house` against a dict default of `invhouse`, so C prints
|
|
130
|
+
* `shape=house` on A and nothing on B.
|
|
131
|
+
* @see lib/cgraph/attr.c:210 addattr · lib/cgraph/write.c:485
|
|
132
|
+
*/
|
|
133
|
+
export function nodeRecord(n: Node): Map<string, string> {
|
|
134
|
+
const rec = new Map<string, string>(n.nodeDefaultsSnapshot ?? []);
|
|
135
|
+
if (!rec.has('label')) rec.set('label', NODENAME_ESC);
|
|
136
|
+
for (const [k, v] of n.attrs) rec.set(k, v);
|
|
137
|
+
return rec;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* One object's attribute block: every name whose value RECORD differs from the
|
|
142
|
+
* writing scope's dict default. A name in the dict but not the record compares
|
|
143
|
+
* as empty and prints as `k=""` — `digraph G { a; node[color=red]; b; }` emits
|
|
144
|
+
* `a [color=""]`, which a record-only walk structurally cannot produce.
|
|
145
|
+
* @see lib/cgraph/write.c:471 write_nondefault_attrs
|
|
146
|
+
*/
|
|
147
|
+
export function objInputParts(
|
|
148
|
+
record: Map<string, string>,
|
|
149
|
+
defaults: Map<string, string>,
|
|
150
|
+
computed: Set<string>,
|
|
151
|
+
): string[] {
|
|
152
|
+
const parts: string[] = [];
|
|
153
|
+
for (const k of new Set([...record.keys(), ...defaults.keys()])) {
|
|
154
|
+
if (computed.has(k)) continue;
|
|
155
|
+
const v = record.get(k) ?? '';
|
|
156
|
+
if (v === (defaults.get(k) ?? '')) continue;
|
|
157
|
+
parts.push(k + '=' + agstrcanon(v));
|
|
158
|
+
}
|
|
159
|
+
return parts;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* A COMPUTED node/edge attribute, dropped when its value equals the applicable
|
|
164
|
+
* dict default.
|
|
165
|
+
*
|
|
166
|
+
* `safe_dcl` declares each computed name with an empty default only when the
|
|
167
|
+
* symbol does not already exist — an input file that says `node [width=0.5]`
|
|
168
|
+
* leaves `sym->defval` at `0.5`. cgraph interns strings, so `agxset`ing a
|
|
169
|
+
* computed `0.5` stores the SAME refstr as the default and
|
|
170
|
+
* write_nondefault_attrs' `data->str[sym->id] != sym->defval` test drops it.
|
|
171
|
+
* tests/graphs/arrows.gv is the clean case: every node emits `height=0.5`
|
|
172
|
+
* (no declared default) but none emits `width`.
|
|
173
|
+
*
|
|
174
|
+
* `formatted` is passed in rather than derived so each call site keeps its own
|
|
175
|
+
* quoting; `raw` is the unquoted text that C would have interned.
|
|
176
|
+
* @see lib/common/utils.c:1065 safe_dcl · lib/cgraph/write.c:485
|
|
177
|
+
*/
|
|
178
|
+
export function computedPart(
|
|
179
|
+
key: string,
|
|
180
|
+
raw: string,
|
|
181
|
+
defaults: Map<string, string> | undefined,
|
|
182
|
+
): string[] {
|
|
183
|
+
if (defaults !== undefined && defaults.get(key) === raw) return [];
|
|
184
|
+
// C agsets the raw computed text (output.c) and lets agwrite canonicalize it
|
|
185
|
+
// on the way out, so the quoting DECISION and the 128-byte line breaking both
|
|
186
|
+
// belong here rather than at the call site: `lheight=0.23` prints bare while
|
|
187
|
+
// a long `pos` is split across lines with a trailing backslash.
|
|
188
|
+
// @see lib/cgraph/write.c:481 write_nondefault_attrs -> write_canonstr
|
|
189
|
+
return [key + '=' + agstrcanon(raw)];
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Attribute names the serializer computes itself; input values are echoed by
|
|
193
|
+
* `graphInputParts`, so these must not be echoed twice. */
|
|
194
|
+
export const COMPUTED_GRAPH_ATTRS = new Set([
|
|
195
|
+
'_draw_', '_ldraw_', 'bb', 'lp', 'lwidth', 'lheight',
|
|
196
|
+
]);
|
|
197
|
+
|
|
198
|
+
/** `COMPUTED_GRAPH_ATTRS` for a renderer that emits draw ops. Only
|
|
199
|
+
* `xdot_begin_graph` agsets `xdotversion` (gvrender_core_dot.c:341); plain
|
|
200
|
+
* `-Tdot` never computes it, so an input `xdotversion=1.7` stays an ordinary
|
|
201
|
+
* attribute there and write.c echoes it like any other. */
|
|
202
|
+
const COMPUTED_GRAPH_ATTRS_XDOT = new Set([...COMPUTED_GRAPH_ATTRS, 'xdotversion']);
|
|
203
|
+
|
|
204
|
+
/** The computed-attr set in force for this renderer. */
|
|
205
|
+
function computedGraphAttrs(emitDraws: boolean): Set<string> {
|
|
206
|
+
return emitDraws ? COMPUTED_GRAPH_ATTRS_XDOT : COMPUTED_GRAPH_ATTRS;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Walk to the root graph (cgraph `agroot`). */
|
|
210
|
+
export function rootOf(g: Graph): Graph {
|
|
211
|
+
let cur: Graph = g;
|
|
212
|
+
while (cur.parent !== null) cur = cur.parent;
|
|
213
|
+
return cur;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* A scope's own graph-attribute dict entries — the INPUT attributes, echoed by
|
|
218
|
+
* write_dict because in cgraph a graph's attributes ARE its dict defaults.
|
|
219
|
+
*
|
|
220
|
+
* For a non-root scope the test is PROVENANCE, not value. `setattr` gives a
|
|
221
|
+
* subgraph its own dict symbol for every attribute the input declares in that
|
|
222
|
+
* scope, and write_dict prints each local symbol whose value is non-empty —
|
|
223
|
+
* there is no comparison against the inherited value, so re-declaring an
|
|
224
|
+
* attribute to the value it already inherited still prints. Oracle-pinned:
|
|
225
|
+
* `digraph G { foo="x"; { foo="x"; a; b } }` emits `foo=x` on the subgraph.
|
|
226
|
+
*
|
|
227
|
+
* The only entries in `attrs` that are NOT local declarations are the ones the
|
|
228
|
+
* builder seeded from the snapshot so cluster-label inheritance survives the
|
|
229
|
+
* layout's cluster rebuilds; those carry no dict symbol in C and are recorded in
|
|
230
|
+
* `seededAttrs`. (A value-equality test stood in for this before the marker
|
|
231
|
+
* existed, and wrongly swallowed genuine re-declarations.)
|
|
232
|
+
*
|
|
233
|
+
* Also reproduces C's EAGER-propagation artifact. `agattr` creating a NEW global
|
|
234
|
+
* graph attribute runs `agapply(root, addattr, rsym, true)`, installing the
|
|
235
|
+
* symbol — with its pre-declaration (empty) default — on every subgraph that
|
|
236
|
+
* ALREADY EXISTS. So a subgraph opened before the declaration carries a local
|
|
237
|
+
* empty value and agwrite prints e.g. `rankdir=""`, while a sibling opened after
|
|
238
|
+
* it inherits and prints nothing. `graphDefaultsSnapshot` records exactly which
|
|
239
|
+
* defaults were in effect when the scope opened, so "declared at root after this
|
|
240
|
+
* scope opened" is `root.attrs.has(k) && !snapshot.has(k)`.
|
|
241
|
+
* @see lib/cgraph/attr.c:287 (agapply/addattr) · lib/cgraph/write.c:262
|
|
242
|
+
*/
|
|
243
|
+
export function graphInputParts(g: Graph, top: boolean, emitDraws: boolean): string[] {
|
|
244
|
+
const computed = computedGraphAttrs(emitDraws);
|
|
245
|
+
const parts: string[] = [];
|
|
246
|
+
const snap = g.graphDefaultsSnapshot;
|
|
247
|
+
const seeded = g.seededAttrs;
|
|
248
|
+
for (const [k, v] of g.attrs) {
|
|
249
|
+
if (computed.has(k)) continue;
|
|
250
|
+
if (!top && seeded !== undefined && seeded.has(k)) continue;
|
|
251
|
+
if (writeDictSkips(v, snap?.get(k))) continue;
|
|
252
|
+
parts.push(k + '=' + agstrcanon(v));
|
|
253
|
+
}
|
|
254
|
+
if (!top && snap !== undefined) parts.push(...eagerEmptyParts(g, snap, computed));
|
|
255
|
+
return parts;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* C's EAGER-propagation artifact. Setting a graph attribute in scope S runs
|
|
260
|
+
* `unviewsubgraphsattr(S, name)`, which walks `agfstsubg(S)` and gives every
|
|
261
|
+
* subgraph with no local definition its own symbol holding the value it had at
|
|
262
|
+
* that moment — empty, when the attribute is only now being declared. agwrite
|
|
263
|
+
* then prints that local `rankdir=""`, while a sibling opened afterwards simply
|
|
264
|
+
* inherits and prints nothing.
|
|
265
|
+
*
|
|
266
|
+
* Two things narrow this. The walk is over DIRECT subgraphs and is NOT
|
|
267
|
+
* recursive, so the trigger is the immediate PARENT declaring the key, not any
|
|
268
|
+
* ancestor: in `A { B { C {…} } graph[label=x] }` only B prints `label=""`,
|
|
269
|
+
* never C. And setattr only reaches `unviewsubgraphsattr` on the branch where
|
|
270
|
+
* the key had no symbol yet, so the parent's declaration must be the FIRST
|
|
271
|
+
* anywhere (`firstGraphDecl`) — once a sibling scope has declared the key, the
|
|
272
|
+
* same statement takes the "new local definition" branch and seeds nothing.
|
|
273
|
+
* @see lib/cgraph/attr.c:232 unviewsubgraphsattr · :257 setattr (branch split)
|
|
274
|
+
*/
|
|
275
|
+
function eagerEmptyParts(g: Graph, snap: Map<string, string>, computed: Set<string>): string[] {
|
|
276
|
+
const parts: string[] = [];
|
|
277
|
+
const p = g.parent;
|
|
278
|
+
if (p === null) return parts;
|
|
279
|
+
// The ROOT is special: a global declaration made anywhere inserts its symbol
|
|
280
|
+
// into the root's OWN dict, so by the time the root declares the key setattr
|
|
281
|
+
// always finds a local symbol and takes the unview branch. A non-root scope
|
|
282
|
+
// only holds a local symbol for a key it declared first.
|
|
283
|
+
const keys = p.parent === null ? p.attrs.keys() : (p.firstGraphDecl ?? []);
|
|
284
|
+
for (const k of keys) {
|
|
285
|
+
if (computed.has(k)) continue;
|
|
286
|
+
if (snap.has(k) || g.attrs.has(k)) continue;
|
|
287
|
+
parts.push(k + '=' + agstrcanon(''));
|
|
288
|
+
}
|
|
289
|
+
return parts;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* `name=value` parts for one attribute-default map, values canonicalized by the
|
|
294
|
+
* `_agstrcanon` port (conditional quoting — `rankdir=LR` bare, `bb="0,0,1,1"`
|
|
295
|
+
* quoted), matching write_canonstr. @see lib/cgraph/write.c:write_canonstr
|
|
296
|
+
*/
|
|
297
|
+
export function dictParts(defaults: Map<string, string>): string[] {
|
|
298
|
+
const parts: string[] = [];
|
|
299
|
+
for (const [k, v] of defaults) parts.push(k + '=' + agstrcanon(v));
|
|
300
|
+
return parts;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Node-dict parts for a scope. At the root, dot's `graph_init` installs an
|
|
305
|
+
* AGNODE `label` default of NODENAME_ESC (`\N`) when absent — the port models
|
|
306
|
+
* attributes as per-object Maps and reproduces that default at each use-site
|
|
307
|
+
* (see the N_label note in common/graph-init.ts), and the serializer is such a
|
|
308
|
+
* use-site. An explicit root-level `node [label=...]` wins over the synthesized
|
|
309
|
+
* default. @see lib/common/input.c:737-739 (N_label)
|
|
310
|
+
*/
|
|
311
|
+
export function nodeDictParts(g: Graph, top: boolean): string[] {
|
|
312
|
+
const defs = new Map(g.nodeDefaults);
|
|
313
|
+
if (top && !defs.has('label')) defs.set('label', NODENAME_ESC);
|
|
314
|
+
return dictParts(defs);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export function echoAttr(attrs: Map<string, string>, key: string): string[] {
|
|
318
|
+
const v = attrs.get(key);
|
|
319
|
+
if (v === undefined || v.length === 0) return [];
|
|
320
|
+
return [key + '="' + agcanonEscape(v) + '"'];
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* write_dict's conditional empty-value skip, as OBSERVED against the oracle.
|
|
325
|
+
*
|
|
326
|
+
* A non-empty local value always prints. An empty one is dropped only when the
|
|
327
|
+
* INHERITED value is present and empty too; an inherited value that is absent
|
|
328
|
+
* or non-empty leaves the empty local value printed, and so does the root,
|
|
329
|
+
* which inherits nothing. Pinned with `digraph G { [foo=<i>;] { foo=""; … } }`:
|
|
330
|
+
* inherited absent PRINTS, inherited `"x"` PRINTS, inherited `""` SKIPS, and
|
|
331
|
+
* the root's own `foo=""` PRINTS in every one of those.
|
|
332
|
+
*
|
|
333
|
+
* Neither half of the C's guard can be read literally. `Agsym_t.print`
|
|
334
|
+
* (cgraph.h:644) is assigned nowhere in the tree, so porting
|
|
335
|
+
* `EMPTY(psym->defval) && psym->print` verbatim makes the parent-empty skip
|
|
336
|
+
* unreachable; and the `view == NULL` skip would drop the root's `foo=""`,
|
|
337
|
+
* which native prints. The observed rule is authoritative here.
|
|
338
|
+
* @see lib/cgraph/write.c:271-278 write_dict
|
|
339
|
+
*/
|
|
340
|
+
export function writeDictSkips(v: string, inherited: string | undefined): boolean {
|
|
341
|
+
return v.length === 0 && inherited === '';
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* `echoAttr` for a GRAPH attribute the layout did not overwrite — same as
|
|
346
|
+
* echoAttr except an EMPTY value can still be emitted, per `writeDictSkips`.
|
|
347
|
+
* So a re-fed dot output whose `{rank=same}` block carries `bb=""`/`lp=""`
|
|
348
|
+
* under a boxed, labelled cluster echoes both back verbatim
|
|
349
|
+
* (tests/share/KW91.gv).
|
|
350
|
+
*/
|
|
351
|
+
export function echoGraphAttr(g: Graph, key: string): string[] {
|
|
352
|
+
const v = g.attrs.get(key);
|
|
353
|
+
if (v === undefined) return [];
|
|
354
|
+
if (writeDictSkips(v, g.graphDefaultsSnapshot?.get(key))) return [];
|
|
355
|
+
return [key + '="' + agcanonEscape(v) + '"'];
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* The `lp`/`lwidth`/`lheight` triple for a graph or cluster, in C's order.
|
|
360
|
+
* `rec_attach_bb` attaches them to the root graph AND recursively to every
|
|
361
|
+
* cluster, but ONLY when the graph carries a label with non-empty text
|
|
362
|
+
* (`GD_label(g) && GD_label(g)->text[0]`) — an absent or empty-text label emits
|
|
363
|
+
* none of the three. `lp` is the label centre in points (`%.5g`); `lwidth` and
|
|
364
|
+
* `lheight` are the label's `dimen` converted to inches and written `%.2f`.
|
|
365
|
+
* When the gate fails, the input's own values survive and are echoed: patchwork
|
|
366
|
+
* and osage never build a cluster label object, so a re-fed dot file's cluster
|
|
367
|
+
* `lp` comes straight back out.
|
|
368
|
+
* @see lib/common/output.c:239-248 rec_attach_bb
|
|
369
|
+
*/
|
|
370
|
+
export function graphLabelAttrs(g: Graph): string[] {
|
|
371
|
+
const label = g.info.label as TextlabelT | undefined;
|
|
372
|
+
if (!label || label.text.length === 0) {
|
|
373
|
+
return [
|
|
374
|
+
...echoGraphAttr(g, 'lp'),
|
|
375
|
+
...echoGraphAttr(g, 'lwidth'),
|
|
376
|
+
...echoGraphAttr(g, 'lheight'),
|
|
377
|
+
];
|
|
378
|
+
}
|
|
379
|
+
return [
|
|
380
|
+
'lp="' + lpStr(label.pos) + '"',
|
|
381
|
+
'lwidth=' + gfmt2(label.dimen.x / POINTS_PER_INCH),
|
|
382
|
+
'lheight=' + gfmt2(label.dimen.y / POINTS_PER_INCH),
|
|
383
|
+
];
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// ---------------------------------------------------------------------------
|
|
387
|
+
// DOT attribute helpers
|
|
388
|
+
// ---------------------------------------------------------------------------
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Format edge spline points for the DOT `pos` attribute. Per bezier: the start
|
|
392
|
+
* endpoint `s,sp` when `sflag` set, then the end endpoint `e,ep` when `eflag`
|
|
393
|
+
* set, then `bez.size` control points — all at `%.5g`, exactly as native's
|
|
394
|
+
* spline serialization. Beziers are separated by `;`, points within one by a
|
|
395
|
+
* space. @see lib/common/output.c:353-372
|
|
396
|
+
*/
|
|
397
|
+
export function formatEdgePos(e: Edge): string {
|
|
398
|
+
const raw = edgePosRaw(e);
|
|
399
|
+
return raw === null ? '' : 'pos="' + raw + '"';
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/** `formatEdgePos`'s value without the `pos="…"` wrapper — the text C interns
|
|
403
|
+
* and compares against the symbol default. `null` when no `pos` is attached. */
|
|
404
|
+
export function edgePosRaw(e: Edge): string | null {
|
|
405
|
+
const spl = e.info.spl;
|
|
406
|
+
if (!spl || spl.list.length === 0) return null;
|
|
407
|
+
// Native's pos loop skips IGNORED edges (output.c:350) — concentrate merges
|
|
408
|
+
// an edge into its opposite and marks the absorbed one IGNORED; it is still
|
|
409
|
+
// drawn (has _draw_) but carries no `pos`. @see lib/common/output.c:349-353
|
|
410
|
+
if (e.info.edge_type === IGNORED) return null;
|
|
411
|
+
// Built imperatively rather than by join() because C's separators are not
|
|
412
|
+
// uniform: beziers are separated by ';' with no space, while the `s,`/`e,`
|
|
413
|
+
// endpoints each carry a trailing space from their own format string.
|
|
414
|
+
let out = '';
|
|
415
|
+
for (let i = 0; i < spl.size; i++) {
|
|
416
|
+
const bez = spl.list[i];
|
|
417
|
+
if (i > 0) out += ';';
|
|
418
|
+
if (bez.sflag) out += 's,' + gfmt5(bez.sp.x) + ',' + gfmt5(bez.sp.y) + ' ';
|
|
419
|
+
if (bez.eflag) out += 'e,' + gfmt5(bez.ep.x) + ',' + gfmt5(bez.ep.y) + ' ';
|
|
420
|
+
for (let j = 0; j < bez.size; j++) {
|
|
421
|
+
if (j > 0) out += ' ';
|
|
422
|
+
const p = bez.list[j];
|
|
423
|
+
out += gfmt5(p.x) + ',' + gfmt5(p.y);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return out;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/** Return the edge connector token. */
|
|
430
|
+
export function edgeConnector(directed: boolean): string {
|
|
431
|
+
return directed ? '->' : '--';
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/** Return true if the graph is directed or strict-directed. */
|
|
435
|
+
export function isDirected(g: Graph): boolean {
|
|
436
|
+
return g.kind === 'directed' || g.kind === 'strict-directed';
|
|
437
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Edge spline + arrow draw-op emission — emit.c's multicolor / tapered / ortho
|
|
5
|
+
* branches, split out of dot.ts for file size. Same abstract-base approach as
|
|
6
|
+
* agwrite.ts: every body moves UNCHANGED and reaches the renderer's emit state
|
|
7
|
+
* (`penwidth`, `styleOp`, `penOp`) through inheritance rather than injection,
|
|
8
|
+
* so each branch stays diffable against the C.
|
|
9
|
+
*
|
|
10
|
+
* @see lib/common/emit.c:2389 multicolor · :2422 tapered · :2583 ortho
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { Edge } from '../../model/edge.js';
|
|
14
|
+
import type { Point } from '../../model/geom.js';
|
|
15
|
+
import { isDirected } from './attrs.js';
|
|
16
|
+
import type { Bezier } from '../../model/geom.js';
|
|
17
|
+
import type { ArrowDrawOp } from '../../common/arrows-types.js';
|
|
18
|
+
import type { GVColor } from '../../common/color.js';
|
|
19
|
+
import type { RenderJob } from '../../gvc/job.js';
|
|
20
|
+
import { EmitState } from '../../gvc/job.js';
|
|
21
|
+
import { resolveRenderColor } from '../color-resolve.js';
|
|
22
|
+
import { taper, taperfun } from '../../common/taper.js';
|
|
23
|
+
import { orthoRoundedRadius } from '../svg-helpers.js';
|
|
24
|
+
import { orthoRoundedPolylines } from '../svg-edge-ortho-radius.js';
|
|
25
|
+
import { parseSegs } from '../../common/multicolor.js';
|
|
26
|
+
import { splitSplineByColor } from '../svg-edge-split.js';
|
|
27
|
+
import { buildOffsetLists, advanceTmpList } from '../../common/edge-offset.js';
|
|
28
|
+
import { xdotFillColor, xdotNum, xdotPenColor, xdotPoint, xdotPoints, xdotStrOp, trimFixed3 } from './xdot-ops.js';
|
|
29
|
+
import { DotWriterBase } from './agwrite.js';
|
|
30
|
+
|
|
31
|
+
/** Pen colour when a colour-list entry is empty. @see lib/common/emit.c:2485 */
|
|
32
|
+
const DEFAULT_COLOR = 'black';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* One arrow primitive as an xdot op. Lifted out of emitArrows' loop unchanged —
|
|
36
|
+
* the four cases and their `filled` variants are exactly arrow_gen's.
|
|
37
|
+
* @see lib/common/arrows.c arrow_gen
|
|
38
|
+
*/
|
|
39
|
+
function emitArrowOp(buf: string[], op: ArrowDrawOp, pen: GVColor): void {
|
|
40
|
+
switch (op.kind) {
|
|
41
|
+
case 'polygon':
|
|
42
|
+
if (op.filled) buf.push(xdotFillColor(pen));
|
|
43
|
+
buf.push(xdotPoints(op.filled ? 'P' : 'p', op.points));
|
|
44
|
+
break;
|
|
45
|
+
case 'ellipse':
|
|
46
|
+
if (op.filled) buf.push(xdotFillColor(pen));
|
|
47
|
+
buf.push(
|
|
48
|
+
(op.filled ? 'E ' : 'e ') + xdotPoint(op.center) +
|
|
49
|
+
xdotNum(op.rx) + ' ' + xdotNum(op.ry) + ' ',
|
|
50
|
+
);
|
|
51
|
+
break;
|
|
52
|
+
case 'polyline':
|
|
53
|
+
buf.push(xdotPoints('L', op.points));
|
|
54
|
+
break;
|
|
55
|
+
case 'bezier':
|
|
56
|
+
buf.push(xdotPoints('B', op.points));
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** emit.c's edge-drawing half, between the serializer base and the renderer. */
|
|
62
|
+
export abstract class EdgeDrawBase extends DotWriterBase {
|
|
63
|
+
/** setlinewidth state per emit_state. @see gvrender_core_dot.c penwidth[] */
|
|
64
|
+
protected penwidth: number[] = new Array(12).fill(1);
|
|
65
|
+
protected abstract styleOp(job: RenderJob): string;
|
|
66
|
+
protected abstract penOp(job: RenderJob): string;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The plain single-colour spline branch of beginEdge — lifted verbatim so the
|
|
70
|
+
* four-way spline-kind chain above stays readable; no branch was reordered or
|
|
71
|
+
* merged. splines=ortho + radius/style=rounded emits straight segments plus
|
|
72
|
+
* corner arcs as polylines (L), else the bezier itself.
|
|
73
|
+
* @see lib/common/emit.c:2583
|
|
74
|
+
*/
|
|
75
|
+
protected emitPlainSpline(e: Edge, list: Bezier[], edraw: string[], job: RenderJob): void {
|
|
76
|
+
const radius = orthoRoundedRadius(e, job);
|
|
77
|
+
const obj = job.obj;
|
|
78
|
+
const origStyle = obj !== null ? [...obj.rawStyle] : [];
|
|
79
|
+
const multi = list.length > 1;
|
|
80
|
+
for (const bez of list) {
|
|
81
|
+
this.emitSplineBezier(bez, radius, edraw, job);
|
|
82
|
+
// arrow_gen (drawn to TDRAW/HDRAW below) resets the job style to
|
|
83
|
+
// defaultlinestyle ("solid") as a side effect. For a multi-bezier spline
|
|
84
|
+
// the NEXT segment's xdot_style re-emits the current rawstyle every call,
|
|
85
|
+
// so a solid edge picks up a bare `S 5 -solid`; C restores the edge's own
|
|
86
|
+
// styles afterward only when it has explicit ones. @see emit.c:2668-2677
|
|
87
|
+
if (obj !== null && multi && (bez.sflag || bez.eflag)) {
|
|
88
|
+
obj.rawStyle = origStyle.length > 0 ? origStyle : ['solid'];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (obj !== null) obj.rawStyle = origStyle;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** One bezier of a plain spline: rounded-ortho corner polylines when a radius
|
|
95
|
+
* applies and the segment has enough control points, else the bezier. */
|
|
96
|
+
protected emitSplineBezier(bez: Bezier, radius: number | null, edraw: string[], job: RenderJob): void {
|
|
97
|
+
const pts = bez.list.slice(0, bez.size);
|
|
98
|
+
const polys = radius !== null && bez.size >= 4 ? orthoRoundedPolylines(pts, radius) : [];
|
|
99
|
+
if (polys.length === 0) {
|
|
100
|
+
edraw.push(this.styleOp(job), this.penOp(job), xdotPoints('B', pts));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
for (const poly of polys) edraw.push(this.styleOp(job), this.penOp(job), xdotPoints('L', poly));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
protected emitTaperedSpline(e: Edge, bz: Bezier | undefined, edraw: string[], job: RenderJob): void {
|
|
107
|
+
if (bz === undefined) return;
|
|
108
|
+
const radfunc = taperfun(e.attrs.get('dir'), isDirected(e.tail.root));
|
|
109
|
+
const verts = taper(bz, radfunc, job.obj?.penWidth ?? 1);
|
|
110
|
+
const edgeColor = job.obj?.penColor ?? { type: 'string', s: 'black' };
|
|
111
|
+
edraw.push(this.styleOp(job));
|
|
112
|
+
edraw.push(xdotPenColor(resolveRenderColor('transparent')));
|
|
113
|
+
edraw.push(xdotFillColor(edgeColor));
|
|
114
|
+
edraw.push(xdotPoints('P', verts));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Emit a split-along-length `;` multicolor edge spline: split each routed
|
|
119
|
+
* bezier along its arc length into one sub-curve per color segment, drawn
|
|
120
|
+
* under that segment's pen. Reuses the same split geometry
|
|
121
|
+
* (splitSplineByColor) as the SVG path. @see lib/common/emit.c:1975 multicolor
|
|
122
|
+
*/
|
|
123
|
+
protected emitSplitSpline(
|
|
124
|
+
bzList: (Bezier | undefined)[],
|
|
125
|
+
colorAttr: string,
|
|
126
|
+
edraw: string[],
|
|
127
|
+
job: RenderJob,
|
|
128
|
+
): { firstColor: string; endColor: string } {
|
|
129
|
+
const segs = parseSegs(colorAttr).segs;
|
|
130
|
+
const firstColor = segs[0]?.color ?? DEFAULT_COLOR;
|
|
131
|
+
let endColor = firstColor;
|
|
132
|
+
for (const bz of bzList) {
|
|
133
|
+
if (bz === undefined || bz.size < 4) continue;
|
|
134
|
+
const split = splitSplineByColor(bz.list.slice(0, bz.size), segs);
|
|
135
|
+
endColor = split.endColor;
|
|
136
|
+
for (const c of split.curves) {
|
|
137
|
+
edraw.push(this.styleOp(job), xdotPenColor(resolveRenderColor(c.color)), xdotPoints('B', c.points));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return { firstColor, endColor };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Emit the parallel-multicolor edge spline: one offset Bézier per color,
|
|
145
|
+
* offset SEP=2.0 perpendicular per pass — reusing the same offset geometry
|
|
146
|
+
* (buildOffsetLists/advanceTmpList) as the SVG parallel-edge path.
|
|
147
|
+
* @see lib/common/emit.c:2443 (parallel multicolor) / svg-parallel-edge.ts
|
|
148
|
+
*/
|
|
149
|
+
protected emitParallelSpline(
|
|
150
|
+
bzList: (Bezier | undefined)[],
|
|
151
|
+
colorAttr: string,
|
|
152
|
+
numc: number,
|
|
153
|
+
edraw: string[],
|
|
154
|
+
job: RenderJob,
|
|
155
|
+
): { headColor: string; tailColor: string } {
|
|
156
|
+
const segData = bzList.map((bz) =>
|
|
157
|
+
bz !== undefined && bz.size >= 4
|
|
158
|
+
? buildOffsetLists(bz.list, (2 + numc) / 2)
|
|
159
|
+
: { offlist: [] as Point[], tmplist: [] as Point[] },
|
|
160
|
+
);
|
|
161
|
+
const colors = parseSegs(colorAttr).segs.map((s) => s.color ?? DEFAULT_COLOR);
|
|
162
|
+
for (const color of colors) {
|
|
163
|
+
const pen = xdotPenColor(resolveRenderColor(color));
|
|
164
|
+
for (const sd of segData) {
|
|
165
|
+
if (sd.offlist.length === 0) continue;
|
|
166
|
+
advanceTmpList(sd.tmplist, sd.offlist);
|
|
167
|
+
edraw.push(this.styleOp(job), pen, xdotPoints('B', sd.tmplist));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// C sets headcolor=tailcolor at cnum==0 and tailcolor again at cnum==1, so
|
|
171
|
+
// the HEAD arrow takes the first colour and the TAIL arrow the second (or
|
|
172
|
+
// the first when only one was given). Note this is the INVERSE of the `;`
|
|
173
|
+
// split branch above (tail=first, head=end). @see lib/common/emit.c:2493-2497
|
|
174
|
+
return {
|
|
175
|
+
headColor: colors[0] ?? DEFAULT_COLOR,
|
|
176
|
+
tailColor: colors[1] ?? colors[0] ?? DEFAULT_COLOR,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Emit one arrow's primitive ops into `buf` (pen/fill from the edge color).
|
|
181
|
+
* arrow_gen sets the default line style ("solid") and the edge penwidth
|
|
182
|
+
* before each primitive, so a non-default penwidth emits `S setlinewidth(N)`
|
|
183
|
+
* once (tracked per HDRAW/TDRAW state). @see arrows.c:arrow_gen */
|
|
184
|
+
protected emitArrows(
|
|
185
|
+
buf: string[], ops: ArrowDrawOp[] | undefined, job: RenderJob, state: EmitState,
|
|
186
|
+
penOverride?: string,
|
|
187
|
+
): void {
|
|
188
|
+
if (ops === undefined) return;
|
|
189
|
+
const pen = penOverride !== undefined
|
|
190
|
+
? resolveRenderColor(penOverride)
|
|
191
|
+
: job.obj?.penColor ?? { type: 'string', s: 'black' };
|
|
192
|
+
const pw = job.obj?.penWidth ?? 1;
|
|
193
|
+
for (const op of ops) {
|
|
194
|
+
if (Math.abs(pw - this.penwidth[state]!) >= 0.0005) {
|
|
195
|
+
this.penwidth[state] = pw;
|
|
196
|
+
buf.push(xdotStrOp('S ', 'setlinewidth(' + trimFixed3(pw) + ')'));
|
|
197
|
+
}
|
|
198
|
+
buf.push(xdotStrOp('S ', 'solid'));
|
|
199
|
+
buf.push(xdotPenColor(pen));
|
|
200
|
+
emitArrowOp(buf, op, pen);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EPL-2.0
|
|
2
|
+
|
|
3
|
+
/** Shared shapes for the dot/xdot renderer and its agwrite serializer. */
|
|
4
|
+
|
|
5
|
+
import type { Graph } from '../../model/graph.js';
|
|
6
|
+
import type { Node } from '../../model/node.js';
|
|
7
|
+
import type { Edge } from '../../model/edge.js';
|
|
8
|
+
|
|
9
|
+
/** Accumulated xdot draw strings for one model object (agset side-table). */
|
|
10
|
+
export interface XdotDraws {
|
|
11
|
+
draw?: string;
|
|
12
|
+
ldraw?: string;
|
|
13
|
+
hdraw?: string;
|
|
14
|
+
tdraw?: string;
|
|
15
|
+
hldraw?: string;
|
|
16
|
+
tldraw?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Mutable state threaded through the recursive agwrite serializer. */
|
|
20
|
+
export interface SerCtx {
|
|
21
|
+
out: string[];
|
|
22
|
+
/** subgraph → preorder number (write.c:subgdfs). */
|
|
23
|
+
preorder: Map<Graph, number>;
|
|
24
|
+
/** node → preorder of the subgraph it was last written in (node_last_written). */
|
|
25
|
+
nodeLW: Map<Node, number>;
|
|
26
|
+
/** edge → preorder of the subgraph it was last written in (edge_last_written). */
|
|
27
|
+
edgeLW: Map<Edge, number>;
|
|
28
|
+
/** objects whose attributes have already been emitted (AGATTRWF/attrs_written). */
|
|
29
|
+
attrsWritten: Set<Node | Edge>;
|
|
30
|
+
/** current indentation depth. */
|
|
31
|
+
level: number;
|
|
32
|
+
}
|