@knowvah/dot-engine 2.7.1 → 2.8.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/dist/api.js.map +2 -2
- package/dist/common/agerr.d.ts +8 -1
- package/dist/common/agerr.d.ts.map +1 -1
- package/dist/common/fma.d.ts.map +1 -1
- package/dist/common/htmltable-emit-fill.d.ts.map +1 -1
- package/dist/common/htmltable-label.d.ts +3 -0
- package/dist/common/htmltable-label.d.ts.map +1 -1
- package/dist/common/htmltable-lex-attvalue.d.ts.map +1 -1
- package/dist/common/htmltable.d.ts +5 -0
- package/dist/common/htmltable.d.ts.map +1 -1
- package/dist/common/layers.d.ts +12 -0
- package/dist/common/layers.d.ts.map +1 -1
- package/dist/common/layout-state.d.ts +8 -0
- package/dist/common/layout-state.d.ts.map +1 -0
- package/dist/common/make-label.d.ts.map +1 -1
- package/dist/common/multicolor.d.ts +6 -3
- package/dist/common/multicolor.d.ts.map +1 -1
- package/dist/common/nodeinit.d.ts.map +1 -1
- package/dist/common/poly-init.d.ts +11 -0
- package/dist/common/poly-init.d.ts.map +1 -1
- package/dist/common/poly-sizing.d.ts +1 -0
- package/dist/common/poly-sizing.d.ts.map +1 -1
- package/dist/common/postproc.d.ts.map +1 -1
- package/dist/common/style-resolve.d.ts +11 -9
- package/dist/common/style-resolve.d.ts.map +1 -1
- package/dist/gvc/device-cluster.d.ts +5 -6
- package/dist/gvc/device-cluster.d.ts.map +1 -1
- package/dist/gvc/device.d.ts.map +1 -1
- package/dist/gvc/first-layer.d.ts +21 -5
- package/dist/gvc/first-layer.d.ts.map +1 -1
- package/dist/gvc/gvrender-set-style.d.ts.map +1 -1
- package/dist/gvc/usershape.d.ts +20 -0
- package/dist/gvc/usershape.d.ts.map +1 -1
- package/dist/index.js +494 -275
- package/dist/index.js.map +4 -4
- package/dist/layout/circo/index.d.ts.map +1 -1
- package/dist/layout/dot/init.d.ts +10 -0
- package/dist/layout/dot/init.d.ts.map +1 -1
- package/dist/layout/dot/splines.d.ts.map +1 -1
- package/dist/layout/fdp/init.d.ts.map +1 -1
- package/dist/layout/neato/conjgrad-mkernel.d.ts.map +1 -1
- package/dist/layout/neato/constrained-majorization.d.ts.map +1 -1
- package/dist/layout/neato/edge-len.d.ts +6 -2
- package/dist/layout/neato/edge-len.d.ts.map +1 -1
- package/dist/layout/neato/init.d.ts.map +1 -1
- package/dist/layout/neato/kk.d.ts.map +1 -1
- package/dist/layout/neato/make-graph-data.d.ts.map +1 -1
- package/dist/layout/neato/matrix-ops.d.ts +0 -6
- package/dist/layout/neato/matrix-ops.d.ts.map +1 -1
- package/dist/layout/neato/smart-ini-x.d.ts.map +1 -1
- package/dist/layout/neato/splines.d.ts.map +1 -1
- package/dist/layout/neato/start.d.ts.map +1 -1
- package/dist/layout/neato/stress-kernel.d.ts +2 -5
- package/dist/layout/neato/stress-kernel.d.ts.map +1 -1
- package/dist/layout/neato/stress.d.ts +6 -0
- package/dist/layout/neato/stress.d.ts.map +1 -1
- package/dist/layout/osage/index.d.ts.map +1 -1
- package/dist/layout/patchwork/index.d.ts.map +1 -1
- package/dist/layout/sfdp/init.d.ts.map +1 -1
- package/dist/layout/twopi/init.d.ts +12 -6
- package/dist/layout/twopi/init.d.ts.map +1 -1
- package/dist/model/cgraph-ops.d.ts +9 -0
- package/dist/model/cgraph-ops.d.ts.map +1 -1
- package/dist/render/color-resolve.d.ts +7 -0
- package/dist/render/color-resolve.d.ts.map +1 -1
- package/dist/render/json.d.ts +17 -2
- package/dist/render/json.d.ts.map +1 -1
- package/dist/render/map.d.ts +3 -5
- package/dist/render/map.d.ts.map +1 -1
- package/dist/render.js +500 -281
- package/dist/render.js.map +4 -4
- package/package.json +1 -1
- package/src/common/agerr.ts +69 -8
- package/src/common/fma.ts +11 -1
- package/src/common/htmltable-emit-fill.ts +11 -7
- package/src/common/htmltable-label.ts +6 -3
- package/src/common/htmltable-lex-attvalue.ts +17 -1
- package/src/common/htmltable.ts +10 -5
- package/src/common/layers.ts +31 -2
- package/src/common/layout-state.ts +35 -0
- package/src/common/make-label.ts +6 -2
- package/src/common/multicolor.ts +12 -6
- package/src/common/nodeinit.ts +2 -1
- package/src/common/poly-init.ts +18 -1
- package/src/common/poly-sizing.ts +4 -4
- package/src/common/postproc.ts +4 -1
- package/src/common/style-resolve.ts +118 -50
- package/src/gvc/device-cluster.ts +31 -39
- package/src/gvc/device.ts +55 -43
- package/src/gvc/first-layer.ts +60 -11
- package/src/gvc/gvrender-set-style.ts +1 -6
- package/src/gvc/usershape.ts +40 -0
- package/src/layout/circo/index.ts +4 -0
- package/src/layout/dot/init.ts +32 -0
- package/src/layout/dot/splines.ts +3 -0
- package/src/layout/fdp/init.ts +17 -0
- package/src/layout/neato/conjgrad-mkernel.ts +2 -3
- package/src/layout/neato/constrained-majorization.ts +2 -1
- package/src/layout/neato/edge-len.ts +39 -7
- package/src/layout/neato/init.ts +8 -3
- package/src/layout/neato/kk.ts +2 -1
- package/src/layout/neato/make-graph-data.ts +1 -2
- package/src/layout/neato/matrix-ops.ts +1 -11
- package/src/layout/neato/smart-ini-x.ts +1 -1
- package/src/layout/neato/splines.ts +3 -1
- package/src/layout/neato/start.ts +3 -2
- package/src/layout/neato/stress-kernel.ts +45 -86
- package/src/layout/neato/stress.ts +19 -0
- package/src/layout/osage/index.ts +8 -1
- package/src/layout/patchwork/index.ts +4 -0
- package/src/layout/sfdp/init.ts +3 -0
- package/src/layout/twopi/init.ts +21 -9
- package/src/layout/twopi/pipeline.ts +3 -3
- package/src/model/cgraph-ops.ts +19 -0
- package/src/render/color-resolve.ts +17 -2
- package/src/render/json.ts +9 -10
- package/src/render/map.ts +5 -6
|
@@ -19,31 +19,24 @@ import type { Node } from '../../model/node.js';
|
|
|
19
19
|
import { drand48 } from '../../common/random.js';
|
|
20
20
|
import { bfs } from './bfs.js';
|
|
21
21
|
import { dijkstra, type VtxData } from './dijkstra.js';
|
|
22
|
+
import {
|
|
23
|
+
conjugateGradientMkernelFused, rightMultWithVectorFFFused, type CgMkernelOptions,
|
|
24
|
+
} from './conjgrad-mkernel.js';
|
|
22
25
|
import {
|
|
23
26
|
copyVectorF,
|
|
24
27
|
invertSqrtVec,
|
|
25
28
|
invertVec,
|
|
26
|
-
maxAbsF,
|
|
27
29
|
orthog1,
|
|
28
|
-
orthog1f,
|
|
29
30
|
rightMultWithVectorFF,
|
|
30
31
|
sqrtVecF,
|
|
31
32
|
squareVec,
|
|
32
33
|
vectorsInnerProductF,
|
|
33
|
-
vectorsMultAdditionF,
|
|
34
|
-
vectorsSubtractionF,
|
|
35
34
|
} from './matrix-ops.js';
|
|
35
|
+
import { fmaf } from './quad-prog-solve.js';
|
|
36
|
+
import { OPT_EXP_FLAG, TOLERANCE_CG } from './stress.js';
|
|
36
37
|
|
|
37
38
|
const fr = Math.fround;
|
|
38
39
|
|
|
39
|
-
/** @see lib/neatogen/stress.h:tolerance_cg */
|
|
40
|
-
const TOLERANCE_CG = 1e-3;
|
|
41
|
-
|
|
42
|
-
/** Stress options bit: weight exponent (1 or 2). @see lib/neatogen/stress.h */
|
|
43
|
-
export const OPT_EXP_FLAG = 2;
|
|
44
|
-
/** Stress options bit: smart initialisation. @see lib/neatogen/stress.h */
|
|
45
|
-
export const OPT_SMART_INIT = 1;
|
|
46
|
-
|
|
47
40
|
/**
|
|
48
41
|
* Packed all-pairs shortest paths over unit weights (BFS hops).
|
|
49
42
|
* @see lib/neatogen/stress.c:compute_apsp_packed
|
|
@@ -115,71 +108,32 @@ export function initLayout(
|
|
|
115
108
|
return pinned;
|
|
116
109
|
}
|
|
117
110
|
|
|
118
|
-
/**
|
|
119
|
-
|
|
120
|
-
*
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
|
|
125
|
-
A: Float32Array;
|
|
126
|
-
x: Float32Array;
|
|
127
|
-
b: Float32Array;
|
|
128
|
-
n: number;
|
|
129
|
-
r: Float32Array;
|
|
130
|
-
p: Float32Array;
|
|
131
|
-
Ap: Float32Array;
|
|
132
|
-
rR: number;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/** One CG step; returns false on a zero-length residual error. */
|
|
136
|
-
function cgStep(st: CgState, last: boolean): boolean {
|
|
137
|
-
const { n } = st;
|
|
138
|
-
orthog1f(n, st.p);
|
|
139
|
-
orthog1f(n, st.x);
|
|
140
|
-
orthog1f(n, st.r);
|
|
141
|
-
rightMultWithVectorFF(st.A, n, st.p, st.Ap);
|
|
142
|
-
orthog1f(n, st.Ap);
|
|
143
|
-
const pAp = vectorsInnerProductF(n, st.p, st.Ap);
|
|
144
|
-
if (pAp === 0) return true; // C: break
|
|
145
|
-
const alpha = st.rR / pAp;
|
|
146
|
-
vectorsMultAdditionF(n, st.x, alpha, st.p);
|
|
147
|
-
if (!last) {
|
|
148
|
-
vectorsMultAdditionF(n, st.r, -alpha, st.Ap);
|
|
149
|
-
const rRNew = vectorsInnerProductF(n, st.r, st.r);
|
|
150
|
-
if (st.rR === 0) return false;
|
|
151
|
-
const betaF = fr(rRNew / st.rR);
|
|
152
|
-
st.rR = rRNew;
|
|
153
|
-
for (let j = 0; j < n; j++) st.p[j] = fr(betaF * st.p[j]!) + st.r[j]!;
|
|
154
|
-
}
|
|
155
|
-
return true;
|
|
111
|
+
/** The right-multiply and conjugate-gradient kernels one caller runs. */
|
|
112
|
+
interface KernelOps {
|
|
113
|
+
/** `acc + tmp*tmp`, the distance accumulation of the Laplacian build. */
|
|
114
|
+
squareAdd(tmp: number, acc: number): number;
|
|
115
|
+
rightMult(m: Float32Array, n: number, vector: Float32Array, result: Float32Array): void;
|
|
116
|
+
/** Returns nonzero on an unexpected zero residual. */
|
|
117
|
+
cg(A: Float32Array, x: Float32Array, b: Float32Array, n: number, o: CgMkernelOptions): number;
|
|
156
118
|
}
|
|
157
119
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
copyVectorF(n, st.r, st.p);
|
|
176
|
-
st.rR = vectorsInnerProductF(n, st.r, st.r);
|
|
177
|
-
|
|
178
|
-
for (let i = 0; i < maxIterations && maxAbsF(n, st.r) > tol; i++) {
|
|
179
|
-
if (!cgStep(st, i >= maxIterations - 1)) return -1;
|
|
180
|
-
}
|
|
181
|
-
return 0;
|
|
182
|
-
}
|
|
120
|
+
/** The oracle's fused kernels, as `stress_majorization_kD_mkernel` runs them. */
|
|
121
|
+
const FUSED_OPS: KernelOps = {
|
|
122
|
+
squareAdd: (tmp, acc) => fmaf(tmp, tmp, acc),
|
|
123
|
+
rightMult: rightMultWithVectorFFFused, cg: conjugateGradientMkernelFused,
|
|
124
|
+
};
|
|
125
|
+
/**
|
|
126
|
+
* The kernels as `stress_majorization_cola` (mode=ipsep) runs them: its squared
|
|
127
|
+
* distances are `square_vec` then `vectors_additionf` (two loops, so the product
|
|
128
|
+
* is rounded before the add), its inlined `right_mult_with_vector_ff` copy is not
|
|
129
|
+
* read from the disassembly (kept unfused), and its conjugate gradient is the same
|
|
130
|
+
* out-of-line `conjugate_gradient_mkernel` the kD kernel calls.
|
|
131
|
+
* @see lib/neatogen/constrained_majorization_ipsep.c:stress_majorization_cola
|
|
132
|
+
*/
|
|
133
|
+
const COLA_OPS: KernelOps = {
|
|
134
|
+
squareAdd: (tmp, acc) => acc + fr(tmp * tmp),
|
|
135
|
+
rightMult: rightMultWithVectorFF, cg: conjugateGradientMkernelFused,
|
|
136
|
+
};
|
|
183
137
|
|
|
184
138
|
/** Laplacian off-diagonal prep + diagonal degrees. @see stress.c (Laplacian computation) */
|
|
185
139
|
function buildLap2(lap2: Float32Array, n: number, exp: number): void {
|
|
@@ -207,9 +161,9 @@ function buildLap2(lap2: Float32Array, n: number, exp: number): void {
|
|
|
207
161
|
/** Per-iteration Laplacian of 1/(d_ij*|p_i-p_j|). @see stress.c (main loop head) */
|
|
208
162
|
function buildLap1(
|
|
209
163
|
lap1: Float32Array, lap2: Float32Array, coords: Float32Array[],
|
|
210
|
-
shape: { n: number; dim: number; exp: number },
|
|
164
|
+
shape: { n: number; dim: number; exp: number; ops: KernelOps },
|
|
211
165
|
): void {
|
|
212
|
-
const { n, dim, exp } = shape;
|
|
166
|
+
const { n, dim, exp, ops } = shape;
|
|
213
167
|
const lapLength = (n * (n + 1)) / 2;
|
|
214
168
|
const degrees = new Float64Array(n);
|
|
215
169
|
const distAccumulator = new Float32Array(n);
|
|
@@ -222,7 +176,7 @@ function buildLap1(
|
|
|
222
176
|
const ck = coords[k]!;
|
|
223
177
|
for (let x = 0; x < len; x++) {
|
|
224
178
|
const tmp = fr(ck[i]! + fr(-1 * ck[i + 1 + x]!));
|
|
225
|
-
distAccumulator[x] = distAccumulator[x]!
|
|
179
|
+
distAccumulator[x] = ops.squareAdd(tmp, distAccumulator[x]!);
|
|
226
180
|
}
|
|
227
181
|
}
|
|
228
182
|
invertSqrtVec(len, distAccumulator);
|
|
@@ -264,32 +218,34 @@ interface KernelState {
|
|
|
264
218
|
b: Float32Array[];
|
|
265
219
|
tmpCoords: Float32Array;
|
|
266
220
|
constantTerm: number;
|
|
221
|
+
ops: KernelOps;
|
|
267
222
|
}
|
|
268
223
|
|
|
269
224
|
/** One stress evaluation. @see stress.c (compute new stress block) */
|
|
270
225
|
function computeStress(st: KernelState): number {
|
|
271
226
|
let stress = 0;
|
|
272
227
|
for (let k = 0; k < st.dim; k++) {
|
|
273
|
-
|
|
228
|
+
st.ops.rightMult(st.lap1, st.n, st.coords[k]!, st.b[k]!);
|
|
274
229
|
stress += vectorsInnerProductF(st.n, st.coords[k]!, st.b[k]!);
|
|
275
230
|
}
|
|
276
231
|
stress *= 2;
|
|
277
232
|
stress += st.constantTerm;
|
|
278
233
|
for (let k = 0; k < st.dim; k++) {
|
|
279
|
-
|
|
234
|
+
st.ops.rightMult(st.lap2, st.n, st.coords[k]!, st.tmpCoords);
|
|
280
235
|
stress -= vectorsInnerProductF(st.n, st.coords[k]!, st.tmpCoords);
|
|
281
236
|
}
|
|
282
237
|
return stress;
|
|
283
238
|
}
|
|
284
239
|
|
|
285
|
-
/** Solve one axis, respecting pinned nodes. @see stress.c (CG block) */
|
|
240
|
+
/** Solve one axis, respecting pinned nodes; nonzero on a CG error. @see stress.c (CG block) */
|
|
286
241
|
function solveAxis(st: KernelState, k: number, havePinned: boolean, nodes: Node[]): number {
|
|
242
|
+
const cg = { tol: TOLERANCE_CG, maxIterations: st.n };
|
|
287
243
|
if (!havePinned) {
|
|
288
|
-
return
|
|
244
|
+
return st.ops.cg(st.lap2, st.coords[k]!, st.b[k]!, st.n, cg);
|
|
289
245
|
}
|
|
290
246
|
copyVectorF(st.n, st.coords[k]!, st.tmpCoords);
|
|
291
|
-
const rc =
|
|
292
|
-
if (rc
|
|
247
|
+
const rc = st.ops.cg(st.lap2, st.tmpCoords, st.b[k]!, st.n, cg);
|
|
248
|
+
if (rc !== 0) return rc;
|
|
293
249
|
for (let i = 0; i < st.n; i++) {
|
|
294
250
|
if (!isFixed(nodes[i]!)) st.coords[k]![i] = st.tmpCoords[i]!;
|
|
295
251
|
}
|
|
@@ -304,6 +260,8 @@ export interface StressOpts {
|
|
|
304
260
|
/** MODEL_* — only the resulting Dij matters here; pass it in. */
|
|
305
261
|
maxi: number;
|
|
306
262
|
epsilon: number;
|
|
263
|
+
/** Run the kernels as `stress_majorization_cola` does (mode=ipsep). */
|
|
264
|
+
cola?: boolean;
|
|
307
265
|
}
|
|
308
266
|
|
|
309
267
|
/**
|
|
@@ -338,20 +296,21 @@ export function stressMajorizationKDMkernel(
|
|
|
338
296
|
b: Array.from({ length: dim }, () => new Float32Array(n)),
|
|
339
297
|
tmpCoords: new Float32Array(n),
|
|
340
298
|
constantTerm,
|
|
299
|
+
ops: o.cola === true ? COLA_OPS : FUSED_OPS,
|
|
341
300
|
};
|
|
342
301
|
|
|
343
302
|
let oldStress = Number.MAX_VALUE;
|
|
344
303
|
let converged = false;
|
|
345
304
|
let iterations = 0;
|
|
346
305
|
for (; iterations < maxi && !converged; iterations++) {
|
|
347
|
-
buildLap1(st.lap1, lap2, coords, { n, dim, exp });
|
|
306
|
+
buildLap1(st.lap1, lap2, coords, { n, dim, exp, ops: st.ops });
|
|
348
307
|
const newStress = computeStress(st);
|
|
349
308
|
if (typeof process !== 'undefined' && process.env['STRESS_DEBUG'] && iterations % 5 === 0) console.error(newStress.toFixed(3));
|
|
350
309
|
const change = Math.abs(oldStress - newStress);
|
|
351
310
|
converged = change / oldStress < o.epsilon || newStress < o.epsilon;
|
|
352
311
|
oldStress = newStress;
|
|
353
312
|
for (let k = 0; k < dim; k++) {
|
|
354
|
-
if (solveAxis(st, k, havePinned, nodes)
|
|
313
|
+
if (solveAxis(st, k, havePinned, nodes) !== 0) return -1;
|
|
355
314
|
}
|
|
356
315
|
}
|
|
357
316
|
|
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
* @see lib/neatogen/stress.h
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
+
import { agwarningf } from '../../common/agerr.js';
|
|
18
|
+
import { lateInt } from '../../common/nodeinit.js';
|
|
19
|
+
import type { Graph } from '../../model/graph.js';
|
|
17
20
|
import type { VtxData } from './dijkstra.js';
|
|
18
21
|
import {
|
|
19
22
|
computeApspPacked,
|
|
@@ -41,6 +44,22 @@ export const OPT_SMART_INIT = 0x4;
|
|
|
41
44
|
/** Exponent flag in opts bitmask (exp=2 when set, exp=1 when clear). */
|
|
42
45
|
export const OPT_EXP_FLAG = 0x3;
|
|
43
46
|
|
|
47
|
+
/** Name of the stress weight-exponent graph attribute. @see lib/neatogen/neatoinit.c:exp_name */
|
|
48
|
+
const EXP_NAME = 'stresswt';
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The `stresswt` exponent, 1 or 2 (default 2).
|
|
52
|
+
* @see lib/neatogen/neatoinit.c:checkExp
|
|
53
|
+
*/
|
|
54
|
+
export function checkExp(g: Graph): number {
|
|
55
|
+
const exp = lateInt(g.attrs.get(EXP_NAME), 2, 0);
|
|
56
|
+
if (exp === 0 || exp > 2) {
|
|
57
|
+
agwarningf(`${EXP_NAME} attribute value must be 1 or 2 - ignoring`);
|
|
58
|
+
return 2;
|
|
59
|
+
}
|
|
60
|
+
return exp;
|
|
61
|
+
}
|
|
62
|
+
|
|
44
63
|
/** Model: MDS (user-supplied edge lengths). */
|
|
45
64
|
export const MODEL_MDS = 1;
|
|
46
65
|
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import type { Graph } from '../../model/graph.js';
|
|
14
|
+
import { gvCleanupEdgeLabels, gvCleanupNodeLabels } from '../dot/init.js';
|
|
14
15
|
import type { Node } from '../../model/node.js';
|
|
15
16
|
import type { Box } from '../../model/geom.js';
|
|
16
17
|
import type { LayoutEngine } from '../../gvc/context.js';
|
|
@@ -507,7 +508,11 @@ export function osageLayout(g: Graph): void {
|
|
|
507
508
|
/** Recursively clear cluster arrays from g and all subclusters. */
|
|
508
509
|
export function cleanupGraphs(g: Graph): void {
|
|
509
510
|
const nClust = g.info.n_cluster ?? 0;
|
|
510
|
-
for (let i = 1; i <= nClust; i++)
|
|
511
|
+
for (let i = 1; i <= nClust; i++) {
|
|
512
|
+
const subg = g.info.clust![i - 1];
|
|
513
|
+
subg.info.label = undefined; // free_label(GD_label(subg))
|
|
514
|
+
cleanupGraphs(subg);
|
|
515
|
+
}
|
|
511
516
|
g.info.clust = undefined;
|
|
512
517
|
g.info.n_cluster = undefined;
|
|
513
518
|
}
|
|
@@ -525,6 +530,8 @@ export function cleanupGraphs(g: Graph): void {
|
|
|
525
530
|
* @see lib/osage/osageinit.c:osage_cleanup
|
|
526
531
|
*/
|
|
527
532
|
export function osageCleanup(g: Graph): void {
|
|
533
|
+
for (const e of g.edges) gvCleanupEdgeLabels(e); // gv_cleanup_edge: per-node agfstout, all edges of root g
|
|
534
|
+
for (const n of g.nodes.values()) gvCleanupNodeLabels(n); // gv_cleanup_node
|
|
528
535
|
for (const n of g.nodes.values()) {
|
|
529
536
|
if (n.info.alg?.kind === 'osage') n.info.alg = undefined;
|
|
530
537
|
}
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import type { Graph } from '../../model/graph.js';
|
|
18
|
+
import { gvCleanupEdgeLabels, gvCleanupNodeLabels } from '../dot/init.js';
|
|
18
19
|
import type { Node } from '../../model/node.js';
|
|
19
20
|
import type { LayoutEngine } from '../../gvc/context.js';
|
|
20
21
|
import { ps2inch } from '../pack/index.js';
|
|
@@ -425,6 +426,9 @@ export function patchworkEngineLayout(g: Graph): void {
|
|
|
425
426
|
|
|
426
427
|
/** @see lib/patchwork/patchworkinit.c:patchwork_cleanup */
|
|
427
428
|
export function patchworkEngineCleanup(g: Graph): void {
|
|
429
|
+
if (g.nodes.size === 0) return; // C: if (!n) return
|
|
430
|
+
for (const e of g.edges) gvCleanupEdgeLabels(e); // gv_cleanup_edge: per-node agfstout, all edges of root g
|
|
431
|
+
for (const n of g.nodes.values()) gvCleanupNodeLabels(n); // gv_cleanup_node
|
|
428
432
|
g.info.clust = undefined;
|
|
429
433
|
g.info.n_cluster = undefined;
|
|
430
434
|
for (const n of g.nodes.values()) { n.info.alg = undefined; }
|
package/src/layout/sfdp/init.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import { RenderError } from '../../errors.js';
|
|
14
14
|
import type { Graph } from '../../model/graph.js';
|
|
15
|
+
import { gvCleanupEdgeLabels, gvCleanupNodeLabels } from '../dot/init.js';
|
|
15
16
|
import { setEdgeTypeFromAttr } from '../dot/index.js';
|
|
16
17
|
import { EDGETYPE_LINE } from '../neato/splines.js';
|
|
17
18
|
import { neatoInitNode } from '../neato/init.js';
|
|
@@ -264,6 +265,8 @@ export function getPos(g: Graph, dim: number): number[] {
|
|
|
264
265
|
* @see lib/sfdpgen/sfdpinit.c:sfdp_cleanup
|
|
265
266
|
*/
|
|
266
267
|
export function sfdpCleanup(g: Graph): void {
|
|
268
|
+
for (const e of g.edges) gvCleanupEdgeLabels(e); // gv_cleanup_edge: per-node agfstout, all edges of root g
|
|
269
|
+
for (const n of g.nodes.values()) gvCleanupNodeLabels(n); // gv_cleanup_node
|
|
267
270
|
for (const n of g.nodes.values()) {
|
|
268
271
|
n.info.pos = undefined;
|
|
269
272
|
n.info.id = undefined;
|
package/src/layout/twopi/init.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import type { ShapeDesc } from '../../common/types.js';
|
|
12
12
|
import type { Graph } from '../../model/graph.js';
|
|
13
|
+
import { gvCleanupEdgeLabels, gvCleanupNodeLabels } from '../dot/init.js';
|
|
13
14
|
import type { Node } from '../../model/node.js';
|
|
14
15
|
import type { Edge } from '../../model/edge.js';
|
|
15
16
|
import type { TwopiAlgData } from '../../model/nodeInfo.js';
|
|
@@ -157,23 +158,34 @@ export function adjustNodes(g: Graph): void {
|
|
|
157
158
|
}
|
|
158
159
|
|
|
159
160
|
/**
|
|
160
|
-
*
|
|
161
|
+
* Free ND_alg of the first node, as twopi_layout does before spline routing.
|
|
161
162
|
*
|
|
162
|
-
* Mirrors the C twopi_layout
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
163
|
+
* Mirrors the C twopi_layout: free(ND_alg(n)) / ND_alg(n) = NULL on the FIRST
|
|
164
|
+
* node only (the contiguous block was allocated as a single unit). Other
|
|
165
|
+
* nodes' ND_alg pointers were interior offsets into that block so they do
|
|
166
|
+
* not need to be freed separately.
|
|
166
167
|
*
|
|
167
168
|
* ORDERING: must be called BEFORE splineEdges because edge routing may
|
|
168
169
|
* store its own data in n.info.alg.
|
|
169
170
|
*
|
|
170
|
-
* @see lib/twopigen/twopiinit.c:twopi_layout (
|
|
171
|
-
* @see lib/twopigen/twopiinit.c:twopi_cleanup
|
|
171
|
+
* @see lib/twopigen/twopiinit.c:twopi_layout (lines 113-115)
|
|
172
172
|
*/
|
|
173
|
-
export function
|
|
173
|
+
export function twopiFreeAlg(g: Graph): void {
|
|
174
174
|
const first = g.nodes.values().next().value as Node | undefined;
|
|
175
175
|
if (first !== undefined && first.info.alg?.kind === 'twopi') {
|
|
176
176
|
first.info.alg = undefined;
|
|
177
177
|
}
|
|
178
|
-
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Release twopi layout state after rendering (the engine's `cleanup`).
|
|
182
|
+
* ND_alg is freed earlier, in twopi_layout (see twopiFreeAlg).
|
|
183
|
+
*
|
|
184
|
+
* @see lib/twopigen/twopiinit.c:twopi_cleanup
|
|
185
|
+
*/
|
|
186
|
+
export function twopiCleanup(g: Graph): void {
|
|
187
|
+
if (g.nodes.size === 0) return; // C: empty graph
|
|
188
|
+
for (const e of g.edges) gvCleanupEdgeLabels(e); // gv_cleanup_edge
|
|
189
|
+
for (const n of g.nodes.values()) gvCleanupNodeLabels(n); // gv_cleanup_node
|
|
190
|
+
g.info.neato_nlist = undefined; // twopi_cleanup_graph
|
|
179
191
|
}
|
|
@@ -11,7 +11,7 @@ import type { Graph } from '../../model/graph.js';
|
|
|
11
11
|
import type { Node } from '../../model/node.js';
|
|
12
12
|
import type { PackInfo } from '../pack/index.js';
|
|
13
13
|
import {
|
|
14
|
-
|
|
14
|
+
twopiFreeAlg,
|
|
15
15
|
findRootNode,
|
|
16
16
|
adjustNodes,
|
|
17
17
|
finaliseCoords,
|
|
@@ -71,7 +71,7 @@ export function layoutSingle(g: Graph, comps: Graph[], globalRoot: Center, setRo
|
|
|
71
71
|
if (setRoot && globalRoot === null && center !== null) {
|
|
72
72
|
g.attrs.set('root', center.name);
|
|
73
73
|
}
|
|
74
|
-
|
|
74
|
+
twopiFreeAlg(g); // ORDERING: before spline routing
|
|
75
75
|
// C: spline_edges(g) — shifts pos to the origin, syncs coord, routes.
|
|
76
76
|
splineEdgesShifted(g);
|
|
77
77
|
finaliseCoords(g);
|
|
@@ -90,7 +90,7 @@ export function layoutMulti(g: Graph, comps: Graph[], globalRoot: Center, pinfo:
|
|
|
90
90
|
layoutComponent(g, sg, globalRoot);
|
|
91
91
|
finaliseCoords(sg);
|
|
92
92
|
}
|
|
93
|
-
|
|
93
|
+
twopiFreeAlg(g); // ORDERING: before spline routing
|
|
94
94
|
packSubgraphs(comps.length, comps, g, pinfo);
|
|
95
95
|
// C: spline_edges(g) — shifts pos to the origin, syncs coord, routes.
|
|
96
96
|
splineEdgesShifted(g);
|
package/src/model/cgraph-ops.ts
CHANGED
|
@@ -26,6 +26,25 @@ import { Node } from './node.js';
|
|
|
26
26
|
*/
|
|
27
27
|
const LOCALNAMEPREFIX = '%';
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* cgraph's id for the first anonymous object, which an anonymous root graph is
|
|
31
|
+
* (agopen maps its NULL name first; idmap hands out odd ids from 1).
|
|
32
|
+
* @see lib/cgraph/id.c:idmap
|
|
33
|
+
*/
|
|
34
|
+
const ANONYMOUS_ROOT_ID = 1;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Graph name as `agnameof` returns it: the DOT name, or `%<id>` for an
|
|
38
|
+
* anonymous graph. The port stores `''` for an anonymous root (dot/xdot
|
|
39
|
+
* re-serialization must not print the `%` name), so it is restored here.
|
|
40
|
+
* Anonymous subgraph ids are not tracked; callers pass the root, as C's
|
|
41
|
+
* neato message sites do.
|
|
42
|
+
* @see lib/cgraph/id.c:agnameof
|
|
43
|
+
*/
|
|
44
|
+
export function agNameOf(g: Graph): string {
|
|
45
|
+
return g.anonymous ? `${LOCALNAMEPREFIX}${ANONYMOUS_ROOT_ID}` : g.name;
|
|
46
|
+
}
|
|
47
|
+
|
|
29
48
|
/**
|
|
30
49
|
* Allocate a fresh node id unique within `root.nodes`. Mirrors C's
|
|
31
50
|
* agnextseq/agmapnametoid id reservation: at layout time the per-parse
|
|
@@ -27,7 +27,7 @@ import type { Edge } from '../model/edge.js';
|
|
|
27
27
|
import { registerRenderState } from '../common/render-state.js';
|
|
28
28
|
import { agerrorf, agwarningf } from '../common/agerr.js';
|
|
29
29
|
import { parseSegsSilent } from '../common/multicolor.js';
|
|
30
|
-
import { findStopColor,
|
|
30
|
+
import { findStopColor, styleTokensQuiet } from '../common/style-resolve.js';
|
|
31
31
|
import { emitOnce } from '../common/emit-once.js';
|
|
32
32
|
|
|
33
33
|
/**
|
|
@@ -122,6 +122,21 @@ export function firstColor(name: string): string {
|
|
|
122
122
|
*/
|
|
123
123
|
export function resolveRenderColor(raw: string, report = true): GVColor {
|
|
124
124
|
if (raw === '' || raw === 'none') return { type: 'none' };
|
|
125
|
+
return resolveColorName(raw, report);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* {@link resolveRenderColor} without the empty-name shortcut: C hands an empty
|
|
130
|
+
* name (a leading ':' cut at the first colon) to colorxlate, which rejects it
|
|
131
|
+
* (`Warning: is not a known color.`) and leaves opaque black.
|
|
132
|
+
* @see lib/gvc/gvrender.c:456 gvrender_set_fillcolor
|
|
133
|
+
*/
|
|
134
|
+
export function resolveFillColorName(raw: string, report = true): GVColor {
|
|
135
|
+
if (raw === 'none') return { type: 'none' };
|
|
136
|
+
return resolveColorName(raw, report);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function resolveColorName(raw: string, report: boolean): GVColor {
|
|
125
140
|
// C keeps a known name verbatim (case preserved); only the compare is
|
|
126
141
|
// case-insensitive (bsearch over a lowercase table).
|
|
127
142
|
const known = KNOWN_COLORS.has(raw.toLowerCase());
|
|
@@ -338,7 +353,7 @@ export function reportEdgeColors(e: Edge): void {
|
|
|
338
353
|
reportColorNames(edgeGraphicsColors({
|
|
339
354
|
color,
|
|
340
355
|
fillcolor: e.attrs.get('fillcolor') ?? '',
|
|
341
|
-
tapered:
|
|
356
|
+
tapered: styleTokensQuiet(e.attrs.get('style')).includes('tapered'),
|
|
342
357
|
arrows: spl.list.map((bz) => ({ sflag: bz.sflag !== 0, eflag: bz.eflag !== 0 })),
|
|
343
358
|
}));
|
|
344
359
|
}
|
package/src/render/json.ts
CHANGED
|
@@ -239,12 +239,8 @@ function applyDraws(attrs: Map<string, string> | undefined, d: XdotDraws | undef
|
|
|
239
239
|
* hand-built graph with no real layout state) so callers fall back cleanly. */
|
|
240
240
|
function collectDrawLookup(g: Graph): DrawLookup | undefined {
|
|
241
241
|
let xg: Graph;
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
// below is faithful for coordinates but LOSSY for draw text containing `"`
|
|
245
|
-
// (a DOT quoted string cannot hold `\` before `"`; a cluster label's `\"`
|
|
246
|
-
// round-trips to `"`, id 2239). The draws map holds the pre-serialization
|
|
247
|
-
// strings C's -Tjson reads directly, so we overlay them onto the re-parse.
|
|
242
|
+
// Our own XdotRenderer (last-registered wins) exposes raw draw strings: the
|
|
243
|
+
// DOT re-parse is LOSSY for draw text containing `"` (id 2239); overlay them.
|
|
248
244
|
const xr = new XdotRenderer();
|
|
249
245
|
try {
|
|
250
246
|
const ctx = createDefaultContext();
|
|
@@ -650,8 +646,8 @@ function latin1Reencode(s: string): string {
|
|
|
650
646
|
return out;
|
|
651
647
|
}
|
|
652
648
|
|
|
653
|
-
export function buildJson(g: Graph, doXDot: boolean): string {
|
|
654
|
-
const draws = doXDot ? collectDrawLookup(g) : undefined;
|
|
649
|
+
export function buildJson(g: Graph, doXDot: boolean, preDraws?: DrawLookup): string {
|
|
650
|
+
const draws = doXDot ? (preDraws ?? collectDrawLookup(g)) : undefined;
|
|
655
651
|
const ctx = assignIds(g, doXDot, draws);
|
|
656
652
|
const root = buildGraphObject(g, true, ctx);
|
|
657
653
|
return JSON.stringify(root, null, 2) + '\n';
|
|
@@ -702,10 +698,13 @@ export class JsonRenderer implements RendererPlugin {
|
|
|
702
698
|
/** @see plugin/core/gvplugin_core.c registration table */
|
|
703
699
|
readonly quality = 0;
|
|
704
700
|
|
|
705
|
-
|
|
701
|
+
private draws: DrawLookup | undefined; // set by beginGraph, cleared by endGraph
|
|
702
|
+
/** @see plugin/core/gvrender_core_json.c:json_begin_graph */
|
|
703
|
+
beginGraph(g: Graph, _job: RenderJob): void { this.draws = collectDrawLookup(g); }
|
|
706
704
|
|
|
707
705
|
endGraph(g: Graph, job: RenderJob): void {
|
|
708
|
-
job.write(buildJson(g, true));
|
|
706
|
+
job.write(buildJson(g, true, this.draws));
|
|
707
|
+
this.draws = undefined;
|
|
709
708
|
}
|
|
710
709
|
|
|
711
710
|
beginNode(_n: Node, _job: RenderJob): void { /* no-op */ }
|
package/src/render/map.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import type { Graph } from '../model/graph.js';
|
|
14
|
+
import { agNameOf } from '../model/cgraph-ops.js';
|
|
14
15
|
import type { Node } from '../model/node.js';
|
|
15
16
|
import type { Edge } from '../model/edge.js';
|
|
16
17
|
import type { Point, Box } from '../model/geom.js';
|
|
@@ -583,13 +584,11 @@ export class PlainExtRenderer implements RendererPlugin {
|
|
|
583
584
|
// Imagemap renderers (cmapx / imap and their no-polygon `_np` variants)
|
|
584
585
|
// ---------------------------------------------------------------------------
|
|
585
586
|
|
|
586
|
-
/** Graph name as `agnameof` returns it
|
|
587
|
-
*
|
|
588
|
-
*
|
|
589
|
-
* the imagemap layer restores the internal name only here.
|
|
590
|
-
* @see lib/cgraph/id.c:agnameof ; lib/cgraph/id.c:idmap (anon → `%1`) */
|
|
587
|
+
/** Graph name as `agnameof` returns it (`%1` for an anonymous root; the port
|
|
588
|
+
* stores `''` there so dot/xdot re-serialization stays unchanged).
|
|
589
|
+
* @see lib/cgraph/id.c:agnameof */
|
|
591
590
|
export function mapGraphName(g: Graph): string {
|
|
592
|
-
return g
|
|
591
|
+
return agNameOf(g);
|
|
593
592
|
}
|
|
594
593
|
|
|
595
594
|
/** First non-empty attr walking parent scopes (agget inheritance). */
|