@knowvah/dot-engine 2.6.11 → 2.6.12
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/render-async.d.ts.map +1 -1
- package/dist/common/agerr.d.ts +37 -0
- package/dist/common/agerr.d.ts.map +1 -0
- package/dist/common/arrows-shapes-poly.d.ts.map +1 -1
- package/dist/common/color.d.ts.map +1 -1
- package/dist/common/crand.d.ts +0 -20
- package/dist/common/crand.d.ts.map +1 -1
- package/dist/common/emit-once.d.ts +6 -0
- package/dist/common/emit-once.d.ts.map +1 -0
- package/dist/common/graph-init.d.ts.map +1 -1
- package/dist/common/htmltable-attr-warn.d.ts.map +1 -1
- package/dist/common/htmltable-emit-fill.d.ts +9 -0
- package/dist/common/htmltable-emit-fill.d.ts.map +1 -1
- package/dist/common/htmltable-emit-rules.d.ts.map +1 -1
- package/dist/common/htmltable-lex-expat.d.ts +1 -6
- package/dist/common/htmltable-lex-expat.d.ts.map +1 -1
- package/dist/common/htmltable.d.ts +1 -7
- package/dist/common/htmltable.d.ts.map +1 -1
- package/dist/common/poly-gencode.d.ts.map +1 -1
- package/dist/common/poly-init.d.ts.map +1 -1
- package/dist/common/postproc.d.ts.map +1 -1
- package/dist/common/random.d.ts.map +1 -1
- package/dist/common/render-state.d.ts +8 -0
- package/dist/common/render-state.d.ts.map +1 -0
- package/dist/common/splines-geom.d.ts +8 -0
- package/dist/common/splines-geom.d.ts.map +1 -1
- package/dist/common/splines-routespl.d.ts.map +1 -1
- package/dist/common/textmeasure-lookup.d.ts.map +1 -1
- package/dist/common/textmeasure.d.ts.map +1 -1
- package/dist/gvc/anchor.d.ts.map +1 -1
- package/dist/gvc/device-cluster.d.ts.map +1 -1
- package/dist/gvc/device-labels.d.ts +14 -0
- package/dist/gvc/device-labels.d.ts.map +1 -1
- package/dist/gvc/device.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +765 -451
- package/dist/index.js.map +4 -4
- package/dist/layout/dot/cluster.d.ts.map +1 -1
- package/dist/layout/dot/conc.d.ts.map +1 -1
- package/dist/layout/dot/decomp.d.ts.map +1 -1
- package/dist/layout/dot/edge-route-clip.d.ts +18 -10
- package/dist/layout/dot/edge-route-clip.d.ts.map +1 -1
- package/dist/layout/dot/mincross-build.d.ts.map +1 -1
- package/dist/layout/dot/mincross-cross.d.ts.map +1 -1
- package/dist/layout/dot/rank.d.ts.map +1 -1
- package/dist/layout/dot/splines.d.ts.map +1 -1
- package/dist/layout/fdp/index.d.ts.map +1 -1
- package/dist/layout/fdp/tlayout-parms.d.ts.map +1 -1
- package/dist/layout/fdp/xlayout.d.ts.map +1 -1
- package/dist/layout/neato/edge-len.d.ts.map +1 -1
- package/dist/layout/neato/fdp-adjust.d.ts.map +1 -1
- package/dist/layout/neato/init.d.ts +4 -10
- package/dist/layout/neato/init.d.ts.map +1 -1
- package/dist/layout/neato/kk.d.ts.map +1 -1
- package/dist/layout/neato/multispline-router.d.ts.map +1 -1
- package/dist/layout/neato/multispline.d.ts.map +1 -1
- package/dist/layout/neato/poly.d.ts.map +1 -1
- package/dist/layout/neato/sgd.d.ts.map +1 -1
- package/dist/layout/neato/start.d.ts.map +1 -1
- package/dist/ortho/index.d.ts.map +1 -1
- package/dist/parser/index.d.ts.map +1 -1
- package/dist/render/color-resolve.d.ts +49 -1
- package/dist/render/color-resolve.d.ts.map +1 -1
- package/dist/render/public.d.ts.map +1 -1
- package/dist/render/svg-edge-split.d.ts +2 -0
- package/dist/render/svg-edge-split.d.ts.map +1 -1
- package/dist/render/xdot-public.d.ts.map +1 -1
- package/dist/render.js +513 -200
- package/dist/render.js.map +4 -4
- package/package.json +1 -1
- package/src/async/render-async.ts +6 -0
- package/src/common/agerr.ts +96 -0
- package/src/common/arrows-shapes-poly.ts +19 -5
- package/src/common/color.ts +38 -10
- package/src/common/crand.ts +8 -2
- package/src/common/emit-once.ts +27 -0
- package/src/common/graph-init.ts +2 -1
- package/src/common/htmltable-attr-warn.ts +3 -4
- package/src/common/htmltable-emit-fill.ts +48 -0
- package/src/common/htmltable-emit-rules.ts +2 -0
- package/src/common/htmltable-lex-expat.ts +78 -23
- package/src/common/htmltable.ts +15 -16
- package/src/common/poly-gencode.ts +14 -2
- package/src/common/poly-init.ts +2 -1
- package/src/common/postproc.ts +4 -1
- package/src/common/random.ts +11 -1
- package/src/common/render-state.ts +36 -0
- package/src/common/splines-geom.ts +8 -4
- package/src/common/splines-routespl.ts +2 -1
- package/src/common/textmeasure-lookup.ts +10 -3
- package/src/common/textmeasure.ts +45 -2
- package/src/gvc/anchor.ts +1 -12
- package/src/gvc/device-cluster.ts +37 -9
- package/src/gvc/device-labels.ts +27 -0
- package/src/gvc/device.ts +18 -6
- package/src/index.ts +3 -0
- package/src/layout/dot/cluster.ts +2 -1
- package/src/layout/dot/conc.ts +6 -5
- package/src/layout/dot/decomp.ts +3 -0
- package/src/layout/dot/edge-route-clip.ts +27 -21
- package/src/layout/dot/mincross-build.ts +2 -0
- package/src/layout/dot/mincross-cross.ts +2 -0
- package/src/layout/dot/rank.ts +2 -0
- package/src/layout/dot/splines.ts +2 -1
- package/src/layout/fdp/index.ts +2 -1
- package/src/layout/fdp/tlayout-parms.ts +9 -2
- package/src/layout/fdp/xlayout.ts +10 -2
- package/src/layout/neato/cdt-surface.ts +6 -6
- package/src/layout/neato/edge-len.ts +2 -1
- package/src/layout/neato/fdp-adjust.ts +3 -2
- package/src/layout/neato/init.ts +11 -11
- package/src/layout/neato/kk.ts +2 -1
- package/src/layout/neato/multispline-router.ts +3 -3
- package/src/layout/neato/multispline.ts +3 -2
- package/src/layout/neato/poly.ts +3 -2
- package/src/layout/neato/sgd.ts +5 -2
- package/src/layout/neato/start.ts +14 -10
- package/src/ortho/index.ts +3 -1
- package/src/parser/dot.js +4 -1
- package/src/parser/dot.pegjs +5 -1
- package/src/parser/index.ts +4 -3
- package/src/render/color-resolve.ts +193 -4
- package/src/render/public.ts +2 -0
- package/src/render/svg-edge-split.ts +4 -2
- package/src/render/xdot-public.ts +2 -0
package/src/common/poly-init.ts
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
DEFAULT_COLOR,
|
|
25
25
|
} from './make-label.js';
|
|
26
26
|
import { computeVertices } from './poly-vertices.js';
|
|
27
|
+
import { agwarningf } from './agerr.js';
|
|
27
28
|
|
|
28
29
|
// Default half-dimensions in points (72 dpi, 0.75 in wide, 0.5 in tall)
|
|
29
30
|
const DEFAULT_LW = 27;
|
|
@@ -117,7 +118,7 @@ export function polyInit(n: Node, g: Graph, measurer: TextMeasurer): void {
|
|
|
117
118
|
// C warns when the custom shape's image cannot be sized (headless: always).
|
|
118
119
|
// @see lib/common/shapes.c:2027-2033
|
|
119
120
|
if (shapefile !== undefined && shapefile !== '' && shapeName !== 'epsf') {
|
|
120
|
-
|
|
121
|
+
agwarningf(`No or improper shapefile="${shapefile}" for node "${n.name}"`, 'error');
|
|
121
122
|
}
|
|
122
123
|
if ((n.info.shape as ShapeDesc).kind === ShapeKind.SH_RECORD) {
|
|
123
124
|
if (n.info.shape_info === undefined) recordNodeInit(n, g, measurer);
|
package/src/common/postproc.ts
CHANGED
|
@@ -26,6 +26,8 @@ import {
|
|
|
26
26
|
RANKDIR_TB, RANKDIR_LR, RANKDIR_BT, RANKDIR_RL,
|
|
27
27
|
} from '../layout/dot/init.js';
|
|
28
28
|
import { addXLabels } from './xlabels-place.js';
|
|
29
|
+
import { registerRenderState } from './render-state.js';
|
|
30
|
+
import { agerrLineOn } from './agerr.js';
|
|
29
31
|
|
|
30
32
|
/** Concentrated multi-edge duplicate — never drawn. @see lib/common/const.h:IGNORED */
|
|
31
33
|
const IGNORED_EDGE = 6;
|
|
@@ -38,6 +40,7 @@ const IGNORED_EDGE = 6;
|
|
|
38
40
|
|
|
39
41
|
let Rankdir = 0;
|
|
40
42
|
let Offset: Point = { x: 0, y: 0 };
|
|
43
|
+
registerRenderState(() => { Rankdir = 0; Offset = { x: 0, y: 0 }; });
|
|
41
44
|
|
|
42
45
|
/** The bezier fields map_edge reads/writes. */
|
|
43
46
|
type MappedBezier = Pick<Bezier, 'list' | 'sflag' | 'eflag' | 'sp' | 'ep' | 'sArrowOps' | 'eArrowOps'>;
|
|
@@ -226,7 +229,7 @@ function translateDrawing(g: Graph): void {
|
|
|
226
229
|
// @see lib/common/postproc.c:map_edge ("lost %s %s edge")
|
|
227
230
|
if (e.info.spl === undefined) {
|
|
228
231
|
if (!(g.info.concentrate ?? false) && (e.info.edge_type ?? 0) !== IGNORED_EDGE) {
|
|
229
|
-
|
|
232
|
+
agerrLineOn('warn', `lost ${e.tail.name} ${e.head.name} edge`);
|
|
230
233
|
}
|
|
231
234
|
continue;
|
|
232
235
|
}
|
package/src/common/random.ts
CHANGED
|
@@ -14,12 +14,22 @@
|
|
|
14
14
|
* @see lib/neatogen/neatoinit.c:checkStart (srand48 call)
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
+
import { registerRenderState } from './render-state.js';
|
|
18
|
+
|
|
17
19
|
const A = BigInt('0x5deece66d');
|
|
18
20
|
const C = BigInt('0xb');
|
|
19
21
|
const MASK48 = (1n << 48n) - 1n;
|
|
20
22
|
const TWO48 = 2 ** 48;
|
|
23
|
+
/**
|
|
24
|
+
* Unseeded generator state: libc's X0 = 0x1234ABCD330E (rand48 seed words
|
|
25
|
+
* 0x1234, 0xABCD, 0x330E); verified on the reference machine — an unseeded
|
|
26
|
+
* drand48() returns 0.39646477376027534.
|
|
27
|
+
* @see POSIX drand48(3)
|
|
28
|
+
*/
|
|
29
|
+
const UNSEEDED_STATE = 0x1234abcd330en;
|
|
21
30
|
|
|
22
|
-
let state =
|
|
31
|
+
let state = UNSEEDED_STATE;
|
|
32
|
+
registerRenderState(() => { state = UNSEEDED_STATE; });
|
|
23
33
|
|
|
24
34
|
/**
|
|
25
35
|
* Seed the generator: X = (seed << 16) | 0x330E (low 32 bits of seed).
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Per-render scope for C process-level state (AD-4).
|
|
5
|
+
*
|
|
6
|
+
* Native Graphviz renders one graph per `dot` process, so every file static —
|
|
7
|
+
* RNG streams, sequence counters, the emit_once warning set — starts each
|
|
8
|
+
* graph at its C initial value (and gvFreeContext's emit_once_reset clears the
|
|
9
|
+
* warning set). The port's module statics outlive a render, so each module
|
|
10
|
+
* owning such state registers a reset that restores its C initial value, and
|
|
11
|
+
* every public render entry calls {@link resetRenderState} before any other
|
|
12
|
+
* work. Render output is then a function of its input alone.
|
|
13
|
+
*
|
|
14
|
+
* Not registered here: caller-installed hooks (setTextMeasurer,
|
|
15
|
+
* setImageSizer, setImageResolver) and debug hooks — they are configuration,
|
|
16
|
+
* not C process state.
|
|
17
|
+
*
|
|
18
|
+
* @see lib/gvc/gvcontext.c:gvFreeContext (emit_once_reset)
|
|
19
|
+
* @see lib/common/emit.c:emit_once_reset
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** Registered resets, run in registration (module evaluation) order. */
|
|
23
|
+
const resets: Array<() => void> = [];
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Register a reset restoring one module's C process state to its initial
|
|
27
|
+
* value. Called once, at module evaluation.
|
|
28
|
+
*/
|
|
29
|
+
export function registerRenderState(reset: () => void): void {
|
|
30
|
+
resets.push(reset);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Restore every registered module's C process state; call first per render. */
|
|
34
|
+
export function resetRenderState(): void {
|
|
35
|
+
for (const reset of resets) reset();
|
|
36
|
+
}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import type { Point, Box } from '../model/geom.js';
|
|
12
12
|
import { bezier as evalBezier } from './bezier.js';
|
|
13
|
+
import { fma } from './fma.js';
|
|
13
14
|
|
|
14
15
|
// ---------------------------------------------------------------------------
|
|
15
16
|
// Approximate equality
|
|
@@ -47,16 +48,19 @@ const HW = 2.0;
|
|
|
47
48
|
const SMALL = 0.0000000001;
|
|
48
49
|
|
|
49
50
|
/**
|
|
50
|
-
* Distance from point p to line a-b, squared.
|
|
51
|
+
* Distance from point p to line a-b, squared. Native (arm64, clang
|
|
52
|
+
* -ffp-contract=on) fuses the numerator as fmadd(p.y-a.y, dx, -(dy*(p.x-a.x)))
|
|
53
|
+
* and the denominator as fmadd(dx, dx, dy*dy): libgvc _ptToLine2 at
|
|
54
|
+
* 0x21214-0x21244, identically in inlined _update_bb_bz and _approx_bezier.
|
|
51
55
|
* @see lib/common/geom.c:ptToLine2
|
|
52
56
|
*/
|
|
53
|
-
function ptToLine2(a: Point, b: Point, p: Point): number {
|
|
57
|
+
export function ptToLine2(a: Point, b: Point, p: Point): number {
|
|
54
58
|
const dx = b.x - a.x;
|
|
55
59
|
const dy = b.y - a.y;
|
|
56
|
-
let a2 = (p.y - a.y
|
|
60
|
+
let a2 = fma(p.y - a.y, dx, -(dy * (p.x - a.x)));
|
|
57
61
|
a2 *= a2; // square — ensures positive
|
|
58
62
|
if (a2 < SMALL) return 0; // avoid 0/0 problems
|
|
59
|
-
return a2 / (dx
|
|
63
|
+
return a2 / fma(dx, dx, dy * dy);
|
|
60
64
|
}
|
|
61
65
|
|
|
62
66
|
/**
|
|
@@ -15,6 +15,7 @@ import { shortestPath, routeSpline, makePolyline } from '../pathplan/index.js';
|
|
|
15
15
|
import type { Poly } from '../pathplan/types.js';
|
|
16
16
|
import { INIT_DELTA, LOOP_TRIES, ROUTESPL_FUDGE } from './splines-constants.js';
|
|
17
17
|
import { fma } from './fma.js';
|
|
18
|
+
import { agerrLineOn } from './agerr.js';
|
|
18
19
|
|
|
19
20
|
// ---------------------------------------------------------------------------
|
|
20
21
|
// Stage-dump hook (diagnosis only; null in production — same pattern as
|
|
@@ -450,7 +451,7 @@ function routeSplinesInternal(pp: Path, polyline: boolean): Point[] | null {
|
|
|
450
451
|
if (pl === null) {
|
|
451
452
|
// C: agerrorf("in routesplines, Pshortestpath failed") — the caller then
|
|
452
453
|
// loses the edge. @see lib/common/routespl.c (Pshortestpath < 0 branch)
|
|
453
|
-
|
|
454
|
+
agerrLineOn('warn', 'in routesplines, Pshortestpath failed');
|
|
454
455
|
return null;
|
|
455
456
|
}
|
|
456
457
|
emitDump('S3_POLY', dumpNames, () => ({
|
|
@@ -8,6 +8,8 @@ import {
|
|
|
8
8
|
ALL_FONT_METRICS,
|
|
9
9
|
type FontFamilyData,
|
|
10
10
|
} from "./textmeasure-lut-data.js";
|
|
11
|
+
import { registerRenderState } from "./render-state.js";
|
|
12
|
+
import { agwarningf } from './agerr.js';
|
|
11
13
|
|
|
12
14
|
/** Times is always present; used as the fallback family. */
|
|
13
15
|
export const TIMES_FALLBACK: FontFamilyData = ALL_FONT_METRICS[0]!;
|
|
@@ -30,8 +32,13 @@ export function normalizeFontName(name: string): string {
|
|
|
30
32
|
return result;
|
|
31
33
|
}
|
|
32
34
|
|
|
33
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* Font names that have already triggered the fallback warning: C's emit_once
|
|
37
|
+
* set, emptied per render (a native run is one process; emit_once_reset).
|
|
38
|
+
* @see lib/common/emit.c:emit_once
|
|
39
|
+
*/
|
|
34
40
|
const warnedFontNames = new Set<string>();
|
|
41
|
+
registerRenderState(() => warnedFontNames.clear());
|
|
35
42
|
|
|
36
43
|
/** @see lib/common/textspan_lut.c:get_metrics_for_font_family */
|
|
37
44
|
export function getFamilyMetrics(fontName: string): FontFamilyData {
|
|
@@ -41,8 +48,8 @@ export function getFamilyMetrics(fontName: string): FontFamilyData {
|
|
|
41
48
|
}
|
|
42
49
|
if (!warnedFontNames.has(fontName)) {
|
|
43
50
|
warnedFontNames.add(fontName);
|
|
44
|
-
|
|
45
|
-
`
|
|
51
|
+
agwarningf(
|
|
52
|
+
`no hard-coded metrics for '${fontName}'. ` +
|
|
46
53
|
`Falling back to 'Times' metrics`,
|
|
47
54
|
);
|
|
48
55
|
}
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
import { type FontFamilyData } from "./textmeasure-lut-data.js";
|
|
11
11
|
import { getFamilyMetrics, normalizeFontName } from "./textmeasure-lookup.js";
|
|
12
12
|
import { canvasFont } from "./css-font.js";
|
|
13
|
+
import { agwarningf } from "./agerr.js";
|
|
14
|
+
import { registerRenderState } from "./render-state.js";
|
|
13
15
|
|
|
14
16
|
/** Number of hard-coded font families in the LUT. */
|
|
15
17
|
export const LUT_FAMILY_COUNT = 11;
|
|
@@ -72,12 +74,53 @@ function getVariantWidths(
|
|
|
72
74
|
// ── Character width lookup ────────────────────────────────────────────────────
|
|
73
75
|
|
|
74
76
|
const SPACE_CHAR_CODE = 32;
|
|
77
|
+
const FIRST_NON_ASCII = 128;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* C's two `static bool warning_already_reported` flags, one per warning.
|
|
81
|
+
* Once per render (AD-4), as one native process renders one graph.
|
|
82
|
+
* @see lib/common/textspan_lut.c:estimate_character_width_canonical
|
|
83
|
+
*/
|
|
84
|
+
const warned = new Set<string>();
|
|
85
|
+
const NON_ASCII_FLAG = "non-ascii";
|
|
86
|
+
const ASCII_FLAG = "ascii";
|
|
87
|
+
|
|
88
|
+
registerRenderState(() => warned.clear());
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The C format strings already begin with `Warning: `, and agwarningf adds its
|
|
92
|
+
* own prefix, so native prints `Warning: Warning: ...`. The trailing newline
|
|
93
|
+
* of the C format is the line terminator `console.warn` supplies.
|
|
94
|
+
*/
|
|
95
|
+
function warnNonAscii(character: number): void {
|
|
96
|
+
if (warned.has(NON_ASCII_FLAG)) return;
|
|
97
|
+
warned.add(NON_ASCII_FLAG);
|
|
98
|
+
agwarningf(
|
|
99
|
+
`Warning: no value for width of non-ASCII character ${character}. Falling back to width of space character`,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function warnAscii(character: number): void {
|
|
104
|
+
if (warned.has(ASCII_FLAG)) return;
|
|
105
|
+
warned.add(ASCII_FLAG);
|
|
106
|
+
agwarningf(
|
|
107
|
+
`Warning: no value for width of ASCII character ${character}. Falling back to 0`,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
75
110
|
|
|
76
111
|
/** @see lib/common/textspan_lut.c:estimate_character_width_canonical */
|
|
77
112
|
function charWidthUnits(widths: readonly number[], charCode: number): number {
|
|
78
|
-
|
|
113
|
+
let code = charCode;
|
|
114
|
+
if (code >= FIRST_NON_ASCII) {
|
|
115
|
+
warnNonAscii(code);
|
|
116
|
+
code = SPACE_CHAR_CODE;
|
|
117
|
+
}
|
|
79
118
|
const w = widths[code] ?? -1;
|
|
80
|
-
|
|
119
|
+
if (w < 0) {
|
|
120
|
+
warnAscii(code);
|
|
121
|
+
return 0;
|
|
122
|
+
}
|
|
123
|
+
return w;
|
|
81
124
|
}
|
|
82
125
|
|
|
83
126
|
/**
|
package/src/gvc/anchor.ts
CHANGED
|
@@ -21,6 +21,7 @@ import { substObjAnchor, preprocessTooltip } from '../common/subst.js';
|
|
|
21
21
|
import { parseStyleFlags } from '../common/style-resolve.js';
|
|
22
22
|
import { nodeAttr } from '../common/poly-init.js';
|
|
23
23
|
import { bezier } from '../common/bezier.js';
|
|
24
|
+
import { ptToLine2 } from '../common/splines-geom.js';
|
|
24
25
|
import type { RendererPlugin } from './context.js';
|
|
25
26
|
import { MapShape, type ObjState, type RenderJob } from './job.js';
|
|
26
27
|
|
|
@@ -231,8 +232,6 @@ export function beginAnchorIf(renderer: RendererPlugin, job: RenderJob): boolean
|
|
|
231
232
|
const DFLT_SAMPLE = 20;
|
|
232
233
|
/** HW: maximum distance from line, in points. @see lib/common/emit.c:739 */
|
|
233
234
|
const MAP_HW = 2.0;
|
|
234
|
-
/** SMALL: 0/0 guard in ptToLine2. @see lib/common/geom.c:176 */
|
|
235
|
-
const MAP_SMALL = 0.0000000001;
|
|
236
235
|
|
|
237
236
|
/**
|
|
238
237
|
* Coordinate-transform context for the imagemap device. `bb` is the unpadded
|
|
@@ -430,16 +429,6 @@ export function computeLabelRectMap(lab: TextlabelT, obj: ObjState, ctx: MapCtx)
|
|
|
430
429
|
|
|
431
430
|
// --- Edge spline hot spots (map_output_bspline) --------------------------
|
|
432
431
|
|
|
433
|
-
/** ptToLine2: squared distance from p to segment (a,b). @see lib/common/geom.c */
|
|
434
|
-
function ptToLine2(a: Point, b: Point, p: Point): number {
|
|
435
|
-
const dx = b.x - a.x;
|
|
436
|
-
const dy = b.y - a.y;
|
|
437
|
-
let a2 = (p.y - a.y) * dx - (p.x - a.x) * dy;
|
|
438
|
-
a2 *= a2;
|
|
439
|
-
if (a2 < MAP_SMALL) return 0;
|
|
440
|
-
return a2 / (dx * dx + dy * dy);
|
|
441
|
-
}
|
|
442
|
-
|
|
443
432
|
/** check_control_points: both handles within HW of the chord. @see emit.c */
|
|
444
433
|
function checkControlPoints(cp: Point[]): boolean {
|
|
445
434
|
return ptToLine2(cp[0]!, cp[3]!, cp[1]!) < MAP_HW * MAP_HW
|
|
@@ -13,7 +13,8 @@ import {
|
|
|
13
13
|
parseStyleFlags, resolvePenColor, resolvePenType, resolvePenWidth,
|
|
14
14
|
resolveClusterFillEx,
|
|
15
15
|
} from '../common/style-resolve.js';
|
|
16
|
-
import { resolveRenderColor, withColorScheme } from '../render/color-resolve.js';
|
|
16
|
+
import { resolveRenderColor, withColorScheme, reportColorNames } from '../render/color-resolve.js';
|
|
17
|
+
import { parseSegs } from '../common/multicolor.js';
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Copy gradient fields from a linear/radial ResolvedFill onto obj.
|
|
@@ -101,14 +102,41 @@ export function applyClusterObjState(sg: Graph, job: RenderJob): boolean {
|
|
|
101
102
|
const obj = job.obj;
|
|
102
103
|
// C wraps the cluster color block with setColorScheme. @see emit.c:3800/3943
|
|
103
104
|
return withColorScheme(clusterAttr(sg, 'colorscheme'), () => {
|
|
105
|
+
// C resolves the fill (set_fillcolor / gradient vals) before the pen,
|
|
106
|
+
// which is set just before gvrender_box. @see emit.c:3846-3868 vs 3907-3913
|
|
107
|
+
const filled = applyClusterFill(obj, fillRes);
|
|
104
108
|
applyClusterPenState(obj, sg);
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
obj.fill = FillType.Solid;
|
|
108
|
-
obj.fillColor = resolveRenderColor(fillRes.color);
|
|
109
|
-
return true;
|
|
110
|
-
}
|
|
111
|
-
applyClusterGradient(obj, fillRes);
|
|
112
|
-
return true;
|
|
109
|
+
reportColorNames(stripedClusterColors(sg));
|
|
110
|
+
return filled;
|
|
113
111
|
});
|
|
114
112
|
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The colours stripedBox sets for a `style=striped` cluster (not rounded): each
|
|
116
|
+
* segment of the fill spec with a positive width, after the pen.
|
|
117
|
+
* @see lib/common/emit.c:3896-3906 emit_clusters (striped branch)
|
|
118
|
+
* @see lib/common/emit.c:595 stripedBox
|
|
119
|
+
*/
|
|
120
|
+
function stripedClusterColors(sg: Graph): string[] {
|
|
121
|
+
const flags = parseStyleFlags(clusterStyle(sg));
|
|
122
|
+
if (!flags.striped || flags.rounded) return [];
|
|
123
|
+
const color = clusterAttr(sg, 'color') ?? '';
|
|
124
|
+
let fill = clusterAttr(sg, 'fillcolor') || color;
|
|
125
|
+
const bg = clusterAttr(sg, 'bgcolor') ?? '';
|
|
126
|
+
if ((!flags.filled || fill === '') && bg !== '') fill = bg;
|
|
127
|
+
const { segs, error } = parseSegs(fill === '' ? 'lightgrey' : fill);
|
|
128
|
+
if (error === 1 || error === 2) return [];
|
|
129
|
+
return segs.flatMap((seg) => (seg.color !== null && seg.t > 0 ? [seg.color] : []));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** The fill half of {@link applyClusterObjState}; true when the box is filled. */
|
|
133
|
+
function applyClusterFill(obj: ObjState, fillRes: ResolvedFill): boolean {
|
|
134
|
+
if (fillRes.kind === 'none') { obj.fill = FillType.None; return false; }
|
|
135
|
+
if (fillRes.kind === 'solid') {
|
|
136
|
+
obj.fill = FillType.Solid;
|
|
137
|
+
obj.fillColor = resolveRenderColor(fillRes.color);
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
applyClusterGradient(obj, fillRes);
|
|
141
|
+
return true;
|
|
142
|
+
}
|
package/src/gvc/device-labels.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import type { Graph } from '../model/graph.js';
|
|
12
12
|
import type { Node } from '../model/node.js';
|
|
13
|
+
import type { Edge } from '../model/edge.js';
|
|
13
14
|
import type { RendererPlugin } from './context.js';
|
|
14
15
|
import type { RenderJob } from './job.js';
|
|
15
16
|
import type { TextlabelT } from '../common/types.js';
|
|
@@ -17,6 +18,8 @@ import type { TextSpan } from '../common/emit-types.js';
|
|
|
17
18
|
import type { PlacedHtml } from '../common/htmltable-pos.js';
|
|
18
19
|
import { gvrenderTextspan, withLabelEmitState } from './textspan-emit.js';
|
|
19
20
|
import { emitHtmlLabel } from '../common/htmltable-emit.js';
|
|
21
|
+
import { reportColorNames } from '../render/color-resolve.js';
|
|
22
|
+
import { htmlLabelColors } from '../common/htmltable-emit-fill.js';
|
|
20
23
|
|
|
21
24
|
/** valign codes stored on textlabel_t. @see lib/common/types.h:textlabel_t.valign */
|
|
22
25
|
const VALIGN_TOP = 't'.charCodeAt(0);
|
|
@@ -38,6 +41,29 @@ function labelSpanX(lp: TextlabelT, just: 'l' | 'n' | 'r'): number {
|
|
|
38
41
|
return lp.pos.x;
|
|
39
42
|
}
|
|
40
43
|
|
|
44
|
+
/**
|
|
45
|
+
* The names emit_label hands to gvrender_resolve_color: an HTML label's walk,
|
|
46
|
+
* or a plain label's fontcolor (set once, before the spans, when there is a
|
|
47
|
+
* span).
|
|
48
|
+
* @see lib/common/labels.c:emit_label
|
|
49
|
+
*/
|
|
50
|
+
export function labelColorNames(lp: TextlabelT): string[] {
|
|
51
|
+
if (lp.html) return lp.u.kind === 'html' ? htmlLabelColors(lp.u.html as PlacedHtml) : [];
|
|
52
|
+
return lp.u.kind === 'txt' && lp.u.nspans >= 1 ? [lp.fontcolor] : [];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Report the unknown colours of an edge's four labels in emit_end_edge order
|
|
57
|
+
* (label, xlabel, head, tail), each only when placed.
|
|
58
|
+
* @see lib/common/emit.c:emit_edge_label
|
|
59
|
+
*/
|
|
60
|
+
export function reportEdgeLabelColors(e: Edge): void {
|
|
61
|
+
const labels = [e.info.label, e.info.xlabel, e.info.head_label, e.info.tail_label];
|
|
62
|
+
for (const lp of labels as Array<TextlabelT | undefined>) {
|
|
63
|
+
if (lp?.set) reportColorNames(labelColorNames(lp));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
41
67
|
/**
|
|
42
68
|
* Emit one label's text spans if present and placed.
|
|
43
69
|
* Shared by edge-label, node-xlabel, and graph-label slots.
|
|
@@ -59,6 +85,7 @@ export function renderOneLabel(
|
|
|
59
85
|
// default pos. Callers mirroring the existence-only sites pass false.
|
|
60
86
|
if (!lp) return;
|
|
61
87
|
if (requireSet && !lp.set) return;
|
|
88
|
+
reportColorNames(labelColorNames(lp));
|
|
62
89
|
// HTML branch: @see lib/common/labels.c:emit_label (226-230)
|
|
63
90
|
// C routes to emit_html_label(job, lp->u.html, lp) using lp->pos as anchor.
|
|
64
91
|
if (lp.html) {
|
package/src/gvc/device.ts
CHANGED
|
@@ -42,11 +42,14 @@ import {
|
|
|
42
42
|
styleTokens,
|
|
43
43
|
} from '../common/style-resolve.js';
|
|
44
44
|
import { isPointNode } from '../common/poly-gencode.js';
|
|
45
|
-
import {
|
|
45
|
+
import {
|
|
46
|
+
resolveRenderColor, withColorScheme, reportEdgeColors, reportColorNames, backgroundColors,
|
|
47
|
+
setRendererKnownColors,
|
|
48
|
+
} from '../render/color-resolve.js';
|
|
46
49
|
import { emitRoundedBezier } from '../common/poly-shapes.js';
|
|
47
50
|
import { applyClusterObjState, clusterStyle, clusterPeripheries } from './device-cluster.js';
|
|
48
51
|
import { transformPoint } from './device-transform.js';
|
|
49
|
-
import { renderOneLabel, renderNodeXLabel, renderClusterLabel } from './device-labels.js';
|
|
52
|
+
import { renderOneLabel, renderNodeXLabel, renderClusterLabel, reportEdgeLabelColors } from './device-labels.js';
|
|
50
53
|
export { transformPoint, applyRotation, applyScale, buildPoint } from './device-transform.js';
|
|
51
54
|
export { renderOneLabel, renderNodeXLabel, renderClusterLabel } from './device-labels.js';
|
|
52
55
|
import { svgNodeId, svgEdgeId, svgClusterId, svgGraphId } from '../render/svg-id.js';
|
|
@@ -156,7 +159,7 @@ function setEdgePen(e: Edge, job: RenderJob): void {
|
|
|
156
159
|
const obj = job.obj;
|
|
157
160
|
const flags = parseStyleFlags(e.attrs.get('style'));
|
|
158
161
|
obj.penColor = withColorScheme(e.attrs.get('colorscheme'),
|
|
159
|
-
() => resolveRenderColor(resolvePenColor(e.attrs.get('color'))));
|
|
162
|
+
() => resolveRenderColor(resolvePenColor(e.attrs.get('color')), false));
|
|
160
163
|
obj.pen = resolvePenType(flags);
|
|
161
164
|
obj.penWidth = resolvePenWidth(flags, e.attrs.get('penwidth'));
|
|
162
165
|
// Raw style tokens for xdot's `S` ops (e.g. explicit style="solid"); SVG
|
|
@@ -189,6 +192,10 @@ export function renderEdge(e: Edge, renderer: RendererPlugin, job: RenderJob): v
|
|
|
189
192
|
// @see lib/common/emit.c:emit_edge (begin/end color context)
|
|
190
193
|
withColorScheme(e.attrs.get('colorscheme'), () => {
|
|
191
194
|
renderer.beginEdge(e, job);
|
|
195
|
+
// emit_edge_graphics, then emit_end_edge's labels: C resolves these names
|
|
196
|
+
// here whatever the renderer draws (the non-drawing ones included).
|
|
197
|
+
reportEdgeColors(e);
|
|
198
|
+
reportEdgeLabelColors(e);
|
|
192
199
|
renderer.endEdge(e, job);
|
|
193
200
|
});
|
|
194
201
|
} finally {
|
|
@@ -351,9 +358,13 @@ function renderPage(g: Graph, renderer: RendererPlugin, job: RenderJob, info: La
|
|
|
351
358
|
resolveObjAnchor(g, labelTextOf(g.info.label), svgGraphId(g, job), job.obj);
|
|
352
359
|
}
|
|
353
360
|
const anchored = beginAnchorIf(renderer, job);
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
361
|
+
// emit_page opens the graph's colorscheme around the background and label.
|
|
362
|
+
withColorScheme(g.attrs.get('colorscheme'), () => {
|
|
363
|
+
reportColorNames(backgroundColors(g.attrs.get('bgcolor'), renderer.type === 'svg'));
|
|
364
|
+
renderer.pageBackground?.(g, job);
|
|
365
|
+
// emit root-graph label before clusters/nodes @see lib/common/emit.c:emit_page
|
|
366
|
+
renderGraphLabel(g, renderer, job);
|
|
367
|
+
});
|
|
357
368
|
if (anchored) renderer.endAnchor?.(job);
|
|
358
369
|
// emit_view: clusters are drawn BEFORE nodes/edges normally, but the map
|
|
359
370
|
// device (EMIT_CLUSTERS_LAST) emits their <area> AFTER, so a node/edge area
|
|
@@ -459,6 +470,7 @@ export function render(ctx: GvcContext, g: Graph, format: string, inlineImages =
|
|
|
459
470
|
// @see lib/common/emit.c:3260 (gvc->rotation=90); :3390 (job->rotation)
|
|
460
471
|
job.rotation = parseLandscape(g) ? 90 : 0;
|
|
461
472
|
job.renderer = renderer;
|
|
473
|
+
setRendererKnownColors(renderer.type === 'svg'); // svg_knowncolors; others have none
|
|
462
474
|
resetHtmlAnchorIds(); // C: fresh anchorId per dot invocation
|
|
463
475
|
for (const n of g.nodes.values()) polyInit(n, g, job.measurer);
|
|
464
476
|
return renderGraph(g, job, renderer);
|
package/src/index.ts
CHANGED
|
@@ -22,6 +22,7 @@ import type { GvError, RenderResult } from './errors.js';
|
|
|
22
22
|
import type { EngineName } from './gvc/context.js';
|
|
23
23
|
import { render as deviceRender } from './gvc/device.js';
|
|
24
24
|
import { createDefaultContext } from './gvc/default-context.js';
|
|
25
|
+
import { resetRenderState } from './common/render-state.js';
|
|
25
26
|
|
|
26
27
|
|
|
27
28
|
|
|
@@ -90,6 +91,7 @@ function classifyError(err: unknown): GvError {
|
|
|
90
91
|
*/
|
|
91
92
|
export function renderSvg(dotSource: string, engine: EngineName): string {
|
|
92
93
|
checkRenderArgs(dotSource, engine);
|
|
94
|
+
resetRenderState(); // AD-4: one render = one native process
|
|
93
95
|
const ctx = createDefaultContext();
|
|
94
96
|
try {
|
|
95
97
|
// parse() is inside the try so any non-ParseError throw (e.g. a raw
|
|
@@ -122,6 +124,7 @@ export function renderSvg(dotSource: string, engine: EngineName): string {
|
|
|
122
124
|
export function tryRenderSvg(dotSource: string, engine: EngineName): RenderResult {
|
|
123
125
|
checkRenderArgs(dotSource, engine);
|
|
124
126
|
try {
|
|
127
|
+
// renderSvg calls resetRenderState first (AD-4).
|
|
125
128
|
return { svg: renderSvg(dotSource, engine) };
|
|
126
129
|
} catch (err: unknown) {
|
|
127
130
|
if (isUsageError(err)) throw err;
|
|
@@ -27,6 +27,7 @@ import { CLUSTER } from './rank.js';
|
|
|
27
27
|
import { makeSlots, safeOtherEdge, makeInterclustChain } from './cluster-path.js';
|
|
28
28
|
// mergeChain lives in classify.ts (circular: classify ↔ cluster — safe for function refs)
|
|
29
29
|
import { mergeChain, portsEq } from './classify.js';
|
|
30
|
+
import { agwarningf } from '../../common/agerr.js';
|
|
30
31
|
|
|
31
32
|
export { CLUSTER_EDGE } from './cluster-path.js';
|
|
32
33
|
export { mapInterclustNode } from './cluster-path.js';
|
|
@@ -297,7 +298,7 @@ export function markClusterNode(clust: Graph, n: Node, g: Graph): void {
|
|
|
297
298
|
if ((n.info.ranktype ?? 0) !== 0) {
|
|
298
299
|
// C warns with the ENCLOSING graph's name, not the cluster's.
|
|
299
300
|
// @see lib/dotgen/cluster.c:317-320
|
|
300
|
-
|
|
301
|
+
agwarningf(`${n.name} was already in a rankset, deleted from cluster ${g.name}`, 'error');
|
|
301
302
|
agDeleteFromCluster(clust, n);
|
|
302
303
|
return;
|
|
303
304
|
}
|
package/src/layout/dot/conc.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { dotScanRanks } from './rank.js';
|
|
|
20
20
|
import { dotRoot, rankGet } from './mincross-utils.js';
|
|
21
21
|
import { nodesInSeq } from './decomp.js';
|
|
22
22
|
import { graphMinrank, graphMaxrank } from './position-aux.js';
|
|
23
|
+
import { agerr, agerrorf, agwarningf } from '../../common/agerr.js';
|
|
23
24
|
|
|
24
25
|
const UP = 0;
|
|
25
26
|
const DOWN = 1;
|
|
@@ -263,13 +264,13 @@ export function fillRankVlist(g: Graph, r: number, root: Graph): number {
|
|
|
263
264
|
const rankArr = g.info.rank!;
|
|
264
265
|
const lead = g.info.rankleader![r];
|
|
265
266
|
if (lead === undefined || lead === null) {
|
|
266
|
-
|
|
267
|
+
agerrorf(`rebuild_vlists: lead is null for rank ${r}`);
|
|
267
268
|
return -1;
|
|
268
269
|
}
|
|
269
270
|
const rootRank = root.info.rank![r];
|
|
270
271
|
if (rootRank.v[lead.info.order ?? 0] !== lead) {
|
|
271
|
-
|
|
272
|
-
`
|
|
272
|
+
agerrorf(
|
|
273
|
+
`rebuild_vlists: rank lead ${lead.name} not in order ` +
|
|
273
274
|
`${lead.info.order ?? 0} of rank ${r}`,
|
|
274
275
|
);
|
|
275
276
|
return -1;
|
|
@@ -292,7 +293,7 @@ export function fillRankVlist(g: Graph, r: number, root: Graph): number {
|
|
|
292
293
|
rankArr[r].v = rootRank.v;
|
|
293
294
|
rankArr[r].vStart = lead.info.order ?? 0;
|
|
294
295
|
const maxi = computeMaxi(g, r);
|
|
295
|
-
if (maxi === -1)
|
|
296
|
+
if (maxi === -1) agwarningf(`degenerate concentrated rank ${g.name},${r}`, 'error');
|
|
296
297
|
rankArr[r].n = maxi + 1;
|
|
297
298
|
return 0;
|
|
298
299
|
}
|
|
@@ -425,7 +426,7 @@ export function dotConcentrate(g: Graph): number {
|
|
|
425
426
|
const clust = g.info.clust ?? [];
|
|
426
427
|
for (let c = 0; c < nCluster; c++) {
|
|
427
428
|
if (rebuildVlists(clust[c]) !== 0) {
|
|
428
|
-
|
|
429
|
+
agerr('prev', 'concentrate=true may not work correctly.');
|
|
429
430
|
return -1;
|
|
430
431
|
}
|
|
431
432
|
}
|
package/src/layout/dot/decomp.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import type { Node } from '../../model/node.js';
|
|
4
4
|
import type { Graph } from '../../model/graph.js';
|
|
5
5
|
import type { EdgeList } from '../../model/nodeInfo.js';
|
|
6
|
+
import { registerRenderState } from '../../common/render-state.js';
|
|
6
7
|
|
|
7
8
|
// ---------------------------------------------------------------------------
|
|
8
9
|
// AGSEQ-order node iteration (agfstnode/agnxtnode mirror)
|
|
@@ -135,7 +136,9 @@ export function decompSearch(g: Graph, start: Node, cmark: number): void {
|
|
|
135
136
|
}
|
|
136
137
|
|
|
137
138
|
// Cmark as const-object property avoids module-level let that confuses Lizard.
|
|
139
|
+
/** @see lib/dotgen/decomp.c:Cmark (C static, 0 at process start) */
|
|
138
140
|
const _dc = { cmark: 0 };
|
|
141
|
+
registerRenderState(() => { _dc.cmark = 0; });
|
|
139
142
|
|
|
140
143
|
// ONE private function immediately before decompose — the acyclic.ts pattern.
|
|
141
144
|
function resolveNode(n: Node, pass: 0 | 1): Node | undefined {
|
|
@@ -12,6 +12,7 @@ import type { Point } from '../../model/geom.js';
|
|
|
12
12
|
import { parseArrow, resolveArrowType } from '../../common/arrows.js';
|
|
13
13
|
import { arrowLength } from '../../common/arrows-shapes.js';
|
|
14
14
|
import { bezier } from '../../common/bezier.js';
|
|
15
|
+
import { fma } from '../../common/fma.js';
|
|
15
16
|
|
|
16
17
|
// ---------------------------------------------------------------------------
|
|
17
18
|
// De Casteljau subdivision
|
|
@@ -115,17 +116,30 @@ export function bezierClipNode(
|
|
|
115
116
|
}
|
|
116
117
|
|
|
117
118
|
/**
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
|
|
119
|
+
* DIST(p, q) = sqrt(SQR(dx) + SQR(dy)) as the oracle compiles it
|
|
120
|
+
* (libgvc 16.1 `_arrowEndClip`: `fmul dy*dy; fmadd dx, dx; fsqrt`), i.e.
|
|
121
|
+
* sqrt(fma(dx, dx, dy * dy)) -- not Math.hypot.
|
|
122
|
+
* @see lib/common/geom.h:DIST
|
|
123
|
+
*/
|
|
124
|
+
export function distFused(p: Point, q: Point): number {
|
|
125
|
+
const dx = p.x - q.x;
|
|
126
|
+
const dy = p.y - q.y;
|
|
127
|
+
return Math.sqrt(fma(dx, dx, dy * dy));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Clip the path endpoint to the arrowhead BASE (not the node boundary).
|
|
123
132
|
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
* 3
|
|
128
|
-
*
|
|
133
|
+
* Ports the bezier part of lib/common/arrows.c:arrowEndClip (:285-330); the
|
|
134
|
+
* `spl->eflag/ep` bookkeeping and the `endp -= 3` back-up (:288-296) live in
|
|
135
|
+
* the callers (compound.ts:arrowEndClipIdx). Line map (C -> here):
|
|
136
|
+
* :299-302 sp[3..0] = ps[endp..], sp[0] = spl->ep -> `sp` below
|
|
137
|
+
* :304 if (elen > 0) -> `elen > 0`
|
|
138
|
+
* :305-307 inside_context {p = &sp[0], r = &elen} -> `inside` (centre = tip)
|
|
139
|
+
* :308 bezier_clip(&ctx, inside, sp, true) -> bezierClipNode, left
|
|
140
|
+
* inside, in ABSOLUTE coordinates (centre 0,0), as C does
|
|
141
|
+
* :311-314 ps[endp..endp+3] = sp[3..0] -> returned array
|
|
142
|
+
* :281-283 inside(): DIST(p, a.p[0]) <= a.r[0] -> distFused <= elen
|
|
129
143
|
*
|
|
130
144
|
* @see lib/common/arrows.c:arrowEndClip
|
|
131
145
|
*/
|
|
@@ -134,18 +148,10 @@ export function arrowEndClip(
|
|
|
134
148
|
arrowTip: Point,
|
|
135
149
|
elen: number,
|
|
136
150
|
): Point[] {
|
|
137
|
-
if (elen <= 0) return path4;
|
|
138
|
-
// Reverse: sp[0]=tip (inside sphere), sp[1..3]=reversed last 3 path points
|
|
139
151
|
const sp: Point[] = [arrowTip, path4[2]!, path4[1]!, path4[0]!];
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const spRel: Point[] = sp.map(p => ({ x: p.x - arrowTip.x, y: p.y - arrowTip.y }));
|
|
144
|
-
const clippedRel = bezierClipLocal(spRel, insideSphere, true);
|
|
145
|
-
const clipped = clippedRel.map(p => ({ x: p.x + arrowTip.x, y: p.y + arrowTip.y }));
|
|
146
|
-
// Un-reverse: the clipped right subdivision = [arrowBase, ..., arrowTip]
|
|
147
|
-
// The path endpoint becomes arrowBase = clipped[0]
|
|
148
|
-
// Un-reverse the full clipped result; all 4 points update via de Casteljau
|
|
152
|
+
const clipped = elen > 0
|
|
153
|
+
? bezierClipNode(sp, 0, 0, (x, y) => distFused({ x, y }, arrowTip) <= elen, true)
|
|
154
|
+
: sp;
|
|
149
155
|
return [clipped[3]!, clipped[2]!, clipped[1]!, clipped[0]!];
|
|
150
156
|
}
|
|
151
157
|
|