@knowvah/dot-engine 1.2.4 → 1.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -0
- package/dist/common/graph-init.d.ts.map +1 -1
- package/dist/common/splines-clip.d.ts +8 -0
- package/dist/common/splines-clip.d.ts.map +1 -1
- package/dist/common/splines-routespl.d.ts +4 -0
- package/dist/common/splines-routespl.d.ts.map +1 -1
- package/dist/index.js +55 -1
- package/dist/index.js.map +2 -2
- package/dist/render.js +55 -1
- package/dist/render.js.map +2 -2
- package/package.json +2 -2
- package/src/common/graph-init.ts +14 -9
- package/src/common/splines-clip.ts +25 -0
- package/src/common/splines-routespl.ts +66 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@knowvah/dot-engine",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.6",
|
|
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",
|
|
@@ -98,7 +98,6 @@
|
|
|
98
98
|
"@typescript/native-preview": "^7.0.0-dev.20260622.1",
|
|
99
99
|
"@vitest/coverage-v8": "^4.1.9",
|
|
100
100
|
"@xmldom/xmldom": "^0.9.10",
|
|
101
|
-
"canvas": "^3.2.3",
|
|
102
101
|
"esbuild": "^0.28.1",
|
|
103
102
|
"fontkit": "^2.0.4",
|
|
104
103
|
"peggy": "^5.0.0",
|
|
@@ -106,6 +105,7 @@
|
|
|
106
105
|
"tsx": "^4.23.1",
|
|
107
106
|
"typedoc": "^0.28.20",
|
|
108
107
|
"typedoc-plugin-markdown": "^4.12.0",
|
|
108
|
+
"@knowvah/vitepress-plugin-dot": "^1.0.0",
|
|
109
109
|
"typedoc-vitepress-theme": "^1.1.3",
|
|
110
110
|
"typescript": "^6.0.0",
|
|
111
111
|
"vitepress": "^1.6.0",
|
package/src/common/graph-init.ts
CHANGED
|
@@ -181,29 +181,34 @@ function parseRatioKind(g: Graph): RatioKind | undefined {
|
|
|
181
181
|
|
|
182
182
|
/**
|
|
183
183
|
* Populate `g.info.drawing` for the ratio kinds whose layout reshape is ported
|
|
184
|
-
* AND corpus-validated: `compress` (compressGraph x-NS, position-cluster.ts)
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
184
|
+
* AND corpus-validated: `compress` (compressGraph x-NS, position-cluster.ts),
|
|
185
|
+
* `fill` (setAspect R_FILL, position-bbox.ts), and numeric `value` (setAspect
|
|
186
|
+
* R_VALUE; corpus coverage: 2621, `ratio=0.5625` — see
|
|
187
|
+
* `.agent-notes/2621-path-structure.md`). C stores the parsed ratio only for
|
|
188
|
+
* R_VALUE (`input.c setRatio: ratio = atof(p); if > 0`). `expand` has the math
|
|
189
|
+
* in setAspect but no corpus coverage, and `auto` needs `idealsize` (unported);
|
|
190
|
+
* both stay deferred — leaving `drawing` unset keeps setAspect a no-op for
|
|
191
|
+
* them.
|
|
189
192
|
*
|
|
190
193
|
* DELIBERATE DEVIATION from C, carried over verbatim from dot's old init: C
|
|
191
194
|
* allocates GD_drawing unconditionally (`input.c:609`) and stores the parsed
|
|
192
195
|
* ratio_kind for EVERY kind. Allocating it here would flip the
|
|
193
196
|
* `drawing === undefined` guards at `dot/position-bbox.ts:142` (setAspect) and
|
|
194
|
-
* `neato/set-aspect.ts:57`, activating the unvalidated expand/
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
+
* `neato/set-aspect.ts:57`, activating the unvalidated expand/auto reshapes and
|
|
198
|
+
* changing dot output. Scope is therefore unchanged by the graph_init
|
|
199
|
+
* consolidation. See `.agent-notes/graph-init-consolidation.md`.
|
|
197
200
|
*
|
|
198
201
|
* @see lib/dotgen/position.c:set_aspect (904), compress_graph (501)
|
|
199
202
|
* @see lib/common/input.c:576 setRatio, 693-694 (size)
|
|
200
203
|
*/
|
|
201
204
|
function parseRatioDrawing(g: Graph): void {
|
|
202
205
|
const kind = parseRatioKind(g);
|
|
203
|
-
if (kind !== 'compress' && kind !== 'fill') return;
|
|
206
|
+
if (kind !== 'compress' && kind !== 'fill' && kind !== 'value') return;
|
|
204
207
|
const sz = parseSizePoints(g.attrs.get('size'));
|
|
205
208
|
g.info.drawing = makeDrawing({
|
|
206
209
|
ratioKind: kind,
|
|
210
|
+
// C: GD_drawing(g)->ratio = atof(p), set only for R_VALUE (setRatio).
|
|
211
|
+
ratio: kind === 'value' ? Number.parseFloat(g.attrs.get('ratio')!) : 0,
|
|
207
212
|
size: sz ? { x: sz.x, y: sz.y } : { x: 0, y: 0 },
|
|
208
213
|
filled: sz?.filled ?? false,
|
|
209
214
|
});
|
|
@@ -472,4 +472,29 @@ export function clipAndInstall(fe: Edge, hn: Node, ps: Point[], pn: number, info
|
|
|
472
472
|
SplineClipHelper.arrowClip(fe, hn, ps, bounds, newspl, info);
|
|
473
473
|
// C: update_bb_bz(&GD_bb(g), cp) — expand g's bb by each installed bezier.
|
|
474
474
|
SplineClipHelper.copyToBezier(newspl, ps, bounds.start, bounds.end, g.info.bb);
|
|
475
|
+
if (clipDump) {
|
|
476
|
+
let cur = fe;
|
|
477
|
+
while (cur.info.to_orig != null && cur.info.edge_type !== 0) cur = cur.info.to_orig;
|
|
478
|
+
clipDump('S5_CLIP', cur.tail.name, cur.head.name, {
|
|
479
|
+
inputPn: pn,
|
|
480
|
+
bounds: { start: bounds.start, end: bounds.end },
|
|
481
|
+
sflag: newspl.sflag, eflag: newspl.eflag,
|
|
482
|
+
sp: { ...newspl.sp }, ep: { ...newspl.ep },
|
|
483
|
+
size: newspl.size,
|
|
484
|
+
// emit reads bz.size, not list.length (memory: bezier-emit-size-not-length)
|
|
485
|
+
list: newspl.list.slice(0, newspl.size).map(p => ({ ...p })),
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Stage-dump hook for the clip/install output (diagnosis only; null in
|
|
492
|
+
* production — same pattern as splines-routespl.ts:setRouteDump).
|
|
493
|
+
*/
|
|
494
|
+
type ClipDumpFn = (stage: string, tail: string, head: string, payload: unknown) => void;
|
|
495
|
+
let clipDump: ClipDumpFn | null = null;
|
|
496
|
+
|
|
497
|
+
/** Install (or clear) the clip-stage dump callback. Diagnosis only. */
|
|
498
|
+
export function setClipDump(fn: ClipDumpFn | null): void {
|
|
499
|
+
clipDump = fn;
|
|
475
500
|
}
|
|
@@ -15,6 +15,50 @@ import { shortestPath, routeSpline, makePolyline } from '../pathplan/index.js';
|
|
|
15
15
|
import type { Poly } from '../pathplan/types.js';
|
|
16
16
|
import { INIT_DELTA, LOOP_TRIES, ROUTESPL_FUDGE } from './splines-constants.js';
|
|
17
17
|
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Stage-dump hook (diagnosis only; null in production — same pattern as
|
|
20
|
+
// mincross-order.ts:setMincrossTrace). The callback receives the routing
|
|
21
|
+
// stage name, the real edge's tail/head names (resolved through to_orig like
|
|
22
|
+
// C's routesplines_ realedge walk), and a stage payload; filtering and
|
|
23
|
+
// serialization are the callback's job. Browser-safe: no env reads here.
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
/** Stage payload consumer installed by a diagnosis harness. */
|
|
27
|
+
export type RouteDumpFn = (
|
|
28
|
+
stage: string, tail: string, head: string, payload: unknown,
|
|
29
|
+
) => void;
|
|
30
|
+
|
|
31
|
+
let routeDump: RouteDumpFn | null = null;
|
|
32
|
+
|
|
33
|
+
/** Install (or clear) the routing stage-dump callback. Diagnosis only. */
|
|
34
|
+
export function setRouteDump(fn: RouteDumpFn | null): void {
|
|
35
|
+
routeDump = fn;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface DumpEdgeLike {
|
|
39
|
+
tail?: { name?: string }; head?: { name?: string };
|
|
40
|
+
info?: { edge_type?: number; to_orig?: DumpEdgeLike };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function dumpIsVirtual(e: DumpEdgeLike): boolean {
|
|
44
|
+
return (e.info?.edge_type ?? 0) !== 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Resolve pp.data through to_orig to the real edge's names, as C does. */
|
|
48
|
+
function dumpEdgeNames(pp: Path): [string, string] {
|
|
49
|
+
let e = pp.data as DumpEdgeLike | null;
|
|
50
|
+
// C: for (realedge = pp->data; realedge && ED_edge_type(realedge) != NORMAL;
|
|
51
|
+
// realedge = ED_to_orig(realedge)); (NORMAL === 0)
|
|
52
|
+
while (e !== null && dumpIsVirtual(e)) e = e.info?.to_orig ?? null;
|
|
53
|
+
if (e === null) return ['?', '?'];
|
|
54
|
+
return [e.tail?.name ?? '?', e.head?.name ?? '?'];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Invoke the dump callback when installed; payload built lazily. */
|
|
58
|
+
function emitDump(stage: string, names: [string, string] | null, mk: () => unknown): void {
|
|
59
|
+
if (routeDump !== null && names !== null) routeDump(stage, names[0], names[1], mk());
|
|
60
|
+
}
|
|
61
|
+
|
|
18
62
|
// ---------------------------------------------------------------------------
|
|
19
63
|
// overlap helper
|
|
20
64
|
// ---------------------------------------------------------------------------
|
|
@@ -353,8 +397,20 @@ function runLimitLoop(
|
|
|
353
397
|
|
|
354
398
|
function routeSplinesInternal(pp: Path, polyline: boolean): Point[] | null {
|
|
355
399
|
const boxes = pp.boxes;
|
|
400
|
+
const dumpNames = routeDump ? dumpEdgeNames(pp) : null;
|
|
401
|
+
emitDump('S1_INPUT', dumpNames, () => ({
|
|
402
|
+
nbox: pp.nbox,
|
|
403
|
+
boxes: pp.boxes.slice(0, pp.nbox).map(b => ({ ll: { ...b.ll }, ur: { ...b.ur } })),
|
|
404
|
+
start: { p: { ...pp.start.p }, theta: pp.start.theta, constrained: pp.start.constrained },
|
|
405
|
+
end: { p: { ...pp.end.p }, theta: pp.end.theta, constrained: pp.end.constrained },
|
|
406
|
+
}));
|
|
356
407
|
if (checkPath(pp.nbox, boxes, pp)) return null;
|
|
357
408
|
const effectiveBoxn = pp.nbox;
|
|
409
|
+
emitDump('S2_CHECKED', dumpNames, () => ({
|
|
410
|
+
nbox: effectiveBoxn,
|
|
411
|
+
boxes: boxes.slice(0, effectiveBoxn).map(b => ({ ll: { ...b.ll }, ur: { ...b.ur } })),
|
|
412
|
+
start: { p: { ...pp.start.p } }, end: { p: { ...pp.end.p } },
|
|
413
|
+
}));
|
|
358
414
|
|
|
359
415
|
let flip = false;
|
|
360
416
|
if (effectiveBoxn > 1 && boxes[0].ll.y > boxes[1].ll.y) {
|
|
@@ -389,6 +445,11 @@ function routeSplinesInternal(pp: Path, polyline: boolean): Point[] | null {
|
|
|
389
445
|
console.warn('in routesplines, Pshortestpath failed');
|
|
390
446
|
return null;
|
|
391
447
|
}
|
|
448
|
+
emitDump('S3_POLY', dumpNames, () => ({
|
|
449
|
+
poly: polypoints.map(p => ({ ...p })),
|
|
450
|
+
eps: [{ ...eps[0] }, { ...eps[1] }],
|
|
451
|
+
pl: pl.map(p => ({ ...p })),
|
|
452
|
+
}));
|
|
392
453
|
|
|
393
454
|
let ps: Point[];
|
|
394
455
|
if (polyline) {
|
|
@@ -399,6 +460,11 @@ function routeSplinesInternal(pp: Path, polyline: boolean): Point[] | null {
|
|
|
399
460
|
ps = routeSpline(edges, pl, evs);
|
|
400
461
|
if (ps.length === 0) return null;
|
|
401
462
|
}
|
|
463
|
+
emitDump('S4_SPL', dumpNames, () => ({
|
|
464
|
+
polyline,
|
|
465
|
+
evs: polyline ? undefined : buildConstraintVectors(pp).map(v => ({ ...v })),
|
|
466
|
+
ps: ps.map(p => ({ ...p })),
|
|
467
|
+
}));
|
|
402
468
|
|
|
403
469
|
if (isTriviallyBounded(ps)) {
|
|
404
470
|
applyTrivialBounds(boxes, effectiveBoxn, ps);
|