Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 125 additions & 22 deletions src/chart/graph/adjustEdge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,62 @@

import * as curveTool from 'zrender/src/core/curve';
import * as vec2 from 'zrender/src/core/vector';
import {getSymbolSize} from './graphHelper';
import Graph from '../../data/Graph';
import { normalizeSymbolSize, normalizeSymbolOffset } from '../../util/symbol';
import {
getSymbolBoundaryDistance,
getSymbolContainChecker,
symbolRotateToRad
} from './nodeShapeHelper';
import Graph, { GraphNode } from '../../data/Graph';

const v1: number[] = [];
const v2: number[] = [];
const v3: number[] = [];
const quadraticAt = curveTool.quadraticAt;
const v2DistSquare = vec2.distSquare;
const mathAbs = Math.abs;

// Curve sampling resolution used to locate where a quadratic edge crosses a node outline.
const CURVE_SAMPLES = 20;
const CURVE_BISECT_ITERATIONS = 12;

interface NodeShapeParams {
symbolType: string
// Half already folded into scale by nodeShapeHelper; these are full sizes/offsets in px,
// pre-multiplied by the node global scale.
size: number[]
rotateRad: number
keepAspect: boolean
offset: number[]
}

/**
* Collect the node's symbol geometry (already scaled by the graph global scale) needed to probe
* its outline. All of these visuals are populated by `visual/symbol` before the view renders.
*/
function getNodeShapeParams(node: GraphNode, globalScale: number): NodeShapeParams {
const rawSize = normalizeSymbolSize(node.getVisual('symbolSize'));
const rawOffset = normalizeSymbolOffset(node.getVisual('symbolOffset'), rawSize);
return {
symbolType: node.getVisual('symbol') || 'circle',
size: [rawSize[0] * globalScale, rawSize[1] * globalScale],
rotateRad: symbolRotateToRad(node.getVisual('symbolRotate')),
keepAspect: node.getVisual('symbolKeepAspect'),
offset: rawOffset ? [rawOffset[0] * globalScale, rawOffset[1] * globalScale] : null
};
}

function getNodeBoundaryDistance(
node: GraphNode, globalScale: number, dirX: number, dirY: number
): number {
const p = getNodeShapeParams(node, globalScale);
return getSymbolBoundaryDistance(p.symbolType, p.size, p.rotateRad, p.keepAspect, p.offset, dirX, dirY);
}

