@knowvah/dot-engine 2.3.0 → 2.4.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/common/arith.d.ts +12 -0
- package/dist/common/arith.d.ts.map +1 -1
- package/dist/common/edge-in-box.d.ts +31 -0
- package/dist/common/edge-in-box.d.ts.map +1 -0
- package/dist/common/htmltable-emit.d.ts +1 -1
- package/dist/common/htmltable-emit.d.ts.map +1 -1
- package/dist/common/make-label.d.ts +3 -1
- package/dist/common/make-label.d.ts.map +1 -1
- package/dist/common/poly-gencode.d.ts +12 -1
- package/dist/common/poly-gencode.d.ts.map +1 -1
- package/dist/gvc/device.d.ts.map +1 -1
- package/dist/gvc/emit-gates.d.ts +35 -0
- package/dist/gvc/emit-gates.d.ts.map +1 -0
- package/dist/gvc/job.d.ts +4 -0
- package/dist/gvc/job.d.ts.map +1 -1
- package/dist/gvc/viewport.d.ts +13 -0
- package/dist/gvc/viewport.d.ts.map +1 -1
- package/dist/index.js +1030 -742
- package/dist/index.js.map +4 -4
- package/dist/layout/circo/blockpath.d.ts +1 -38
- package/dist/layout/circo/blockpath.d.ts.map +1 -1
- package/dist/layout/circo/crossings.d.ts +59 -0
- package/dist/layout/circo/crossings.d.ts.map +1 -0
- package/dist/layout/dot/edge-route-chain.d.ts +5 -3
- package/dist/layout/dot/edge-route-chain.d.ts.map +1 -1
- package/dist/layout/dot/edge-route.d.ts.map +1 -1
- package/dist/layout/dot/index.d.ts +10 -2
- package/dist/layout/dot/index.d.ts.map +1 -1
- package/dist/layout/dot/ortho-adapter.d.ts +7 -3
- package/dist/layout/dot/ortho-adapter.d.ts.map +1 -1
- package/dist/layout/dot/pack-components.d.ts +4 -1
- package/dist/layout/dot/pack-components.d.ts.map +1 -1
- package/dist/layout/dot/splines-flat-multi.d.ts +7 -5
- package/dist/layout/dot/splines-flat-multi.d.ts.map +1 -1
- package/dist/layout/dot/splines-flat.d.ts +2 -13
- package/dist/layout/dot/splines-flat.d.ts.map +1 -1
- package/dist/layout/dot/splines-route.d.ts.map +1 -1
- package/dist/layout/neato/constraint-adjust.d.ts +32 -0
- package/dist/layout/neato/constraint-adjust.d.ts.map +1 -1
- package/dist/layout/neato/multispline.d.ts.map +1 -1
- package/dist/layout/neato/splines.d.ts.map +1 -1
- package/dist/layout/pack/poly-place.d.ts.map +1 -1
- package/dist/layout/sfdp/__fixtures__/pcp-rotate-native.d.ts +23 -0
- package/dist/layout/sfdp/__fixtures__/pcp-rotate-native.d.ts.map +1 -0
- package/dist/layout/sfdp/spring-driver.d.ts +6 -0
- package/dist/layout/sfdp/spring-driver.d.ts.map +1 -1
- package/dist/ortho/index.d.ts +4 -1
- package/dist/ortho/index.d.ts.map +1 -1
- package/dist/ortho/maze.d.ts +5 -2
- package/dist/ortho/maze.d.ts.map +1 -1
- package/dist/ortho/partition.d.ts +8 -1
- package/dist/ortho/partition.d.ts.map +1 -1
- package/dist/ortho/trap-segment.d.ts +2 -1
- package/dist/ortho/trap-segment.d.ts.map +1 -1
- package/dist/ortho/trapezoid.d.ts.map +1 -1
- package/dist/parser/index.d.ts +0 -7
- package/dist/parser/index.d.ts.map +1 -1
- package/dist/parser/number-scan.d.ts +31 -0
- package/dist/parser/number-scan.d.ts.map +1 -0
- package/dist/pathplan/route.d.ts.map +1 -1
- package/dist/pathplan/solvers.d.ts +15 -3
- package/dist/pathplan/solvers.d.ts.map +1 -1
- package/dist/render/dot/edge-draw.d.ts +12 -0
- package/dist/render/dot/edge-draw.d.ts.map +1 -1
- package/dist/render/svg-graph.d.ts +6 -5
- package/dist/render/svg-graph.d.ts.map +1 -1
- package/dist/render/svg.d.ts +2 -21
- package/dist/render/svg.d.ts.map +1 -1
- package/dist/render.js +1030 -742
- package/dist/render.js.map +4 -4
- package/dist/xdot/parse.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/common/arith.ts +25 -0
- package/src/common/edge-in-box.ts +54 -0
- package/src/common/graph-init.ts +3 -3
- package/src/common/htmltable-emit.ts +4 -2
- package/src/common/make-label.ts +3 -1
- package/src/common/poly-gencode.ts +18 -4
- package/src/gvc/device.ts +8 -41
- package/src/gvc/edge-labels.ts +30 -13
- package/src/gvc/emit-gates.ts +89 -0
- package/src/gvc/job.ts +4 -0
- package/src/gvc/viewport.ts +29 -0
- package/src/layout/circo/blockpath.ts +6 -125
- package/src/layout/circo/crossings.ts +171 -0
- package/src/layout/dot/__fixtures__/2784.dot +1 -0
- package/src/layout/dot/__fixtures__/2784.native-16.1.svg +48 -0
- package/src/layout/dot/edge-route-chain.ts +11 -5
- package/src/layout/dot/edge-route.ts +19 -8
- package/src/layout/dot/index.ts +14 -4
- package/src/layout/dot/ortho-adapter.ts +11 -6
- package/src/layout/dot/pack-components.ts +11 -4
- package/src/layout/dot/splines-flat-multi.ts +21 -8
- package/src/layout/dot/splines-flat.ts +8 -35
- package/src/layout/dot/splines-route.ts +10 -2
- package/src/layout/dot/splines.ts +6 -6
- package/src/layout/neato/constraint-adjust.ts +10 -4
- package/src/layout/neato/multispline.ts +26 -9
- package/src/layout/neato/splines.ts +7 -7
- package/src/layout/pack/poly-place.ts +9 -2
- package/src/layout/sfdp/__fixtures__/pcp-rotate-native.ts +94 -0
- package/src/layout/sfdp/spring-driver.ts +45 -13
- package/src/ortho/index.ts +22 -13
- package/src/ortho/maze.ts +8 -2
- package/src/ortho/partition.ts +17 -2
- package/src/ortho/trap-segment.ts +47 -24
- package/src/ortho/trapezoid.ts +24 -15
- package/src/parser/dot.js +345 -380
- package/src/parser/dot.pegjs +25 -10
- package/src/parser/index.ts +15 -0
- package/src/parser/number-scan.ts +118 -0
- package/src/pathplan/route.ts +79 -34
- package/src/pathplan/solvers.ts +78 -50
- package/src/render/dot/edge-draw.ts +25 -0
- package/src/render/svg-graph.ts +77 -47
- package/src/render/svg-helpers.ts +2 -2
- package/src/render/svg.ts +4 -41
- package/src/xdot/parse.ts +19 -5
package/dist/xdot/parse.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../../src/xdot/parse.ts"],"names":[],"mappings":"AACA;;;;;GAKG;AAEH,OAAO,KAAK,EACV,IAAI,EAAU,QAAQ,EAAE,YAAY,EACf,SAAS,EACC,WAAW,EAC3C,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../../src/xdot/parse.ts"],"names":[],"mappings":"AACA;;;;;GAKG;AAEH,OAAO,KAAK,EACV,IAAI,EAAU,QAAQ,EAAE,YAAY,EACf,SAAS,EACC,WAAW,EAC3C,MAAM,YAAY,CAAC;AAMpB,KAAK,EAAE,CAAC,CAAC,IAAI;IAAE,GAAG,EAAE,CAAC,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC;AA+U5C,wBAAgB,SAAS,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,EAAE,CAAC,QAAQ,CAAC,CAAyC;AACxG,wBAAgB,aAAa,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,EAAE,CAAC,YAAY,CAAC,CAA6C;AACpH,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,EAAE,CAAC,MAAM,CAAC,CAA2C;AAC1G,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,EAAE,CAAC,SAAS,CAAC,CAAgD;AACvH,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,WAAW,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAE5G;AACD,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,WAAW,CAAC,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAE1F;AACD,wBAAgB,SAAS,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAAoC;AACrF,wBAAgB,cAAc,CAAC,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAAyC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@knowvah/dot-engine",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "Faithful TypeScript port of Graphviz (dot, neato, fdp, sfdp, circo, twopi, osage, patchwork) — no C: no native binary, no WASM. Oracle-verified against the C implementation, runs in the browser.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"graphviz",
|
package/src/common/arith.ts
CHANGED
|
@@ -38,6 +38,31 @@ export function cround(v: number): number {
|
|
|
38
38
|
return v >= 0 ? Math.floor(v + 0.5) : Math.ceil(v - 0.5);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
const INT64_MAX = 2n ** 63n - 1n;
|
|
42
|
+
const INT64_MIN = -(2n ** 63n);
|
|
43
|
+
const TWO_POW_63 = 2 ** 63;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* `ROUND(f)` as the native arm64 build computes it where clang vectorises the
|
|
47
|
+
* conversion: `fcvtzs.2d` (double → int64, saturating, NaN → 0) followed by
|
|
48
|
+
* `xtn.2s` / `uzp1.4s`, which keep the LOW 32 bits. The `(int)` cast of an
|
|
49
|
+
* out-of-range double is undefined in C; this is the oracle binary's
|
|
50
|
+
* behaviour (objdump of libgvc `_gvRenderJobs`, the inlined
|
|
51
|
+
* init_job_pagination: job->width/height and pageBoundingBox). Identical to
|
|
52
|
+
* {@link cround} whenever the result fits in an int; only these vectorised
|
|
53
|
+
* sites use it — scalar ROUND sites compile to `fcvtzs w` (saturate at int32).
|
|
54
|
+
* @see lib/common/emit.c:init_job_pagination (job->width, pageBoundingBox)
|
|
55
|
+
*/
|
|
56
|
+
export function croundNarrow32(v: number): number {
|
|
57
|
+
const r = cround(v);
|
|
58
|
+
let i64: bigint;
|
|
59
|
+
if (Number.isNaN(r)) i64 = 0n;
|
|
60
|
+
else if (r >= TWO_POW_63) i64 = INT64_MAX;
|
|
61
|
+
else if (r < -TWO_POW_63) i64 = INT64_MIN;
|
|
62
|
+
else i64 = BigInt(r);
|
|
63
|
+
return Number(BigInt.asIntN(32, i64));
|
|
64
|
+
}
|
|
65
|
+
|
|
41
66
|
/**
|
|
42
67
|
* Degrees to radians in C's operation order, `(deg)/180.0 * M_PI`.
|
|
43
68
|
* `deg * Math.PI / 180` rounds differently for some angles (e.g. 33°).
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* emit_edge's edge_in_box gate, shared by the device walk (edge comments) and
|
|
5
|
+
* the SVG / xdot renderers (edge group and draw ops). Moved out of
|
|
6
|
+
* render/svg.ts unchanged.
|
|
7
|
+
*
|
|
8
|
+
* @see lib/common/emit.c:edge_in_box, lib/common/utils.c:overlap_label
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { Box } from '../model/geom.js';
|
|
12
|
+
import { boxOverlap } from '../model/geom.js';
|
|
13
|
+
import type { Edge } from '../model/edge.js';
|
|
14
|
+
import type { TextlabelT } from './types.js';
|
|
15
|
+
|
|
16
|
+
/** overlap_label: does the label's box (pos ± dimen/2) overlap clip box b?
|
|
17
|
+
* @see lib/common/utils.c:overlap_label */
|
|
18
|
+
function overlapLabel(lp: TextlabelT, b: Box): boolean {
|
|
19
|
+
const sx = lp.dimen.x / 2;
|
|
20
|
+
const sy = lp.dimen.y / 2;
|
|
21
|
+
const bb: Box = {
|
|
22
|
+
ll: { x: lp.pos.x - sx, y: lp.pos.y - sy },
|
|
23
|
+
ur: { x: lp.pos.x + sx, y: lp.pos.y + sy },
|
|
24
|
+
};
|
|
25
|
+
return boxOverlap(b, bb);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Faithful-to-outcome `edge_in_box`: true when the edge has something to draw.
|
|
30
|
+
* C's gvrender defers the `<g>` until a draw op, so an edge whose content falls
|
|
31
|
+
* entirely outside the clip emits no group (emit.c:edge_in_box tests each piece
|
|
32
|
+
* vs job->clip via boxf_overlap/overlap_label).
|
|
33
|
+
*
|
|
34
|
+
* The only piece that can legitimately fall OUTSIDE the clip on this corpus is a
|
|
35
|
+
* degenerate labeled-flat label: it is spline-less, so map_edge leaves it at its
|
|
36
|
+
* internal (un-normalized) x-frame position while the clip is the final frame.
|
|
37
|
+
* The `overlapLabel` test on the main label reproduces C's draw-iff-overlap for
|
|
38
|
+
* exactly that case — on-canvas (2368) → drawn; off-canvas (2368_1, negative
|
|
39
|
+
* un-normalized x) → suppressed. A present spline always lies within the clip
|
|
40
|
+
* (GD_bb contains it), and head/tail/xlabels are positioned at on-canvas
|
|
41
|
+
* endpoints, so those keep their existing unconditional triggers — adding an
|
|
42
|
+
* overlap test there only risks suppressing legitimately-drawn content (it
|
|
43
|
+
* regressed neato/circo edges whose spl.bb is computed differently). The `set`
|
|
44
|
+
* guard on the main label is kept: the port retains an unplaced (`pos=0,0`,
|
|
45
|
+
* `set=false`) label on merged back-edges where C has none.
|
|
46
|
+
* @see lib/common/emit.c:edge_in_box, lib/common/utils.c:overlap_label
|
|
47
|
+
*/
|
|
48
|
+
export function edgeHasDrawableContent(e: Edge, clip: Box): boolean {
|
|
49
|
+
const i = e.info;
|
|
50
|
+
return i.spl !== undefined
|
|
51
|
+
|| (i.label !== undefined && i.label.set && overlapLabel(i.label, clip))
|
|
52
|
+
|| i.xlabel !== undefined
|
|
53
|
+
|| i.head_label !== undefined || i.tail_label !== undefined;
|
|
54
|
+
}
|
package/src/common/graph-init.ts
CHANGED
|
@@ -264,9 +264,9 @@ export function graphInit(g: Graph, useRankdir: boolean): void {
|
|
|
264
264
|
// allocated unconditionally; see parseRatioDrawing's note (it would change
|
|
265
265
|
// dot output by activating the unported ratio modes).
|
|
266
266
|
|
|
267
|
-
//
|
|
268
|
-
//
|
|
269
|
-
// port
|
|
267
|
+
// `postaction` reparse: removed upstream in 16.1.0 (41e482146 reverted the
|
|
268
|
+
// feature and dropped agmemconcat), so graph_init no longer reads it. The
|
|
269
|
+
// port never implemented it; nothing to mirror.
|
|
270
270
|
|
|
271
271
|
// SKIPPED — fontpath / DOTFONTPATH setenv (input.c:619-629): browser-
|
|
272
272
|
// impossible (no environment, no GDFONTPATH).
|
|
@@ -330,7 +330,7 @@ function emitTableRules(
|
|
|
330
330
|
* Emit a fully positioned HTML table label.
|
|
331
331
|
*
|
|
332
332
|
* Draw order mirrors C emit_html_tbl:
|
|
333
|
-
* 1. Open anchor 2. Table BGCOLOR 3.
|
|
333
|
+
* 1. Open anchor 2. Table BGCOLOR 3. Rules 4. Border 5. Cells 6. Close anchor
|
|
334
334
|
*
|
|
335
335
|
* @see lib/common/htmltable.c:emit_html_tbl
|
|
336
336
|
*/
|
|
@@ -360,11 +360,13 @@ export function emitHtmlLabel(
|
|
|
360
360
|
emitBgFill({ bgcolor: placed.bgcolor, box: placed.box, pos, border: placed.border,
|
|
361
361
|
renderer, job, gradientangle: placed.gradientangle, style: placed.style });
|
|
362
362
|
}
|
|
363
|
-
|
|
363
|
+
// Graphviz 16.1 (f0d07786b): rules and border precede the cells so cell
|
|
364
|
+
// text is painted above table-level shapes.
|
|
364
365
|
emitTableRules(placed, pos, renderer, job);
|
|
365
366
|
if (placed.border > 0) {
|
|
366
367
|
emitBorder({ box: placed.box, pos, border: placed.border, color: placed.color, sides: placed.sides, style: placed.style }, renderer, job);
|
|
367
368
|
}
|
|
369
|
+
for (const cell of placed.cells) emitHtmlCell(cell, pos, renderer, job);
|
|
368
370
|
}
|
|
369
371
|
if (inAnchor) endHtmlAnchor(renderer, job);
|
|
370
372
|
if (clustersLast && initHtmlAnchor(anchorData, tblBox, renderer, job)) {
|
package/src/common/make-label.ts
CHANGED
|
@@ -19,7 +19,9 @@ import type { Graph } from '../model/graph.js';
|
|
|
19
19
|
import { htmlEntityUTF8 } from './html-entities.js';
|
|
20
20
|
|
|
21
21
|
export const DEFAULT_FONTSIZE = 14.0;
|
|
22
|
-
|
|
22
|
+
/** C's default fontname (non-Windows); renderers alias it, e.g. SVG
|
|
23
|
+
* font-family="Times,serif". @see lib/common/const.h:67 */
|
|
24
|
+
export const DEFAULT_FONTNAME = 'Times-Roman';
|
|
23
25
|
export const DEFAULT_COLOR = 'black';
|
|
24
26
|
|
|
25
27
|
// htmlEntityUTF8 moved to html-entities.ts (a leaf module) so the HTML lexer
|
|
@@ -53,7 +53,18 @@ function isMulticolor(fillcolor: string): boolean {
|
|
|
53
53
|
// Periphery ring renderers
|
|
54
54
|
// ---------------------------------------------------------------------------
|
|
55
55
|
|
|
56
|
-
/**
|
|
56
|
+
/**
|
|
57
|
+
* Render one ellipse periphery ring for a node. poly_gencode hands
|
|
58
|
+
* gvrender_ellipse the two ABSOLUTE corners AF = vertex + ND_coord; it passes
|
|
59
|
+
* the renderer the centre mid_pointf(AF[0], AF[1]) and the corner AF[1], and
|
|
60
|
+
* the renderer prints rx/ry as corner - centre. Computing the radii in that
|
|
61
|
+
* order (not from the relative vertices) keeps C's rounding: at |coord| where
|
|
62
|
+
* the double spacing exceeds the radius, coord ± r rounds back to coord and the
|
|
63
|
+
* radius collapses to 0 (corpus 2784, y = 1.08e18).
|
|
64
|
+
* @see lib/common/shapes.c:poly_gencode (AF[i] = P + ND_coord, gvrender_ellipse)
|
|
65
|
+
* @see lib/gvc/gvrender.c:gvrender_ellipse (af[0] = mid_pointf, af[1] = pf[1])
|
|
66
|
+
* @see plugin/core/gvrender_core_svg.c:svg_ellipse (rx = A[1].x - A[0].x)
|
|
67
|
+
*/
|
|
57
68
|
export function renderEllipse(
|
|
58
69
|
ring: Point[],
|
|
59
70
|
coord: Point,
|
|
@@ -61,9 +72,12 @@ export function renderEllipse(
|
|
|
61
72
|
job: RenderJob,
|
|
62
73
|
filled: boolean,
|
|
63
74
|
): void {
|
|
64
|
-
const
|
|
65
|
-
const
|
|
66
|
-
const
|
|
75
|
+
const af0 = { x: ring[0]!.x + coord.x, y: ring[0]!.y + coord.y };
|
|
76
|
+
const af1 = { x: ring[1]!.x + coord.x, y: ring[1]!.y + coord.y };
|
|
77
|
+
const mid = { x: (af0.x + af1.x) / 2.0, y: (af0.y + af1.y) / 2.0 };
|
|
78
|
+
const rx = af1.x - mid.x;
|
|
79
|
+
const ry = af1.y - mid.y;
|
|
80
|
+
const center = transformPoint(mid, job);
|
|
67
81
|
renderer.ellipse(center, rx, ry, filled, job);
|
|
68
82
|
}
|
|
69
83
|
|
package/src/gvc/device.ts
CHANGED
|
@@ -12,10 +12,9 @@
|
|
|
12
12
|
* @see lib/gvc/gvdevice.c
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import
|
|
16
|
-
import { boxOverlap, POINTS_PER_INCH } from '../model/geom.js';
|
|
15
|
+
import { POINTS_PER_INCH } from '../model/geom.js';
|
|
17
16
|
import { lateDouble } from '../common/nodeinit.js';
|
|
18
|
-
import { parseDrawingSize, initJobViewportZoom, parseLandscape, parseGraphPad, parseGraphMargin } from './viewport.js';
|
|
17
|
+
import { parseDrawingSize, initJobViewportZoom, parseLandscape, parseGraphPad, parseGraphMargin, setupPageClip } from './viewport.js';
|
|
19
18
|
import type { Graph } from '../model/graph.js';
|
|
20
19
|
import type { Node } from '../model/node.js';
|
|
21
20
|
import type { Edge } from '../model/edge.js';
|
|
@@ -51,6 +50,7 @@ import { renderOneLabel, renderNodeXLabel, renderClusterLabel } from './device-l
|
|
|
51
50
|
export { transformPoint, applyRotation, applyScale, buildPoint } from './device-transform.js';
|
|
52
51
|
export { renderOneLabel, renderNodeXLabel, renderClusterLabel } from './device-labels.js';
|
|
53
52
|
import { svgNodeId, svgEdgeId, svgClusterId, svgGraphId } from '../render/svg-id.js';
|
|
53
|
+
import { nodeInBox, emitNodeComments, emitEdgeComments } from './emit-gates.js';
|
|
54
54
|
|
|
55
55
|
// ---------------------------------------------------------------------------
|
|
56
56
|
// AD-1 (image-api): `inlineImages` on RenderJob via module augmentation
|
|
@@ -78,43 +78,6 @@ function labelTextOf(lp: unknown): string | null {
|
|
|
78
78
|
return (lp as TextlabelT | undefined)?.text ?? null;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
/**
|
|
82
|
-
* A node's own layout bounding box: coord ± (lw/rw, ht/2).
|
|
83
|
-
* @see lib/common/types.h:ND_bb
|
|
84
|
-
* @see lib/common/emit.c:4140-4143 init_bb_node
|
|
85
|
-
*/
|
|
86
|
-
function nodeBBox(n: Node): Box {
|
|
87
|
-
const { coord, lw, rw, ht } = n.info;
|
|
88
|
-
return {
|
|
89
|
-
ll: { x: coord.x - lw, y: coord.y - ht / 2 },
|
|
90
|
-
ur: { x: coord.x + rw, y: coord.y + ht / 2 },
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* node_in_box: is the node's own bbox within job's page/view clip?
|
|
96
|
-
*
|
|
97
|
-
* C's `job->clip` is derived from `job->bb` (= `GD_bb(g)`) expanded by
|
|
98
|
-
* `job->pad` on every side (init_job_viewport's `job->bb = gvc->bb ±
|
|
99
|
-
* job->pad`, feeding `job->view`/`job->focus`, which setup_page's `job->clip`
|
|
100
|
-
* algebraically reduces back to for the single-page, unpaginated case this
|
|
101
|
-
* port implements — Z and rotation cancel out of that derivation). This port
|
|
102
|
-
* has no separate `page=` pagination model, so `job.bb` padded by `job.pad`
|
|
103
|
-
* IS `job->clip` here.
|
|
104
|
-
*
|
|
105
|
-
* @see lib/common/emit.c:1636-1639 node_in_box
|
|
106
|
-
* @see lib/common/emit.c:1808 emit_node gate
|
|
107
|
-
* @see lib/common/emit.c:3356 init_job_viewport (job->bb = gvc->bb ± pad)
|
|
108
|
-
* @see lib/common/emit.c:1553-1564 setup_page (job->clip from focus/view)
|
|
109
|
-
*/
|
|
110
|
-
function nodeInBox(n: Node, job: RenderJob): boolean {
|
|
111
|
-
const clip: Box = {
|
|
112
|
-
ll: { x: job.bb.ll.x - job.pad.x, y: job.bb.ll.y - job.pad.y },
|
|
113
|
-
ur: { x: job.bb.ur.x + job.pad.x, y: job.bb.ur.y + job.pad.y },
|
|
114
|
-
};
|
|
115
|
-
return boxOverlap(nodeBBox(n), clip);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
81
|
/**
|
|
119
82
|
* Inner body of renderNode — runs inside the push/pop try block.
|
|
120
83
|
* Extracted to keep renderNode under the 30-line hook limit.
|
|
@@ -150,6 +113,7 @@ export function renderNode(n: Node, renderer: RendererPlugin, job: RenderJob, do
|
|
|
150
113
|
done.add(n);
|
|
151
114
|
// "and is in page/view" gate. @see lib/common/emit.c:1808 node_in_box
|
|
152
115
|
if (!nodeInBox(n, job)) return;
|
|
116
|
+
emitNodeComments(n, renderer, job); // before the invis shortcircuit, as C
|
|
153
117
|
// Shortcircuit invisible nodes: C omits the whole node before emit_begin_node.
|
|
154
118
|
// C reads late_string(n, N_style, ""), i.e. agxget — which resolves the
|
|
155
119
|
// node's DECLARATION-SCOPE default, so `{ node [style=invis] p q }` hides p
|
|
@@ -201,6 +165,7 @@ function setEdgePen(e: Edge, job: RenderJob): void {
|
|
|
201
165
|
}
|
|
202
166
|
|
|
203
167
|
export function renderEdge(e: Edge, renderer: RendererPlugin, job: RenderJob): void {
|
|
168
|
+
emitEdgeComments(e, renderer, job); // before the invis shortcircuit, as C
|
|
204
169
|
// Shortcircuit invisible edges: C omits the whole edge (no group/title/path)
|
|
205
170
|
// before emit_begin_edge. @see lib/common/emit.c:emit_edge (style "invis" return)
|
|
206
171
|
// The "invisible" alias likewise suppresses all edge draw ops via PEN_NONE
|
|
@@ -467,7 +432,9 @@ export function render(ctx: GvcContext, g: Graph, format: string, inlineImages =
|
|
|
467
432
|
// altering the ptf path, carry Z in job.scale instead — which is exactly what
|
|
468
433
|
// C's SVG group emits (svg_begin_page prints job->scale.x/y = zoom*dpi/72),
|
|
469
434
|
// not job->zoom. job.scale is read only by the SVG group/dims.
|
|
470
|
-
const
|
|
435
|
+
const drawingSize = parseDrawingSize(g.attrs.get('size'));
|
|
436
|
+
const z = initJobViewportZoom(job.bb, drawingSize, job.pad);
|
|
437
|
+
job.clip = setupPageClip(job.bb, drawingSize, job.pad, z); // @see emit.c:setup_page
|
|
471
438
|
// init_job_dpi (emit.c:3333): GD_drawing->dpi = late_double(dpi, resolution, 0)
|
|
472
439
|
// (input.c:713). When >0 it becomes job->dpi; otherwise the SVG device's
|
|
473
440
|
// default_dpi = 72 (gvrender_core_svg.c:814) applies. init_job_viewport then
|
package/src/gvc/edge-labels.ts
CHANGED
|
@@ -77,19 +77,36 @@ function emitEdgeLabel(
|
|
|
77
77
|
emitState?: EmitState,
|
|
78
78
|
): void {
|
|
79
79
|
if (!lp?.set) return; // emit_edge_label: lbl == NULL || !lbl->set
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
80
|
+
withObjEmitState(job, emitState ?? EmitState.ELabel, () => {
|
|
81
|
+
const open = hs.url !== null || hs.explicit;
|
|
82
|
+
if (open) openAnchorWith(renderer, job, hs.url, hs.tooltip, hs.target, id);
|
|
83
|
+
// Head/tail labels route to EMIT_HLABEL/EMIT_TLABEL (→ _hldraw_/_tldraw_);
|
|
84
|
+
// the center label keeps its default EMIT_ELABEL. @see emit.c:emit_edge
|
|
85
|
+
const savedOverride = job.labelEmitOverride;
|
|
86
|
+
if (emitState !== undefined) job.labelEmitOverride = emitState;
|
|
87
|
+
try {
|
|
88
|
+
renderOneLabel(lp, renderer, job);
|
|
89
|
+
} finally {
|
|
90
|
+
job.labelEmitOverride = savedOverride;
|
|
91
|
+
}
|
|
92
|
+
if (spl) emitAttachment(lp, spl, renderer, job); // emit.c:2918
|
|
93
|
+
if (open) renderer.endAnchor?.(job);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* emit_edge_label's emit-state window: C sets obj->emit_state = lkind before
|
|
99
|
+
* the anchor/label/attachment and restores the old state at the end, so the
|
|
100
|
+
* decorate attachment polyline lands in the label xbuf (_ldraw_), not _draw_.
|
|
101
|
+
* @see lib/common/emit.c:emit_edge_label (2909-2910, 2925)
|
|
102
|
+
*/
|
|
103
|
+
function withObjEmitState(job: RenderJob, lkind: EmitState, fn: () => void): void {
|
|
104
|
+
const obj = job.obj;
|
|
105
|
+
if (obj === null) { fn(); return; }
|
|
106
|
+
const oldEmitState = obj.emitState;
|
|
107
|
+
obj.emitState = lkind;
|
|
108
|
+
fn();
|
|
109
|
+
obj.emitState = oldEmitState;
|
|
93
110
|
}
|
|
94
111
|
|
|
95
112
|
/**
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* emit_node / emit_edge preambles: the node_in_box gate and the
|
|
5
|
+
* gvrender_comment calls C makes before an object's style=invis shortcircuit.
|
|
6
|
+
* Split from device.ts to keep that file under the size cap.
|
|
7
|
+
*
|
|
8
|
+
* @see lib/common/emit.c:emit_node, emit_edge
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { Box } from '../model/geom.js';
|
|
12
|
+
import { boxOverlap } from '../model/geom.js';
|
|
13
|
+
import type { Node } from '../model/node.js';
|
|
14
|
+
import type { Edge } from '../model/edge.js';
|
|
15
|
+
import type { RendererPlugin } from './context.js';
|
|
16
|
+
import type { RenderJob } from './job.js';
|
|
17
|
+
import { nodeAttr } from '../common/poly-init.js';
|
|
18
|
+
import { edgeHasDrawableContent } from '../common/edge-in-box.js';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A node's own layout bounding box: coord ± (lw/rw, ht/2).
|
|
22
|
+
* @see lib/common/types.h:ND_bb
|
|
23
|
+
* @see lib/common/emit.c:4140-4143 init_bb_node
|
|
24
|
+
*/
|
|
25
|
+
function nodeBBox(n: Node): Box {
|
|
26
|
+
const { coord, lw, rw, ht } = n.info;
|
|
27
|
+
return {
|
|
28
|
+
ll: { x: coord.x - lw, y: coord.y - ht / 2 },
|
|
29
|
+
ur: { x: coord.x + rw, y: coord.y + ht / 2 },
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* node_in_box: is the node's own bbox within job's page/view clip?
|
|
35
|
+
*
|
|
36
|
+
* C's `job->clip` is derived from `job->bb` (= `GD_bb(g)`) expanded by
|
|
37
|
+
* `job->pad` on every side (init_job_viewport's `job->bb = gvc->bb ±
|
|
38
|
+
* job->pad`, feeding `job->view`/`job->focus`, which setup_page's `job->clip`
|
|
39
|
+
* algebraically reduces back to for the single-page, unpaginated case this
|
|
40
|
+
* port implements — Z and rotation cancel out of that derivation). This port
|
|
41
|
+
* has no separate `page=` pagination model, so `job.bb` padded by `job.pad`
|
|
42
|
+
* IS `job->clip` here.
|
|
43
|
+
*
|
|
44
|
+
* @see lib/common/emit.c:1636-1639 node_in_box
|
|
45
|
+
* @see lib/common/emit.c:1808 emit_node gate
|
|
46
|
+
* @see lib/common/emit.c:3356 init_job_viewport (job->bb = gvc->bb ± pad)
|
|
47
|
+
* @see lib/common/emit.c:1553-1564 setup_page (job->clip from focus/view)
|
|
48
|
+
*/
|
|
49
|
+
export function nodeInBox(n: Node, job: RenderJob): boolean {
|
|
50
|
+
const clip: Box = {
|
|
51
|
+
ll: { x: job.bb.ll.x - job.pad.x, y: job.bb.ll.y - job.pad.y },
|
|
52
|
+
ur: { x: job.bb.ur.x + job.pad.x, y: job.bb.ur.y + job.pad.y },
|
|
53
|
+
};
|
|
54
|
+
return boxOverlap(nodeBBox(n), clip);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* gvrender_comment: pass a non-empty string to the renderer's comment hook
|
|
59
|
+
* (SVG is the only ported renderer that has one).
|
|
60
|
+
* @see lib/gvc/gvrender.c:gvrender_comment
|
|
61
|
+
*/
|
|
62
|
+
function gvrenderComment(renderer: RendererPlugin, job: RenderJob, str: string | undefined): void {
|
|
63
|
+
if (str === undefined || str.length === 0) return;
|
|
64
|
+
renderer.comment?.(str, job);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The node's name and `comment` attribute, emitted after node_in_box and
|
|
69
|
+
* before the invis shortcircuit (an invisible node still gets its comments).
|
|
70
|
+
* @see lib/common/emit.c:emit_node (gvrender_comment x2)
|
|
71
|
+
*/
|
|
72
|
+
export function emitNodeComments(n: Node, renderer: RendererPlugin, job: RenderJob): void {
|
|
73
|
+
gvrenderComment(renderer, job, n.name);
|
|
74
|
+
gvrenderComment(renderer, job, nodeAttr(n, n.root, 'comment'));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* When edge_in_box passes: the `tail->head` name (`--` when the head's graph
|
|
79
|
+
* is undirected) and the `comment` attribute, before the invis shortcircuit.
|
|
80
|
+
* The renderers re-test the same gate for the edge group.
|
|
81
|
+
* @see lib/common/emit.c:emit_edge (gvrender_comment x2)
|
|
82
|
+
*/
|
|
83
|
+
export function emitEdgeComments(e: Edge, renderer: RendererPlugin, job: RenderJob): void {
|
|
84
|
+
if (!edgeHasDrawableContent(e, job.bb)) return;
|
|
85
|
+
const kind = e.head.root.kind; // agisdirected(agraphof(aghead(e)))
|
|
86
|
+
const op = kind === 'directed' || kind === 'strict-directed' ? '->' : '--';
|
|
87
|
+
gvrenderComment(renderer, job, e.tail.name + op + e.head.name);
|
|
88
|
+
gvrenderComment(renderer, job, e.attrs.get('comment'));
|
|
89
|
+
}
|
package/src/gvc/job.ts
CHANGED
|
@@ -275,6 +275,10 @@ export class RenderJob {
|
|
|
275
275
|
|
|
276
276
|
bb: Box = { ll: { x: 0, y: 0 }, ur: { x: 0, y: 0 } };
|
|
277
277
|
pad: Point = { x: 0, y: 0 };
|
|
278
|
+
/** Current page clip in graph units, as setup_page computes it from the
|
|
279
|
+
* viewport focus/view (see setupPageClip). Read by the SVG page translate and
|
|
280
|
+
* background. @see lib/gvc/gvcjob.h:GVJ_s.clip; lib/common/emit.c:setup_page */
|
|
281
|
+
clip: Box = { ll: { x: 0, y: 0 }, ur: { x: 0, y: 0 } };
|
|
278
282
|
/** Graph-level `margin=` (init_job_margin); page-pagination margin, distinct
|
|
279
283
|
* from node/cluster label margin. @see lib/common/emit.c:3309-3331 */
|
|
280
284
|
margin: Point = { x: 0, y: 0 };
|
package/src/gvc/viewport.ts
CHANGED
|
@@ -201,3 +201,32 @@ export function initJobViewportZoom(bb: Box, size: DrawingSize | null, pad: Poin
|
|
|
201
201
|
if (drawingNeedsFit(size, szx, szy)) return Math.min(size.x / szx, size.y / szy);
|
|
202
202
|
return 1.0;
|
|
203
203
|
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* job->clip for the single-page devices this port drives, evaluated in C's
|
|
207
|
+
* order: init_job_viewport pads the bb (job->bb = bb ± pad, sz = job->bb.UR -
|
|
208
|
+
* job->bb.LL, size= substituting a degenerate sz axis), takes the focus as the
|
|
209
|
+
* centre of the UNPADDED bb and view = Z * sz; init_job_pagination sets
|
|
210
|
+
* pageSize = imageSize / zoom (imageSize = view, rotated to page orientation
|
|
211
|
+
* and back); setup_page sets clip.LL = focus + pageSize * (elem - size / 2.)
|
|
212
|
+
* with pagesArrayElem 0 / pagesArraySize 1 and clip.UR = clip.LL + pageSize.
|
|
213
|
+
* Algebraically bb ± pad, but not in floating point: where the pad is below the
|
|
214
|
+
* double spacing (corpus 2784, y = 2.16e18) clip.LL.y is 0, not -pad.
|
|
215
|
+
* @see lib/common/emit.c:init_job_viewport, init_job_pagination, setup_page
|
|
216
|
+
*/
|
|
217
|
+
export function setupPageClip(bb: Box, size: DrawingSize | null, pad: Point, z: number): Box {
|
|
218
|
+
const jbLL = { x: bb.ll.x - pad.x, y: bb.ll.y - pad.y };
|
|
219
|
+
const jbUR = { x: bb.ur.x + pad.x, y: bb.ur.y + pad.y };
|
|
220
|
+
const sz = { x: jbUR.x - jbLL.x, y: jbUR.y - jbLL.y };
|
|
221
|
+
if (size !== null && size.x > 0.001 && size.y > 0.001) {
|
|
222
|
+
if (sz.x <= 0.001) sz.x = size.x;
|
|
223
|
+
if (sz.y <= 0.001) sz.y = size.y;
|
|
224
|
+
}
|
|
225
|
+
const focus = { x: 0.5 * (bb.ll.x + bb.ur.x), y: 0.5 * (bb.ll.y + bb.ur.y) };
|
|
226
|
+
const pageSize = { x: (z * sz.x) / z, y: (z * sz.y) / z };
|
|
227
|
+
const ll = {
|
|
228
|
+
x: focus.x + pageSize.x * (0 - 1 / 2.0),
|
|
229
|
+
y: focus.y + pageSize.y * (0 - 1 / 2.0),
|
|
230
|
+
};
|
|
231
|
+
return { ll, ur: { x: ll.x + pageSize.x, y: ll.y + pageSize.y } };
|
|
232
|
+
}
|
|
@@ -14,6 +14,12 @@ import type { Node } from '../../model/node.js';
|
|
|
14
14
|
import type { Block, SubGraph, CircState, DerivedNode, DerivedEdge } from './blocks.js';
|
|
15
15
|
import { FLAGS_ONPATH, FLAGS_ISPARENT } from './blocks.js';
|
|
16
16
|
import { gvQsort } from '../../util/bsd-qsort.js';
|
|
17
|
+
import { derivedOf, spanIncident, reduceEdgeCrossings } from './crossings.js';
|
|
18
|
+
|
|
19
|
+
export {
|
|
20
|
+
derivedOf, closeOpenEdge, agfstedgeLists, countCrossings, insertNode, reducePass,
|
|
21
|
+
reduceEdgeCrossings, spanIncident,
|
|
22
|
+
} from './crossings.js';
|
|
17
23
|
|
|
18
24
|
export type SpanNode = {
|
|
19
25
|
dn: DerivedNode; tparent: SpanNode | null; visited: boolean;
|
|
@@ -35,135 +41,10 @@ export function blockGraph(allEdges: DerivedEdge[], sn: Block): void {
|
|
|
35
41
|
}
|
|
36
42
|
}
|
|
37
43
|
|
|
38
|
-
// ---------------------------------------------------------------------------
|
|
39
|
-
// Crossing counting helpers (each branch extracted to keep CCN low)
|
|
40
|
-
// ---------------------------------------------------------------------------
|
|
41
|
-
|
|
42
|
-
export function derivedOf(n: Node): DerivedNode | null {
|
|
43
|
-
return (n.info.alg as { derivedNode?: DerivedNode } | undefined)?.derivedNode ?? null;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function closeOpenEdge(e: DerivedEdge, d: DerivedNode, opened: DerivedEdge[]): number {
|
|
47
|
-
let cross = 0;
|
|
48
|
-
for (const o of opened) {
|
|
49
|
-
if (o.order > e.order && o.tail !== d && o.head !== d) cross++;
|
|
50
|
-
}
|
|
51
|
-
return cross;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function openEdgesAt(d: DerivedNode, edges: DerivedEdge[], opened: DerivedEdge[], order: number): void {
|
|
55
|
-
for (const e of edges) {
|
|
56
|
-
if (e.order === 0 && (e.tail === d || e.head === d)) { e.order = order; opened.push(e); }
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* C's count_all_crossings — INCLUDING its load-bearing bug: remove_edge keys
|
|
62
|
-
* the open-edge dict by the half-edge POINTER handed to it, and the closing
|
|
63
|
-
* node's agfstedge always yields the OPPOSITE half from the one stored when
|
|
64
|
-
* the edge was opened, so dtdelete never matches and the list only grows
|
|
65
|
-
* (circogen/edgelist.c:cmpItem on Agedge_t*). Each edge's close therefore
|
|
66
|
-
* counts EVERY edge opened after it (nested as well as interleaved spans),
|
|
67
|
-
* minus edges incident to the closing node — an inflated count that reduce()
|
|
68
|
-
* uses to accept moves. Do not "fix" this to true interleave counting: the
|
|
69
|
-
* final circle order depends on it.
|
|
70
|
-
* @see lib/circogen/blockpath.c:count_all_crossings
|
|
71
|
-
* @see lib/circogen/edgelist.c:remove_edge
|
|
72
|
-
*/
|
|
73
|
-
export function countCrossings(list: Node[], edges: DerivedEdge[]): number {
|
|
74
|
-
for (const e of edges) e.order = 0;
|
|
75
|
-
const opened: DerivedEdge[] = [];
|
|
76
|
-
let crossings = 0; let order = 1;
|
|
77
|
-
for (const n of list) {
|
|
78
|
-
const d = derivedOf(n);
|
|
79
|
-
if (!d) continue;
|
|
80
|
-
// Close pass: incident edges already opened at their other endpoint
|
|
81
|
-
// (edges opened AT this node still have order 0 — the open pass below
|
|
82
|
-
// runs after, so each edge closes exactly once).
|
|
83
|
-
for (const e of edges) {
|
|
84
|
-
if (e.order > 0 && (e.tail === d || e.head === d))
|
|
85
|
-
crossings += closeOpenEdge(e, d, opened);
|
|
86
|
-
}
|
|
87
|
-
openEdgesAt(d, edges, opened, order++);
|
|
88
|
-
}
|
|
89
|
-
return crossings;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// ---------------------------------------------------------------------------
|
|
93
|
-
// Crossing reduction
|
|
94
|
-
// ---------------------------------------------------------------------------
|
|
95
|
-
|
|
96
|
-
export function insertNode(list: Node[], cn: Node, neighbor: Node, pos: number): void {
|
|
97
|
-
const i = list.indexOf(cn); if (i !== -1) list.splice(i, 1);
|
|
98
|
-
const ni = list.indexOf(neighbor);
|
|
99
|
-
if (ni === -1) { list.push(cn); return; }
|
|
100
|
-
list.splice(pos === 0 ? ni : ni + 1, 0, cn);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* One reduce pass, mirroring C's reduce (blockpath.c:439): iterate the
|
|
105
|
-
* SUBGRAPH's nodes in agfstnode (root-seq) order — NOT the current list
|
|
106
|
-
* order — and each node's incident edges in agfstedge order; the neighbour
|
|
107
|
-
* is agtail(e) unless that is curnode. The first-improving-move order is
|
|
108
|
-
* load-bearing for the final arrangement.
|
|
109
|
-
*/
|
|
110
|
-
export function reducePass(
|
|
111
|
-
list: Node[], edges: DerivedEdge[], cnt: { v: number }, nodes: DerivedNode[],
|
|
112
|
-
): void {
|
|
113
|
-
const ord = new Map<DerivedNode, number>();
|
|
114
|
-
nodes.forEach((dn, i) => ord.set(dn, i));
|
|
115
|
-
const seq = new Map<DerivedEdge, number>();
|
|
116
|
-
edges.forEach((e, i) => seq.set(e, i));
|
|
117
|
-
for (const dn of nodes) {
|
|
118
|
-
for (const e of spanIncident(dn, edges, ord, seq)) {
|
|
119
|
-
let neighbor = e.tail;
|
|
120
|
-
if (neighbor === dn) neighbor = e.head;
|
|
121
|
-
for (const pos of [0, 1] as const) {
|
|
122
|
-
const copy = [...list];
|
|
123
|
-
insertNode(list, dn.orig, neighbor.orig, pos);
|
|
124
|
-
const nc = countCrossings(list, edges);
|
|
125
|
-
if (nc < cnt.v) { cnt.v = nc; if (cnt.v === 0) return; }
|
|
126
|
-
else { list.length = 0; list.push(...copy); }
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const CROSS_ITER = 10;
|
|
133
|
-
|
|
134
|
-
/** @see lib/circogen/blockpath.c:reduce_edge_crossings */
|
|
135
|
-
export function reduceEdgeCrossings(
|
|
136
|
-
list: Node[], edges: DerivedEdge[], nodes: DerivedNode[],
|
|
137
|
-
): Node[] {
|
|
138
|
-
const cnt = { v: countCrossings(list, edges) };
|
|
139
|
-
for (let i = 0; i < CROSS_ITER && cnt.v > 0; i++) {
|
|
140
|
-
const prev = cnt.v; reducePass(list, edges, cnt, nodes);
|
|
141
|
-
if (prev === cnt.v) break;
|
|
142
|
-
}
|
|
143
|
-
return list;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
44
|
// ---------------------------------------------------------------------------
|
|
147
45
|
// Spanning tree
|
|
148
46
|
// ---------------------------------------------------------------------------
|
|
149
47
|
|
|
150
|
-
/**
|
|
151
|
-
* Incident edges of dn in agfstedge order over the (possibly pruned)
|
|
152
|
-
* subgraph edge set: out-edges (by head order, seq) then in-edges (by tail
|
|
153
|
-
* order, seq). `ord`/`seq` maps carry the derived root's node and edge
|
|
154
|
-
* creation orders. @see lib/cgraph/edge.c:agfstedge
|
|
155
|
-
*/
|
|
156
|
-
export function spanIncident(
|
|
157
|
-
dn: DerivedNode, edges: DerivedEdge[],
|
|
158
|
-
ord: Map<DerivedNode, number>, seq: Map<DerivedEdge, number>,
|
|
159
|
-
): DerivedEdge[] {
|
|
160
|
-
const out = edges.filter((e) => e.tail === dn)
|
|
161
|
-
.sort((a, b) => ((ord.get(a.head) ?? 0) - (ord.get(b.head) ?? 0)) || ((seq.get(a) ?? 0) - (seq.get(b) ?? 0)));
|
|
162
|
-
const inn = edges.filter((e) => e.head === dn)
|
|
163
|
-
.sort((a, b) => ((ord.get(a.tail) ?? 0) - (ord.get(b.tail) ?? 0)) || ((seq.get(a) ?? 0) - (seq.get(b) ?? 0)));
|
|
164
|
-
return [...out, ...inn];
|
|
165
|
-
}
|
|
166
|
-
|
|
167
48
|
export function dfsSpan(
|
|
168
49
|
sn: SpanNode, map: Map<DerivedNode, SpanNode>, edges: DerivedEdge[],
|
|
169
50
|
ord: Map<DerivedNode, number>, seq: Map<DerivedEdge, number>,
|