@knowvah/dot-engine 2.0.1 → 2.0.2
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/async/collect.d.ts.map +1 -1
- package/dist/async/render-async.d.ts.map +1 -1
- package/dist/gvc/device-labels.d.ts +30 -0
- package/dist/gvc/device-labels.d.ts.map +1 -0
- package/dist/gvc/device-transform.d.ts +22 -0
- package/dist/gvc/device-transform.d.ts.map +1 -0
- package/dist/gvc/device.d.ts +2 -42
- package/dist/gvc/device.d.ts.map +1 -1
- package/dist/index.js +73 -69
- package/dist/index.js.map +4 -4
- package/dist/layout/dot/graph-label.d.ts +10 -0
- package/dist/layout/dot/graph-label.d.ts.map +1 -1
- package/dist/layout/fdp/xlayout.d.ts +1 -1
- package/dist/layout/neato/dijkstra.d.ts +0 -32
- package/dist/layout/neato/dijkstra.d.ts.map +1 -1
- package/dist/layout/neato/fdp-adjust.d.ts +12 -1
- package/dist/layout/neato/fdp-adjust.d.ts.map +1 -1
- package/dist/layout/neato/index.d.ts +5 -5
- package/dist/layout/neato/index.d.ts.map +1 -1
- package/dist/layout/sfdp/index.d.ts.map +1 -1
- package/dist/render/public.d.ts +5 -0
- package/dist/render/public.d.ts.map +1 -1
- package/dist/render.js +70 -66
- package/dist/render.js.map +4 -4
- package/package.json +1 -1
- package/src/async/collect.ts +2 -14
- package/src/async/render-async.ts +1 -9
- package/src/gvc/device-labels.ts +98 -0
- package/src/gvc/device-transform.ts +58 -0
- package/src/gvc/device.ts +8 -132
- package/src/layout/dot/graph-label.ts +6 -1
- package/src/layout/fdp/xlayout.ts +3 -2
- package/src/layout/neato/dijkstra.ts +0 -141
- package/src/layout/neato/fdp-adjust.ts +25 -3
- package/src/layout/neato/index.ts +13 -12
- package/src/layout/sfdp/index.ts +2 -1
- package/src/render/public.ts +6 -5
- package/dist/layout/neato/overlap.d.ts +0 -85
- package/dist/layout/neato/overlap.d.ts.map +0 -1
- package/src/layout/neato/overlap.ts +0 -158
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@knowvah/dot-engine",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.2",
|
|
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/async/collect.ts
CHANGED
|
@@ -19,12 +19,12 @@ import type { Edge } from '../model/edge.js';
|
|
|
19
19
|
import type { Node } from '../model/node.js';
|
|
20
20
|
import type { TextVariantFlags } from '../common/textmeasure.js';
|
|
21
21
|
import { canvasFont } from '../common/css-font.js';
|
|
22
|
-
import { DEFAULT_FONTNAME, DEFAULT_FONTSIZE } from '../common/make-label.js';
|
|
23
22
|
import { isHtmlValue, htmlValueContent } from '../common/html-string.js';
|
|
24
23
|
import { nodeAttr, readFontAttrs } from '../common/poly-init.js';
|
|
25
24
|
import { initFontEdgeAttr, initFontLabelEdgeAttr } from '../common/edge-label-init.js';
|
|
26
25
|
import { parseHtmlLabel } from '../common/htmltable-parse.js';
|
|
27
26
|
import { isACluster } from '../layout/dot/rank.js';
|
|
27
|
+
import { readFontParams } from '../layout/dot/graph-label.js';
|
|
28
28
|
import type {
|
|
29
29
|
HtmlCellContent, HtmlLabel, HtmlTable, HtmlTextItem,
|
|
30
30
|
} from '../common/htmltable-types.js';
|
|
@@ -160,21 +160,9 @@ function collectEdge(acc: Acc, e: Edge): void {
|
|
|
160
160
|
addLabel(acc, e.attrs.get('taillabel'), lfi);
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
-
/**
|
|
164
|
-
* Root/cluster label font. A subgraph reads through its parse-time defaults
|
|
165
|
-
* snapshot, as doGraphLabel does. @see src/layout/dot/graph-label.ts:readFontParams
|
|
166
|
-
*/
|
|
167
|
-
function graphLabelFont(sg: Graph): LabelFont {
|
|
168
|
-
const get = (k: string): string | undefined => sg.attrs.get(k) ?? sg.graphDefaultsSnapshot?.get(k);
|
|
169
|
-
return {
|
|
170
|
-
fontname: get('fontname') ?? DEFAULT_FONTNAME,
|
|
171
|
-
fontsize: parseFloat(get('fontsize') ?? '') || DEFAULT_FONTSIZE,
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
|
|
175
163
|
function collectGraphLabels(acc: Acc, sg: Graph, root: Graph): void {
|
|
176
164
|
if (isACluster(sg)) {
|
|
177
|
-
addLabel(acc, sg.attrs.get('label') ?? sg.graphDefaultsSnapshot?.get('label'),
|
|
165
|
+
addLabel(acc, sg.attrs.get('label') ?? sg.graphDefaultsSnapshot?.get('label'), readFontParams(sg));
|
|
178
166
|
}
|
|
179
167
|
for (const child of sg.subgraphs.values()) collectGraphLabels(acc, child, root);
|
|
180
168
|
}
|
|
@@ -16,6 +16,7 @@ import { parse } from '../parser/index.js';
|
|
|
16
16
|
import { invalidArgType, rethrowAtBoundary } from '../errors.js';
|
|
17
17
|
import type { EngineName } from '../gvc/context.js';
|
|
18
18
|
import { createDefaultContext } from '../gvc/default-context.js';
|
|
19
|
+
import { checkRenderArgs } from '../render/public.js';
|
|
19
20
|
import { render as deviceRender } from '../gvc/device.js';
|
|
20
21
|
import type { ImageSizer } from '../common/htmltable-types.js';
|
|
21
22
|
import type { ImageResolver } from '../gvc/image-resolver.js';
|
|
@@ -62,15 +63,6 @@ export interface AsyncSvgResult {
|
|
|
62
63
|
fontIssues: FontIssue[];
|
|
63
64
|
}
|
|
64
65
|
|
|
65
|
-
/** Reject a bad `g` / `format` / `opts` before any work starts (mirrors `render`). */
|
|
66
|
-
function checkRenderArgs(g: unknown, format: unknown, opts: unknown): void {
|
|
67
|
-
if (typeof g !== 'object' || g === null) throw invalidArgType('g', 'object', g);
|
|
68
|
-
if (typeof format !== 'string') throw invalidArgType('format', 'string', format);
|
|
69
|
-
if (opts !== undefined && (typeof opts !== 'object' || opts === null)) {
|
|
70
|
-
throw invalidArgType('opts', 'object or undefined', opts);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
66
|
/** `document.fonts` when a document exposes one (absent in Node and Workers). */
|
|
75
67
|
function defaultFontSet(): FontSetLike | undefined {
|
|
76
68
|
const doc: unknown = Reflect.get(globalThis, 'document');
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Label emission shared by edge labels, node xlabels and cluster/graph
|
|
5
|
+
* labels (split from device.ts for the file-size cap; re-exported there).
|
|
6
|
+
*
|
|
7
|
+
* @see lib/common/labels.c:emit_label
|
|
8
|
+
* @see lib/common/emit.c:emit_label
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { Graph } from '../model/graph.js';
|
|
12
|
+
import type { Node } from '../model/node.js';
|
|
13
|
+
import type { RendererPlugin } from './context.js';
|
|
14
|
+
import type { RenderJob } from './job.js';
|
|
15
|
+
import type { TextlabelT } from '../common/types.js';
|
|
16
|
+
import type { TextSpan } from '../common/emit-types.js';
|
|
17
|
+
import type { PlacedHtml } from '../common/htmltable-pos.js';
|
|
18
|
+
import { gvrenderTextspan, withLabelEmitState } from './textspan-emit.js';
|
|
19
|
+
import { emitHtmlLabel } from '../common/htmltable-emit.js';
|
|
20
|
+
|
|
21
|
+
/** valign codes stored on textlabel_t. @see lib/common/types.h:textlabel_t.valign */
|
|
22
|
+
const VALIGN_TOP = 't'.charCodeAt(0);
|
|
23
|
+
const VALIGN_BOTTOM = 'b'.charCodeAt(0);
|
|
24
|
+
|
|
25
|
+
/** First-span baseline y per valign. @see lib/common/labels.c:emit_label (240-251) */
|
|
26
|
+
function labelFirstSpanY(lp: TextlabelT): number {
|
|
27
|
+
if (lp.valign === VALIGN_TOP) return lp.pos.y + lp.space.y / 2.0 - lp.fontsize;
|
|
28
|
+
if (lp.valign === VALIGN_BOTTOM) {
|
|
29
|
+
return lp.pos.y - lp.space.y / 2.0 + lp.dimen.y - lp.fontsize;
|
|
30
|
+
}
|
|
31
|
+
return lp.pos.y + lp.dimen.y / 2.0 - lp.fontsize;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Span x position per justification. @see lib/common/labels.c:emit_label (254-266) */
|
|
35
|
+
function labelSpanX(lp: TextlabelT, just: 'l' | 'n' | 'r'): number {
|
|
36
|
+
if (just === 'l') return lp.pos.x - lp.space.x / 2.0;
|
|
37
|
+
if (just === 'r') return lp.pos.x + lp.space.x / 2.0;
|
|
38
|
+
return lp.pos.x;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Emit one label's text spans if present and placed.
|
|
43
|
+
* Shared by edge-label, node-xlabel, and graph-label slots.
|
|
44
|
+
* URL/anchor/map machinery and E_decorate attachment (emit.c:emit_attachment)
|
|
45
|
+
* are not ported, matching the live path's AD-2 scope.
|
|
46
|
+
* @see lib/common/emit.c:emit_label
|
|
47
|
+
* @see lib/common/labels.c:emit_label
|
|
48
|
+
*/
|
|
49
|
+
export function renderOneLabel(
|
|
50
|
+
lp: TextlabelT | undefined,
|
|
51
|
+
renderer: RendererPlugin,
|
|
52
|
+
job: RenderJob,
|
|
53
|
+
requireSet = true,
|
|
54
|
+
): void {
|
|
55
|
+
// C gates `->set` at the xlabel/edge-label CALL SITES (emit.c:1829,
|
|
56
|
+
// emit_edge_label:2891), but draws root-graph and cluster labels on
|
|
57
|
+
// existence alone (emit.c:3656, 3920) — an unplaced label (e.g. fdp's
|
|
58
|
+
// non-comparable-clusters abort skips gv_postprocess) still renders at its
|
|
59
|
+
// default pos. Callers mirroring the existence-only sites pass false.
|
|
60
|
+
if (!lp) return;
|
|
61
|
+
if (requireSet && !lp.set) return;
|
|
62
|
+
// HTML branch: @see lib/common/labels.c:emit_label (226-230)
|
|
63
|
+
// C routes to emit_html_label(job, lp->u.html, lp) using lp->pos as anchor.
|
|
64
|
+
if (lp.html) {
|
|
65
|
+
if (lp.u.kind === 'html') {
|
|
66
|
+
// Route the whole HTML label (table box/fill polygons + text) into the
|
|
67
|
+
// object's LABEL emit-state so its ops land in _ldraw_, not _draw_.
|
|
68
|
+
const html = lp.u.html as PlacedHtml;
|
|
69
|
+
const pos = lp.pos;
|
|
70
|
+
withLabelEmitState(job, () => emitHtmlLabel(html, pos, renderer, job));
|
|
71
|
+
}
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (lp.u.kind !== 'txt' || lp.u.nspans < 1) return;
|
|
75
|
+
let y = labelFirstSpanY(lp);
|
|
76
|
+
for (let i = 0; i < lp.u.nspans; i++) {
|
|
77
|
+
const span = lp.u.span[i] as TextSpan | undefined;
|
|
78
|
+
if (!span) break;
|
|
79
|
+
// Emit only visible spans; the baseline still advances below so blank
|
|
80
|
+
// lines reserve vertical space. @see gvrender_textspan (gvrender.c:419).
|
|
81
|
+
gvrenderTextspan(renderer, { x: labelSpanX(lp, span.just), y }, span, job);
|
|
82
|
+
y -= span.size.y; // UL position for next span (unconditional)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Emit node external label (ND_xlabel) if placed.
|
|
88
|
+
* Must run inside the node group, after codefn (shape draw), matching C order.
|
|
89
|
+
* @see lib/common/emit.c:emit_node (1829-1830)
|
|
90
|
+
*/
|
|
91
|
+
export function renderNodeXLabel(n: Node, renderer: RendererPlugin, job: RenderJob): void {
|
|
92
|
+
renderOneLabel(n.info.xlabel as TextlabelT | undefined, renderer, job);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Cluster labels go through the single emit_label port. @see labels.c:emit_label */
|
|
96
|
+
export function renderClusterLabel(sg: Graph, renderer: RendererPlugin, job: RenderJob): void {
|
|
97
|
+
renderOneLabel(sg.info.label as TextlabelT | undefined, renderer, job, false);
|
|
98
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Graph-to-device point transform (split from device.ts for the file-size
|
|
5
|
+
* cap; re-exported there).
|
|
6
|
+
*
|
|
7
|
+
* @see lib/gvc/gvrender.c:gvrender_ptf
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { Point } from '../model/geom.js';
|
|
11
|
+
import { type RenderJob, GVRENDER_DOES_TRANSFORM } from './job.js';
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// transformPoint — @see lib/gvc/gvrender.c:gvrender_ptf
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Transform a point from graph to device coordinates.
|
|
19
|
+
* Short-circuits when GVRENDER_DOES_TRANSFORM is set (renderer owns mapping).
|
|
20
|
+
*
|
|
21
|
+
* @see lib/gvc/gvrender.c:gvrender_ptf
|
|
22
|
+
*/
|
|
23
|
+
export function transformPoint(p: Point, job: RenderJob): Point {
|
|
24
|
+
if ((job.flags & GVRENDER_DOES_TRANSFORM) !== 0) {
|
|
25
|
+
return p;
|
|
26
|
+
}
|
|
27
|
+
const sx = job.zoom * job.devscale.x;
|
|
28
|
+
const sy = job.zoom * job.devscale.y;
|
|
29
|
+
const tx = job.translation.x;
|
|
30
|
+
const ty = job.translation.y;
|
|
31
|
+
// ADR-2: SVG landscape rotation lives entirely in the graph `<g>` group
|
|
32
|
+
// transform (svg_begin_page emits rotate(-job->rotation)); inner coords stay
|
|
33
|
+
// in the unrotated frame. The ptf rotation branch (applyRotation) is the
|
|
34
|
+
// raster/imagemap path and must NOT fire here, else job.rotation=90 would
|
|
35
|
+
// double-rotate every SVG coordinate. applyRotation stays exported as the
|
|
36
|
+
// faithful gvrender_ptf port (currently dead). @see ADR-2; gvrender.c:gvrender_ptf
|
|
37
|
+
return applyScale(p, tx, ty, sx, sy);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Rotation branch: out.x = -(p.y+ty)*sx, out.y = (p.x+tx)*sy @see gvrender_ptf */
|
|
41
|
+
export function applyRotation(p: Point, tx: number, ty: number, sx: number, sy: number): Point {
|
|
42
|
+
const x = -(p.y + ty) * sx;
|
|
43
|
+
const y = (p.x + tx) * sy;
|
|
44
|
+
return buildPoint(x, y);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** No-rotation branch: out.x = (p.x+tx)*sx, out.y = (p.y+ty)*sy @see gvrender_ptf */
|
|
48
|
+
export function applyScale(p: Point, tx: number, ty: number, sx: number, sy: number): Point {
|
|
49
|
+
const x = (p.x + tx) * sx;
|
|
50
|
+
const y = (p.y + ty) * sy;
|
|
51
|
+
return buildPoint(x, y);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Construct a Point value. Extracted to avoid inline object literals in return position. */
|
|
55
|
+
export function buildPoint(x: number, y: number): Point {
|
|
56
|
+
const pt: Point = { x, y };
|
|
57
|
+
return pt;
|
|
58
|
+
}
|
package/src/gvc/device.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EPL-2.0
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Device render loop
|
|
4
|
+
* Device render loop (point transform: device-transform.ts; label emission:
|
|
5
|
+
* device-labels.ts).
|
|
5
6
|
*
|
|
6
7
|
* Ports gvrender_ptf, gvrender_begin_graph, and gvrender_end_graph from
|
|
7
8
|
* lib/gvc/gvrender.c. render() is the top-level entry point; it lives here
|
|
@@ -11,7 +12,7 @@
|
|
|
11
12
|
* @see lib/gvc/gvdevice.c
|
|
12
13
|
*/
|
|
13
14
|
|
|
14
|
-
import type {
|
|
15
|
+
import type { Box } from '../model/geom.js';
|
|
15
16
|
import { boxOverlap, POINTS_PER_INCH } from '../model/geom.js';
|
|
16
17
|
import { lateDouble } from '../common/nodeinit.js';
|
|
17
18
|
import { parseDrawingSize, initJobViewportZoom, parseLandscape, parseGraphPad, parseGraphMargin } from './viewport.js';
|
|
@@ -21,22 +22,18 @@ import type { Edge } from '../model/edge.js';
|
|
|
21
22
|
import type { RendererPlugin } from './context.js';
|
|
22
23
|
import { GvcContext, PenType } from './context.js';
|
|
23
24
|
import { invalidArgType } from '../errors.js';
|
|
24
|
-
import { gvrenderTextspan, withLabelEmitState } from './textspan-emit.js';
|
|
25
25
|
import { resolveEdgeAnchor, resolveObjAnchor, beginAnchorIf } from './anchor.js';
|
|
26
26
|
import type { ShapeDesc, TextlabelT } from '../common/types.js';
|
|
27
|
-
import type { TextSpan } from '../common/emit-types.js';
|
|
28
27
|
import { type LayerInfo, parseLayers } from '../common/layers.js';
|
|
29
|
-
import { RenderJob,
|
|
28
|
+
import { RenderJob, EMIT_CLUSTERS_LAST, createObjState, ObjType, EmitState } from './job.js';
|
|
30
29
|
import { walkNodesAndEdges } from './emit-walk.js';
|
|
31
30
|
import { polyInit } from '../common/poly-init.js';
|
|
32
|
-
import { emitHtmlLabel } from '../common/htmltable-emit.js';
|
|
33
31
|
import {
|
|
34
32
|
setHtmlAnchorObj,
|
|
35
33
|
setHtmlObjImgscale,
|
|
36
34
|
resetHtmlAnchorIds,
|
|
37
35
|
} from '../common/htmltable-emit-rules.js';
|
|
38
36
|
import { nodeAttr } from '../common/poly-init.js';
|
|
39
|
-
import type { PlacedHtml } from '../common/htmltable-pos.js';
|
|
40
37
|
import {
|
|
41
38
|
parseStyleFlags,
|
|
42
39
|
resolvePenColor,
|
|
@@ -49,10 +46,10 @@ import { isPointNode } from '../common/poly-gencode.js';
|
|
|
49
46
|
import { resolveRenderColor, withColorScheme } from '../render/color-resolve.js';
|
|
50
47
|
import { emitRoundedBezier } from '../common/poly-shapes.js';
|
|
51
48
|
import { applyClusterObjState, clusterStyle, clusterPeripheries } from './device-cluster.js';
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
49
|
+
import { transformPoint } from './device-transform.js';
|
|
50
|
+
import { renderOneLabel, renderNodeXLabel, renderClusterLabel } from './device-labels.js';
|
|
51
|
+
export { transformPoint, applyRotation, applyScale, buildPoint } from './device-transform.js';
|
|
52
|
+
export { renderOneLabel, renderNodeXLabel, renderClusterLabel } from './device-labels.js';
|
|
56
53
|
import { svgNodeId, svgEdgeId, svgClusterId, svgGraphId } from '../render/svg-id.js';
|
|
57
54
|
|
|
58
55
|
// ---------------------------------------------------------------------------
|
|
@@ -72,53 +69,6 @@ declare module './job.js' {
|
|
|
72
69
|
}
|
|
73
70
|
}
|
|
74
71
|
|
|
75
|
-
// ---------------------------------------------------------------------------
|
|
76
|
-
// transformPoint — @see lib/gvc/gvrender.c:gvrender_ptf
|
|
77
|
-
// ---------------------------------------------------------------------------
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Transform a point from graph to device coordinates.
|
|
81
|
-
* Short-circuits when GVRENDER_DOES_TRANSFORM is set (renderer owns mapping).
|
|
82
|
-
*
|
|
83
|
-
* @see lib/gvc/gvrender.c:gvrender_ptf
|
|
84
|
-
*/
|
|
85
|
-
export function transformPoint(p: Point, job: RenderJob): Point {
|
|
86
|
-
if ((job.flags & GVRENDER_DOES_TRANSFORM) !== 0) {
|
|
87
|
-
return p;
|
|
88
|
-
}
|
|
89
|
-
const sx = job.zoom * job.devscale.x;
|
|
90
|
-
const sy = job.zoom * job.devscale.y;
|
|
91
|
-
const tx = job.translation.x;
|
|
92
|
-
const ty = job.translation.y;
|
|
93
|
-
// ADR-2: SVG landscape rotation lives entirely in the graph `<g>` group
|
|
94
|
-
// transform (svg_begin_page emits rotate(-job->rotation)); inner coords stay
|
|
95
|
-
// in the unrotated frame. The ptf rotation branch (applyRotation) is the
|
|
96
|
-
// raster/imagemap path and must NOT fire here, else job.rotation=90 would
|
|
97
|
-
// double-rotate every SVG coordinate. applyRotation stays exported as the
|
|
98
|
-
// faithful gvrender_ptf port (currently dead). @see ADR-2; gvrender.c:gvrender_ptf
|
|
99
|
-
return applyScale(p, tx, ty, sx, sy);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/** Rotation branch: out.x = -(p.y+ty)*sx, out.y = (p.x+tx)*sy @see gvrender_ptf */
|
|
103
|
-
export function applyRotation(p: Point, tx: number, ty: number, sx: number, sy: number): Point {
|
|
104
|
-
const x = -(p.y + ty) * sx;
|
|
105
|
-
const y = (p.x + tx) * sy;
|
|
106
|
-
return buildPoint(x, y);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/** No-rotation branch: out.x = (p.x+tx)*sx, out.y = (p.y+ty)*sy @see gvrender_ptf */
|
|
110
|
-
export function applyScale(p: Point, tx: number, ty: number, sx: number, sy: number): Point {
|
|
111
|
-
const x = (p.x + tx) * sx;
|
|
112
|
-
const y = (p.y + ty) * sy;
|
|
113
|
-
return buildPoint(x, y);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/** Construct a Point value. Extracted to avoid inline object literals in return position. */
|
|
117
|
-
export function buildPoint(x: number, y: number): Point {
|
|
118
|
-
const pt: Point = { x, y };
|
|
119
|
-
return pt;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
72
|
// ---------------------------------------------------------------------------
|
|
123
73
|
// Node/Edge walk helpers
|
|
124
74
|
// ---------------------------------------------------------------------------
|
|
@@ -282,80 +232,6 @@ export function renderEdge(e: Edge, renderer: RendererPlugin, job: RenderJob): v
|
|
|
282
232
|
}
|
|
283
233
|
}
|
|
284
234
|
|
|
285
|
-
/** valign codes stored on textlabel_t. @see lib/common/types.h:textlabel_t.valign */
|
|
286
|
-
const VALIGN_TOP = 't'.charCodeAt(0);
|
|
287
|
-
const VALIGN_BOTTOM = 'b'.charCodeAt(0);
|
|
288
|
-
|
|
289
|
-
/** First-span baseline y per valign. @see lib/common/labels.c:emit_label (240-251) */
|
|
290
|
-
function labelFirstSpanY(lp: TextlabelT): number {
|
|
291
|
-
if (lp.valign === VALIGN_TOP) return lp.pos.y + lp.space.y / 2.0 - lp.fontsize;
|
|
292
|
-
if (lp.valign === VALIGN_BOTTOM) {
|
|
293
|
-
return lp.pos.y - lp.space.y / 2.0 + lp.dimen.y - lp.fontsize;
|
|
294
|
-
}
|
|
295
|
-
return lp.pos.y + lp.dimen.y / 2.0 - lp.fontsize;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
/** Span x position per justification. @see lib/common/labels.c:emit_label (254-266) */
|
|
299
|
-
function labelSpanX(lp: TextlabelT, just: 'l' | 'n' | 'r'): number {
|
|
300
|
-
if (just === 'l') return lp.pos.x - lp.space.x / 2.0;
|
|
301
|
-
if (just === 'r') return lp.pos.x + lp.space.x / 2.0;
|
|
302
|
-
return lp.pos.x;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
/**
|
|
306
|
-
* Emit one label's text spans if present and placed.
|
|
307
|
-
* Shared by edge-label, node-xlabel, and graph-label slots.
|
|
308
|
-
* URL/anchor/map machinery and E_decorate attachment (emit.c:emit_attachment)
|
|
309
|
-
* are not ported, matching the live path's AD-2 scope.
|
|
310
|
-
* @see lib/common/emit.c:emit_label
|
|
311
|
-
* @see lib/common/labels.c:emit_label
|
|
312
|
-
*/
|
|
313
|
-
export function renderOneLabel(
|
|
314
|
-
lp: TextlabelT | undefined,
|
|
315
|
-
renderer: RendererPlugin,
|
|
316
|
-
job: RenderJob,
|
|
317
|
-
requireSet = true,
|
|
318
|
-
): void {
|
|
319
|
-
// C gates `->set` at the xlabel/edge-label CALL SITES (emit.c:1829,
|
|
320
|
-
// emit_edge_label:2891), but draws root-graph and cluster labels on
|
|
321
|
-
// existence alone (emit.c:3656, 3920) — an unplaced label (e.g. fdp's
|
|
322
|
-
// non-comparable-clusters abort skips gv_postprocess) still renders at its
|
|
323
|
-
// default pos. Callers mirroring the existence-only sites pass false.
|
|
324
|
-
if (!lp) return;
|
|
325
|
-
if (requireSet && !lp.set) return;
|
|
326
|
-
// HTML branch: @see lib/common/labels.c:emit_label (226-230)
|
|
327
|
-
// C routes to emit_html_label(job, lp->u.html, lp) using lp->pos as anchor.
|
|
328
|
-
if (lp.html) {
|
|
329
|
-
if (lp.u.kind === 'html') {
|
|
330
|
-
// Route the whole HTML label (table box/fill polygons + text) into the
|
|
331
|
-
// object's LABEL emit-state so its ops land in _ldraw_, not _draw_.
|
|
332
|
-
const html = lp.u.html as PlacedHtml;
|
|
333
|
-
const pos = lp.pos;
|
|
334
|
-
withLabelEmitState(job, () => emitHtmlLabel(html, pos, renderer, job));
|
|
335
|
-
}
|
|
336
|
-
return;
|
|
337
|
-
}
|
|
338
|
-
if (lp.u.kind !== 'txt' || lp.u.nspans < 1) return;
|
|
339
|
-
let y = labelFirstSpanY(lp);
|
|
340
|
-
for (let i = 0; i < lp.u.nspans; i++) {
|
|
341
|
-
const span = lp.u.span[i] as TextSpan | undefined;
|
|
342
|
-
if (!span) break;
|
|
343
|
-
// Emit only visible spans; the baseline still advances below so blank
|
|
344
|
-
// lines reserve vertical space. @see gvrender_textspan (gvrender.c:419).
|
|
345
|
-
gvrenderTextspan(renderer, { x: labelSpanX(lp, span.just), y }, span, job);
|
|
346
|
-
y -= span.size.y; // UL position for next span (unconditional)
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
/**
|
|
351
|
-
* Emit node external label (ND_xlabel) if placed.
|
|
352
|
-
* Must run inside the node group, after codefn (shape draw), matching C order.
|
|
353
|
-
* @see lib/common/emit.c:emit_node (1829-1830)
|
|
354
|
-
*/
|
|
355
|
-
export function renderNodeXLabel(n: Node, renderer: RendererPlugin, job: RenderJob): void {
|
|
356
|
-
renderOneLabel(n.info.xlabel as TextlabelT | undefined, renderer, job);
|
|
357
|
-
}
|
|
358
|
-
|
|
359
235
|
/**
|
|
360
236
|
* Emit root-graph label (GD_label) if present and placed.
|
|
361
237
|
* Must run before clusters and nodes (emit_page order: background, then label,
|
|
@@ -60,7 +60,12 @@ function graphAttrInherited(sg: Graph, key: string): string | undefined {
|
|
|
60
60
|
return sg.attrs.get(key) ?? sg.graphDefaultsSnapshot?.get(key);
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
|
|
63
|
+
/**
|
|
64
|
+
* Graph/cluster label font, read through graphAttrInherited.
|
|
65
|
+
* @internal also used by src/async/collect.ts to request the same face.
|
|
66
|
+
* @see lib/common/input.c:do_graph_label
|
|
67
|
+
*/
|
|
68
|
+
export function readFontParams(sg: Graph): { fontsize: number; fontname: string; fontcolor: string } {
|
|
64
69
|
return {
|
|
65
70
|
fontsize: parseFloat(graphAttrInherited(sg, 'fontsize') ?? '') || DEFAULT_FONTSIZE,
|
|
66
71
|
fontname: graphAttrInherited(sg, 'fontname') ?? DEFAULT_FONTNAME,
|
|
@@ -21,7 +21,7 @@ import { RenderError } from '../../errors.js';
|
|
|
21
21
|
import type { Graph } from '../../model/graph.js';
|
|
22
22
|
import type { Node } from '../../model/node.js';
|
|
23
23
|
import { sepFactor, type ExpandT } from '../neato/sep-factor.js';
|
|
24
|
-
import { fdpAdjust, overlapPrismTries } from '../neato/fdp-adjust.js';
|
|
24
|
+
import { fdpAdjust, overlapPrismTries, warnUnrecognizedOverlap } from '../neato/fdp-adjust.js';
|
|
25
25
|
import { scAdjust } from '../neato/sc-adjust.js';
|
|
26
26
|
import {
|
|
27
27
|
type XParams,
|
|
@@ -295,7 +295,7 @@ function parseOverlapTries(ovlp: string): { tries: number; rest: string } {
|
|
|
295
295
|
/**
|
|
296
296
|
* Overlap-removal dispatch on the overlap attribute:
|
|
297
297
|
* ""/null → default "9:prism"; "n:mode" → n x_layout tries, then
|
|
298
|
-
*
|
|
298
|
+
* removeOverlapAs with mode; "true" → keep overlaps.
|
|
299
299
|
* @see lib/fdpgen/xlayout.c:fdp_xLayout
|
|
300
300
|
*/
|
|
301
301
|
export function fdpXLayout(g: Graph, xpms: XParams): void {
|
|
@@ -321,6 +321,7 @@ export function fdpXLayout(g: Graph, xpms: XParams): void {
|
|
|
321
321
|
*/
|
|
322
322
|
function removeOverlapAs(g: Graph, flag: string): void {
|
|
323
323
|
if (g.nodes.size < 2) return; // removeOverlapWith: <2 nodes short-circuits
|
|
324
|
+
warnUnrecognizedOverlap(flag); // getAdjustMode
|
|
324
325
|
normalizeG(g); // removeOverlapWith runs normalize + simpleScale first
|
|
325
326
|
simpleScale(g);
|
|
326
327
|
const ntry = overlapPrismTries(flag);
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EPL-2.0
|
|
2
2
|
/** @see lib/neatogen/dijkstra.c */
|
|
3
3
|
|
|
4
|
-
import type { GraphSgd } from './sgd.js';
|
|
5
|
-
|
|
6
4
|
/**
|
|
7
5
|
* Sparse-graph vertex descriptor used by neato layout algorithms.
|
|
8
6
|
* Matches `vtx_data` in lib/neatogen/sparsegraph.h.
|
|
@@ -31,9 +29,6 @@ export interface TermSgd {
|
|
|
31
29
|
w: number; // weight = 1/d²
|
|
32
30
|
}
|
|
33
31
|
|
|
34
|
-
/** Sentinel used for unvisited BFS nodes (matches C INT_MAX). */
|
|
35
|
-
const BFS_INF = 0x7fffffff;
|
|
36
|
-
|
|
37
32
|
// ---------------------------------------------------------------------------
|
|
38
33
|
// Min-heap (keyed by Float32Array distances)
|
|
39
34
|
// ---------------------------------------------------------------------------
|
|
@@ -158,139 +153,3 @@ export function dijkstra(
|
|
|
158
153
|
dijkstraRelax(closest, closestDist, graph, dist, heap);
|
|
159
154
|
}
|
|
160
155
|
}
|
|
161
|
-
|
|
162
|
-
// ---------------------------------------------------------------------------
|
|
163
|
-
// Public: All-pairs shortest paths (packed upper-triangular)
|
|
164
|
-
// ---------------------------------------------------------------------------
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* BFS single-source distances for unweighted graphs.
|
|
168
|
-
* Fills `dist` with hop counts; unreachable nodes get BFS_INF (0x7fffffff).
|
|
169
|
-
* edges[0] of each vertex is the self-loop and is skipped.
|
|
170
|
-
* @see lib/neatogen/bfs.c:bfs
|
|
171
|
-
*/
|
|
172
|
-
export function bfsDistances(
|
|
173
|
-
src: number,
|
|
174
|
-
graph: VtxData[],
|
|
175
|
-
n: number,
|
|
176
|
-
dist: Float32Array,
|
|
177
|
-
): void {
|
|
178
|
-
for (let i = 0; i < n; i++) dist[i] = BFS_INF;
|
|
179
|
-
dist[src] = 0;
|
|
180
|
-
const queue = new Int32Array(n);
|
|
181
|
-
let head = 0, tail = 0;
|
|
182
|
-
queue[tail++] = src;
|
|
183
|
-
while (head < tail) {
|
|
184
|
-
const u = queue[head++];
|
|
185
|
-
const d = dist[u] + 1;
|
|
186
|
-
const vtx = graph[u];
|
|
187
|
-
for (let e = 1; e < vtx.nedges; e++) {
|
|
188
|
-
const v = vtx.edges[e];
|
|
189
|
-
if (dist[v] === BFS_INF) { dist[v] = d; queue[tail++] = v; }
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* All-pairs shortest paths (unweighted), packed upper-triangular.
|
|
196
|
-
* Length n*(n+1)/2; index for (i,j) j>=i: i*(2n-i-1)/2 + j.
|
|
197
|
-
* @see lib/neatogen/stress.c:compute_apsp_packed
|
|
198
|
-
*/
|
|
199
|
-
export function computeApspPacked(graph: VtxData[], n: number): Float32Array {
|
|
200
|
-
const result = new Float32Array(n * (n + 1) / 2);
|
|
201
|
-
const di = new Float32Array(n);
|
|
202
|
-
let count = 0;
|
|
203
|
-
for (let i = 0; i < n; i++) {
|
|
204
|
-
bfsDistances(i, graph, n, di);
|
|
205
|
-
for (let j = i; j < n; j++) result[count++] = di[j];
|
|
206
|
-
}
|
|
207
|
-
return result;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* All-pairs shortest paths (weighted), packed upper-triangular.
|
|
212
|
-
* Length n*(n+1)/2.
|
|
213
|
-
* @see lib/neatogen/stress.c:compute_weighted_apsp_packed
|
|
214
|
-
*/
|
|
215
|
-
export function computeWeightedApspPacked(
|
|
216
|
-
graph: VtxData[],
|
|
217
|
-
n: number,
|
|
218
|
-
): Float32Array {
|
|
219
|
-
const result = new Float32Array(n * (n + 1) / 2);
|
|
220
|
-
const di = new Float32Array(n);
|
|
221
|
-
let count = 0;
|
|
222
|
-
for (let i = 0; i < n; i++) {
|
|
223
|
-
dijkstra(i, graph, n, di);
|
|
224
|
-
for (let j = i; j < n; j++) result[count++] = di[j];
|
|
225
|
-
}
|
|
226
|
-
return result;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// ---------------------------------------------------------------------------
|
|
230
|
-
// dijkstraSgd helpers
|
|
231
|
-
// ---------------------------------------------------------------------------
|
|
232
|
-
|
|
233
|
-
/** Initialise SGD dists from source's adjacency list. @internal */
|
|
234
|
-
export function sgdInitDist(
|
|
235
|
-
graph: GraphSgd,
|
|
236
|
-
src: number,
|
|
237
|
-
dists: Float32Array,
|
|
238
|
-
): void {
|
|
239
|
-
dists[src] = 0;
|
|
240
|
-
const end = graph.sources[src + 1];
|
|
241
|
-
for (let i = graph.sources[src]; i < end; i++) {
|
|
242
|
-
dists[graph.targets[i]] = graph.weights[i];
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/** Relax SGD neighbours of `closest` and push updates onto `heap`. @internal */
|
|
247
|
-
export function sgdRelaxNeighbors(
|
|
248
|
-
graph: GraphSgd,
|
|
249
|
-
closest: number,
|
|
250
|
-
d: number,
|
|
251
|
-
dists: Float32Array,
|
|
252
|
-
heap: number[],
|
|
253
|
-
): void {
|
|
254
|
-
const end = graph.sources[closest + 1];
|
|
255
|
-
for (let i = graph.sources[closest]; i < end; i++) {
|
|
256
|
-
const target = graph.targets[i];
|
|
257
|
-
const nd = d + graph.weights[i];
|
|
258
|
-
if (nd < dists[target]) {
|
|
259
|
-
dists[target] = nd;
|
|
260
|
-
heapPush(heap, dists, target);
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// ---------------------------------------------------------------------------
|
|
266
|
-
// Public: dijkstraSgd
|
|
267
|
-
// ---------------------------------------------------------------------------
|
|
268
|
-
|
|
269
|
-
/**
|
|
270
|
-
* Single-source shortest paths for the SGD engine.
|
|
271
|
-
* Writes terms for all reachable `j < src` and pinned nodes.
|
|
272
|
-
* Returns the count of terms written.
|
|
273
|
-
*
|
|
274
|
-
* @see lib/neatogen/dijkstra.c:dijkstra_sgd
|
|
275
|
-
*/
|
|
276
|
-
export function dijkstraSgd(
|
|
277
|
-
graph: GraphSgd,
|
|
278
|
-
src: number,
|
|
279
|
-
terms: TermSgd[],
|
|
280
|
-
): number {
|
|
281
|
-
const n = graph.n;
|
|
282
|
-
const dists = new Float32Array(n).fill(Infinity);
|
|
283
|
-
sgdInitDist(graph, src, dists);
|
|
284
|
-
const heap = heapBuildExcluding(src, n, dists);
|
|
285
|
-
let offset = 0;
|
|
286
|
-
while (heap.length > 0) {
|
|
287
|
-
const closest = heapPop(heap, dists);
|
|
288
|
-
const d = dists[closest];
|
|
289
|
-
if (!isFinite(d)) break;
|
|
290
|
-
if (graph.pinneds[closest] || closest < src) {
|
|
291
|
-
terms[offset++] = { i: src, j: closest, d, w: 1 / (d * d) };
|
|
292
|
-
}
|
|
293
|
-
sgdRelaxNeighbors(graph, closest, d, dists, heap);
|
|
294
|
-
}
|
|
295
|
-
return offset;
|
|
296
|
-
}
|
|
@@ -151,12 +151,26 @@ const ADJUST_MODES = new Map<string, readonly [number, string]>([
|
|
|
151
151
|
['porthoyx', [AM_PORTHOYX, 'yx pseudo-orthogonal constraints']],
|
|
152
152
|
]);
|
|
153
153
|
|
|
154
|
+
/**
|
|
155
|
+
* getAdjustMode's fallback warning: a value that is not a mode name, not
|
|
156
|
+
* prism*, and not a boolean is used as `false` (prism) with this warning.
|
|
157
|
+
* @see lib/neatogen/adjust.c:getAdjustMode (unmappable branch)
|
|
158
|
+
*/
|
|
159
|
+
export function warnUnrecognizedOverlap(flag: string | undefined): void {
|
|
160
|
+
if (flag === undefined || flag === '') return;
|
|
161
|
+
const s = flag.toLowerCase();
|
|
162
|
+
if (s.startsWith('prism') || NAMED_MODES.has(s)) return;
|
|
163
|
+
if (mapBoolDflt(flag, false) === mapBoolDflt(flag, true)) return;
|
|
164
|
+
console.warn(`Unrecognized overlap value "${flag}" - using false`);
|
|
165
|
+
}
|
|
166
|
+
|
|
154
167
|
/**
|
|
155
168
|
* Resolve an `overlap` value to its adjust mode (the PRISM try count lands in
|
|
156
|
-
* `value`).
|
|
169
|
+
* `value`), warning as C does for an unrecognized value.
|
|
157
170
|
* @see lib/neatogen/adjust.c:getAdjustMode
|
|
158
171
|
*/
|
|
159
172
|
export function getAdjustMode(flag: string | undefined): AdjustData {
|
|
173
|
+
warnUnrecognizedOverlap(flag);
|
|
160
174
|
const ntry = overlapPrismTries(flag);
|
|
161
175
|
if (ntry !== null) return { mode: AM_PRISM, print: 'prism', value: ntry };
|
|
162
176
|
const named = ADJUST_MODES.get((flag ?? '').toLowerCase());
|
|
@@ -227,6 +241,14 @@ export function removeOverlapWith(g: Graph, am: AdjustData): number {
|
|
|
227
241
|
* @see lib/neatogen/adjust.c:adjustNodes / removeOverlapAs
|
|
228
242
|
*/
|
|
229
243
|
export function adjustNodesFull(g: Graph): number {
|
|
230
|
-
|
|
231
|
-
return removeOverlapWith(g,
|
|
244
|
+
if (g.nodes.size < 2) return 0; // removeOverlapAs returns before getAdjustMode
|
|
245
|
+
return removeOverlapWith(g, graphAdjustMode(g));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* The graph's `overlap` value resolved once (agget falls back to the root).
|
|
250
|
+
* @see lib/neatogen/adjust.c:graphAdjustMode
|
|
251
|
+
*/
|
|
252
|
+
export function graphAdjustMode(g: Graph): AdjustData {
|
|
253
|
+
return getAdjustMode(g.attrs.get('overlap') ?? g.root.attrs.get('overlap'));
|
|
232
254
|
}
|