@knowvah/dot-engine 2.7.0 → 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/attr-get.d.ts +30 -0
- package/dist/common/attr-get.d.ts.map +1 -0
- 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 +13 -12
- 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 +512 -286
- package/dist/index.js.map +4 -4
- package/dist/layout/circo/index.d.ts.map +1 -1
- package/dist/layout/dot/graph-label.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 +518 -292
- package/dist/render.js.map +4 -4
- package/package.json +1 -1
- package/src/common/agerr.ts +69 -8
- package/src/common/attr-get.ts +46 -0
- 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 +20 -4
- package/src/common/multicolor.ts +12 -6
- package/src/common/nodeinit.ts +2 -1
- package/src/common/poly-init.ts +22 -19
- 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/graph-label.ts +1 -15
- 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
|
@@ -89,6 +89,7 @@ export interface PolySizeParams {
|
|
|
89
89
|
* @see lib/common/shapes.c:2132-2145
|
|
90
90
|
*/
|
|
91
91
|
nojustify?: boolean;
|
|
92
|
+
imageSize?: Point; // padded image size in points, shapes.c:2020-2051 imagesize
|
|
92
93
|
}
|
|
93
94
|
|
|
94
95
|
/** Node geometry in points: left/right half-widths and height. */
|
|
@@ -550,7 +551,8 @@ export function polySize(p: PolySizeParams): PolySizeResult {
|
|
|
550
551
|
const init = initialSizePts(p);
|
|
551
552
|
const dimen = quantize(padLabelDimen(p.labelDimen, p.margin, p.isPlain), p.quantumIn);
|
|
552
553
|
const { sides, isBox } = effectiveShape(p);
|
|
553
|
-
|
|
554
|
+
const img = p.imageSize ?? { x: 0, y: 0 }; // shapes.c:2055: bb = max(dimen, imagesize)
|
|
555
|
+
let bb: Point = { x: Math.max(dimen.x, img.x), y: Math.max(dimen.y, img.y) };
|
|
554
556
|
// C shapes.c:2073-2085 dispatch: box → exact fit; poly_desc shapes (star,
|
|
555
557
|
// cylinder) → their size_gen; everything else → generic ellipse fit.
|
|
556
558
|
if (isBox) {
|
|
@@ -583,8 +585,6 @@ export function polySize(p: PolySizeParams): PolySizeResult {
|
|
|
583
585
|
* Compute the label's justification borders (ND_label->space) and vertical
|
|
584
586
|
* alignment — the "space available for label" block of poly_init. `bb` is the
|
|
585
587
|
* final (constrained) box; `minBb` is the pre-constraint shape-fit box.
|
|
586
|
-
* Image sizing is unported, so `imagesize` is 0 (which matches C for the
|
|
587
|
-
* missing-image repro cases); revisit when image loading lands.
|
|
588
588
|
* @see lib/common/shapes.c:poly_init (2132-2152)
|
|
589
589
|
*/
|
|
590
590
|
function labelSpace(
|
|
@@ -604,7 +604,7 @@ function labelSpace(
|
|
|
604
604
|
spaceX = dimen.x - spacex;
|
|
605
605
|
}
|
|
606
606
|
// space.y: !fixedshape → dimen.y + (bb.y - min_bb.y); fixedshape keeps dimen.y.
|
|
607
|
-
const spaceY = fixedshape ? dimen.y : dimen.y + (bb.y - minBb.y);
|
|
607
|
+
const spaceY = fixedshape ? dimen.y : dimen.y + ((bb.y - minBb.y) + Math.max(0, (p.imageSize?.y ?? 0) - dimen.y));
|
|
608
608
|
return { space: { x: spaceX, y: spaceY }, valign: labelValign(p) };
|
|
609
609
|
}
|
|
610
610
|
|
package/src/common/postproc.ts
CHANGED
|
@@ -27,6 +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 { getState, GVSPLINES } from './layout-state.js';
|
|
30
31
|
import { agerrorf } from './agerr.js';
|
|
31
32
|
|
|
32
33
|
/** Concentrated multi-edge duplicate — never drawn. @see lib/common/const.h:IGNORED */
|
|
@@ -221,7 +222,9 @@ function translateDrawing(g: Graph): void {
|
|
|
221
222
|
|
|
222
223
|
for (const v of g.nodes.values()) {
|
|
223
224
|
mapNode(v);
|
|
224
|
-
// State == GVSPLINES
|
|
225
|
+
// C: `if (State == GVSPLINES)` guards the whole edge loop; layouts that
|
|
226
|
+
// never route (patchwork, osage) leave State at GVBEGIN.
|
|
227
|
+
if (getState() !== GVSPLINES) continue;
|
|
225
228
|
for (const e of g.edges) {
|
|
226
229
|
if (e.tail !== v) continue;
|
|
227
230
|
// C map_edge: a spline-less (non-concentrate, non-IGNORED) edge was
|
|
@@ -22,8 +22,9 @@
|
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
24
|
import { PenType } from '../gvc/context.js';
|
|
25
|
+
import { agerrorf, agwarningf } from './agerr.js';
|
|
25
26
|
import { parseGradientSpec } from './htmltable-emit-fill.js';
|
|
26
|
-
import { parseSegs } from './multicolor.js';
|
|
27
|
+
import { MSG_MANY_COLORS, parseSegs } from './multicolor.js';
|
|
27
28
|
|
|
28
29
|
// ---------------------------------------------------------------------------
|
|
29
30
|
// Types
|
|
@@ -156,28 +157,113 @@ function zeroFlags(): PolyStyleFlags {
|
|
|
156
157
|
};
|
|
157
158
|
}
|
|
158
159
|
|
|
159
|
-
/** C parse_style truncates the style list at FUNLIMIT tokens. @see lib/common/emit.c:
|
|
160
|
+
/** C parse_style truncates the style list at FUNLIMIT tokens. @see lib/common/emit.c:3998 FUNLIMIT */
|
|
160
161
|
const FUNLIMIT = 64;
|
|
161
162
|
|
|
163
|
+
/** Characters `gv_isspace` accepts. @see lib/cgraph/gv_ctype.h:gv_isspace */
|
|
164
|
+
const STYLE_SPACE: ReadonlySet<string> = new Set([' ', '\t', '\n', '\v', '\f', '\r']);
|
|
165
|
+
|
|
166
|
+
/** @see lib/common/emit.c:3949 is_style_delim ('\0' is the end of the C string) */
|
|
167
|
+
function isStyleDelim(c: string | undefined): boolean {
|
|
168
|
+
return c === undefined || c === '(' || c === ')' || c === ',';
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** One `style_token` result: `(`, `)`, an identifier, or the end (`null`). */
|
|
172
|
+
interface StyleToken {
|
|
173
|
+
readonly type: '(' | ')' | 'id';
|
|
174
|
+
readonly text: string;
|
|
175
|
+
/** Index in the style string just past the token. */
|
|
176
|
+
readonly next: number;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Skips spaces and commas, then reads one token: a parenthesis, or everything
|
|
181
|
+
* up to the next delimiter (trailing spaces included). Null at the end.
|
|
182
|
+
* @see lib/common/emit.c:3974 style_token
|
|
183
|
+
*/
|
|
184
|
+
function styleToken(s: string, from: number): StyleToken | null {
|
|
185
|
+
let p = from;
|
|
186
|
+
while (p < s.length && (STYLE_SPACE.has(s[p]) || s[p] === ',')) p++;
|
|
187
|
+
if (p >= s.length) return null;
|
|
188
|
+
if (s[p] === '(' || s[p] === ')') return { type: s[p] as '(' | ')', text: s[p], next: p + 1 };
|
|
189
|
+
const start = p;
|
|
190
|
+
while (!isStyleDelim(s[p])) p++;
|
|
191
|
+
return { type: 'id', text: s.slice(start, p), next: p };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Why `parseStyle` produced an empty list, or null when it did not. */
|
|
195
|
+
type StyleFault = 'nesting' | 'unmatched-close' | 'unmatched-open' | 'truncate';
|
|
196
|
+
|
|
197
|
+
/** Result of the tokenizer: the list (empty on a fault) and the fault, if any. */
|
|
198
|
+
interface StyleParse {
|
|
199
|
+
readonly tokens: string[];
|
|
200
|
+
readonly fault: StyleFault | null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const NO_TOKENS: StyleParse['tokens'] = [];
|
|
204
|
+
|
|
162
205
|
/**
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
|
|
206
|
+
* parse_style without the diagnostics. A token's `(args)` are appended to it,
|
|
207
|
+
* as C stores them after the token's NUL: `setlinewidth( 2 )` → `setlinewidth(2 )`.
|
|
208
|
+
* @see lib/common/emit.c:4010 parse_style
|
|
209
|
+
*/
|
|
210
|
+
function parseStyleQuiet(s: string): StyleParse {
|
|
211
|
+
const end = s.indexOf('\0');
|
|
212
|
+
const str = end < 0 ? s : s.slice(0, end);
|
|
213
|
+
const names: string[] = [];
|
|
214
|
+
const args: string[][] = [];
|
|
215
|
+
let inParens = false;
|
|
216
|
+
for (let c = styleToken(str, 0); c !== null; c = styleToken(str, c.next)) {
|
|
217
|
+
if (c.type === '(') {
|
|
218
|
+
if (inParens) return { tokens: NO_TOKENS, fault: 'nesting' };
|
|
219
|
+
inParens = true;
|
|
220
|
+
} else if (c.type === ')') {
|
|
221
|
+
if (!inParens) return { tokens: NO_TOKENS, fault: 'unmatched-close' };
|
|
222
|
+
inParens = false;
|
|
223
|
+
} else if (inParens) {
|
|
224
|
+
args[names.length - 1]?.push(c.text);
|
|
225
|
+
} else {
|
|
226
|
+
if (names.length === FUNLIMIT - 1) return { tokens: NO_TOKENS, fault: 'truncate' };
|
|
227
|
+
names.push(c.text);
|
|
228
|
+
args.push([]);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (inParens) return { tokens: NO_TOKENS, fault: 'unmatched-open' };
|
|
232
|
+
const tokens = names.map((n, i) => (args[i].length === 0 ? n : `${n}(${args[i].join(',')})`));
|
|
233
|
+
return { tokens, fault: null };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** The agerr line for a fault, byte for byte. @see lib/common/emit.c:4027-4062 */
|
|
237
|
+
function reportStyleFault(fault: StyleFault, s: string): void {
|
|
238
|
+
if (fault === 'nesting') agerrorf(`nesting not allowed in style: ${s}`);
|
|
239
|
+
else if (fault === 'unmatched-close') agerrorf(`unmatched ')' in style: ${s}`);
|
|
240
|
+
else if (fault === 'unmatched-open') agerrorf(`unmatched '(' in style: ${s}`);
|
|
241
|
+
else agwarningf(`truncating style '${s}'`);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* The raw style tokens C's `parse_style` produces, untrimmed, `(args)`
|
|
246
|
+
* attached; empty on a fault, which it reports (agerr) on every call. These
|
|
247
|
+
* are what xdot's `xdot_style` re-emits as `S` ops. Call it where C calls
|
|
248
|
+
* `parse_style`; use `styleTokensQuiet` to read a list C has already parsed.
|
|
167
249
|
* @see lib/common/emit.c:4010 parse_style · gvrender_core_dot.c:161 xdot_style
|
|
168
250
|
*/
|
|
169
251
|
export function styleTokens(style: string | undefined): string[] {
|
|
170
252
|
if (!style) return [];
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
253
|
+
const { tokens, fault } = parseStyleQuiet(style);
|
|
254
|
+
if (fault !== null) reportStyleFault(fault, style);
|
|
255
|
+
return tokens;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** `styleTokens` without the diagnostics, for re-reads of a style C parsed once. */
|
|
259
|
+
export function styleTokensQuiet(style: string | undefined): string[] {
|
|
260
|
+
return style ? parseStyleQuiet(style).tokens : [];
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** The name C compares: parse_style stores a token's `(args)` after its NUL. */
|
|
264
|
+
export function styleName(token: string): string {
|
|
265
|
+
const paren = token.indexOf('(');
|
|
266
|
+
return paren < 0 ? token : token.slice(0, paren);
|
|
181
267
|
}
|
|
182
268
|
|
|
183
269
|
/**
|
|
@@ -187,9 +273,9 @@ export function styleTokens(style: string | undefined): string[] {
|
|
|
187
273
|
* @see lib/gvc/gvrender.c:501
|
|
188
274
|
*/
|
|
189
275
|
function parseSetLineWidth(token: string): number | null {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
const v = parseFloat(m[1]);
|
|
276
|
+
if (styleName(token) !== 'setlinewidth') return null;
|
|
277
|
+
const m = /^[^(]*\(([^,)]*)/.exec(token);
|
|
278
|
+
const v = m === null ? Number.NaN : parseFloat(m[1]);
|
|
193
279
|
return Number.isNaN(v) ? 0 : v;
|
|
194
280
|
}
|
|
195
281
|
|
|
@@ -227,29 +313,19 @@ function firstSolidColor(raw: string): string {
|
|
|
227
313
|
/**
|
|
228
314
|
* Parse a graphviz style attribute string into a flag set.
|
|
229
315
|
*
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
* the style list — the observable effect is an empty style list (no flags
|
|
234
|
-
* applied), which we reproduce by returning zeroFlags().
|
|
316
|
+
* Tokenizes like C's parse_style (untrimmed tokens, empty list on a fault) and
|
|
317
|
+
* reports nothing: C's callers parse once and reuse the list. A
|
|
318
|
+
* `setlinewidth(N)` token captures its pen-width argument.
|
|
235
319
|
*
|
|
236
320
|
* @see lib/common/emit.c:4010 parse_style (FUNLIMIT truncation)
|
|
237
321
|
* @see lib/gvc/gvrender.c:481 gvrender_set_style (setlinewidth → penwidth)
|
|
238
322
|
*/
|
|
239
323
|
export function parseStyleFlags(style: string | undefined): PolyStyleFlags {
|
|
240
324
|
const flags = zeroFlags();
|
|
241
|
-
|
|
242
|
-
let fun = 0;
|
|
243
|
-
for (const raw of style.split(',')) {
|
|
244
|
-
const token = raw.trim();
|
|
245
|
-
if (token.length === 0) continue;
|
|
246
|
-
// C truncates at the FUNLIMIT-th token, skipping the pointer-construction
|
|
247
|
-
// loop → the returned list is empty → no flags apply.
|
|
248
|
-
if (fun === FUNLIMIT - 1) return zeroFlags();
|
|
249
|
-
fun++;
|
|
325
|
+
for (const token of styleTokensQuiet(style)) {
|
|
250
326
|
const lw = parseSetLineWidth(token);
|
|
251
327
|
if (lw !== null) { flags.setLineWidth = lw; continue; }
|
|
252
|
-
|
|
328
|
+
applyToken(flags, styleName(token));
|
|
253
329
|
}
|
|
254
330
|
return flags;
|
|
255
331
|
}
|
|
@@ -274,16 +350,7 @@ export function parseStyleFlags(style: string | undefined): PolyStyleFlags {
|
|
|
274
350
|
* @see lib/common/shapes.c:point_gencode (point_style whitelist)
|
|
275
351
|
*/
|
|
276
352
|
export function styleHasInvisibleAlias(style: string | undefined): boolean {
|
|
277
|
-
|
|
278
|
-
let fun = 0;
|
|
279
|
-
for (const raw of style.split(',')) {
|
|
280
|
-
const token = raw.trim();
|
|
281
|
-
if (token.length === 0) continue;
|
|
282
|
-
if (fun === FUNLIMIT - 1) return false;
|
|
283
|
-
fun++;
|
|
284
|
-
if (token === 'invisible') return true;
|
|
285
|
-
}
|
|
286
|
-
return false;
|
|
353
|
+
return styleTokensQuiet(style).some((token) => styleName(token) === 'invisible');
|
|
287
354
|
}
|
|
288
355
|
|
|
289
356
|
export function resolvePenType(flags: PolyStyleFlags): PenType {
|
|
@@ -418,11 +485,10 @@ export function findStopColor(
|
|
|
418
485
|
colorlist: string,
|
|
419
486
|
): { fillColor: string; stopColor: string; frac: number } | null {
|
|
420
487
|
const { segs, error } = parseSegs(colorlist);
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
if (seg0.color === null) return null;
|
|
488
|
+
const seg0 = segs[0];
|
|
489
|
+
const seg1 = segs[1];
|
|
490
|
+
if (error !== 0 || seg0 === undefined || seg1 === undefined || seg0.color === null) return null;
|
|
491
|
+
if (segs.length > 2) agwarningf(MSG_MANY_COLORS);
|
|
426
492
|
const fillColor = seg0.color;
|
|
427
493
|
const stopColor = seg1.color ?? DEFAULT_COLOR;
|
|
428
494
|
const frac = seg0.hasFraction ? seg0.t
|
|
@@ -445,7 +511,9 @@ function buildResolvedFill(
|
|
|
445
511
|
angle: number,
|
|
446
512
|
): ResolvedFill {
|
|
447
513
|
const stop = findStopColor(rawColor);
|
|
448
|
-
|
|
514
|
+
// gvrender_set_fillcolor cuts a colour list at the first ':' (and so does the
|
|
515
|
+
// unknown-colour warning it can raise).
|
|
516
|
+
if (stop === null) return { kind: 'solid', color: rawColor.split(':', 1)[0]! };
|
|
449
517
|
const kind = flags.radial ? 'radial' : 'linear';
|
|
450
518
|
return { kind, fillColor: stop.fillColor, stopColor: stop.stopColor, frac: stop.frac, angle };
|
|
451
519
|
}
|
|
@@ -17,8 +17,9 @@ import {
|
|
|
17
17
|
resolveClusterFillEx, styleTokens,
|
|
18
18
|
} from '../common/style-resolve.js';
|
|
19
19
|
import { gvrenderSetStyle, checkClusterStyleTokens } from './gvrender-set-style.js';
|
|
20
|
-
import { resolveRenderColor,
|
|
20
|
+
import { resolveRenderColor, reportColorNames } from '../render/color-resolve.js';
|
|
21
21
|
import { parseSegs } from '../common/multicolor.js';
|
|
22
|
+
import { graphAttrInherited } from '../common/attr-get.js';
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
25
|
* Copy gradient fields from a linear/radial ResolvedFill onto obj.
|
|
@@ -36,19 +37,14 @@ function applyClusterGradient(
|
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
/**
|
|
39
|
-
* Read a cluster graph-attribute
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* @see lib/common/emit.c:emit_clusters (cluster attrs via agxget)
|
|
40
|
+
* Read a cluster graph-attribute as C's `agget(sg, key)` does: the cluster's
|
|
41
|
+
* own value, else the ancestor defaults snapshotted when `agsubg` opened it.
|
|
42
|
+
* A root `graph[key=...]` declared after the cluster is NOT seen (item 89).
|
|
43
|
+
* @see lib/cgraph/graph.c:agsubg
|
|
44
|
+
* @see lib/common/emit.c:emit_clusters (cluster attrs via agget/agxget)
|
|
45
45
|
*/
|
|
46
46
|
export function clusterAttr(sg: Graph, key: string): string | undefined {
|
|
47
|
-
|
|
48
|
-
const v = g.attrs.get(key);
|
|
49
|
-
if (v !== undefined) return v;
|
|
50
|
-
}
|
|
51
|
-
return undefined;
|
|
47
|
+
return graphAttrInherited(sg, key);
|
|
52
48
|
}
|
|
53
49
|
|
|
54
50
|
/** Cluster `style` with ancestor inheritance. @see clusterAttr */
|
|
@@ -107,15 +103,13 @@ export function applyClusterObjState(sg: Graph, job: RenderJob): boolean {
|
|
|
107
103
|
// emit_clusters: gvrender_set_style(job, checkClusterStyle list) precedes the
|
|
108
104
|
// colour resolution. @see lib/common/emit.c:3808
|
|
109
105
|
gvrenderSetStyle(checkClusterStyleTokens(styleTokens(clusterStyle(sg))));
|
|
110
|
-
//
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
return filled;
|
|
118
|
-
});
|
|
106
|
+
// Runs inside renderCluster's setColorScheme window. @see emit.c:3800/3943
|
|
107
|
+
// C resolves the fill (set_fillcolor / gradient vals) before the pen,
|
|
108
|
+
// which is set just before gvrender_box. @see emit.c:3846-3868 vs 3907-3913
|
|
109
|
+
const filled = applyClusterFill(obj, fillRes);
|
|
110
|
+
applyClusterPenState(obj, sg);
|
|
111
|
+
reportColorNames(stripedClusterColors(sg));
|
|
112
|
+
return filled;
|
|
119
113
|
}
|
|
120
114
|
|
|
121
115
|
/**
|
|
@@ -164,24 +158,22 @@ function stripedClusterColors(sg: Graph): string[] {
|
|
|
164
158
|
export function renderStripedClusterBox(
|
|
165
159
|
sg: Graph, renderer: RendererPlugin, job: RenderJob,
|
|
166
160
|
): void {
|
|
167
|
-
// stripedBox runs inside
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
renderer.polygon(rawPts.map((p) => transformPoint(p, job)), false, job);
|
|
184
|
-
});
|
|
161
|
+
// stripedBox runs inside renderCluster's setColorScheme window (3800/3943).
|
|
162
|
+
const { ll, ur } = sg.info.bb!;
|
|
163
|
+
const af = [
|
|
164
|
+
{ x: ll.x, y: ll.y }, { x: ur.x, y: ll.y }, { x: ur.x, y: ur.y }, { x: ll.x, y: ur.y },
|
|
165
|
+
].map((p) => transformPoint(p, job));
|
|
166
|
+
// stripedBox passes filled=1 (never GRADIENT) to gvrender_polygon; the port's
|
|
167
|
+
// renderers read the gradient from obj.fill, so show them a solid fill.
|
|
168
|
+
const obj = job.obj;
|
|
169
|
+
const savedFill = obj?.fill;
|
|
170
|
+
if (obj !== null && savedFill !== undefined) obj.fill = FillType.Solid;
|
|
171
|
+
stripedBox(job, af, stripedFillSpec(sg), false, renderer, () => `in cluster ${sg.name}`);
|
|
172
|
+
if (obj !== null && savedFill !== undefined) obj.fill = savedFill;
|
|
173
|
+
const rawPts = [
|
|
174
|
+
{ x: ll.x, y: ll.y }, { x: ll.x, y: ur.y }, { x: ur.x, y: ur.y }, { x: ur.x, y: ll.y },
|
|
175
|
+
];
|
|
176
|
+
renderer.polygon(rawPts.map((p) => transformPoint(p, job)), false, job);
|
|
185
177
|
}
|
|
186
178
|
|
|
187
179
|
/** The fill half of {@link applyClusterObjState}; true when the box is filled. */
|
package/src/gvc/device.ts
CHANGED
|
@@ -26,7 +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 {
|
|
29
|
+
import { firstLayer, nextLayer, numPhysicalLayers, validLayer } from './first-layer.js';
|
|
30
30
|
import { polyInit } from '../common/poly-init.js';
|
|
31
31
|
import {
|
|
32
32
|
setHtmlAnchorObj,
|
|
@@ -48,7 +48,7 @@ import {
|
|
|
48
48
|
setRendererKnownColors,
|
|
49
49
|
} from '../render/color-resolve.js';
|
|
50
50
|
import { emitRoundedBezier } from '../common/poly-shapes.js';
|
|
51
|
-
import { applyClusterObjState, clusterStyle, clusterPeripheries, isStripedCluster, renderStripedClusterBox } from './device-cluster.js';
|
|
51
|
+
import { applyClusterObjState, clusterAttr, clusterStyle, clusterPeripheries, isStripedCluster, renderStripedClusterBox } from './device-cluster.js';
|
|
52
52
|
import { transformPoint } from './device-transform.js';
|
|
53
53
|
import { gvrenderSetStyle, edgeStyleResets } from './gvrender-set-style.js';
|
|
54
54
|
import { renderOneLabel, renderNodeXLabel, renderClusterLabel, reportEdgeLabelColors } from './device-labels.js';
|
|
@@ -156,7 +156,7 @@ export function renderNode(n: Node, renderer: RendererPlugin, job: RenderJob, do
|
|
|
156
156
|
* Set the edge pen color/type/width on job.obj from the edge's attrs — mirrors
|
|
157
157
|
* emit_edge_graphics. (AD1: style flows through job.obj, not ad-hoc renderer reads.)
|
|
158
158
|
*/
|
|
159
|
-
function setEdgePen(e: Edge, job: RenderJob): void {
|
|
159
|
+
function setEdgePen(e: Edge, job: RenderJob, styles: readonly string[]): void {
|
|
160
160
|
if (job.obj === null) return;
|
|
161
161
|
const obj = job.obj;
|
|
162
162
|
const flags = parseStyleFlags(e.attrs.get('style'));
|
|
@@ -166,7 +166,7 @@ function setEdgePen(e: Edge, job: RenderJob): void {
|
|
|
166
166
|
obj.penWidth = resolvePenWidth(flags, e.attrs.get('penwidth'));
|
|
167
167
|
// Raw style tokens for xdot's `S` ops (e.g. explicit style="solid"); SVG
|
|
168
168
|
// reads only obj.pen, so this is xdot-only. @see gvrender_core_dot.c xdot_style
|
|
169
|
-
obj.rawStyle =
|
|
169
|
+
obj.rawStyle = [...styles];
|
|
170
170
|
// emit_begin_edge: `if (styles && ED_spl(e)) gvrender_set_style(job, styles)`
|
|
171
171
|
// @see lib/common/emit.c:2726
|
|
172
172
|
if (e.info.spl !== undefined) gvrenderSetStyle(obj.rawStyle);
|
|
@@ -178,13 +178,16 @@ export function renderEdge(e: Edge, renderer: RendererPlugin, job: RenderJob): v
|
|
|
178
178
|
// before emit_begin_edge. @see lib/common/emit.c:emit_edge (style "invis" return)
|
|
179
179
|
// The "invisible" alias likewise suppresses all edge draw ops via PEN_NONE
|
|
180
180
|
// (edges never use the point-shape whitelist). @see lib/gvc/gvrender.c:497
|
|
181
|
+
// emit_edge parses the style once (reporting its faults); the rest reuse the list.
|
|
182
|
+
// @see lib/common/emit.c:2013 emit_edge
|
|
181
183
|
const edgeStyle = e.attrs.get('style');
|
|
184
|
+
const styles = styleTokens(edgeStyle);
|
|
182
185
|
if (parseStyleFlags(edgeStyle).invis || styleHasInvisibleAlias(edgeStyle)) return;
|
|
183
186
|
// push_obj_state in emit_begin_edge (line 2715)
|
|
184
187
|
const obj = createObjState(ObjType.Edge);
|
|
185
188
|
obj.emitState = EmitState.EDraw;
|
|
186
189
|
job.pushObj(obj);
|
|
187
|
-
setEdgePen(e, job);
|
|
190
|
+
setEdgePen(e, job, styles);
|
|
188
191
|
try {
|
|
189
192
|
// @see lib/common/emit.c:emit_begin_edge / getObjId
|
|
190
193
|
setHtmlAnchorObj(svgEdgeId(e, job), labelTextOf(e.info.label), job.obj ?? undefined);
|
|
@@ -202,7 +205,6 @@ export function renderEdge(e: Edge, renderer: RendererPlugin, job: RenderJob): v
|
|
|
202
205
|
reportEdgeColors(e);
|
|
203
206
|
// arrow_gen resets the style; C sets the edge's styles again after each
|
|
204
207
|
// flagged bezier of a multi-bezier spline. @see emit.c:2047, :2677
|
|
205
|
-
const styles = styleTokens(e.attrs.get('style'));
|
|
206
208
|
for (let i = edgeStyleResets(e, styles); i > 0; i--) gvrenderSetStyle(styles);
|
|
207
209
|
reportEdgeLabelColors(e);
|
|
208
210
|
renderer.endEdge(e, job);
|
|
@@ -252,6 +254,41 @@ function renderClusterBox(
|
|
|
252
254
|
renderer.polygon(rawPts.map(p => transformPoint(p, job)), filled, job);
|
|
253
255
|
}
|
|
254
256
|
|
|
257
|
+
/**
|
|
258
|
+
* The body of emit_clusters between setColorScheme and its restore.
|
|
259
|
+
* @see lib/common/emit.c:3800-3943 emit_clusters
|
|
260
|
+
*/
|
|
261
|
+
function drawClusterInScheme(sg: Graph, renderer: RendererPlugin, job: RenderJob): void {
|
|
262
|
+
// Resolve and wire cluster fill/pen into job.obj before the polygon.
|
|
263
|
+
// emit_clusters:3805-3874 — must happen before gvrender_box.
|
|
264
|
+
const filled = applyClusterObjState(sg, job);
|
|
265
|
+
// Cluster id is clust<AGSEQ seq> (getObjId); obj.id feeds emitGradientDefs
|
|
266
|
+
// the "clustN_l_0" gradient prefix. seq is stable on the subgraph, so the
|
|
267
|
+
// begin/assign order below is no longer load-bearing.
|
|
268
|
+
// @see lib/common/emit.c:209 getObjId (AGRAPH/cluster: pfx="clust")
|
|
269
|
+
renderer.beginCluster?.(sg, job);
|
|
270
|
+
if (job.obj !== null) job.obj.id = svgClusterId(sg, job);
|
|
271
|
+
// @see lib/common/emit.c:emit_begin_cluster / getObjId
|
|
272
|
+
setHtmlAnchorObj(svgClusterId(sg, job), labelTextOf(sg.info.label), job.obj ?? undefined);
|
|
273
|
+
// Resolve and open the cluster anchor around the box + label (closed before
|
|
274
|
+
// the cluster's child nodes — sibling of them). @see emit.c:3803.
|
|
275
|
+
if (job.obj !== null) {
|
|
276
|
+
resolveObjAnchor(sg, labelTextOf(sg.info.label), svgClusterId(sg, job), job.obj);
|
|
277
|
+
}
|
|
278
|
+
const anchored = beginAnchorIf(renderer, job);
|
|
279
|
+
// C draws the boundary box only when peripheries != 0, or (peripheries == 0
|
|
280
|
+
// and the cluster is filled). peripheries=0 + unfilled emits nothing.
|
|
281
|
+
// style=invis sets PEN_NONE and gvrender_polygon skips the whole box
|
|
282
|
+
// (fill included); the group/title still emit.
|
|
283
|
+
// @see lib/common/emit.c:3907-3917, lib/gvc/gvrender.c:543
|
|
284
|
+
if ((clusterPeripheries(sg) !== 0 || filled || isStripedCluster(sg)) && job.obj?.pen !== PenType.None) {
|
|
285
|
+
renderClusterBox(sg, filled, renderer, job);
|
|
286
|
+
}
|
|
287
|
+
renderClusterLabel(sg, renderer, job);
|
|
288
|
+
if (anchored) renderer.endAnchor?.(job);
|
|
289
|
+
renderer.endCluster?.(sg, job);
|
|
290
|
+
}
|
|
291
|
+
|
|
255
292
|
/**
|
|
256
293
|
* Render one cluster: box, label, endCluster, then recurse into sub-clusters.
|
|
257
294
|
* push_obj_state in emit_begin_cluster (3762); pop in emit_end_cluster (3774).
|
|
@@ -271,34 +308,10 @@ function renderOneCluster(sg: Graph, renderer: RendererPlugin, job: RenderJob):
|
|
|
271
308
|
obj.emitState = EmitState.CDraw;
|
|
272
309
|
job.pushObj(obj);
|
|
273
310
|
try {
|
|
274
|
-
//
|
|
275
|
-
//
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
// the "clustN_l_0" gradient prefix. seq is stable on the subgraph, so the
|
|
279
|
-
// begin/assign order below is no longer load-bearing.
|
|
280
|
-
// @see lib/common/emit.c:209 getObjId (AGRAPH/cluster: pfx="clust")
|
|
281
|
-
renderer.beginCluster?.(sg, job);
|
|
282
|
-
if (job.obj !== null) job.obj.id = svgClusterId(sg, job);
|
|
283
|
-
// @see lib/common/emit.c:emit_begin_cluster / getObjId
|
|
284
|
-
setHtmlAnchorObj(svgClusterId(sg, job), labelTextOf(sg.info.label), job.obj ?? undefined);
|
|
285
|
-
// Resolve and open the cluster anchor around the box + label (closed before
|
|
286
|
-
// the cluster's child nodes — sibling of them). @see emit.c:3803.
|
|
287
|
-
if (job.obj !== null) {
|
|
288
|
-
resolveObjAnchor(sg, labelTextOf(sg.info.label), svgClusterId(sg, job), job.obj);
|
|
289
|
-
}
|
|
290
|
-
const anchored = beginAnchorIf(renderer, job);
|
|
291
|
-
// C draws the boundary box only when peripheries != 0, or (peripheries == 0
|
|
292
|
-
// and the cluster is filled). peripheries=0 + unfilled emits nothing.
|
|
293
|
-
// style=invis sets PEN_NONE and gvrender_polygon skips the whole box
|
|
294
|
-
// (fill included); the group/title still emit.
|
|
295
|
-
// @see lib/common/emit.c:3907-3917, lib/gvc/gvrender.c:543
|
|
296
|
-
if ((clusterPeripheries(sg) !== 0 || filled || isStripedCluster(sg)) && job.obj?.pen !== PenType.None) {
|
|
297
|
-
renderClusterBox(sg, filled, renderer, job);
|
|
298
|
-
}
|
|
299
|
-
renderClusterLabel(sg, renderer, job);
|
|
300
|
-
if (anchored) renderer.endAnchor?.(job);
|
|
301
|
-
renderer.endCluster?.(sg, job);
|
|
311
|
+
// C opens setColorScheme right after emit_begin_cluster and restores it after
|
|
312
|
+
// emit_label(EMIT_CLABEL): the window spans fill, pen, box and label.
|
|
313
|
+
// @see lib/common/emit.c:3800 / :3943 emit_clusters
|
|
314
|
+
withColorScheme(clusterAttr(sg, 'colorscheme'), () => drawClusterInScheme(sg, renderer, job));
|
|
302
315
|
} finally {
|
|
303
316
|
// pop_obj_state in emit_end_cluster (line 3774), before sub-cluster recursion
|
|
304
317
|
job.popObj();
|
|
@@ -341,18 +354,17 @@ export function renderGraph(g: Graph, job: RenderJob, renderer: RendererPlugin):
|
|
|
341
354
|
job.pushObj(obj);
|
|
342
355
|
try {
|
|
343
356
|
const info = parseLayers(g);
|
|
344
|
-
//
|
|
345
|
-
|
|
346
|
-
const layered = renderer.beginLayer !== undefined && info.numLayers > 1;
|
|
347
|
-
job.numLayers = layered ? info.numLayers : 1;
|
|
357
|
+
// emit_graph sets job->layerNum = 0 before emit_begin_graph.
|
|
358
|
+
job.layerNum = 0;
|
|
348
359
|
job.layerIDs = info.layerIDs;
|
|
349
360
|
renderer.beginGraph(g, job);
|
|
350
|
-
|
|
351
|
-
for (
|
|
352
|
-
|
|
353
|
-
|
|
361
|
+
const cur = firstLayer(job, info, renderer.beginLayer !== undefined);
|
|
362
|
+
for (; validLayer(job); nextLayer(job, cur)) {
|
|
363
|
+
const layered = numPhysicalLayers(job, info) > 1;
|
|
364
|
+
// gvrender_begin_layer/end_layer are no-ops without a plugin callback.
|
|
365
|
+
if (layered) renderer.beginLayer?.(info.layerIDs[job.layerNum]!, job);
|
|
354
366
|
renderPage(g, renderer, job, info);
|
|
355
|
-
if (layered) renderer.endLayer
|
|
367
|
+
if (layered) renderer.endLayer?.(job);
|
|
356
368
|
}
|
|
357
369
|
renderer.endGraph(g, job);
|
|
358
370
|
} finally {
|
package/src/gvc/first-layer.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EPL-2.0
|
|
2
2
|
/**
|
|
3
|
-
* The
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* The layer walk of `emit_graph`: `for (firstlayer; validlayer; nextlayer)`
|
|
4
|
+
* over either every layer or the `layerselect` list, plus the
|
|
5
|
+
* `GVDEVICE_DOES_LAYERS` warning. `GVDEVICE_DOES_LAYERS` is
|
|
6
|
+
* `renderer.beginLayer !== undefined`.
|
|
7
7
|
*/
|
|
8
8
|
import { agwarningf } from '../common/agerr.js';
|
|
9
|
+
import type { LayerInfo } from '../common/layers.js';
|
|
10
|
+
import type { RenderJob } from './job.js';
|
|
9
11
|
|
|
10
12
|
/** Port format ids that spell a native `-T` name differently (`output_langname`). */
|
|
11
13
|
const NATIVE_LANGNAME: ReadonlyMap<string, string> = new Map([
|
|
@@ -13,14 +15,61 @@ const NATIVE_LANGNAME: ReadonlyMap<string, string> = new Map([
|
|
|
13
15
|
['imap-np', 'imap_np'],
|
|
14
16
|
]);
|
|
15
17
|
|
|
18
|
+
/** C's `int **listp`: the selected-layer list and the read position in it. */
|
|
19
|
+
export interface LayerCursor {
|
|
20
|
+
list: number[] | null;
|
|
21
|
+
pos: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function warnUnsupported(job: RenderJob): void {
|
|
25
|
+
agwarningf(`layers not supported in ${NATIVE_LANGNAME.get(job.format) ?? job.format} output`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Number of layers emitted (begin/end layer wrap them only when above one).
|
|
30
|
+
* @see lib/common/emit.c:numPhysicalLayers
|
|
31
|
+
*/
|
|
32
|
+
export function numPhysicalLayers(job: RenderJob, info: LayerInfo): number {
|
|
33
|
+
return info.layerList !== null ? info.layerList[0]! : job.numLayers;
|
|
34
|
+
}
|
|
35
|
+
|
|
16
36
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
37
|
+
* Called after beginGraph, as emit_graph calls firstlayer after
|
|
38
|
+
* emit_begin_graph: for `-Tjson` the nested xdot render in json_begin_graph
|
|
39
|
+
* therefore warns first. With a list, a device without layers keeps only the
|
|
40
|
+
* first entry by overwriting the second with the sentinel (as C does, in place).
|
|
21
41
|
* @see lib/common/emit.c:firstlayer
|
|
22
42
|
*/
|
|
23
|
-
export function
|
|
24
|
-
|
|
25
|
-
|
|
43
|
+
export function firstLayer(job: RenderJob, info: LayerInfo, doesLayers: boolean): LayerCursor {
|
|
44
|
+
job.numLayers = info.numLayers;
|
|
45
|
+
const list = info.layerList;
|
|
46
|
+
if (list !== null) {
|
|
47
|
+
if (list[0]! > 1 && !doesLayers) {
|
|
48
|
+
warnUnsupported(job);
|
|
49
|
+
list[2] = job.numLayers + 1; // only one layer printed
|
|
50
|
+
}
|
|
51
|
+
job.layerNum = list[1]!;
|
|
52
|
+
return { list, pos: 2 };
|
|
53
|
+
}
|
|
54
|
+
if (job.numLayers > 1 && !doesLayers) {
|
|
55
|
+
warnUnsupported(job);
|
|
56
|
+
job.numLayers = 1;
|
|
57
|
+
}
|
|
58
|
+
job.layerNum = 1;
|
|
59
|
+
return { list: null, pos: 0 };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** @see lib/common/emit.c:validlayer */
|
|
63
|
+
export function validLayer(job: RenderJob): boolean {
|
|
64
|
+
return job.layerNum <= job.numLayers;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** @see lib/common/emit.c:nextlayer */
|
|
68
|
+
export function nextLayer(job: RenderJob, cur: LayerCursor): void {
|
|
69
|
+
if (cur.list !== null) {
|
|
70
|
+
job.layerNum = cur.list[cur.pos]!;
|
|
71
|
+
cur.pos++;
|
|
72
|
+
} else {
|
|
73
|
+
job.layerNum++;
|
|
74
|
+
}
|
|
26
75
|
}
|