mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-09-18 14:59:53 +02:00
Some cleanup
This commit is contained in:
@@ -32,26 +32,8 @@
|
|||||||
href="https://fonts.googleapis.com/css2?family=Kalam:wght@300;400;700&family=Rubik+Mono+One&display=swap"
|
href="https://fonts.googleapis.com/css2?family=Kalam:wght@300;400;700&family=Rubik+Mono+One&display=swap"
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
/>
|
/>
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
||||||
<link
|
|
||||||
href="https://fonts.googleapis.com/css2?family=Recursive:wght@300..1000&display=swap"
|
|
||||||
rel="stylesheet"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.recursive-mermaid {
|
|
||||||
font-family: 'Recursive', sans-serif;
|
|
||||||
font-optical-sizing: auto;
|
|
||||||
font-weight: 500;
|
|
||||||
font-style: normal;
|
|
||||||
font-variation-settings:
|
|
||||||
'slnt' 0,
|
|
||||||
'CASL' 0,
|
|
||||||
'CRSV' 0.5,
|
|
||||||
'MONO' 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
body {
|
||||||
/* background: rgb(221, 208, 208); */
|
/* background: rgb(221, 208, 208); */
|
||||||
/* background: #333; */
|
/* background: #333; */
|
||||||
@@ -63,9 +45,7 @@
|
|||||||
h1 {
|
h1 {
|
||||||
color: grey;
|
color: grey;
|
||||||
}
|
}
|
||||||
.mermaid {
|
|
||||||
border: 1px solid red;
|
|
||||||
}
|
|
||||||
.mermaid2 {
|
.mermaid2 {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -103,11 +83,6 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.class2 {
|
|
||||||
fill: red;
|
|
||||||
fill-opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* tspan {
|
/* tspan {
|
||||||
font-size: 6px !important;
|
font-size: 6px !important;
|
||||||
} */
|
} */
|
||||||
@@ -134,16 +109,57 @@
|
|||||||
---
|
---
|
||||||
config:
|
config:
|
||||||
layout: elk
|
layout: elk
|
||||||
flowchart:
|
|
||||||
curve: linear
|
|
||||||
---
|
---
|
||||||
flowchart LR
|
flowchart-elk TB
|
||||||
D["Use<br/>the<br/>editor"] -- Mermaid js --> I["fa:fa-code Text"]
|
internet
|
||||||
I --> D & D
|
nat
|
||||||
D@{ shape: question}
|
router
|
||||||
I@{ shape: question}
|
compute1
|
||||||
</pre>
|
|
||||||
<pre id="diagram4" class="mermaid2">
|
subgraph project
|
||||||
|
router
|
||||||
|
nat
|
||||||
|
subgraph subnet1
|
||||||
|
compute1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
%% router --> subnet1
|
||||||
|
subnet1 --> nat
|
||||||
|
%% nat --> internet
|
||||||
|
</pre
|
||||||
|
>
|
||||||
|
<pre id="diagram4" class="mermaid">
|
||||||
|
---
|
||||||
|
config:
|
||||||
|
layout: elk
|
||||||
|
---
|
||||||
|
flowchart-elk TB
|
||||||
|
internet
|
||||||
|
nat
|
||||||
|
router
|
||||||
|
lb1
|
||||||
|
lb2
|
||||||
|
compute1
|
||||||
|
compute2
|
||||||
|
subgraph project
|
||||||
|
router
|
||||||
|
nat
|
||||||
|
subgraph subnet1
|
||||||
|
compute1
|
||||||
|
lb1
|
||||||
|
end
|
||||||
|
subgraph subnet2
|
||||||
|
compute2
|
||||||
|
lb2
|
||||||
|
end
|
||||||
|
end
|
||||||
|
internet --> router
|
||||||
|
router --> subnet1 & subnet2
|
||||||
|
subnet1 & subnet2 --> nat --> internet
|
||||||
|
</pre
|
||||||
|
>
|
||||||
|
<pre id="diagram4" class="mermaid">
|
||||||
---
|
---
|
||||||
config:
|
config:
|
||||||
layout: elk
|
layout: elk
|
||||||
@@ -566,7 +582,6 @@ kanban
|
|||||||
<script type="module">
|
<script type="module">
|
||||||
import mermaid from './mermaid.esm.mjs';
|
import mermaid from './mermaid.esm.mjs';
|
||||||
import layouts from './mermaid-layout-elk.esm.mjs';
|
import layouts from './mermaid-layout-elk.esm.mjs';
|
||||||
import tidyTreeLayouts from './mermaid-layout-tidy-tree.esm.mjs';
|
|
||||||
|
|
||||||
const staticBellIconPack = {
|
const staticBellIconPack = {
|
||||||
prefix: 'fa6-regular',
|
prefix: 'fa6-regular',
|
||||||
@@ -592,7 +607,6 @@ kanban
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
mermaid.registerLayoutLoaders(layouts);
|
mermaid.registerLayoutLoaders(layouts);
|
||||||
mermaid.registerLayoutLoaders(tidyTreeLayouts);
|
|
||||||
mermaid.parseError = function (err, hash) {
|
mermaid.parseError = function (err, hash) {
|
||||||
console.error('Mermaid error: ', err);
|
console.error('Mermaid error: ', err);
|
||||||
};
|
};
|
||||||
@@ -603,7 +617,7 @@ kanban
|
|||||||
alert('It worked');
|
alert('It worked');
|
||||||
}
|
}
|
||||||
await mermaid.initialize({
|
await mermaid.initialize({
|
||||||
// theme: 'forest',
|
// theme: 'base',
|
||||||
// theme: 'default',
|
// theme: 'default',
|
||||||
// theme: 'forest',
|
// theme: 'forest',
|
||||||
// handDrawnSeed: 12,
|
// handDrawnSeed: 12,
|
||||||
@@ -614,7 +628,11 @@ kanban
|
|||||||
// layout: 'fixed',
|
// layout: 'fixed',
|
||||||
// htmlLabels: false,
|
// htmlLabels: false,
|
||||||
flowchart: { titleTopMargin: 10 },
|
flowchart: { titleTopMargin: 10 },
|
||||||
fontFamily: "'Recursive', sans-serif",
|
|
||||||
|
// fontFamily: 'Caveat',
|
||||||
|
// fontFamily: 'Kalam',
|
||||||
|
// fontFamily: 'courier',
|
||||||
|
fontFamily: 'arial',
|
||||||
sequence: {
|
sequence: {
|
||||||
actorFontFamily: 'courier',
|
actorFontFamily: 'courier',
|
||||||
noteFontFamily: 'courier',
|
noteFontFamily: 'courier',
|
||||||
|
67
packages/mermaid-layout-elk/src/__tests__/geometry.spec.ts
Normal file
67
packages/mermaid-layout-elk/src/__tests__/geometry.spec.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import {
|
||||||
|
intersection,
|
||||||
|
ensureTrulyOutside,
|
||||||
|
makeInsidePoint,
|
||||||
|
tryNodeIntersect,
|
||||||
|
replaceEndpoint,
|
||||||
|
type RectLike,
|
||||||
|
type P,
|
||||||
|
} from '../geometry.js';
|
||||||
|
|
||||||
|
const approx = (a: number, b: number, eps = 1e-6) => Math.abs(a - b) < eps;
|
||||||
|
|
||||||
|
describe('geometry helpers', () => {
|
||||||
|
it('intersection: vertical approach hits bottom border', () => {
|
||||||
|
const rect: RectLike = { x: 0, y: 0, width: 100, height: 50 };
|
||||||
|
const h = rect.height / 2; // 25
|
||||||
|
const outside: P = { x: 0, y: 100 };
|
||||||
|
const inside: P = { x: 0, y: 0 };
|
||||||
|
const res = intersection(rect, outside, inside);
|
||||||
|
expect(approx(res.x, 0)).toBe(true);
|
||||||
|
expect(approx(res.y, h)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ensureTrulyOutside nudges near-boundary point outward', () => {
|
||||||
|
const rect: RectLike = { x: 0, y: 0, width: 100, height: 50 };
|
||||||
|
// near bottom boundary (y ~ h)
|
||||||
|
const near: P = { x: 0, y: rect.height / 2 - 0.2 };
|
||||||
|
const out = ensureTrulyOutside(rect, near, 10);
|
||||||
|
expect(out.y).toBeGreaterThan(rect.height / 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('makeInsidePoint keeps x for vertical and y from center', () => {
|
||||||
|
const rect: RectLike = { x: 10, y: 5, width: 100, height: 50 };
|
||||||
|
const outside: P = { x: 10, y: 40 };
|
||||||
|
const center: P = { x: 99, y: -123 }; // center y should be used
|
||||||
|
const inside = makeInsidePoint(rect, outside, center);
|
||||||
|
expect(inside.x).toBe(outside.x);
|
||||||
|
expect(inside.y).toBe(center.y);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('tryNodeIntersect returns null for wrong-side intersections', () => {
|
||||||
|
const rect: RectLike = { x: 0, y: 0, width: 100, height: 50 };
|
||||||
|
const outside: P = { x: -50, y: 0 };
|
||||||
|
const node = { intersect: () => ({ x: 10, y: 0 }) } as any; // right side of center
|
||||||
|
const res = tryNodeIntersect(node, rect, outside);
|
||||||
|
expect(res).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('replaceEndpoint dedup removes end/start appropriately', () => {
|
||||||
|
const pts: P[] = [
|
||||||
|
{ x: 0, y: 0 },
|
||||||
|
{ x: 1, y: 1 },
|
||||||
|
];
|
||||||
|
// remove duplicate end
|
||||||
|
replaceEndpoint(pts, 'end', { x: 1, y: 1 });
|
||||||
|
expect(pts.length).toBe(1);
|
||||||
|
|
||||||
|
const pts2: P[] = [
|
||||||
|
{ x: 0, y: 0 },
|
||||||
|
{ x: 1, y: 1 },
|
||||||
|
];
|
||||||
|
// remove duplicate start
|
||||||
|
replaceEndpoint(pts2, 'start', { x: 0, y: 0 });
|
||||||
|
expect(pts2.length).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
187
packages/mermaid-layout-elk/src/geometry.ts
Normal file
187
packages/mermaid-layout-elk/src/geometry.ts
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
/* Geometry utilities extracted from render.ts for reuse and testing */
|
||||||
|
|
||||||
|
export interface P {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RectLike {
|
||||||
|
x: number; // center x
|
||||||
|
y: number; // center y
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
padding?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NodeLike {
|
||||||
|
intersect?: (p: P) => P | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EPS = 1;
|
||||||
|
export const PUSH_OUT = 10;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute intersection between a rectangle (center x/y, width/height) and the line
|
||||||
|
* segment from insidePoint -\> outsidePoint. Returns the point on the rectangle border.
|
||||||
|
*
|
||||||
|
* This version avoids snapping to outsidePoint when certain variables evaluate to 0
|
||||||
|
* (previously caused vertical top/bottom cases to miss the border). It only enforces
|
||||||
|
* axis-constant behavior for purely vertical/horizontal approaches.
|
||||||
|
*/
|
||||||
|
export const intersection = (node: RectLike, outsidePoint: P, insidePoint: P): P => {
|
||||||
|
const x = node.x;
|
||||||
|
const y = node.y;
|
||||||
|
|
||||||
|
const dx = Math.abs(x - insidePoint.x);
|
||||||
|
const w = node.width / 2;
|
||||||
|
let r = insidePoint.x < outsidePoint.x ? w - dx : w + dx;
|
||||||
|
const h = node.height / 2;
|
||||||
|
|
||||||
|
const Q = Math.abs(outsidePoint.y - insidePoint.y);
|
||||||
|
const R = Math.abs(outsidePoint.x - insidePoint.x);
|
||||||
|
|
||||||
|
if (Math.abs(y - outsidePoint.y) * w > Math.abs(x - outsidePoint.x) * h) {
|
||||||
|
// Intersection is top or bottom of rect.
|
||||||
|
const q = insidePoint.y < outsidePoint.y ? outsidePoint.y - h - y : y - h - outsidePoint.y;
|
||||||
|
r = (R * q) / Q;
|
||||||
|
const res = {
|
||||||
|
x: insidePoint.x < outsidePoint.x ? insidePoint.x + r : insidePoint.x - R + r,
|
||||||
|
y: insidePoint.y < outsidePoint.y ? insidePoint.y + Q - q : insidePoint.y - Q + q,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Keep axis-constant special-cases only
|
||||||
|
if (R === 0) {
|
||||||
|
res.x = outsidePoint.x;
|
||||||
|
}
|
||||||
|
if (Q === 0) {
|
||||||
|
res.y = outsidePoint.y;
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
} else {
|
||||||
|
// Intersection on sides of rect
|
||||||
|
if (insidePoint.x < outsidePoint.x) {
|
||||||
|
r = outsidePoint.x - w - x;
|
||||||
|
} else {
|
||||||
|
r = x - w - outsidePoint.x;
|
||||||
|
}
|
||||||
|
const q = (Q * r) / R;
|
||||||
|
let _x = insidePoint.x < outsidePoint.x ? insidePoint.x + R - r : insidePoint.x - R + r;
|
||||||
|
let _y = insidePoint.y < outsidePoint.y ? insidePoint.y + q : insidePoint.y - q;
|
||||||
|
|
||||||
|
// Only handle axis-constant cases
|
||||||
|
if (R === 0) {
|
||||||
|
_x = outsidePoint.x;
|
||||||
|
}
|
||||||
|
if (Q === 0) {
|
||||||
|
_y = outsidePoint.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { x: _x, y: _y };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const outsideNode = (node: RectLike, point: P): boolean => {
|
||||||
|
const x = node.x;
|
||||||
|
const y = node.y;
|
||||||
|
const dx = Math.abs(point.x - x);
|
||||||
|
const dy = Math.abs(point.y - y);
|
||||||
|
const w = node.width / 2;
|
||||||
|
const h = node.height / 2;
|
||||||
|
return dx >= w || dy >= h;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ensureTrulyOutside = (bounds: RectLike, p: P, push = PUSH_OUT): P => {
|
||||||
|
const dx = Math.abs(p.x - bounds.x);
|
||||||
|
const dy = Math.abs(p.y - bounds.y);
|
||||||
|
const w = bounds.width / 2;
|
||||||
|
const h = bounds.height / 2;
|
||||||
|
if (Math.abs(dx - w) < EPS || Math.abs(dy - h) < EPS) {
|
||||||
|
const dirX = p.x - bounds.x;
|
||||||
|
const dirY = p.y - bounds.y;
|
||||||
|
const len = Math.sqrt(dirX * dirX + dirY * dirY);
|
||||||
|
if (len > 0) {
|
||||||
|
return {
|
||||||
|
x: bounds.x + (dirX / len) * (len + push),
|
||||||
|
y: bounds.y + (dirY / len) * (len + push),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return p;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const makeInsidePoint = (bounds: RectLike, outside: P, center: P): P => {
|
||||||
|
const isVertical = Math.abs(outside.x - bounds.x) < EPS;
|
||||||
|
const isHorizontal = Math.abs(outside.y - bounds.y) < EPS;
|
||||||
|
return {
|
||||||
|
x: isVertical
|
||||||
|
? outside.x
|
||||||
|
: outside.x < bounds.x
|
||||||
|
? bounds.x - bounds.width / 4
|
||||||
|
: bounds.x + bounds.width / 4,
|
||||||
|
y: isHorizontal ? outside.y : center.y,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const tryNodeIntersect = (node: NodeLike, bounds: RectLike, outside: P): P | null => {
|
||||||
|
if (!node?.intersect) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const res = node.intersect(outside);
|
||||||
|
if (!res) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const wrongSide =
|
||||||
|
(outside.x < bounds.x && res.x > bounds.x) || (outside.x > bounds.x && res.x < bounds.x);
|
||||||
|
if (wrongSide) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const dist = Math.hypot(outside.x - res.x, outside.y - res.y);
|
||||||
|
if (dist <= EPS) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fallbackIntersection = (bounds: RectLike, outside: P, center: P): P => {
|
||||||
|
const inside = makeInsidePoint(bounds, outside, center);
|
||||||
|
return intersection(bounds, outside, inside);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const computeNodeIntersection = (
|
||||||
|
node: NodeLike,
|
||||||
|
bounds: RectLike,
|
||||||
|
outside: P,
|
||||||
|
center: P
|
||||||
|
): P => {
|
||||||
|
const outside2 = ensureTrulyOutside(bounds, outside);
|
||||||
|
return tryNodeIntersect(node, bounds, outside2) ?? fallbackIntersection(bounds, outside2, center);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const replaceEndpoint = (
|
||||||
|
points: P[],
|
||||||
|
which: 'start' | 'end',
|
||||||
|
value: P | null | undefined,
|
||||||
|
tol = 0.1
|
||||||
|
) => {
|
||||||
|
if (!value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const isDup = points.some((p, i) =>
|
||||||
|
which === 'start'
|
||||||
|
? i > 0 && Math.abs(p.x - value.x) < tol && Math.abs(p.y - value.y) < tol
|
||||||
|
: i < points.length - 1 && Math.abs(p.x - value.x) < tol && Math.abs(p.y - value.y) < tol
|
||||||
|
);
|
||||||
|
if (isDup) {
|
||||||
|
if (which === 'start') {
|
||||||
|
points.shift();
|
||||||
|
} else {
|
||||||
|
points.pop();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (which === 'start') {
|
||||||
|
points[0] = value;
|
||||||
|
} else {
|
||||||
|
points[points.length - 1] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
@@ -4,6 +4,14 @@ import { curveLinear } from 'd3';
|
|||||||
import ELK from 'elkjs/lib/elk.bundled.js';
|
import ELK from 'elkjs/lib/elk.bundled.js';
|
||||||
import { type TreeData, findCommonAncestor } from './find-common-ancestor.js';
|
import { type TreeData, findCommonAncestor } from './find-common-ancestor.js';
|
||||||
|
|
||||||
|
import {
|
||||||
|
type P,
|
||||||
|
type RectLike,
|
||||||
|
outsideNode,
|
||||||
|
computeNodeIntersection,
|
||||||
|
replaceEndpoint,
|
||||||
|
} from './geometry.js';
|
||||||
|
|
||||||
type Node = LayoutData['nodes'][number];
|
type Node = LayoutData['nodes'][number];
|
||||||
|
|
||||||
// Minimal structural type to avoid depending on d3 Selection typings
|
// Minimal structural type to avoid depending on d3 Selection typings
|
||||||
@@ -241,6 +249,112 @@ export const render = async (
|
|||||||
/**
|
/**
|
||||||
* Add edges to graph based on parsed graph definition
|
* Add edges to graph based on parsed graph definition
|
||||||
*/
|
*/
|
||||||
|
// Edge helper maps and utilities (de-duplicated)
|
||||||
|
const ARROW_MAP: Record<string, [string, string]> = {
|
||||||
|
arrow_open: ['arrow_open', 'arrow_open'],
|
||||||
|
arrow_cross: ['arrow_open', 'arrow_cross'],
|
||||||
|
double_arrow_cross: ['arrow_cross', 'arrow_cross'],
|
||||||
|
arrow_point: ['arrow_open', 'arrow_point'],
|
||||||
|
double_arrow_point: ['arrow_point', 'arrow_point'],
|
||||||
|
arrow_circle: ['arrow_open', 'arrow_circle'],
|
||||||
|
double_arrow_circle: ['arrow_circle', 'arrow_circle'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const computeStroke = (
|
||||||
|
stroke: string | undefined,
|
||||||
|
defaultStyle?: string,
|
||||||
|
defaultLabelStyle?: string
|
||||||
|
) => {
|
||||||
|
// Defaults correspond to 'normal'
|
||||||
|
let thickness = 'normal';
|
||||||
|
let pattern = 'solid';
|
||||||
|
let style = '';
|
||||||
|
let labelStyle = '';
|
||||||
|
|
||||||
|
if (stroke === 'dotted') {
|
||||||
|
pattern = 'dotted';
|
||||||
|
style = 'fill:none;stroke-width:2px;stroke-dasharray:3;';
|
||||||
|
} else if (stroke === 'thick') {
|
||||||
|
thickness = 'thick';
|
||||||
|
style = 'stroke-width: 3.5px;fill:none;';
|
||||||
|
} else {
|
||||||
|
// normal
|
||||||
|
style = defaultStyle ?? 'fill:none;';
|
||||||
|
if (defaultLabelStyle !== undefined) {
|
||||||
|
labelStyle = defaultLabelStyle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { thickness, pattern, style, labelStyle };
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCurve = (edgeInterpolate: any, edgesDefaultInterpolate: any, confCurve: any) => {
|
||||||
|
if (edgeInterpolate !== undefined) {
|
||||||
|
return interpolateToCurve(edgeInterpolate, curveLinear);
|
||||||
|
}
|
||||||
|
if (edgesDefaultInterpolate !== undefined) {
|
||||||
|
return interpolateToCurve(edgesDefaultInterpolate, curveLinear);
|
||||||
|
}
|
||||||
|
// @ts-ignore TODO: fix this
|
||||||
|
return interpolateToCurve(confCurve, curveLinear);
|
||||||
|
};
|
||||||
|
const buildEdgeData = (
|
||||||
|
edge: any,
|
||||||
|
defaults: {
|
||||||
|
defaultStyle?: string;
|
||||||
|
defaultLabelStyle?: string;
|
||||||
|
defaultInterpolate?: any;
|
||||||
|
confCurve: any;
|
||||||
|
},
|
||||||
|
common: any
|
||||||
|
) => {
|
||||||
|
const edgeData: any = { style: '', labelStyle: '' };
|
||||||
|
edgeData.minlen = edge.length || 1;
|
||||||
|
// maintain legacy behavior
|
||||||
|
edge.text = edge.label;
|
||||||
|
|
||||||
|
// Arrowhead fill vs none
|
||||||
|
edgeData.arrowhead = edge.type === 'arrow_open' ? 'none' : 'normal';
|
||||||
|
|
||||||
|
// Arrow types
|
||||||
|
const arrowMap = ARROW_MAP[edge.type] ?? ARROW_MAP.arrow_open;
|
||||||
|
edgeData.arrowTypeStart = arrowMap[0];
|
||||||
|
edgeData.arrowTypeEnd = arrowMap[1];
|
||||||
|
|
||||||
|
// Optional edge label positioning flags
|
||||||
|
edgeData.startLabelRight = edge.startLabelRight;
|
||||||
|
edgeData.endLabelLeft = edge.endLabelLeft;
|
||||||
|
|
||||||
|
// Stroke
|
||||||
|
const strokeRes = computeStroke(edge.stroke, defaults.defaultStyle, defaults.defaultLabelStyle);
|
||||||
|
edgeData.thickness = strokeRes.thickness;
|
||||||
|
edgeData.pattern = strokeRes.pattern;
|
||||||
|
edgeData.style = (edgeData.style || '') + (strokeRes.style || '');
|
||||||
|
edgeData.labelStyle = (edgeData.labelStyle || '') + (strokeRes.labelStyle || '');
|
||||||
|
|
||||||
|
// Curve
|
||||||
|
// @ts-ignore - defaults.confCurve is present at runtime but missing in type
|
||||||
|
edgeData.curve = getCurve(edge.interpolate, defaults.defaultInterpolate, defaults.confCurve);
|
||||||
|
|
||||||
|
// Arrowhead style + labelpos when we have label text
|
||||||
|
const hasText = (edge?.text ?? '') !== '';
|
||||||
|
if (hasText) {
|
||||||
|
edgeData.arrowheadStyle = 'fill: #333';
|
||||||
|
edgeData.labelpos = 'c';
|
||||||
|
} else if (edge.style !== undefined) {
|
||||||
|
edgeData.arrowheadStyle = 'fill: #333';
|
||||||
|
}
|
||||||
|
|
||||||
|
edgeData.labelType = edge.labelType;
|
||||||
|
edgeData.label = (edge?.text ?? '').replace(common.lineBreakRegex, '\n');
|
||||||
|
|
||||||
|
if (edge.style === undefined) {
|
||||||
|
edgeData.style = edgeData.style ?? 'stroke: #333; stroke-width: 1.5px;fill:none;';
|
||||||
|
}
|
||||||
|
|
||||||
|
edgeData.labelStyle = edgeData.labelStyle.replace('color:', 'fill:');
|
||||||
|
return edgeData;
|
||||||
|
};
|
||||||
|
|
||||||
const addEdges = async function (
|
const addEdges = async function (
|
||||||
dataForLayout: { edges: any; direction?: string },
|
dataForLayout: { edges: any; direction?: string },
|
||||||
graph: {
|
graph: {
|
||||||
@@ -297,99 +411,18 @@ export const render = async (
|
|||||||
const linkNameStart = 'LS_' + edge.start;
|
const linkNameStart = 'LS_' + edge.start;
|
||||||
const linkNameEnd = 'LE_' + edge.end;
|
const linkNameEnd = 'LE_' + edge.end;
|
||||||
|
|
||||||
const edgeData: any = { style: '', labelStyle: '' };
|
|
||||||
edgeData.minlen = edge.length || 1;
|
|
||||||
edge.text = edge.label;
|
|
||||||
// Set link type for rendering
|
|
||||||
if (edge.type === 'arrow_open') {
|
|
||||||
edgeData.arrowhead = 'none';
|
|
||||||
} else {
|
|
||||||
edgeData.arrowhead = 'normal';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check of arrow types, placed here in order not to break old rendering
|
|
||||||
edgeData.arrowTypeStart = 'arrow_open';
|
|
||||||
edgeData.arrowTypeEnd = 'arrow_open';
|
|
||||||
|
|
||||||
/* eslint-disable no-fallthrough */
|
|
||||||
switch (edge.type) {
|
|
||||||
case 'double_arrow_cross':
|
|
||||||
edgeData.arrowTypeStart = 'arrow_cross';
|
|
||||||
case 'arrow_cross':
|
|
||||||
edgeData.arrowTypeEnd = 'arrow_cross';
|
|
||||||
break;
|
|
||||||
case 'double_arrow_point':
|
|
||||||
edgeData.arrowTypeStart = 'arrow_point';
|
|
||||||
case 'arrow_point':
|
|
||||||
edgeData.arrowTypeEnd = 'arrow_point';
|
|
||||||
break;
|
|
||||||
case 'double_arrow_circle':
|
|
||||||
edgeData.arrowTypeStart = 'arrow_circle';
|
|
||||||
case 'arrow_circle':
|
|
||||||
edgeData.arrowTypeEnd = 'arrow_circle';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
let style = '';
|
|
||||||
let labelStyle = '';
|
|
||||||
|
|
||||||
edgeData.startLabelRight = edge.startLabelRight;
|
|
||||||
edgeData.endLabelLeft = edge.endLabelLeft;
|
|
||||||
|
|
||||||
switch (edge.stroke) {
|
|
||||||
case 'normal':
|
|
||||||
style = 'fill:none;';
|
|
||||||
if (defaultStyle !== undefined) {
|
|
||||||
style = defaultStyle;
|
|
||||||
}
|
|
||||||
if (defaultLabelStyle !== undefined) {
|
|
||||||
labelStyle = defaultLabelStyle;
|
|
||||||
}
|
|
||||||
edgeData.thickness = 'normal';
|
|
||||||
edgeData.pattern = 'solid';
|
|
||||||
break;
|
|
||||||
case 'dotted':
|
|
||||||
edgeData.thickness = 'normal';
|
|
||||||
edgeData.pattern = 'dotted';
|
|
||||||
edgeData.style = 'fill:none;stroke-width:2px;stroke-dasharray:3;';
|
|
||||||
break;
|
|
||||||
case 'thick':
|
|
||||||
edgeData.thickness = 'thick';
|
|
||||||
edgeData.pattern = 'solid';
|
|
||||||
edgeData.style = 'stroke-width: 3.5px;fill:none;';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
edgeData.style += style;
|
|
||||||
edgeData.labelStyle += labelStyle;
|
|
||||||
|
|
||||||
const conf = getConfig();
|
const conf = getConfig();
|
||||||
if (edge.interpolate !== undefined) {
|
const edgeData = buildEdgeData(
|
||||||
edgeData.curve = interpolateToCurve(edge.interpolate, curveLinear);
|
edge,
|
||||||
} else if (edges.defaultInterpolate !== undefined) {
|
{
|
||||||
edgeData.curve = interpolateToCurve(edges.defaultInterpolate, curveLinear);
|
defaultStyle,
|
||||||
} else {
|
defaultLabelStyle,
|
||||||
// @ts-ignore TODO: fix this
|
defaultInterpolate: edges.defaultInterpolate,
|
||||||
edgeData.curve = interpolateToCurve(conf.curve, curveLinear);
|
// @ts-ignore - conf.curve exists at runtime but is missing from typing
|
||||||
}
|
confCurve: conf.curve,
|
||||||
|
},
|
||||||
if (edge.text === undefined) {
|
common
|
||||||
if (edge.style !== undefined) {
|
);
|
||||||
edgeData.arrowheadStyle = 'fill: #333';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
edgeData.arrowheadStyle = 'fill: #333';
|
|
||||||
edgeData.labelpos = 'c';
|
|
||||||
}
|
|
||||||
|
|
||||||
edgeData.labelType = edge.labelType;
|
|
||||||
edgeData.label = (edge?.text || '').replace(common.lineBreakRegex, '\n');
|
|
||||||
|
|
||||||
if (edge.style === undefined) {
|
|
||||||
edgeData.style = edgeData.style || 'stroke: #333; stroke-width: 1.5px;fill:none;';
|
|
||||||
}
|
|
||||||
|
|
||||||
edgeData.labelStyle = edgeData.labelStyle.replace('color:', 'fill:');
|
|
||||||
|
|
||||||
edgeData.id = linkId;
|
edgeData.id = linkId;
|
||||||
edgeData.classes = 'flowchart-link ' + linkNameStart + ' ' + linkNameEnd;
|
edgeData.classes = 'flowchart-link ' + linkNameStart + ' ' + linkNameEnd;
|
||||||
@@ -460,422 +493,94 @@ export const render = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const intersection = (
|
// Node bounds helpers (global)
|
||||||
node: { x: any; y: any; width: number; height: number },
|
const getEffectiveGroupWidth = (node: any): number => {
|
||||||
outsidePoint: { x: number; y: number },
|
const labelW = node?.labels?.[0]?.width ?? 0;
|
||||||
insidePoint: { x: number; y: number }
|
const padding = node?.padding ?? 0;
|
||||||
) => {
|
return Math.max(node.width ?? 0, labelW + padding);
|
||||||
log.debug(`intersection calc abc89:
|
|
||||||
outsidePoint: ${JSON.stringify(outsidePoint)}
|
|
||||||
insidePoint : ${JSON.stringify(insidePoint)}
|
|
||||||
node : x:${node.x} y:${node.y} w:${node.width} h:${node.height}`);
|
|
||||||
const x = node.x;
|
|
||||||
const y = node.y;
|
|
||||||
|
|
||||||
const dx = Math.abs(x - insidePoint.x);
|
|
||||||
// const dy = Math.abs(y - insidePoint.y);
|
|
||||||
const w = node.width / 2;
|
|
||||||
let r = insidePoint.x < outsidePoint.x ? w - dx : w + dx;
|
|
||||||
const h = node.height / 2;
|
|
||||||
|
|
||||||
const Q = Math.abs(outsidePoint.y - insidePoint.y);
|
|
||||||
const R = Math.abs(outsidePoint.x - insidePoint.x);
|
|
||||||
|
|
||||||
if (Math.abs(y - outsidePoint.y) * w > Math.abs(x - outsidePoint.x) * h) {
|
|
||||||
// Intersection is top or bottom of rect.
|
|
||||||
const q = insidePoint.y < outsidePoint.y ? outsidePoint.y - h - y : y - h - outsidePoint.y;
|
|
||||||
r = (R * q) / Q;
|
|
||||||
const res = {
|
|
||||||
x: insidePoint.x < outsidePoint.x ? insidePoint.x + r : insidePoint.x - R + r,
|
|
||||||
y: insidePoint.y < outsidePoint.y ? insidePoint.y + Q - q : insidePoint.y - Q + q,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Keep axis-constant special-cases but do not snap to outsidePoint when r===0
|
const boundsFor = (node: any): RectLike => {
|
||||||
if (R === 0) {
|
const width = node?.isGroup ? getEffectiveGroupWidth(node) : node.width;
|
||||||
// Vertical approach: x is constant
|
return {
|
||||||
res.x = outsidePoint.x;
|
x: node.offset.posX + node.width / 2,
|
||||||
}
|
y: node.offset.posY + node.height / 2,
|
||||||
if (Q === 0) {
|
width,
|
||||||
// Horizontal approach: y is constant
|
height: node.height,
|
||||||
res.y = outsidePoint.y;
|
padding: node.padding,
|
||||||
}
|
|
||||||
|
|
||||||
log.debug(`abc89 topp/bott calc, Q ${Q}, q ${q}, R ${R}, r ${r}`, res); // cspell: disable-line
|
|
||||||
|
|
||||||
return res;
|
|
||||||
} else {
|
|
||||||
// Intersection onn sides of rect
|
|
||||||
if (insidePoint.x < outsidePoint.x) {
|
|
||||||
r = outsidePoint.x - w - x;
|
|
||||||
} else {
|
|
||||||
// r = outsidePoint.x - w - x;
|
|
||||||
r = x - w - outsidePoint.x;
|
|
||||||
}
|
|
||||||
const q = (Q * r) / R;
|
|
||||||
// OK let _x = insidePoint.x < outsidePoint.x ? insidePoint.x + R - r : insidePoint.x + dx - w;
|
|
||||||
// OK let _x = insidePoint.x < outsidePoint.x ? insidePoint.x + R - r : outsidePoint.x + r;
|
|
||||||
let _x = insidePoint.x < outsidePoint.x ? insidePoint.x + R - r : insidePoint.x - R + r;
|
|
||||||
// let _x = insidePoint.x < outsidePoint.x ? insidePoint.x + R - r : outsidePoint.x + r;
|
|
||||||
let _y = insidePoint.y < outsidePoint.y ? insidePoint.y + q : insidePoint.y - q;
|
|
||||||
log.debug(`sides calc abc89, Q ${Q}, q ${q}, R ${R}, r ${r}`, { _x, _y });
|
|
||||||
// Do not snap to outsidePoint when r===0; only handle axis-constant cases
|
|
||||||
if (R === 0) {
|
|
||||||
_x = outsidePoint.x;
|
|
||||||
}
|
|
||||||
if (Q === 0) {
|
|
||||||
_y = outsidePoint.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { x: _x, y: _y };
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
const outsideNode = (
|
|
||||||
node: { x: any; y: any; width: number; height: number },
|
|
||||||
point: { x: number; y: number }
|
|
||||||
) => {
|
|
||||||
const x = node.x;
|
|
||||||
const y = node.y;
|
|
||||||
const dx = Math.abs(point.x - x);
|
|
||||||
const dy = Math.abs(point.y - y);
|
|
||||||
const w = node.width / 2;
|
|
||||||
const h = node.height / 2;
|
|
||||||
if (dx >= w || dy >= h) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const cutter2 = (startNode: any, endNode: any, _points: any[]) => {
|
const cutter2 = (startNode: any, endNode: any, _points: any[]) => {
|
||||||
const startBounds = {
|
const startBounds = boundsFor(startNode);
|
||||||
x: startNode.offset.posX + startNode.width / 2,
|
const endBounds = boundsFor(endNode);
|
||||||
y: startNode.offset.posY + startNode.height / 2,
|
|
||||||
width: startNode.width,
|
|
||||||
height: startNode.height,
|
|
||||||
padding: startNode.padding,
|
|
||||||
};
|
|
||||||
const endBounds = {
|
|
||||||
x: endNode.offset.posX + endNode.width / 2,
|
|
||||||
y: endNode.offset.posY + endNode.height / 2,
|
|
||||||
width: endNode.width,
|
|
||||||
height: endNode.height,
|
|
||||||
padding: endNode.padding,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (_points.length === 0) {
|
if (_points.length === 0) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy the original points array
|
// Copy the original points array
|
||||||
const points = [..._points];
|
const points: P[] = [..._points] as P[];
|
||||||
|
|
||||||
// The first point is the center of sNode, the last point is the center of eNode
|
// The first point is the center of sNode, the last point is the center of eNode
|
||||||
const startCenter = points[0];
|
const startCenter = points[0];
|
||||||
const endCenter = points[points.length - 1];
|
const endCenter = points[points.length - 1];
|
||||||
|
|
||||||
log.debug('UIO cutter2: startCenter:', startCenter);
|
// Minimal, structured logging for diagnostics
|
||||||
log.debug('UIO cutter2: endCenter:', endCenter);
|
log.debug('UIO cutter2: bounds', { startBounds, endBounds });
|
||||||
|
log.debug('UIO cutter2: original points', _points);
|
||||||
|
|
||||||
let firstOutsideStartIndex = -1;
|
let firstOutsideStartIndex = -1;
|
||||||
let lastOutsideEndIndex = -1;
|
|
||||||
|
|
||||||
// Single iteration through the array
|
// Single iteration through the array
|
||||||
for (const [i, point] of points.entries()) {
|
for (const [i, point] of points.entries()) {
|
||||||
// Check if this is the first point outside the start node
|
|
||||||
if (firstOutsideStartIndex === -1 && outsideNode(startBounds, point)) {
|
if (firstOutsideStartIndex === -1 && outsideNode(startBounds, point)) {
|
||||||
firstOutsideStartIndex = i;
|
firstOutsideStartIndex = i;
|
||||||
log.debug('UIO cutter2: First point outside start node at index', i, point);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this point is outside the end node (keep updating to find the last one)
|
|
||||||
if (outsideNode(endBounds, point)) {
|
if (outsideNode(endBounds, point)) {
|
||||||
lastOutsideEndIndex = i;
|
// keep scanning; we'll also scan from the end for the last outside point
|
||||||
log.debug('UIO cutter2: Point outside end node at index', i, point);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.debug(
|
|
||||||
'UIO cutter2: firstOutsideStartIndex:',
|
|
||||||
firstOutsideStartIndex,
|
|
||||||
'lastOutsideEndIndex:',
|
|
||||||
lastOutsideEndIndex
|
|
||||||
);
|
|
||||||
log.debug('UIO cutter2: startBounds:', startBounds);
|
|
||||||
log.debug('UIO cutter2: endBounds:', endBounds);
|
|
||||||
log.debug('UIO cutter2: original points:', _points);
|
|
||||||
|
|
||||||
// Calculate intersection with start node if we found a point outside it
|
// Calculate intersection with start node if we found a point outside it
|
||||||
if (firstOutsideStartIndex !== -1) {
|
if (firstOutsideStartIndex !== -1) {
|
||||||
const outsidePointForStart = points[firstOutsideStartIndex];
|
const outsidePointForStart = points[firstOutsideStartIndex];
|
||||||
let actualOutsideStart = outsidePointForStart;
|
const startIntersection = computeNodeIntersection(
|
||||||
|
startNode,
|
||||||
// Quick check: if the point is very close to the node boundary, move it further out
|
|
||||||
const dxStart = Math.abs(outsidePointForStart.x - startBounds.x);
|
|
||||||
const dyStart = Math.abs(outsidePointForStart.y - startBounds.y);
|
|
||||||
const wStart = startBounds.width / 2;
|
|
||||||
const hStart = startBounds.height / 2;
|
|
||||||
|
|
||||||
log.debug('UIO cutter2: Checking if start outside point is truly outside:', {
|
|
||||||
outsidePoint: outsidePointForStart,
|
|
||||||
dx: dxStart,
|
|
||||||
dy: dyStart,
|
|
||||||
w: wStart,
|
|
||||||
h: hStart,
|
|
||||||
isOnBoundary: Math.abs(dxStart - wStart) < 1 || Math.abs(dyStart - hStart) < 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
// If the point is on or very close to the boundary, move it further out
|
|
||||||
if (Math.abs(dxStart - wStart) < 1 || Math.abs(dyStart - hStart) < 1) {
|
|
||||||
log.debug('UIO cutter2: Start outside point is on boundary, creating truly outside point');
|
|
||||||
const directionX = outsidePointForStart.x - startBounds.x;
|
|
||||||
const directionY = outsidePointForStart.y - startBounds.y;
|
|
||||||
const length = Math.sqrt(directionX * directionX + directionY * directionY);
|
|
||||||
|
|
||||||
if (length > 0) {
|
|
||||||
// Move the point 10 pixels further out in the same direction
|
|
||||||
actualOutsideStart = {
|
|
||||||
x: startBounds.x + (directionX / length) * (length + 10),
|
|
||||||
y: startBounds.y + (directionY / length) * (length + 10),
|
|
||||||
};
|
|
||||||
log.debug('UIO cutter2: Created truly outside start point:', actualOutsideStart);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let startIntersection;
|
|
||||||
|
|
||||||
// Try using the node's intersect method first
|
|
||||||
if (startNode.intersect) {
|
|
||||||
startIntersection = startNode.intersect(actualOutsideStart);
|
|
||||||
log.debug('UIO cutter2: startNode.intersect result:', startIntersection);
|
|
||||||
|
|
||||||
// Check if the intersection is on the wrong side of the node
|
|
||||||
const isWrongSideStart =
|
|
||||||
(actualOutsideStart.x < startBounds.x && startIntersection.x > startBounds.x) ||
|
|
||||||
(actualOutsideStart.x > startBounds.x && startIntersection.x < startBounds.x);
|
|
||||||
|
|
||||||
if (isWrongSideStart) {
|
|
||||||
log.debug('UIO cutter2: startNode.intersect returned wrong side, setting to null');
|
|
||||||
startIntersection = null;
|
|
||||||
} else {
|
|
||||||
// Check if the intersection is valid (distance > 1)
|
|
||||||
const distanceStart = Math.sqrt(
|
|
||||||
(actualOutsideStart.x - startIntersection.x) ** 2 +
|
|
||||||
(actualOutsideStart.y - startIntersection.y) ** 2
|
|
||||||
);
|
|
||||||
log.debug(
|
|
||||||
'UIO cutter2: Distance from start outside point to intersection:',
|
|
||||||
distanceStart
|
|
||||||
);
|
|
||||||
if (distanceStart <= 1) {
|
|
||||||
log.debug('UIO cutter2: startNode.intersect distance too small, setting to null');
|
|
||||||
startIntersection = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log.debug('UIO cutter2: startNode.intersect method not available');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback to intersection function
|
|
||||||
if (!startIntersection) {
|
|
||||||
// Create a proper inside point that's on the correct side of the node
|
|
||||||
// The inside point should be between the outside point and the far edge
|
|
||||||
const isVerticalStart = Math.abs(actualOutsideStart.x - startBounds.x) < 1;
|
|
||||||
const isHorizontalStart = Math.abs(actualOutsideStart.y - startBounds.y) < 1;
|
|
||||||
const insidePointStart = {
|
|
||||||
x: isVerticalStart
|
|
||||||
? actualOutsideStart.x
|
|
||||||
: actualOutsideStart.x < startBounds.x
|
|
||||||
? startBounds.x - startBounds.width / 4
|
|
||||||
: startBounds.x + startBounds.width / 4,
|
|
||||||
y: isHorizontalStart ? actualOutsideStart.y : startCenter.y,
|
|
||||||
};
|
|
||||||
|
|
||||||
log.debug('UIO cutter2: Using fallback intersection function for start with:', {
|
|
||||||
startBounds,
|
startBounds,
|
||||||
actualOutsideStart,
|
outsidePointForStart,
|
||||||
insidePoint: insidePointStart,
|
startCenter
|
||||||
startCenter,
|
|
||||||
});
|
|
||||||
startIntersection = intersection(startBounds, actualOutsideStart, insidePointStart);
|
|
||||||
log.debug('UIO cutter2: Fallback start intersection result:', startIntersection);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replace the first point with the intersection
|
|
||||||
if (startIntersection) {
|
|
||||||
// Check if the intersection is the same as any existing point
|
|
||||||
const isDuplicate = points.some(
|
|
||||||
(p, index) =>
|
|
||||||
index > 0 &&
|
|
||||||
Math.abs(p.x - startIntersection.x) < 0.1 &&
|
|
||||||
Math.abs(p.y - startIntersection.y) < 0.1
|
|
||||||
);
|
);
|
||||||
|
log.debug('UIO cutter2: start intersection', startIntersection);
|
||||||
if (isDuplicate) {
|
replaceEndpoint(points, 'start', startIntersection);
|
||||||
log.info(
|
|
||||||
'UIO cutter2: Start intersection is duplicate of existing point, removing first point instead'
|
|
||||||
);
|
|
||||||
points.shift(); // Remove the first point instead of replacing it
|
|
||||||
} else {
|
|
||||||
log.info(
|
|
||||||
'UIO cutter2: Replacing first point',
|
|
||||||
points[0],
|
|
||||||
'with intersection',
|
|
||||||
startIntersection
|
|
||||||
);
|
|
||||||
if (Infinity === startIntersection.x || Infinity === startIntersection.y) {
|
|
||||||
log.info('UIO cutter2: Start intersection out of bounds');
|
|
||||||
} else {
|
|
||||||
log.info('UIO cutter2: Replacing first point with intersection:', startIntersection);
|
|
||||||
points[0] = startIntersection;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate intersection with end node
|
// Calculate intersection with end node
|
||||||
// Need to recalculate indices since we may have removed the first point
|
|
||||||
let outsidePointForEnd = null;
|
let outsidePointForEnd = null;
|
||||||
let outsideIndexForEnd = -1;
|
let outsideIndexForEnd = -1;
|
||||||
|
|
||||||
// Find the last point that's outside the end node in the current points array
|
|
||||||
for (let i = points.length - 1; i >= 0; i--) {
|
for (let i = points.length - 1; i >= 0; i--) {
|
||||||
if (outsideNode(endBounds, points[i])) {
|
if (outsideNode(endBounds, points[i])) {
|
||||||
outsidePointForEnd = points[i];
|
outsidePointForEnd = points[i];
|
||||||
outsideIndexForEnd = i;
|
outsideIndexForEnd = i;
|
||||||
log.debug('UIO cutter2: Found point outside end node at current index:', i, points[i]);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!outsidePointForEnd && points.length > 1) {
|
if (!outsidePointForEnd && points.length > 1) {
|
||||||
// No points outside end node, try using the second-to-last point
|
|
||||||
log.debug('UIO cutter2: No points outside end node, trying second-to-last point');
|
|
||||||
outsidePointForEnd = points[points.length - 2];
|
outsidePointForEnd = points[points.length - 2];
|
||||||
outsideIndexForEnd = points.length - 2;
|
outsideIndexForEnd = points.length - 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (outsidePointForEnd) {
|
if (outsidePointForEnd) {
|
||||||
// Check if the outside point is actually on the boundary (distance = 0 from intersection)
|
const endIntersection = computeNodeIntersection(
|
||||||
// If so, we need to create a truly outside point
|
endNode,
|
||||||
let actualOutsidePoint = outsidePointForEnd;
|
|
||||||
|
|
||||||
// Quick check: if the point is very close to the node boundary, move it further out
|
|
||||||
const dx = Math.abs(outsidePointForEnd.x - endBounds.x);
|
|
||||||
const dy = Math.abs(outsidePointForEnd.y - endBounds.y);
|
|
||||||
const w = endBounds.width / 2;
|
|
||||||
const h = endBounds.height / 2;
|
|
||||||
|
|
||||||
log.debug('UIO cutter2: Checking if outside point is truly outside:', {
|
|
||||||
outsidePoint: outsidePointForEnd,
|
|
||||||
dx,
|
|
||||||
dy,
|
|
||||||
w,
|
|
||||||
h,
|
|
||||||
isOnBoundary: Math.abs(dx - w) < 1 || Math.abs(dy - h) < 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
// If the point is on or very close to the boundary, move it further out
|
|
||||||
if (Math.abs(dx - w) < 1 || Math.abs(dy - h) < 1) {
|
|
||||||
log.debug('UIO cutter2: Outside point is on boundary, creating truly outside point');
|
|
||||||
// Move the point further away from the node center
|
|
||||||
const directionX = outsidePointForEnd.x - endBounds.x;
|
|
||||||
const directionY = outsidePointForEnd.y - endBounds.y;
|
|
||||||
const length = Math.sqrt(directionX * directionX + directionY * directionY);
|
|
||||||
|
|
||||||
if (length > 0) {
|
|
||||||
// Move the point 10 pixels further out in the same direction
|
|
||||||
actualOutsidePoint = {
|
|
||||||
x: endBounds.x + (directionX / length) * (length + 10),
|
|
||||||
y: endBounds.y + (directionY / length) * (length + 10),
|
|
||||||
};
|
|
||||||
log.debug('UIO cutter2: Created truly outside point:', actualOutsidePoint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let endIntersection;
|
|
||||||
|
|
||||||
// Try using the node's intersect method first
|
|
||||||
if (endNode.intersect) {
|
|
||||||
endIntersection = endNode.intersect(actualOutsidePoint);
|
|
||||||
log.debug('UIO cutter2: endNode.intersect result:', endIntersection);
|
|
||||||
|
|
||||||
// Check if the intersection is on the wrong side of the node
|
|
||||||
const isWrongSide =
|
|
||||||
(actualOutsidePoint.x < endBounds.x && endIntersection.x > endBounds.x) ||
|
|
||||||
(actualOutsidePoint.x > endBounds.x && endIntersection.x < endBounds.x);
|
|
||||||
|
|
||||||
if (isWrongSide) {
|
|
||||||
log.debug('UIO cutter2: endNode.intersect returned wrong side, setting to null');
|
|
||||||
endIntersection = null;
|
|
||||||
} else {
|
|
||||||
// Check if the intersection is valid (distance > 1)
|
|
||||||
const distance = Math.sqrt(
|
|
||||||
(actualOutsidePoint.x - endIntersection.x) ** 2 +
|
|
||||||
(actualOutsidePoint.y - endIntersection.y) ** 2
|
|
||||||
);
|
|
||||||
log.debug('UIO cutter2: Distance from outside point to intersection:', distance);
|
|
||||||
if (distance <= 1) {
|
|
||||||
log.debug('UIO cutter2: endNode.intersect distance too small, setting to null');
|
|
||||||
endIntersection = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log.debug('UIO cutter2: endNode.intersect method not available');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback to intersection function
|
|
||||||
if (!endIntersection) {
|
|
||||||
// Create a proper inside point that's on the correct side of the node
|
|
||||||
// The inside point should be between the outside point and the far edge
|
|
||||||
const isVerticalEnd = Math.abs(actualOutsidePoint.x - endBounds.x) < 1;
|
|
||||||
const isHorizontalEnd = Math.abs(actualOutsidePoint.y - endBounds.y) < 1;
|
|
||||||
const insidePoint = {
|
|
||||||
x: isVerticalEnd
|
|
||||||
? actualOutsidePoint.x
|
|
||||||
: actualOutsidePoint.x < endBounds.x
|
|
||||||
? endBounds.x - endBounds.width / 4
|
|
||||||
: endBounds.x + endBounds.width / 4,
|
|
||||||
y: isHorizontalEnd ? actualOutsidePoint.y : endCenter.y,
|
|
||||||
};
|
|
||||||
|
|
||||||
log.debug('UIO cutter2: Using fallback intersection function with:', {
|
|
||||||
endBounds,
|
endBounds,
|
||||||
actualOutsidePoint,
|
outsidePointForEnd,
|
||||||
insidePoint,
|
endCenter
|
||||||
endCenter,
|
|
||||||
});
|
|
||||||
endIntersection = intersection(endBounds, actualOutsidePoint, insidePoint);
|
|
||||||
log.debug('UIO cutter2: Fallback intersection result:', endIntersection);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replace the last point with the intersection
|
|
||||||
if (endIntersection) {
|
|
||||||
// Check if the intersection is the same as any existing point
|
|
||||||
const isDuplicate = points.some(
|
|
||||||
(p, index) =>
|
|
||||||
index < points.length - 1 &&
|
|
||||||
Math.abs(p.x - endIntersection.x) < 0.1 &&
|
|
||||||
Math.abs(p.y - endIntersection.y) < 0.1
|
|
||||||
);
|
);
|
||||||
|
log.debug('UIO cutter2: end intersection', { endIntersection, outsideIndexForEnd });
|
||||||
if (isDuplicate) {
|
replaceEndpoint(points, 'end', endIntersection);
|
||||||
log.debug(
|
|
||||||
'UIO cutter2: End intersection is duplicate of existing point, removing last point instead'
|
|
||||||
);
|
|
||||||
points.pop(); // Remove the last point instead of replacing it
|
|
||||||
} else {
|
|
||||||
log.debug(
|
|
||||||
'UIO cutter2: Replacing last point',
|
|
||||||
points[points.length - 1],
|
|
||||||
'with intersection',
|
|
||||||
endIntersection,
|
|
||||||
'using outside point at index',
|
|
||||||
outsideIndexForEnd
|
|
||||||
);
|
|
||||||
points[points.length - 1] = endIntersection;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log.debug('UIO cutter2: No suitable outside point found for end node intersection');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Final cleanup: Check if the last point is too close to the previous point
|
// Final cleanup: Check if the last point is too close to the previous point
|
||||||
@@ -885,37 +590,17 @@ export const render = async (
|
|||||||
const distance = Math.sqrt(
|
const distance = Math.sqrt(
|
||||||
(lastPoint.x - secondLastPoint.x) ** 2 + (lastPoint.y - secondLastPoint.y) ** 2
|
(lastPoint.x - secondLastPoint.x) ** 2 + (lastPoint.y - secondLastPoint.y) ** 2
|
||||||
);
|
);
|
||||||
|
|
||||||
// If the distance is very small (less than 2 pixels), remove the last point
|
|
||||||
if (distance < 2) {
|
if (distance < 2) {
|
||||||
log.debug(
|
log.debug('UIO cutter2: trimming tail point (too close)', {
|
||||||
'UIO cutter2: Last point too close to previous point, removing it. Distance:',
|
distance,
|
||||||
distance
|
lastPoint,
|
||||||
);
|
secondLastPoint,
|
||||||
log.debug('UIO cutter2: Removing last point:', lastPoint, 'keeping:', secondLastPoint);
|
});
|
||||||
points.pop();
|
points.pop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.debug('UIO cutter2: Final points:', points);
|
log.debug('UIO cutter2: final points', points);
|
||||||
|
|
||||||
// Debug: Check which side of the end node we're ending at
|
|
||||||
if (points.length > 0) {
|
|
||||||
const finalPoint = points[points.length - 1];
|
|
||||||
const endNodeCenter = endBounds.x;
|
|
||||||
const endNodeLeftEdge = endNodeCenter - endBounds.width / 2;
|
|
||||||
const endNodeRightEdge = endNodeCenter + endBounds.width / 2;
|
|
||||||
|
|
||||||
log.debug('UIO cutter2: End node analysis:', {
|
|
||||||
finalPoint,
|
|
||||||
endNodeCenter,
|
|
||||||
endNodeLeftEdge,
|
|
||||||
endNodeRightEdge,
|
|
||||||
endingSide: finalPoint.x < endNodeCenter ? 'LEFT' : 'RIGHT',
|
|
||||||
distanceFromLeftEdge: Math.abs(finalPoint.x - endNodeLeftEdge),
|
|
||||||
distanceFromRightEdge: Math.abs(finalPoint.x - endNodeRightEdge),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return points;
|
return points;
|
||||||
};
|
};
|
||||||
@@ -982,7 +667,7 @@ export const render = async (
|
|||||||
log.info('Drawing flowchart using v4 renderer', elk);
|
log.info('Drawing flowchart using v4 renderer', elk);
|
||||||
|
|
||||||
// Set the direction of the graph based on the parsed information
|
// Set the direction of the graph based on the parsed information
|
||||||
const dir = data4Layout.direction || 'DOWN';
|
const dir = data4Layout.direction ?? 'DOWN';
|
||||||
elkGraph.layoutOptions['elk.direction'] = dir2ElkDirection(dir);
|
elkGraph.layoutOptions['elk.direction'] = dir2ElkDirection(dir);
|
||||||
|
|
||||||
// Create the lookup db for the subgraphs and their children to used when creating
|
// Create the lookup db for the subgraphs and their children to used when creating
|
||||||
|
Reference in New Issue
Block a user