const v1: number[] = [];
const v2: number[] = [];
const v3: number[] = [];
/**
* Legacy circle intersection, kept as a fallback for the rare cases where the generic outline
* search cannot find a crossing (e.g. an unfillable node symbol or a degenerate offset).
*/
function intersectCurveCircle(
curvePoints: number[][],
center: number[],
Expand Down Expand Up @@ -94,14 +141,75 @@ function intersectCurveCircle(
return t;
}

// Adjust edge to avoid
export default function adjustEdge(graph: Graph, scale: number) {
/**
* Find the parameter `t` where the quadratic `curvePoints` crosses `node`'s transformed outline.
* The node sits at one end of the curve (`fromStart` ? `t = 0` : `t = 1`), which is inside the
* outline; we walk toward the other end to find the exit crossing, then refine by bisection.
* Falls back to the circle intersection if no clean crossing is found.
*/
function intersectCurveNode(
curvePoints: number[][],
node: GraphNode,
globalScale: number,
fromStart: boolean
): number {
const p0 = curvePoints[0];
const p1 = curvePoints[1];
const p2 = curvePoints[2];
const p = getNodeShapeParams(node, globalScale);

const cx = fromStart ? p0[0] : p2[0];
const cy = fromStart ? p0[1] : p2[1];
const contain = getSymbolContainChecker(p.symbolType, p.size, p.rotateRad, p.keepAspect, p.offset);

function isInside(t: number): boolean {
const x = quadraticAt(p0[0], p1[0], p2[0], t);
const y = quadraticAt(p0[1], p1[1], p2[1], t);
return contain(x - cx, y - cy);
}

// Bracket the outline crossing between an inside `t` and an adjacent outside `t`.
let inT = fromStart ? 0 : 1;
let outT = -1;
if (isInside(inT)) {
for (let i = 1; i <= CURVE_SAMPLES; i++) {
const t = fromStart ? i / CURVE_SAMPLES : 1 - i / CURVE_SAMPLES;
if (!isInside(t)) {
outT = t;
break;
}
inT = t;
}
}

if (outT < 0) {
// Endpoint not inside its own outline, or the whole curve lies inside it.
const radius = Math.max(p.size[0], p.size[1]) / 2;
return intersectCurveCircle(curvePoints, [cx, cy], radius);
}

let lo = inT;
let hi = outT;
for (let i = 0; i < CURVE_BISECT_ITERATIONS; i++) {
const mid = (lo + hi) / 2;
if (isInside(mid)) {
lo = mid;
}
else {
hi = mid;
}
}
return (lo + hi) / 2;
}

// Adjust edge endpoints so an edge's end-symbol sits flush against the node outline instead of
// being hidden underneath it, accounting for the node symbol's shape and transform.
export default function adjustEdge(graph: Graph, globalScale: number) {
const tmp0: number[] = [];
const quadraticSubdivide = curveTool.quadraticSubdivide;
const pts: number[][] = [[], [], []];
const pts2: number[][] = [[], []];
const v: number[] = [];
scale /= 2;

graph.eachEdge(function (edge, idx) {
const linePoints = edge.getLayout();
Expand All @@ -124,9 +232,7 @@ export default function adjustEdge(graph: Graph, scale: number) {
vec2.copy(pts[1], originalPoints[2]);
vec2.copy(pts[2], originalPoints[1]);
if (fromSymbol && fromSymbol !== 'none') {
const symbolSize = getSymbolSize(edge.node1);

const t = intersectCurveCircle(pts, originalPoints[0], symbolSize * scale);
const t = intersectCurveNode(pts, edge.node1, globalScale, true);
// Subdivide and get the second
quadraticSubdivide(pts[0][0], pts[1][0], pts[2][0], t, tmp0);
pts[0][0] = tmp0[3];
Expand All @@ -136,9 +242,7 @@ export default function adjustEdge(graph: Graph, scale: number) {
pts[1][1] = tmp0[4];
}
if (toSymbol && toSymbol !== 'none') {
const symbolSize = getSymbolSize(edge.node2);

const t = intersectCurveCircle(pts, originalPoints[1], symbolSize * scale);
const t = intersectCurveNode(pts, edge.node2, globalScale, false);
// Subdivide and get the first
quadraticSubdivide(pts[0][0], pts[1][0], pts[2][0], t, tmp0);
pts[1][0] = tmp0[1];
Expand All @@ -160,18 +264,17 @@ export default function adjustEdge(graph: Graph, scale: number) {
vec2.sub(v, pts2[1], pts2[0]);
vec2.normalize(v, v);
if (fromSymbol && fromSymbol !== 'none') {

const symbolSize = getSymbolSize(edge.node1);

vec2.scaleAndAdd(pts2[0], pts2[0], v, symbolSize * scale);
// Approach direction at node1 points toward node2.
const dist = getNodeBoundaryDistance(edge.node1, globalScale, v[0], v[1]);
vec2.scaleAndAdd(pts2[0], pts2[0], v, dist);
}
if (toSymbol && toSymbol !== 'none') {
const symbolSize = getSymbolSize(edge.node2);

vec2.scaleAndAdd(pts2[1], pts2[1], v, -symbolSize * scale);
// Approach direction at node2 points toward node1.
const dist = getNodeBoundaryDistance(edge.node2, globalScale, -v[0], -v[1]);
vec2.scaleAndAdd(pts2[1], pts2[1], v, -dist);
}
vec2.copy(linePoints[0], pts2[0]);
vec2.copy(linePoints[1], pts2[1]);
}
});
}
}
Loading