@knowvah/dot-engine 2.6.12 → 2.7.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 +1 -1
- package/dist/api.js +18 -4
- package/dist/api.js.map +3 -3
- package/dist/common/htmltable-emit-fill.d.ts +20 -2
- package/dist/common/htmltable-emit-fill.d.ts.map +1 -1
- package/dist/common/htmltable-emit.d.ts.map +1 -1
- package/dist/common/htmltable-lex-expat.d.ts.map +1 -1
- package/dist/common/htmltable-pos-runs.d.ts +7 -4
- package/dist/common/htmltable-pos-runs.d.ts.map +1 -1
- package/dist/common/htmltable.d.ts.map +1 -1
- package/dist/common/libm-trig.d.ts +5 -0
- package/dist/common/libm-trig.d.ts.map +1 -0
- package/dist/common/multicolor.d.ts +21 -13
- package/dist/common/multicolor.d.ts.map +1 -1
- package/dist/common/poly-gencode.d.ts +1 -2
- package/dist/common/poly-gencode.d.ts.map +1 -1
- package/dist/common/poly-init.d.ts.map +1 -1
- package/dist/common/splines-clip.d.ts +7 -0
- package/dist/common/splines-clip.d.ts.map +1 -1
- package/dist/gvc/context.d.ts +6 -0
- package/dist/gvc/context.d.ts.map +1 -1
- package/dist/gvc/device-cluster.d.ts +15 -0
- 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 +9 -0
- package/dist/gvc/first-layer.d.ts.map +1 -0
- package/dist/gvc/gvrender-set-style.d.ts +28 -0
- package/dist/gvc/gvrender-set-style.d.ts.map +1 -0
- package/dist/gvc/usershape.d.ts +18 -0
- package/dist/gvc/usershape.d.ts.map +1 -1
- package/dist/index.js +2460 -901
- package/dist/index.js.map +4 -4
- package/dist/layout/dot/compound.d.ts.map +1 -1
- package/dist/layout/dot/edge-route-clip.d.ts +14 -4
- package/dist/layout/dot/edge-route-clip.d.ts.map +1 -1
- package/dist/layout/dot/edge-route-routing.d.ts.map +1 -1
- package/dist/layout/neato/__fixtures__/dijkstra-f.d.ts +11 -0
- package/dist/layout/neato/__fixtures__/dijkstra-f.d.ts.map +1 -0
- package/dist/layout/neato/__fixtures__/hier-imds.d.ts +36 -0
- package/dist/layout/neato/__fixtures__/hier-imds.d.ts.map +1 -0
- package/dist/layout/neato/__fixtures__/hier-majorization.d.ts +32 -0
- package/dist/layout/neato/__fixtures__/hier-majorization.d.ts.map +1 -0
- package/dist/layout/neato/cdt-surface.d.ts.map +1 -1
- package/dist/layout/neato/compute-hierarchy.d.ts +24 -0
- package/dist/layout/neato/compute-hierarchy.d.ts.map +1 -0
- package/dist/layout/neato/conjgrad-mkernel.d.ts +20 -0
- package/dist/layout/neato/conjgrad-mkernel.d.ts.map +1 -0
- package/dist/layout/neato/conjgrad.d.ts +19 -0
- package/dist/layout/neato/conjgrad.d.ts.map +1 -1
- package/dist/layout/neato/constrained-majorization.d.ts +26 -0
- package/dist/layout/neato/constrained-majorization.d.ts.map +1 -0
- package/dist/layout/neato/dijkstra.d.ts +33 -16
- package/dist/layout/neato/dijkstra.d.ts.map +1 -1
- package/dist/layout/neato/gts-triangle.d.ts +14 -0
- package/dist/layout/neato/gts-triangle.d.ts.map +1 -0
- package/dist/layout/neato/init.d.ts +2 -12
- package/dist/layout/neato/init.d.ts.map +1 -1
- package/dist/layout/neato/kkutils.d.ts +20 -0
- package/dist/layout/neato/kkutils.d.ts.map +1 -0
- package/dist/layout/neato/make-graph-data.d.ts +43 -0
- package/dist/layout/neato/make-graph-data.d.ts.map +1 -0
- package/dist/layout/neato/matrix-ops.d.ts +46 -0
- package/dist/layout/neato/matrix-ops.d.ts.map +1 -1
- package/dist/layout/neato/opt-arrangement.d.ts +20 -0
- package/dist/layout/neato/opt-arrangement.d.ts.map +1 -0
- package/dist/layout/neato/quad-prog-solve.d.ts +44 -0
- package/dist/layout/neato/quad-prog-solve.d.ts.map +1 -0
- package/dist/layout/neato/smart-ini-x.d.ts +9 -0
- package/dist/layout/neato/smart-ini-x.d.ts.map +1 -0
- package/dist/layout/neato/splines.d.ts +1 -1
- package/dist/layout/neato/splines.d.ts.map +1 -1
- package/dist/layout/neato/start.d.ts +4 -1
- package/dist/layout/neato/start.d.ts.map +1 -1
- package/dist/layout/neato/stress.d.ts +8 -0
- package/dist/layout/neato/stress.d.ts.map +1 -1
- package/dist/model/edge.d.ts +1 -1
- package/dist/model/edge.d.ts.map +1 -1
- package/dist/model/lazy-attrs.d.ts +10 -0
- package/dist/model/lazy-attrs.d.ts.map +1 -0
- package/dist/model/node.d.ts +1 -1
- package/dist/model/node.d.ts.map +1 -1
- package/dist/ortho/index.d.ts.map +1 -1
- package/dist/render/dot/edge-draw.d.ts +1 -1
- package/dist/render/dot/edge-draw.d.ts.map +1 -1
- package/dist/render/svg-multicolor.d.ts +2 -2
- package/dist/render/svg-multicolor.d.ts.map +1 -1
- package/dist/render.js +2340 -781
- package/dist/render.js.map +4 -4
- package/package.json +1 -1
- package/src/common/graph-init.ts +1 -1
- package/src/common/htmltable-emit-fill.ts +56 -11
- package/src/common/htmltable-emit.ts +4 -3
- package/src/common/htmltable-lex-expat.ts +21 -3
- package/src/common/htmltable-pos-runs.ts +11 -6
- package/src/common/htmltable.ts +28 -116
- package/src/common/libm-trig.ts +43 -0
- package/src/common/multicolor.ts +88 -24
- package/src/common/poly-gencode.ts +16 -8
- package/src/common/poly-init.ts +19 -1
- package/src/common/postproc.ts +2 -2
- package/src/common/splines-clip.ts +15 -12
- package/src/common/splines-routespl.ts +2 -2
- package/src/common/textmeasure-lookup.ts +1 -1
- package/src/gvc/context.ts +8 -0
- package/src/gvc/device-cluster.ts +63 -8
- package/src/gvc/device.ts +16 -2
- package/src/gvc/first-layer.ts +26 -0
- package/src/gvc/gvrender-set-style.ts +114 -0
- package/src/gvc/usershape.ts +26 -0
- package/src/layout/dot/cluster.ts +1 -1
- package/src/layout/dot/compound.ts +3 -4
- package/src/layout/dot/conc.ts +1 -1
- package/src/layout/dot/edge-route-clip.ts +19 -7
- package/src/layout/dot/edge-route-routing.ts +11 -3
- package/src/layout/dot/fastgr.ts +2 -2
- package/src/layout/neato/__fixtures__/dijkstra-f.ts +17 -0
- package/src/layout/neato/__fixtures__/hier/c1554.gv +33 -0
- package/src/layout/neato/__fixtures__/hier/c1554.plain +26 -0
- package/src/layout/neato/__fixtures__/hier/c1554.xdot +161 -0
- package/src/layout/neato/__fixtures__/hier/cases/cycle.gv +1 -0
- package/src/layout/neato/__fixtures__/hier/cases/cycle.plain +10 -0
- package/src/layout/neato/__fixtures__/hier/cases/cycle.stderr +0 -0
- package/src/layout/neato/__fixtures__/hier/cases/dirnone.gv +1 -0
- package/src/layout/neato/__fixtures__/hier/cases/dirnone.plain +11 -0
- package/src/layout/neato/__fixtures__/hier/cases/dirnone.stderr +0 -0
- package/src/layout/neato/__fixtures__/hier/cases/epsilon.gv +1 -0
- package/src/layout/neato/__fixtures__/hier/cases/epsilon.plain +13 -0
- package/src/layout/neato/__fixtures__/hier/cases/epsilon.stderr +0 -0
- package/src/layout/neato/__fixtures__/hier/cases/levelsgap.gv +1 -0
- package/src/layout/neato/__fixtures__/hier/cases/levelsgap.plain +8 -0
- package/src/layout/neato/__fixtures__/hier/cases/levelsgap.stderr +0 -0
- package/src/layout/neato/__fixtures__/hier/cases/mds.gv +1 -0
- package/src/layout/neato/__fixtures__/hier/cases/mds.plain +10 -0
- package/src/layout/neato/__fixtures__/hier/cases/mds.stderr +0 -0
- package/src/layout/neato/__fixtures__/hier/cases/multiedge.gv +1 -0
- package/src/layout/neato/__fixtures__/hier/cases/multiedge.plain +10 -0
- package/src/layout/neato/__fixtures__/hier/cases/multiedge.stderr +0 -0
- package/src/layout/neato/__fixtures__/hier/cases/neglevelsgap.gv +1 -0
- package/src/layout/neato/__fixtures__/hier/cases/neglevelsgap.plain +8 -0
- package/src/layout/neato/__fixtures__/hier/cases/neglevelsgap.stderr +0 -0
- package/src/layout/neato/__fixtures__/hier/cases/startrandom.gv +1 -0
- package/src/layout/neato/__fixtures__/hier/cases/startrandom.plain +10 -0
- package/src/layout/neato/__fixtures__/hier/cases/startrandom.stderr +0 -0
- package/src/layout/neato/__fixtures__/hier/cases/startregular.gv +1 -0
- package/src/layout/neato/__fixtures__/hier/cases/startregular.plain +10 -0
- package/src/layout/neato/__fixtures__/hier/cases/startregular.stderr +0 -0
- package/src/layout/neato/__fixtures__/hier/cases/tdist.gv +1 -0
- package/src/layout/neato/__fixtures__/hier/cases/tdist.plain +13 -0
- package/src/layout/neato/__fixtures__/hier/cases/tdist.stderr +0 -0
- package/src/layout/neato/__fixtures__/hier/cases/undirected.gv +1 -0
- package/src/layout/neato/__fixtures__/hier/cases/undirected.plain +10 -0
- package/src/layout/neato/__fixtures__/hier/cases/undirected.stderr +0 -0
- package/src/layout/neato/__fixtures__/hier/hier-probe.gv +1 -0
- package/src/layout/neato/__fixtures__/hier/hier-probe.plain +16 -0
- package/src/layout/neato/__fixtures__/hier/hier-probe.xdot +62 -0
- package/src/layout/neato/__fixtures__/hier-imds.ts +151 -0
- package/src/layout/neato/__fixtures__/hier-majorization.ts +145 -0
- package/src/layout/neato/cdt-surface.ts +6 -16
- package/src/layout/neato/compute-hierarchy.ts +90 -0
- package/src/layout/neato/conjgrad-mkernel.ts +141 -0
- package/src/layout/neato/conjgrad.ts +208 -0
- package/src/layout/neato/constrained-majorization.ts +479 -0
- package/src/layout/neato/dijkstra.ts +218 -92
- package/src/layout/neato/edge-len.ts +2 -2
- package/src/layout/neato/fdp-adjust.ts +3 -3
- package/src/layout/neato/gts-triangle.ts +42 -0
- package/src/layout/neato/init.ts +57 -57
- package/src/layout/neato/kk.ts +2 -2
- package/src/layout/neato/kkutils.ts +96 -0
- package/src/layout/neato/make-graph-data.ts +166 -0
- package/src/layout/neato/matrix-ops.ts +138 -0
- package/src/layout/neato/opt-arrangement.ts +86 -0
- package/src/layout/neato/quad-prog-solve.ts +489 -0
- package/src/layout/neato/smart-ini-x.ts +503 -0
- package/src/layout/neato/splines.ts +4 -4
- package/src/layout/neato/start.ts +7 -6
- package/src/layout/neato/stress.ts +28 -0
- package/src/model/edge.ts +3 -2
- package/src/model/lazy-attrs.ts +36 -0
- package/src/model/node.ts +3 -2
- package/src/ortho/index.ts +2 -3
- package/src/parser/dot.js +1 -1
- package/src/parser/dot.pegjs +1 -1
- package/src/parser/index.ts +1 -1
- package/src/render/color-resolve.ts +2 -2
- package/src/render/dot/edge-draw.ts +6 -3
- package/src/render/dot.ts +1 -1
- package/src/render/svg-edge-split.ts +2 -2
- package/src/render/svg-multicolor.ts +4 -3
- package/src/render/svg-parallel-edge.ts +2 -2
package/src/common/multicolor.ts
CHANGED
|
@@ -13,6 +13,10 @@
|
|
|
13
13
|
* @see lib/common/emit.c:470 parseSegs
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
+
import { agerr, agerrorf, agwarningf } from './agerr.js';
|
|
17
|
+
import type { Edge } from '../model/edge.js';
|
|
18
|
+
import { registerRenderState } from './render-state.js';
|
|
19
|
+
|
|
16
20
|
// ---------------------------------------------------------------------------
|
|
17
21
|
// Types
|
|
18
22
|
// ---------------------------------------------------------------------------
|
|
@@ -38,6 +42,28 @@ export interface ColorSeg {
|
|
|
38
42
|
// Constants
|
|
39
43
|
// ---------------------------------------------------------------------------
|
|
40
44
|
|
|
45
|
+
/**
|
|
46
|
+
* C's `static atomic_flag warned` in parseSegs: set by the first Total-size
|
|
47
|
+
* warning or Illegal-value error in the process, silencing both afterwards.
|
|
48
|
+
* Reset per render (AD-4).
|
|
49
|
+
* @see lib/common/emit.c:470 parseSegs
|
|
50
|
+
*/
|
|
51
|
+
const parseSegsState = { warned: false };
|
|
52
|
+
|
|
53
|
+
registerRenderState(() => {
|
|
54
|
+
parseSegsState.warned = false;
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
/** `atomic_flag_test_and_set(&warned)` negated: true when this call claimed it. */
|
|
58
|
+
function claimWarned(): boolean {
|
|
59
|
+
if (parseSegsState.warned) return false;
|
|
60
|
+
parseSegsState.warned = true;
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** A claim that never fires: parses without touching or consuming the flag. */
|
|
65
|
+
const claimNever = (): boolean => false;
|
|
66
|
+
|
|
41
67
|
/** @see lib/common/emit.c:451 EPS */
|
|
42
68
|
const EPS = 1e-5;
|
|
43
69
|
|
|
@@ -76,7 +102,7 @@ function appendSeg(
|
|
|
76
102
|
token: string,
|
|
77
103
|
left: number,
|
|
78
104
|
segs: ColorSeg[],
|
|
79
|
-
|
|
105
|
+
claim: () => boolean,
|
|
80
106
|
): { left: number; rval: 0 | 3 } | null {
|
|
81
107
|
const { color, v } = getSegLen(token);
|
|
82
108
|
if (v < 0) return null;
|
|
@@ -85,10 +111,7 @@ function appendSeg(
|
|
|
85
111
|
let rval: 0 | 3 = 0;
|
|
86
112
|
const del = t - left;
|
|
87
113
|
if (del > 0) {
|
|
88
|
-
if (!aeq0(del) &&
|
|
89
|
-
warnedExceeded.v = true;
|
|
90
|
-
rval = 3;
|
|
91
|
-
}
|
|
114
|
+
if (!aeq0(del) && claim()) rval = 3;
|
|
92
115
|
t = left;
|
|
93
116
|
}
|
|
94
117
|
left -= t;
|
|
@@ -128,34 +151,20 @@ function trimTrailing(segs: ColorSeg[]): void {
|
|
|
128
151
|
// ---------------------------------------------------------------------------
|
|
129
152
|
|
|
130
153
|
/**
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
* Fractions are optional and non-negative; their sum must not exceed 1.
|
|
134
|
-
* Segments without an explicit fraction share the remaining weight equally;
|
|
135
|
-
* if none exist the remainder is added to the last segment.
|
|
136
|
-
*
|
|
137
|
-
* Return codes (mirror C):
|
|
138
|
-
* 0 → ok; 1 → silent error; 2 → error with message; 3 → warning (sum > 1).
|
|
139
|
-
* On rv 1 or 2 the returned segs array is empty.
|
|
140
|
-
*
|
|
154
|
+
* The parse of `parseSegs` with the once-per-process flag abstracted as `claim`.
|
|
141
155
|
* @see lib/common/emit.c:470 parseSegs
|
|
142
156
|
*/
|
|
143
|
-
|
|
157
|
+
function scanSegs(
|
|
144
158
|
colorlist: string,
|
|
159
|
+
claim: () => boolean,
|
|
145
160
|
): { segs: ColorSeg[]; error: 0 | 1 | 2 | 3 } {
|
|
146
161
|
const segs: ColorSeg[] = [];
|
|
147
162
|
let left = 1;
|
|
148
163
|
let rval: 0 | 3 = 0;
|
|
149
|
-
let badFloatCount = 0;
|
|
150
|
-
const warnedExceeded = { v: false };
|
|
151
164
|
|
|
152
165
|
for (const token of colorlist.split(':')) {
|
|
153
|
-
const result = appendSeg(token, left, segs,
|
|
154
|
-
if (result === null) {
|
|
155
|
-
const errCode: 1 | 2 = badFloatCount === 0 ? 2 : 1;
|
|
156
|
-
badFloatCount++;
|
|
157
|
-
return { segs: [], error: errCode };
|
|
158
|
-
}
|
|
166
|
+
const result = appendSeg(token, left, segs, claim);
|
|
167
|
+
if (result === null) return { segs: [], error: claim() ? 2 : 1 };
|
|
159
168
|
if (result.rval === 3) rval = 3;
|
|
160
169
|
left = result.left;
|
|
161
170
|
if (aeq0(left)) { left = 0; break; }
|
|
@@ -165,3 +174,58 @@ export function parseSegs(
|
|
|
165
174
|
trimTrailing(segs);
|
|
166
175
|
return { segs, error: rval };
|
|
167
176
|
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Parse `"c1;f1:c2;f2:…"` into segments.
|
|
180
|
+
*
|
|
181
|
+
* Fractions are optional and non-negative; their sum must not exceed 1.
|
|
182
|
+
* Segments without an explicit fraction share the remaining weight equally;
|
|
183
|
+
* if none exist the remainder is added to the last segment.
|
|
184
|
+
*
|
|
185
|
+
* Return codes (mirror C):
|
|
186
|
+
* 0 → ok; 1 → silent error; 2 → error with message; 3 → warning (sum > 1).
|
|
187
|
+
* On rv 1 or 2 the returned segs array is empty.
|
|
188
|
+
*
|
|
189
|
+
* The rv 2 error and rv 3 warning are printed here, once per process (the
|
|
190
|
+
* shared `warned` flag). C's callers then append an AGPREV line for rv > 1;
|
|
191
|
+
* `context` supplies it (e.g. `in edge a -> b`): the warning continues the
|
|
192
|
+
* same line as C's AGPREV does, the error's context follows on its own line.
|
|
193
|
+
*
|
|
194
|
+
* @see lib/common/emit.c:470 parseSegs
|
|
195
|
+
*/
|
|
196
|
+
export function parseSegs(
|
|
197
|
+
colorlist: string,
|
|
198
|
+
context?: () => string,
|
|
199
|
+
): { segs: ColorSeg[]; error: 0 | 1 | 2 | 3 } {
|
|
200
|
+
const result = scanSegs(colorlist, claimWarned);
|
|
201
|
+
if (result.error === 3) {
|
|
202
|
+
const tail = context === undefined ? '' : context();
|
|
203
|
+
agwarningf(`Total size > 1 in "${colorlist}" color spec ${tail}`);
|
|
204
|
+
} else if (result.error === 2) {
|
|
205
|
+
agerrorf(`Illegal value in "${colorlist}" color attribute; float expected after ';'`);
|
|
206
|
+
if (context !== undefined) agerr('prev', context());
|
|
207
|
+
}
|
|
208
|
+
return result;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* {@link parseSegs} for port-internal re-parses that have no C counterpart
|
|
213
|
+
* call (pre-passes, parallel edges): same segments and error class, but never
|
|
214
|
+
* prints and never consumes the once-per-process flag.
|
|
215
|
+
*/
|
|
216
|
+
export function parseSegsSilent(
|
|
217
|
+
colorlist: string,
|
|
218
|
+
): { segs: ColorSeg[]; error: 0 | 1 | 2 | 3 } {
|
|
219
|
+
return scanSegs(colorlist, claimNever);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* The AGPREV line C's `multicolor` appends for an rv > 1 parseSegs.
|
|
224
|
+
* @see lib/common/emit.c:1985 multicolor
|
|
225
|
+
*/
|
|
226
|
+
export function edgeSegsContext(e: Edge): () => string {
|
|
227
|
+
return () => {
|
|
228
|
+
const directed = !e.tail.root.kind.endsWith('undirected');
|
|
229
|
+
return `in edge ${e.tail.name}${directed ? ' -> ' : ' -- '}${e.head.name}`;
|
|
230
|
+
};
|
|
231
|
+
}
|
|
@@ -22,6 +22,7 @@ import { FillType } from '../gvc/context.js';
|
|
|
22
22
|
import { emitHtmlLabel } from './htmltable-emit.js';
|
|
23
23
|
import { transformPoint } from '../gvc/device.js';
|
|
24
24
|
import { nodeAttr } from './poly-init.js';
|
|
25
|
+
import { gvrenderSetStyle, checkStyleTokens } from '../gvc/gvrender-set-style.js';
|
|
25
26
|
import { substObjAnchor, preprocessTooltip } from './subst.js';
|
|
26
27
|
import type { ResolvedFill, PolyStyleFlags } from './style-resolve.js';
|
|
27
28
|
import {
|
|
@@ -96,6 +97,7 @@ interface RingCtx {
|
|
|
96
97
|
job: RenderJob;
|
|
97
98
|
style: PolyStyleFlags;
|
|
98
99
|
fillcolor: string;
|
|
100
|
+
nodeName: string; // agnameof(n): the `in node %s` AGPREV of a parseSegs rv > 1
|
|
99
101
|
shape: number;
|
|
100
102
|
diagonals: boolean;
|
|
101
103
|
rounded: boolean;
|
|
@@ -114,7 +116,7 @@ function renderEllipseRing(
|
|
|
114
116
|
const pf = ring.map(
|
|
115
117
|
(v) => transformPoint({ x: v.x + coord.x, y: v.y + coord.y }, ctx.job),
|
|
116
118
|
);
|
|
117
|
-
wedgedEllipse(ctx.job, pf, ctx.fillcolor, ctx.renderer);
|
|
119
|
+
wedgedEllipse(ctx.job, pf, ctx.fillcolor, ctx.renderer, () => `in node ${ctx.nodeName}`);
|
|
118
120
|
renderEllipse(ring, coord, ctx.renderer, ctx.job, false); // boundary unfilled
|
|
119
121
|
} else {
|
|
120
122
|
renderEllipse(ring, coord, ctx.renderer, ctx.job, filled);
|
|
@@ -124,14 +126,12 @@ function renderEllipseRing(
|
|
|
124
126
|
}
|
|
125
127
|
|
|
126
128
|
/**
|
|
127
|
-
*
|
|
128
|
-
* style.striped + j==0.
|
|
129
|
+
* Striped j===0 ring: the multicolor box plus an unfilled boundary polygon.
|
|
129
130
|
* @see lib/common/shapes.c:poly_gencode :3037-3043
|
|
130
131
|
*/
|
|
131
|
-
/** Striped j===0 ring: the multicolor box plus an unfilled boundary polygon. */
|
|
132
132
|
function renderStripedRing(ring: Point[], coord: Point, ctx: RingCtx): void {
|
|
133
133
|
const af = ring.map((v) => transformPoint({ x: v.x + coord.x, y: v.y + coord.y }, ctx.job));
|
|
134
|
-
stripedBox(ctx.job, af, ctx.fillcolor, true, ctx.renderer);
|
|
134
|
+
stripedBox(ctx.job, af, ctx.fillcolor, true, ctx.renderer, () => `in node ${ctx.nodeName}`);
|
|
135
135
|
renderPolygon(ring, coord, ctx.renderer, ctx.job, false); // boundary unfilled
|
|
136
136
|
}
|
|
137
137
|
|
|
@@ -367,6 +367,13 @@ function applyPenState(obj: ObjState, styleAttr: string | undefined,
|
|
|
367
367
|
export function applyNodeStyle(obj: ObjState, n: Node, job: RenderJob): boolean {
|
|
368
368
|
const styleAttr = nodeAttr(n, n.root, 'style');
|
|
369
369
|
const colorAttr = nodeAttr(n, n.root, 'color');
|
|
370
|
+
// stylenode: gvrender_set_style(job, checkStyle list), before any colour is
|
|
371
|
+
// resolved; point_gencode passes its own fixed list instead (never unknown).
|
|
372
|
+
// @see lib/common/shapes.c:536 stylenode · :3257 point_gencode
|
|
373
|
+
if (!isPointNode(n)) {
|
|
374
|
+
const shape = n.info.shape as ShapeDesc | undefined;
|
|
375
|
+
gvrenderSetStyle(checkStyleTokens(styleTokens(styleAttr), shape?.polygon ?? null));
|
|
376
|
+
}
|
|
370
377
|
// The node's `colorscheme` window is NOT opened here: C opens it once around
|
|
371
378
|
// the whole node emission (emit_begin_node..emit_end_node), which covers both
|
|
372
379
|
// this color block and the label textspans. Both callers of this function are
|
|
@@ -391,7 +398,8 @@ export function applyNodeStyle(obj: ObjState, n: Node, job: RenderJob): boolean
|
|
|
391
398
|
*/
|
|
392
399
|
function prepareDrawPoly(poly: PolygonT, filled: boolean, obj: ObjState): PolygonT {
|
|
393
400
|
if (poly.peripheries !== 0 || !filled) return poly;
|
|
394
|
-
|
|
401
|
+
// gvrender_set_pencolor resolves (and may report) the name like any other.
|
|
402
|
+
obj.penColor = resolveRenderColor('transparent');
|
|
395
403
|
return { ...poly, peripheries: 1 };
|
|
396
404
|
}
|
|
397
405
|
|
|
@@ -446,6 +454,7 @@ function resolveNodeDrawCtx(job: RenderJob, n: Node): NodeDrawCtx | null {
|
|
|
446
454
|
job,
|
|
447
455
|
style: styleFlags,
|
|
448
456
|
fillcolor: nodeAttr(n, n.root, 'fillcolor') ?? '',
|
|
457
|
+
nodeName: n.name,
|
|
449
458
|
...specialCornerFlags(drawPoly.option, styleFlags),
|
|
450
459
|
};
|
|
451
460
|
return { poly: drawPoly, coord, filled, ringCtx };
|
|
@@ -467,8 +476,7 @@ function specialCornerFlags(
|
|
|
467
476
|
}
|
|
468
477
|
|
|
469
478
|
/**
|
|
470
|
-
* Render node shape (polygon/ellipse) and label.
|
|
471
|
-
* Shape codefn — called from walkNodes.
|
|
479
|
+
* Render node shape (polygon/ellipse) and label; shape codefn, from walkNodes.
|
|
472
480
|
* @see lib/common/shapes.c:poly_gencode
|
|
473
481
|
*/
|
|
474
482
|
export function polyGencode(rawJob: unknown, rawNode: unknown): void {
|
package/src/common/poly-init.ts
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
} from './make-label.js';
|
|
26
26
|
import { computeVertices } from './poly-vertices.js';
|
|
27
27
|
import { agwarningf } from './agerr.js';
|
|
28
|
+
import { imageIsSupplied, imageOpenWarning } from '../gvc/usershape.js';
|
|
28
29
|
|
|
29
30
|
// Default half-dimensions in points (72 dpi, 0.75 in wide, 0.5 in tall)
|
|
30
31
|
const DEFAULT_LW = 27;
|
|
@@ -111,6 +112,22 @@ export function buildNodeLabel(n: Node, g: Graph, measurer: TextMeasurer): void
|
|
|
111
112
|
n.info.label = makeAnyLabel(content, isHtml, font, measurer, n);
|
|
112
113
|
}
|
|
113
114
|
|
|
115
|
+
/**
|
|
116
|
+
* poly_init's `image` branch (a non-custom shape): when the caller supplies no
|
|
117
|
+
* image, gvusershape_size fails and C warns after gvusershape_open's own
|
|
118
|
+
* warning. The port never sizes node images.
|
|
119
|
+
* @see lib/common/shapes.c:2043-2046
|
|
120
|
+
*/
|
|
121
|
+
function warnMissingImage(n: Node, g: Graph, shapefile: string | undefined): void {
|
|
122
|
+
if (shapefile !== undefined && shapefile !== '' && nodeAttr(n, g, 'shape') !== 'epsf') return;
|
|
123
|
+
const image = nodeAttr(n, g, 'image');
|
|
124
|
+
if (image === undefined || image === '') return;
|
|
125
|
+
const gvc = g.root.info.gvc;
|
|
126
|
+
if (imageIsSupplied(image, gvc?.imageSizer, gvc?.imageResolver)) return;
|
|
127
|
+
agwarningf(imageOpenWarning(image));
|
|
128
|
+
agwarningf(`No or improper image="${image}" for node "${n.name}"`);
|
|
129
|
+
}
|
|
130
|
+
|
|
114
131
|
export function polyInit(n: Node, g: Graph, measurer: TextMeasurer): void {
|
|
115
132
|
const shapeName = nodeAttr(n, g, 'shape') ?? 'ellipse';
|
|
116
133
|
const shapefile = nodeAttr(n, g, 'shapefile');
|
|
@@ -118,12 +135,13 @@ export function polyInit(n: Node, g: Graph, measurer: TextMeasurer): void {
|
|
|
118
135
|
// C warns when the custom shape's image cannot be sized (headless: always).
|
|
119
136
|
// @see lib/common/shapes.c:2027-2033
|
|
120
137
|
if (shapefile !== undefined && shapefile !== '' && shapeName !== 'epsf') {
|
|
121
|
-
agwarningf(`No or improper shapefile="${shapefile}" for node "${n.name}"
|
|
138
|
+
agwarningf(`No or improper shapefile="${shapefile}" for node "${n.name}"`);
|
|
122
139
|
}
|
|
123
140
|
if ((n.info.shape as ShapeDesc).kind === ShapeKind.SH_RECORD) {
|
|
124
141
|
if (n.info.shape_info === undefined) recordNodeInit(n, g, measurer);
|
|
125
142
|
return;
|
|
126
143
|
}
|
|
144
|
+
warnMissingImage(n, g, shapefile);
|
|
127
145
|
buildNodeLabel(n, g, measurer);
|
|
128
146
|
|
|
129
147
|
// Layout-time common_init_node already installed the attr-resolved
|
package/src/common/postproc.ts
CHANGED
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
} from '../layout/dot/init.js';
|
|
28
28
|
import { addXLabels } from './xlabels-place.js';
|
|
29
29
|
import { registerRenderState } from './render-state.js';
|
|
30
|
-
import {
|
|
30
|
+
import { agerrorf } from './agerr.js';
|
|
31
31
|
|
|
32
32
|
/** Concentrated multi-edge duplicate — never drawn. @see lib/common/const.h:IGNORED */
|
|
33
33
|
const IGNORED_EDGE = 6;
|
|
@@ -229,7 +229,7 @@ function translateDrawing(g: Graph): void {
|
|
|
229
229
|
// @see lib/common/postproc.c:map_edge ("lost %s %s edge")
|
|
230
230
|
if (e.info.spl === undefined) {
|
|
231
231
|
if (!(g.info.concentrate ?? false) && (e.info.edge_type ?? 0) !== IGNORED_EDGE) {
|
|
232
|
-
|
|
232
|
+
agerrorf(`lost ${e.tail.name} ${e.head.name} edge`);
|
|
233
233
|
}
|
|
234
234
|
continue;
|
|
235
235
|
}
|
|
@@ -16,7 +16,7 @@ import type { Edge } from '../model/edge.js';
|
|
|
16
16
|
import type { Node } from '../model/node.js';
|
|
17
17
|
import { approxEqPt, bezierClip, updateBbBz, InsideContext } from './splines-geom.js';
|
|
18
18
|
import { MILLIPOINT, ARR_NONE } from './splines-constants.js';
|
|
19
|
-
import { arrowClipLength } from '../layout/dot/edge-route-clip.js';
|
|
19
|
+
import { arrowClipLength, distFused } from '../layout/dot/edge-route-clip.js';
|
|
20
20
|
import {
|
|
21
21
|
arrowDrawOpsForEnd, edgeArrowName, edgeArrowsize,
|
|
22
22
|
} from '../layout/dot/edge-route-arrow.js';
|
|
@@ -25,6 +25,14 @@ import { resolvePenWidth, parseStyleFlags } from './style-resolve.js';
|
|
|
25
25
|
/** Normal (filled triangle) arrowhead. @see lib/common/const.h:ARR_TYPE_NORM */
|
|
26
26
|
export const ARR_NORM = 1;
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* DIST(p, q) as the oracle compiles it: sqrt(fma(dx, dx, dy * dy)), not
|
|
30
|
+
* Math.hypot (libgvc 16.1 `_arrowEndClip`/`_arrowStartClip`, incl. the inlined
|
|
31
|
+
* sphere_ctx inside(), and `_arrowOrthoClip`).
|
|
32
|
+
* @see lib/common/geom.h:DIST
|
|
33
|
+
*/
|
|
34
|
+
export const dist: (p: Point, q: Point) => number = distFused;
|
|
35
|
+
|
|
28
36
|
// Internal shape accessor type for host objects that may carry a shape.
|
|
29
37
|
type ShapeHost = { shape?: { fns?: { insidefn?: ((c: InsideContext, p: Point) => boolean) | null } | null } | null };
|
|
30
38
|
|
|
@@ -123,11 +131,6 @@ class SplineClipHelper {
|
|
|
123
131
|
return [sflag, eflag];
|
|
124
132
|
}
|
|
125
133
|
|
|
126
|
-
/** DIST(p, q). */
|
|
127
|
-
static dist(p: Point, q: Point): number {
|
|
128
|
-
return Math.hypot(p.x - q.x, p.y - q.y);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
134
|
/** Sphere inside-fn around `tip` with radius `r` for bezierClip. */
|
|
132
135
|
static sphereCtx(tip: Point, r: number): {
|
|
133
136
|
ctx: InsideContext;
|
|
@@ -135,7 +138,7 @@ class SplineClipHelper {
|
|
|
135
138
|
} {
|
|
136
139
|
return {
|
|
137
140
|
ctx: { nodeCoord: { x: 0, y: 0 }, rw: 0, bp: null },
|
|
138
|
-
fn: (_c: InsideContext, p: Point) =>
|
|
141
|
+
fn: (_c: InsideContext, p: Point) => dist(p, tip) <= r,
|
|
139
142
|
};
|
|
140
143
|
}
|
|
141
144
|
|
|
@@ -170,7 +173,7 @@ class SplineClipHelper {
|
|
|
170
173
|
SplineClipHelper.clipArrowsize(e), SplineClipHelper.clipPenwidth(e));
|
|
171
174
|
spl.eflag = eflag;
|
|
172
175
|
spl.ep = ps[endp + 3]!;
|
|
173
|
-
if (endp > startp &&
|
|
176
|
+
if (endp > startp && dist(ps[endp]!, ps[endp + 3]!) < elen) {
|
|
174
177
|
endp -= 3;
|
|
175
178
|
}
|
|
176
179
|
const sp: Point[] = [spl.ep, ps[endp + 2]!, ps[endp + 1]!, ps[endp]!];
|
|
@@ -198,7 +201,7 @@ class SplineClipHelper {
|
|
|
198
201
|
SplineClipHelper.clipArrowsize(e), SplineClipHelper.clipPenwidth(e));
|
|
199
202
|
spl.sflag = sflag;
|
|
200
203
|
spl.sp = ps[startp]!;
|
|
201
|
-
if (endp > startp &&
|
|
204
|
+
if (endp > startp && dist(ps[startp]!, ps[startp + 3]!) < slen) {
|
|
202
205
|
startp += 3;
|
|
203
206
|
}
|
|
204
207
|
const sp: Point[] = [ps[startp + 3]!, ps[startp + 2]!, ps[startp + 1]!, spl.sp];
|
|
@@ -293,7 +296,7 @@ class SplineClipHelper {
|
|
|
293
296
|
if (sflag && eflag && endp === startp) {
|
|
294
297
|
// two arrows on one segment
|
|
295
298
|
const p = ps[endp]!; const q = ps[endp + 3]!;
|
|
296
|
-
const d =
|
|
299
|
+
const d = dist(p, q);
|
|
297
300
|
let hlen = hlen0; let tlen = tlen0;
|
|
298
301
|
if (hlen + tlen >= d) { hlen = tlen = d / 3.0; }
|
|
299
302
|
const s = { x: p.x, y: p.y }; const t = { x: q.x, y: q.y };
|
|
@@ -315,7 +318,7 @@ class SplineClipHelper {
|
|
|
315
318
|
if (eflag) {
|
|
316
319
|
let hlen = hlen0;
|
|
317
320
|
const p = ps[endp]!; const q = ps[endp + 3]!;
|
|
318
|
-
const maxd = 0.9 *
|
|
321
|
+
const maxd = 0.9 * dist(p, q);
|
|
319
322
|
if (hlen >= maxd) hlen = maxd;
|
|
320
323
|
const r = { x: p.x, y: p.y };
|
|
321
324
|
if (p.y === q.y) { r.y = p.y; r.x = p.x < q.x ? q.x - hlen : q.x + hlen; }
|
|
@@ -326,7 +329,7 @@ class SplineClipHelper {
|
|
|
326
329
|
if (sflag) {
|
|
327
330
|
let tlen = tlen0;
|
|
328
331
|
const p = ps[startp]!; const q = ps[startp + 3]!;
|
|
329
|
-
const maxd = 0.9 *
|
|
332
|
+
const maxd = 0.9 * dist(p, q);
|
|
330
333
|
if (tlen >= maxd) tlen = maxd;
|
|
331
334
|
const r = { x: p.x, y: p.y };
|
|
332
335
|
if (p.y === q.y) { r.y = p.y; r.x = p.x < q.x ? p.x + tlen : p.x - tlen; }
|
|
@@ -15,7 +15,7 @@ import { shortestPath, routeSpline, makePolyline } from '../pathplan/index.js';
|
|
|
15
15
|
import type { Poly } from '../pathplan/types.js';
|
|
16
16
|
import { INIT_DELTA, LOOP_TRIES, ROUTESPL_FUDGE } from './splines-constants.js';
|
|
17
17
|
import { fma } from './fma.js';
|
|
18
|
-
import {
|
|
18
|
+
import { agerrorf } from './agerr.js';
|
|
19
19
|
|
|
20
20
|
// ---------------------------------------------------------------------------
|
|
21
21
|
// Stage-dump hook (diagnosis only; null in production — same pattern as
|
|
@@ -451,7 +451,7 @@ function routeSplinesInternal(pp: Path, polyline: boolean): Point[] | null {
|
|
|
451
451
|
if (pl === null) {
|
|
452
452
|
// C: agerrorf("in routesplines, Pshortestpath failed") — the caller then
|
|
453
453
|
// loses the edge. @see lib/common/routespl.c (Pshortestpath < 0 branch)
|
|
454
|
-
|
|
454
|
+
agerrorf('in routesplines, Pshortestpath failed');
|
|
455
455
|
return null;
|
|
456
456
|
}
|
|
457
457
|
emitDump('S3_POLY', dumpNames, () => ({
|
|
@@ -49,7 +49,7 @@ export function getFamilyMetrics(fontName: string): FontFamilyData {
|
|
|
49
49
|
if (!warnedFontNames.has(fontName)) {
|
|
50
50
|
warnedFontNames.add(fontName);
|
|
51
51
|
agwarningf(
|
|
52
|
-
`no hard-coded metrics for '${fontName}'. ` +
|
|
52
|
+
`Warning: no hard-coded metrics for '${fontName}'. ` +
|
|
53
53
|
`Falling back to 'Times' metrics`,
|
|
54
54
|
);
|
|
55
55
|
}
|
package/src/gvc/context.ts
CHANGED
|
@@ -26,6 +26,7 @@ import type { DebugOptions } from '../debug.js';
|
|
|
26
26
|
import type { ImageSizer } from '../common/htmltable-types.js';
|
|
27
27
|
import type { ImageResolver } from './image-resolver.js';
|
|
28
28
|
import type { RenderJob } from './job.js'; // scaffold in T25; full class in T26
|
|
29
|
+
import { resetRenderState } from '../common/render-state.js';
|
|
29
30
|
import { RenderError, invalidArgType, invalidArgValue } from '../errors.js';
|
|
30
31
|
|
|
31
32
|
// ---------------------------------------------------------------------------
|
|
@@ -285,6 +286,12 @@ export class GvcContext {
|
|
|
285
286
|
* rendering happens in between and must see the layout state
|
|
286
287
|
* (e.g. cluster arrays).
|
|
287
288
|
*
|
|
289
|
+
* Contract (AD-4/AD-8): first resets the C process state (RNG streams,
|
|
290
|
+
* sequence counters, once-only warnings), so each `layout` call behaves
|
|
291
|
+
* as one native run — drive one graph per `layout`. Caller-installed hooks
|
|
292
|
+
* (setTextMeasurer, setImageSizer, setImageResolver, the per-context
|
|
293
|
+
* measurer/sizer/resolver) are configuration and are not reset.
|
|
294
|
+
*
|
|
288
295
|
* @throws TypeError (ERR_INVALID_ARG_TYPE / ERR_INVALID_ARG_VALUE) for a
|
|
289
296
|
* bad `g`, or an engine argument that is not registered
|
|
290
297
|
* @throws RenderError (UNKNOWN_LAYOUT) if the `layout` attribute names no
|
|
@@ -295,6 +302,7 @@ export class GvcContext {
|
|
|
295
302
|
* @see lib/gvc/gvlayout.c:gvLayoutJobs
|
|
296
303
|
*/
|
|
297
304
|
layout(g: Graph, engineName: EngineName): void {
|
|
305
|
+
resetRenderState(); // AD-8: idempotent under the public entries' own reset
|
|
298
306
|
const selected = this.checkLayoutArgs(g, engineName);
|
|
299
307
|
// C gvLayoutJobs: the graph's `layout` ATTRIBUTE unconditionally
|
|
300
308
|
// overrides the selected engine (-K / API choice); an unrecognized
|
|
@@ -6,13 +6,17 @@
|
|
|
6
6
|
|
|
7
7
|
import type { Graph } from '../model/graph.js';
|
|
8
8
|
import type { ObjState } from './job.js';
|
|
9
|
+
import type { RendererPlugin } from './context.js';
|
|
10
|
+
import { transformPoint } from './device-transform.js';
|
|
11
|
+
import { stripedBox } from '../render/svg-multicolor.js';
|
|
9
12
|
import { RenderJob } from './job.js';
|
|
10
13
|
import { FillType } from './context.js';
|
|
11
14
|
import type { ResolvedFill, ClusterAttrs } from '../common/style-resolve.js';
|
|
12
15
|
import {
|
|
13
16
|
parseStyleFlags, resolvePenColor, resolvePenType, resolvePenWidth,
|
|
14
|
-
resolveClusterFillEx,
|
|
17
|
+
resolveClusterFillEx, styleTokens,
|
|
15
18
|
} from '../common/style-resolve.js';
|
|
19
|
+
import { gvrenderSetStyle, checkClusterStyleTokens } from './gvrender-set-style.js';
|
|
16
20
|
import { resolveRenderColor, withColorScheme, reportColorNames } from '../render/color-resolve.js';
|
|
17
21
|
import { parseSegs } from '../common/multicolor.js';
|
|
18
22
|
|
|
@@ -100,6 +104,9 @@ export function applyClusterObjState(sg: Graph, job: RenderJob): boolean {
|
|
|
100
104
|
const fillRes = resolveClusterFillEx(clusterAttrsOf(sg));
|
|
101
105
|
if (job.obj === null) return false;
|
|
102
106
|
const obj = job.obj;
|
|
107
|
+
// emit_clusters: gvrender_set_style(job, checkClusterStyle list) precedes the
|
|
108
|
+
// colour resolution. @see lib/common/emit.c:3808
|
|
109
|
+
gvrenderSetStyle(checkClusterStyleTokens(styleTokens(clusterStyle(sg))));
|
|
103
110
|
// C wraps the cluster color block with setColorScheme. @see emit.c:3800/3943
|
|
104
111
|
return withColorScheme(clusterAttr(sg, 'colorscheme'), () => {
|
|
105
112
|
// C resolves the fill (set_fillcolor / gradient vals) before the pen,
|
|
@@ -112,23 +119,71 @@ export function applyClusterObjState(sg: Graph, job: RenderJob): boolean {
|
|
|
112
119
|
}
|
|
113
120
|
|
|
114
121
|
/**
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
* @see lib/common/emit.c:
|
|
118
|
-
* @see lib/common/emit.c:595 stripedBox
|
|
122
|
+
* True when emit_clusters takes the striped branch: `style=striped` and not
|
|
123
|
+
* `rounded` (the rounded branch is tested first).
|
|
124
|
+
* @see lib/common/emit.c:3877-3892 emit_clusters (rounded / striped / plain)
|
|
119
125
|
*/
|
|
120
|
-
function
|
|
126
|
+
export function isStripedCluster(sg: Graph): boolean {
|
|
127
|
+
const flags = parseStyleFlags(clusterStyle(sg));
|
|
128
|
+
return flags.striped && !flags.rounded;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The fillcolor string emit_clusters hands to stripedBox.
|
|
133
|
+
* @see lib/common/emit.c:3820-3850 emit_clusters (fillcolor selection)
|
|
134
|
+
*/
|
|
135
|
+
function stripedFillSpec(sg: Graph): string {
|
|
121
136
|
const flags = parseStyleFlags(clusterStyle(sg));
|
|
122
|
-
if (!flags.striped || flags.rounded) return [];
|
|
123
137
|
const color = clusterAttr(sg, 'color') ?? '';
|
|
124
138
|
let fill = clusterAttr(sg, 'fillcolor') || color;
|
|
125
139
|
const bg = clusterAttr(sg, 'bgcolor') ?? '';
|
|
126
140
|
if ((!flags.filled || fill === '') && bg !== '') fill = bg;
|
|
127
|
-
|
|
141
|
+
return fill === '' ? 'lightgrey' : fill;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* The colours stripedBox sets for a `style=striped` cluster (not rounded): each
|
|
146
|
+
* segment of the fill spec with a positive width, after the pen.
|
|
147
|
+
* @see lib/common/emit.c:3896-3906 emit_clusters (striped branch)
|
|
148
|
+
* @see lib/common/emit.c:595 stripedBox
|
|
149
|
+
*/
|
|
150
|
+
function stripedClusterColors(sg: Graph): string[] {
|
|
151
|
+
if (!isStripedCluster(sg)) return [];
|
|
152
|
+
const { segs, error } = parseSegs(stripedFillSpec(sg), () => `in cluster ${sg.name}`);
|
|
128
153
|
if (error === 1 || error === 2) return [];
|
|
129
154
|
return segs.flatMap((seg) => (seg.color !== null && seg.t > 0 ? [seg.color] : []));
|
|
130
155
|
}
|
|
131
156
|
|
|
157
|
+
/**
|
|
158
|
+
* Draw a `style=striped` cluster box: stripedBox over the bb corners, then the
|
|
159
|
+
* unfilled bb outline (pen already "transparent" for `peripheries=0`). The
|
|
160
|
+
* once-per-render Total-size warning is claimed by stripedClusterColors when
|
|
161
|
+
* the pen state is applied, so the parse here does not print twice.
|
|
162
|
+
* @see lib/common/emit.c:3892-3906 emit_clusters (striped branch)
|
|
163
|
+
*/
|
|
164
|
+
export function renderStripedClusterBox(
|
|
165
|
+
sg: Graph, renderer: RendererPlugin, job: RenderJob,
|
|
166
|
+
): void {
|
|
167
|
+
// stripedBox runs inside emit_clusters' setColorScheme window (3800/3943).
|
|
168
|
+
withColorScheme(clusterAttr(sg, 'colorscheme'), () => {
|
|
169
|
+
const { ll, ur } = sg.info.bb!;
|
|
170
|
+
const af = [
|
|
171
|
+
{ x: ll.x, y: ll.y }, { x: ur.x, y: ll.y }, { x: ur.x, y: ur.y }, { x: ll.x, y: ur.y },
|
|
172
|
+
].map((p) => transformPoint(p, job));
|
|
173
|
+
// stripedBox passes filled=1 (never GRADIENT) to gvrender_polygon; the port's
|
|
174
|
+
// renderers read the gradient from obj.fill, so show them a solid fill.
|
|
175
|
+
const obj = job.obj;
|
|
176
|
+
const savedFill = obj?.fill;
|
|
177
|
+
if (obj !== null && savedFill !== undefined) obj.fill = FillType.Solid;
|
|
178
|
+
stripedBox(job, af, stripedFillSpec(sg), false, renderer, () => `in cluster ${sg.name}`);
|
|
179
|
+
if (obj !== null && savedFill !== undefined) obj.fill = savedFill;
|
|
180
|
+
const rawPts = [
|
|
181
|
+
{ x: ll.x, y: ll.y }, { x: ll.x, y: ur.y }, { x: ur.x, y: ur.y }, { x: ur.x, y: ll.y },
|
|
182
|
+
];
|
|
183
|
+
renderer.polygon(rawPts.map((p) => transformPoint(p, job)), false, job);
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
132
187
|
/** The fill half of {@link applyClusterObjState}; true when the box is filled. */
|
|
133
188
|
function applyClusterFill(obj: ObjState, fillRes: ResolvedFill): boolean {
|
|
134
189
|
if (fillRes.kind === 'none') { obj.fill = FillType.None; return false; }
|
package/src/gvc/device.ts
CHANGED
|
@@ -26,6 +26,7 @@ import type { ShapeDesc, TextlabelT } from '../common/types.js';
|
|
|
26
26
|
import { type LayerInfo, parseLayers } from '../common/layers.js';
|
|
27
27
|
import { RenderJob, EMIT_CLUSTERS_LAST, createObjState, ObjType, EmitState } from './job.js';
|
|
28
28
|
import { walkNodesAndEdges } from './emit-walk.js';
|
|
29
|
+
import { warnLayersUnsupported } from './first-layer.js';
|
|
29
30
|
import { polyInit } from '../common/poly-init.js';
|
|
30
31
|
import {
|
|
31
32
|
setHtmlAnchorObj,
|
|
@@ -47,8 +48,9 @@ import {
|
|
|
47
48
|
setRendererKnownColors,
|
|
48
49
|
} from '../render/color-resolve.js';
|
|
49
50
|
import { emitRoundedBezier } from '../common/poly-shapes.js';
|
|
50
|
-
import { applyClusterObjState, clusterStyle, clusterPeripheries } from './device-cluster.js';
|
|
51
|
+
import { applyClusterObjState, clusterStyle, clusterPeripheries, isStripedCluster, renderStripedClusterBox } from './device-cluster.js';
|
|
51
52
|
import { transformPoint } from './device-transform.js';
|
|
53
|
+
import { gvrenderSetStyle, edgeStyleResets } from './gvrender-set-style.js';
|
|
52
54
|
import { renderOneLabel, renderNodeXLabel, renderClusterLabel, reportEdgeLabelColors } from './device-labels.js';
|
|
53
55
|
export { transformPoint, applyRotation, applyScale, buildPoint } from './device-transform.js';
|
|
54
56
|
export { renderOneLabel, renderNodeXLabel, renderClusterLabel } from './device-labels.js';
|
|
@@ -165,6 +167,9 @@ function setEdgePen(e: Edge, job: RenderJob): void {
|
|
|
165
167
|
// Raw style tokens for xdot's `S` ops (e.g. explicit style="solid"); SVG
|
|
166
168
|
// reads only obj.pen, so this is xdot-only. @see gvrender_core_dot.c xdot_style
|
|
167
169
|
obj.rawStyle = styleTokens(e.attrs.get('style'));
|
|
170
|
+
// emit_begin_edge: `if (styles && ED_spl(e)) gvrender_set_style(job, styles)`
|
|
171
|
+
// @see lib/common/emit.c:2726
|
|
172
|
+
if (e.info.spl !== undefined) gvrenderSetStyle(obj.rawStyle);
|
|
168
173
|
}
|
|
169
174
|
|
|
170
175
|
export function renderEdge(e: Edge, renderer: RendererPlugin, job: RenderJob): void {
|
|
@@ -195,6 +200,10 @@ export function renderEdge(e: Edge, renderer: RendererPlugin, job: RenderJob): v
|
|
|
195
200
|
// emit_edge_graphics, then emit_end_edge's labels: C resolves these names
|
|
196
201
|
// here whatever the renderer draws (the non-drawing ones included).
|
|
197
202
|
reportEdgeColors(e);
|
|
203
|
+
// arrow_gen resets the style; C sets the edge's styles again after each
|
|
204
|
+
// flagged bezier of a multi-bezier spline. @see emit.c:2047, :2677
|
|
205
|
+
const styles = styleTokens(e.attrs.get('style'));
|
|
206
|
+
for (let i = edgeStyleResets(e, styles); i > 0; i--) gvrenderSetStyle(styles);
|
|
198
207
|
reportEdgeLabelColors(e);
|
|
199
208
|
renderer.endEdge(e, job);
|
|
200
209
|
});
|
|
@@ -230,6 +239,10 @@ function renderClusterBox(
|
|
|
230
239
|
sg: Graph, filled: boolean, renderer: RendererPlugin, job: RenderJob,
|
|
231
240
|
): void {
|
|
232
241
|
const { ll, ur } = sg.info.bb!;
|
|
242
|
+
if (isStripedCluster(sg)) {
|
|
243
|
+
renderStripedClusterBox(sg, renderer, job);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
233
246
|
if (parseStyleFlags(clusterStyle(sg)).rounded) {
|
|
234
247
|
const af = [{ x: ll.x, y: ll.y }, { x: ur.x, y: ll.y }, { x: ur.x, y: ur.y }, { x: ll.x, y: ur.y }];
|
|
235
248
|
emitRoundedBezier(af, { x: 0, y: 0 }, filled, { renderer, job });
|
|
@@ -280,7 +293,7 @@ function renderOneCluster(sg: Graph, renderer: RendererPlugin, job: RenderJob):
|
|
|
280
293
|
// style=invis sets PEN_NONE and gvrender_polygon skips the whole box
|
|
281
294
|
// (fill included); the group/title still emit.
|
|
282
295
|
// @see lib/common/emit.c:3907-3917, lib/gvc/gvrender.c:543
|
|
283
|
-
if ((clusterPeripheries(sg) !== 0 || filled) && job.obj?.pen !== PenType.None) {
|
|
296
|
+
if ((clusterPeripheries(sg) !== 0 || filled || isStripedCluster(sg)) && job.obj?.pen !== PenType.None) {
|
|
284
297
|
renderClusterBox(sg, filled, renderer, job);
|
|
285
298
|
}
|
|
286
299
|
renderClusterLabel(sg, renderer, job);
|
|
@@ -334,6 +347,7 @@ export function renderGraph(g: Graph, job: RenderJob, renderer: RendererPlugin):
|
|
|
334
347
|
job.numLayers = layered ? info.numLayers : 1;
|
|
335
348
|
job.layerIDs = info.layerIDs;
|
|
336
349
|
renderer.beginGraph(g, job);
|
|
350
|
+
warnLayersUnsupported(job.format, info.numLayers, renderer.beginLayer !== undefined);
|
|
337
351
|
for (let ln = 1; ln <= job.numLayers; ln++) {
|
|
338
352
|
job.layerNum = ln;
|
|
339
353
|
if (layered) renderer.beginLayer!(info.layerIDs[ln]!, job);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EPL-2.0
|
|
2
|
+
/**
|
|
3
|
+
* The `GVDEVICE_DOES_LAYERS` check of `firstlayer`: a device without it prints
|
|
4
|
+
* one layer and warns. The one-layer effect is `renderGraph`'s `layered` flag
|
|
5
|
+
* (only svg has `beginLayer`; the other native layer-capable devices, ps/pov,
|
|
6
|
+
* are not ported); this file owns the warning.
|
|
7
|
+
*/
|
|
8
|
+
import { agwarningf } from '../common/agerr.js';
|
|
9
|
+
|
|
10
|
+
/** Port format ids that spell a native `-T` name differently (`output_langname`). */
|
|
11
|
+
const NATIVE_LANGNAME: ReadonlyMap<string, string> = new Map([
|
|
12
|
+
['cmapx-np', 'cmapx_np'],
|
|
13
|
+
['imap-np', 'imap_np'],
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Print `layers not supported in <output_langname> output` when the graph has
|
|
18
|
+
* more than one layer and the device cannot draw layers. Called after
|
|
19
|
+
* beginGraph, as emit_graph calls firstlayer after emit_begin_graph: for
|
|
20
|
+
* `-Tjson` the nested xdot render in json_begin_graph therefore warns first.
|
|
21
|
+
* @see lib/common/emit.c:firstlayer
|
|
22
|
+
*/
|
|
23
|
+
export function warnLayersUnsupported(format: string, numLayers: number, doesLayers: boolean): void {
|
|
24
|
+
if (numLayers <= 1 || doesLayers) return;
|
|
25
|
+
agwarningf(`layers not supported in ${NATIVE_LANGNAME.get(format) ?? format} output`);
|
|
26
|
+
}
|