mirror of
https://gitee.com/antv/g6.git
synced 2024-12-05 05:09:07 +08:00
fix: conflict from polyline
This commit is contained in:
commit
036cf1acd8
152
demos/default-edges.html
Normal file
152
demos/default-edges.html
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>内置的边</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="mountNode"></div>
|
||||||
|
<script src="../build/g6.js"></script>
|
||||||
|
<script>
|
||||||
|
const graph = new G6.Graph({
|
||||||
|
container: 'mountNode',
|
||||||
|
width: 800,
|
||||||
|
height: 600,
|
||||||
|
defaultNode: {
|
||||||
|
size: [40, 40]
|
||||||
|
},
|
||||||
|
modes: {
|
||||||
|
default: [ 'brush-select', 'drag-node' ]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
* data
|
||||||
|
*/
|
||||||
|
const data = {
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
id: 'node1',
|
||||||
|
label: 'node1',
|
||||||
|
x: 100,
|
||||||
|
y: 100,
|
||||||
|
shape: 'circle'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'node4',
|
||||||
|
label: 'node4',
|
||||||
|
x: 150,
|
||||||
|
y: 300,
|
||||||
|
shape: 'rect'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'node5',
|
||||||
|
label: 'node5',
|
||||||
|
x: 200,
|
||||||
|
y: 100,
|
||||||
|
shape: 'rect'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'node6',
|
||||||
|
label: 'node6',
|
||||||
|
x: 350,
|
||||||
|
y: 300,
|
||||||
|
shape: 'rect'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'node7',
|
||||||
|
label: 'node7',
|
||||||
|
x: 350,
|
||||||
|
y: 200,
|
||||||
|
shape: 'rect'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'node8',
|
||||||
|
label: 'node8',
|
||||||
|
x: 350,
|
||||||
|
y: 450,
|
||||||
|
shape: 'rect'
|
||||||
|
},
|
||||||
|
],
|
||||||
|
edges: [
|
||||||
|
// {
|
||||||
|
// source: 'node1',
|
||||||
|
// target: 'node4',
|
||||||
|
// shape: 'polyline',
|
||||||
|
// label: 'edge1-4',
|
||||||
|
// controlPoints: [
|
||||||
|
// { x: 100, y: 200 },
|
||||||
|
// ],
|
||||||
|
// style: {
|
||||||
|
// radius: 10,
|
||||||
|
// offset: 20
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// source: 'node4',
|
||||||
|
// target: 'node5',
|
||||||
|
// shape: 'polyline',
|
||||||
|
// label: 'edge5-7',
|
||||||
|
// style: {
|
||||||
|
// lineWidth: 1.5,
|
||||||
|
// stroke: '#888',
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// source: 'node4',
|
||||||
|
// target: 'node7',
|
||||||
|
// shape: 'polyline'
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// source: 'node6',
|
||||||
|
// target: 'node4',
|
||||||
|
// shape: 'polyline'
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
source: 'node4',
|
||||||
|
target: 'node8',
|
||||||
|
shape: 'polyline',
|
||||||
|
style: {
|
||||||
|
radius: 10,
|
||||||
|
offset: 20
|
||||||
|
}
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
graph.data(data)
|
||||||
|
graph.render()
|
||||||
|
|
||||||
|
graph.on('edge:click', evt => {
|
||||||
|
console.log(evt)
|
||||||
|
const { item } = evt
|
||||||
|
graph.updateItem(item, {
|
||||||
|
style: {
|
||||||
|
lineWidth: 3,
|
||||||
|
stroke: 'red'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// const hasState = item.hasState('select')
|
||||||
|
// if(!hasState) {
|
||||||
|
// graph.setItemState(item, 'select', true);
|
||||||
|
// }
|
||||||
|
})
|
||||||
|
|
||||||
|
graph.on('edge:mouseenter', evt => {
|
||||||
|
const { item } = evt;
|
||||||
|
graph.setItemState(item, 'hover', true);
|
||||||
|
})
|
||||||
|
|
||||||
|
graph.on('edge:mouseleave', evt => {
|
||||||
|
const { item } = evt;
|
||||||
|
const hasState = item.hasState('select')
|
||||||
|
if(!hasState) {
|
||||||
|
graph.setItemState(item, 'hover', false);
|
||||||
|
graph.setItemState(item, 'running', false);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -102,6 +102,7 @@
|
|||||||
"test": "torch --compile --renderer --opts test/mocha.opts --recursive ./test/unit",
|
"test": "torch --compile --renderer --opts test/mocha.opts --recursive ./test/unit",
|
||||||
"test-live": "torch --compile --interactive --watch --opts test/mocha.opts --recursive ./test/unit/",
|
"test-live": "torch --compile --interactive --watch --opts test/mocha.opts --recursive ./test/unit/",
|
||||||
"test-live-shape": "torch --compile --interactive --watch --opts test/mocha.opts --recursive ./test/unit/shape/nodes/modelRect-spec.js",
|
"test-live-shape": "torch --compile --interactive --watch --opts test/mocha.opts --recursive ./test/unit/shape/nodes/modelRect-spec.js",
|
||||||
|
"test-live-util": "torch --compile --interactive --watch --opts test/mocha.opts --recursive ./test/unit/shape/edge-spec.js",
|
||||||
"test-bugs": "torch --compile --renderer --recursive ./test/bugs",
|
"test-bugs": "torch --compile --renderer --recursive ./test/bugs",
|
||||||
"test-bugs-live": "torch --compile --interactive --watch --recursive ./test/bugs",
|
"test-bugs-live": "torch --compile --interactive --watch --recursive ./test/bugs",
|
||||||
"test-all": "npm run test && npm run test-bugs",
|
"test-all": "npm run test && npm run test-bugs",
|
||||||
|
@ -152,13 +152,23 @@ const singleEdgeDefinition = Util.mix({}, SingleShapeMixin, {
|
|||||||
attrs: shapeStyle
|
attrs: shapeStyle
|
||||||
});
|
});
|
||||||
return shape;
|
return shape;
|
||||||
|
},
|
||||||
|
drawLabel(cfg, group) {
|
||||||
|
const customStyle = this.getCustomConfig(cfg) || {};
|
||||||
|
const defaultConfig = customStyle.default || {};
|
||||||
|
const labelCfg = Util.deepMix({}, this.options.default.labelCfg, defaultConfig.labelCfg, cfg.labelCfg);
|
||||||
|
const labelStyle = this.getLabelStyle(cfg, labelCfg, group);
|
||||||
|
const label = group.addShape('text', {
|
||||||
|
attrs: labelStyle
|
||||||
|
});
|
||||||
|
return label;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 直线
|
// // 直线
|
||||||
Shape.registerEdge('single-line', singleEdgeDefinition);
|
Shape.registerEdge('single-line', singleEdgeDefinition);
|
||||||
|
|
||||||
// 直线, 不支持控制点
|
// // 直线, 不支持控制点
|
||||||
Shape.registerEdge('line', {
|
Shape.registerEdge('line', {
|
||||||
// 控制点不生效
|
// 控制点不生效
|
||||||
getControlPoints() {
|
getControlPoints() {
|
||||||
@ -166,8 +176,8 @@ Shape.registerEdge('line', {
|
|||||||
}
|
}
|
||||||
}, 'single-line');
|
}, 'single-line');
|
||||||
|
|
||||||
// 折线,支持多个控制点
|
// // 折线,支持多个控制点
|
||||||
Shape.registerEdge('polyline', {}, 'single-line');
|
// Shape.registerEdge('polyline', {}, 'single-line');
|
||||||
|
|
||||||
// 直线
|
// 直线
|
||||||
Shape.registerEdge('spline', {
|
Shape.registerEdge('spline', {
|
||||||
|
1
src/shape/edges/index.js
Normal file
1
src/shape/edges/index.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
require('./polyline');
|
483
src/shape/edges/polyline.js
Normal file
483
src/shape/edges/polyline.js
Normal file
@ -0,0 +1,483 @@
|
|||||||
|
const Shape = require('../shape');
|
||||||
|
const Util = require('../../util/index');
|
||||||
|
|
||||||
|
const CLS_SHAPE_SUFFIX = '-shape';
|
||||||
|
const CLS_LABEL_SUFFIX = '-label';
|
||||||
|
|
||||||
|
function getBBoxFromPoint(point) {
|
||||||
|
const { x, y } = point;
|
||||||
|
return {
|
||||||
|
centerX: x, centerY: y,
|
||||||
|
minX: x, minY: y, maxX: x, maxY: y,
|
||||||
|
height: 0, width: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function getBBoxFromPoints(points = []) {
|
||||||
|
const xs = [];
|
||||||
|
const ys = [];
|
||||||
|
points.forEach(p => {
|
||||||
|
xs.push(p.x);
|
||||||
|
ys.push(p.y);
|
||||||
|
});
|
||||||
|
const minX = Math.min.apply(Math, xs);
|
||||||
|
const maxX = Math.max.apply(Math, xs);
|
||||||
|
const minY = Math.min.apply(Math, ys);
|
||||||
|
const maxY = Math.max.apply(Math, ys);
|
||||||
|
return {
|
||||||
|
centerX: (minX + maxX) / 2, centerY: (minY + maxY) / 2,
|
||||||
|
maxX, maxY, minX, minY,
|
||||||
|
height: (maxY - minY), width: (maxX - minX)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function isBBoxesOverlapping(b1, b2) {
|
||||||
|
return Math.abs(b1.centerX - b2.centerX) * 2 < (b1.width + b2.width) &&
|
||||||
|
Math.abs(b1.centerY - b2.centerY) * 2 < (b1.height + b2.height);
|
||||||
|
}
|
||||||
|
function simplifyPolyline(points) {
|
||||||
|
points = filterConnectPoints(points);
|
||||||
|
return points;
|
||||||
|
}
|
||||||
|
function filterConnectPoints(points) {
|
||||||
|
// pre-process: remove duplicated points
|
||||||
|
const result = [];
|
||||||
|
const pointsMap = {};
|
||||||
|
points.forEach(p => {
|
||||||
|
const id = p.id = `${p.x}-${p.y}`;
|
||||||
|
pointsMap[id] = p;
|
||||||
|
});
|
||||||
|
Util.each(pointsMap, p => {
|
||||||
|
result.push(p);
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
function getSimplePolyline(sPoint, tPoint) {
|
||||||
|
return [
|
||||||
|
sPoint,
|
||||||
|
{ x: sPoint.x, y: tPoint.y },
|
||||||
|
tPoint
|
||||||
|
];
|
||||||
|
}
|
||||||
|
function getExpandedBBox(bbox, offset) {
|
||||||
|
if (bbox.width === 0 && bbox.height === 0) { // when it is a point
|
||||||
|
return bbox;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
centerX: bbox.centerX, centerY: bbox.centerY,
|
||||||
|
minX: bbox.minX - offset, minY: bbox.minY - offset, maxX: bbox.maxX + offset, maxY: bbox.maxY + offset,
|
||||||
|
height: bbox.height + 2 * offset, width: bbox.width + 2 * offset
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function getExpandedBBoxPoint(bbox, point) {
|
||||||
|
const isHorizontal = isHorizontalPort(point, bbox);
|
||||||
|
if (isHorizontal) {
|
||||||
|
return { x: point.x > bbox.centerX ? bbox.maxX : bbox.minX, y: point.y };
|
||||||
|
}
|
||||||
|
return { x: point.x, y: point.y > bbox.centerY ? bbox.maxY : bbox.minY };
|
||||||
|
}
|
||||||
|
function isHorizontalPort(port, bbox) {
|
||||||
|
const dx = Math.abs(port.x - bbox.centerX);
|
||||||
|
const dy = Math.abs(port.y - bbox.centerY);
|
||||||
|
return (dx / bbox.width) > (dy / bbox.height);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @param {object} b1 bbox1
|
||||||
|
* @param {object} b2 bbox2
|
||||||
|
* @return {object} { centerX, centerY, height, maxX, maxY, minX, minY, width }
|
||||||
|
**/
|
||||||
|
function mergeBBox(b1, b2) {
|
||||||
|
const minX = Math.min(b1.minX, b2.minX);
|
||||||
|
const minY = Math.min(b1.minY, b2.minY);
|
||||||
|
const maxX = Math.max(b1.maxX, b2.maxX);
|
||||||
|
const maxY = Math.max(b1.maxY, b2.maxY);
|
||||||
|
return {
|
||||||
|
centerX: (minX + maxX) / 2, centerY: (minY + maxY) / 2,
|
||||||
|
minX, minY, maxX, maxY,
|
||||||
|
height: maxY - minY, width: maxX - minX
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function getPointsFromBBox(bbox) {
|
||||||
|
// anticlockwise
|
||||||
|
const { minX, minY, maxX, maxY } = bbox;
|
||||||
|
return [
|
||||||
|
{ x: minX, y: minY },
|
||||||
|
{ x: maxX, y: minY },
|
||||||
|
{ x: maxX, y: maxY },
|
||||||
|
{ x: minX, y: maxY }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
function isPointOutsideBBox(point, bbox) {
|
||||||
|
const { x, y } = point;
|
||||||
|
return x < bbox.minX || x > bbox.maxX || y < bbox.minY || y > bbox.maxY;
|
||||||
|
}
|
||||||
|
function getBBoxCrossPointsByPoint(bbox, point) {
|
||||||
|
return getBBoxXCrossPoints(bbox, point.x).concat(getBBoxYCrossPoints(bbox, point.y));
|
||||||
|
}
|
||||||
|
function getBBoxXCrossPoints(bbox, x) {
|
||||||
|
if (x < bbox.minX || x > bbox.maxX) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
{ x, y: bbox.minY },
|
||||||
|
{ x, y: bbox.maxY }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
function getBBoxYCrossPoints(bbox, y) {
|
||||||
|
if (y < bbox.minY || y > bbox.maxY) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
{ x: bbox.minX, y },
|
||||||
|
{ x: bbox.maxX, y }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
function distance(p1, p2) {
|
||||||
|
return Math.abs(p1.x - p2.x) + Math.abs(p1.y - p2.y);
|
||||||
|
}
|
||||||
|
function _costByPoints(p, points) {
|
||||||
|
const offset = -2;
|
||||||
|
let result = 0;
|
||||||
|
points.forEach(point => {
|
||||||
|
if (point) {
|
||||||
|
if (p.x === point.x) result += offset;
|
||||||
|
if (p.y === point.y) result += offset;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
function heuristicCostEstimate(p, ps, pt, source, target) {
|
||||||
|
return (distance(p, ps) + distance(p, pt)) + _costByPoints(p, [ ps, pt, source, target ]);
|
||||||
|
}
|
||||||
|
function reconstructPath(pathPoints, pointById, cameFrom, currentId, iterator = 0) {
|
||||||
|
pathPoints.unshift(pointById[currentId]);
|
||||||
|
if (cameFrom[currentId] && cameFrom[currentId] !== currentId && iterator <= 100) {
|
||||||
|
reconstructPath(pathPoints, pointById, cameFrom, cameFrom[currentId], iterator + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function removeFrom(arr, item) {
|
||||||
|
const index = arr.indexOf(item);
|
||||||
|
if (index > -1) {
|
||||||
|
arr.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function isSegmentsIntersected(p0, p1, p2, p3) {
|
||||||
|
const s1_x = p1.x - p0.x;
|
||||||
|
const s1_y = p1.y - p0.y;
|
||||||
|
const s2_x = p3.x - p2.x;
|
||||||
|
const s2_y = p3.y - p2.y;
|
||||||
|
|
||||||
|
const s = (-s1_y * (p0.x - p2.x) + s1_x * (p0.y - p2.y)) / (-s2_x * s1_y + s1_x * s2_y);
|
||||||
|
const t = (s2_x * (p0.y - p2.y) - s2_y * (p0.x - p2.x)) / (-s2_x * s1_y + s1_x * s2_y);
|
||||||
|
|
||||||
|
return (s >= 0 && s <= 1 && t >= 0 && t <= 1);
|
||||||
|
}
|
||||||
|
function isSegmentCrossingBBox(p1, p2, bbox) {
|
||||||
|
if (bbox.width === bbox.height === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const [ pa, pb, pc, pd ] = getPointsFromBBox(bbox);
|
||||||
|
return isSegmentsIntersected(p1, p2, pa, pb) ||
|
||||||
|
isSegmentsIntersected(p1, p2, pa, pd) ||
|
||||||
|
isSegmentsIntersected(p1, p2, pb, pc) ||
|
||||||
|
isSegmentsIntersected(p1, p2, pc, pd);
|
||||||
|
}
|
||||||
|
function getNeighborPoints(points, point, bbox1, bbox2) {
|
||||||
|
const neighbors = [];
|
||||||
|
points.forEach(p => {
|
||||||
|
if (p !== point) {
|
||||||
|
if (p.x === point.x || p.y === point.y) {
|
||||||
|
if (!isSegmentCrossingBBox(p, point, bbox1) && !isSegmentCrossingBBox(p, point, bbox2)) {
|
||||||
|
neighbors.push(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return filterConnectPoints(neighbors);
|
||||||
|
}
|
||||||
|
function pathFinder(points, start, goal, sBBox, tBBox, os, ot) { // A-Star Algorithm
|
||||||
|
const closedSet = [];
|
||||||
|
const openSet = [ start ];
|
||||||
|
const cameFrom = {};
|
||||||
|
const gScore = {}; // all default values are Infinity
|
||||||
|
const fScore = {}; // all default values are Infinity
|
||||||
|
|
||||||
|
gScore[start.id] = 0;
|
||||||
|
fScore[start.id] = heuristicCostEstimate(start, goal, start);
|
||||||
|
|
||||||
|
const pointById = {};
|
||||||
|
points.forEach(p => {
|
||||||
|
pointById[p.id] = p;
|
||||||
|
});
|
||||||
|
|
||||||
|
while (openSet.length) {
|
||||||
|
let current;
|
||||||
|
let lowestFScore = Infinity;
|
||||||
|
openSet.forEach(p => {
|
||||||
|
if (fScore[p.id] < lowestFScore) {
|
||||||
|
lowestFScore = fScore[p.id];
|
||||||
|
current = p;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (current === goal) { // ending condition
|
||||||
|
const pathPoints = [];
|
||||||
|
reconstructPath(pathPoints, pointById, cameFrom, goal.id);
|
||||||
|
return pathPoints;
|
||||||
|
}
|
||||||
|
|
||||||
|
removeFrom(openSet, current);
|
||||||
|
closedSet.push(current);
|
||||||
|
|
||||||
|
getNeighborPoints(points, current, sBBox, tBBox).forEach(neighbor => {
|
||||||
|
if (closedSet.indexOf(neighbor) !== -1) return;
|
||||||
|
|
||||||
|
if (openSet.indexOf(neighbor) === -1) {
|
||||||
|
openSet.push(neighbor);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tentativeGScore = fScore[current.id] + distance(current, neighbor);// + distance(neighbor, goal);
|
||||||
|
|
||||||
|
if (gScore[neighbor.id] && (tentativeGScore >= gScore[neighbor.id])) return;
|
||||||
|
|
||||||
|
cameFrom[neighbor.id] = current.id;
|
||||||
|
gScore[neighbor.id] = tentativeGScore;
|
||||||
|
fScore[neighbor.id] = gScore[neighbor.id] + heuristicCostEstimate(neighbor, goal, start, os, ot);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// throw new Error('Cannot find path');
|
||||||
|
return [ start, goal ];
|
||||||
|
}
|
||||||
|
function getPathWithBorderRadiusByPolyline(points, borderRadius) {
|
||||||
|
// TODO
|
||||||
|
const pathSegments = [];
|
||||||
|
const startPoint = points[0];
|
||||||
|
pathSegments.push(`M${startPoint.x} ${startPoint.y}`);
|
||||||
|
points.forEach((p, i) => {
|
||||||
|
const p1 = points[i + 1];
|
||||||
|
const p2 = points[i + 2];
|
||||||
|
if (p1 && p2) {
|
||||||
|
if (isBending(p, p1, p2)) {
|
||||||
|
const [ ps, pt ] = getBorderRadiusPoints(p, p1, p2, borderRadius);
|
||||||
|
pathSegments.push(`L${ps.x} ${ps.y}`);
|
||||||
|
pathSegments.push(`Q${p1.x} ${p1.y} ${pt.x} ${pt.y}`);
|
||||||
|
pathSegments.push(`L${pt.x} ${pt.y}`);
|
||||||
|
} else {
|
||||||
|
pathSegments.push(`L${p1.x} ${p1.y}`);
|
||||||
|
}
|
||||||
|
} else if (p1) {
|
||||||
|
pathSegments.push(`L${p1.x} ${p1.y}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return pathSegments.join('');
|
||||||
|
}
|
||||||
|
function isBending(p0, p1, p2) {
|
||||||
|
return !((p0.x === p1.x === p2.x) || (p0.y === p1.y === p2.y));
|
||||||
|
}
|
||||||
|
function getBorderRadiusPoints(p0, p1, p2, r) {
|
||||||
|
const d0 = distance(p0, p1);
|
||||||
|
const d1 = distance(p2, p1);
|
||||||
|
if (d0 < r) {
|
||||||
|
r = d0;
|
||||||
|
}
|
||||||
|
if (d1 < r) {
|
||||||
|
r = d1;
|
||||||
|
}
|
||||||
|
const ps = {
|
||||||
|
x: p1.x - r / d0 * (p1.x - p0.x),
|
||||||
|
y: p1.y - r / d0 * (p1.y - p0.y)
|
||||||
|
};
|
||||||
|
const pt = {
|
||||||
|
x: p1.x - r / d1 * (p1.x - p2.x),
|
||||||
|
y: p1.y - r / d1 * (p1.y - p2.y)
|
||||||
|
};
|
||||||
|
return [ ps, pt ];
|
||||||
|
}
|
||||||
|
function getPolylinePoints(start, end, sNode, tNode, offset) {
|
||||||
|
const sBBox = sNode && sNode.getBBox() ? sNode.getBBox() : getBBoxFromPoint(start);
|
||||||
|
const tBBox = tNode && tNode.getBBox() ? tNode.getBBox() : getBBoxFromPoint(end);
|
||||||
|
if (isBBoxesOverlapping(sBBox, tBBox)) { // source and target nodes are overlapping
|
||||||
|
return simplifyPolyline(getSimplePolyline(start, end));
|
||||||
|
}
|
||||||
|
const sxBBox = getExpandedBBox(sBBox, offset);
|
||||||
|
const txBBox = getExpandedBBox(tBBox, offset);
|
||||||
|
if (isBBoxesOverlapping(sxBBox, txBBox)) { // the expanded bounding boxes of source and target nodes are overlapping
|
||||||
|
return simplifyPolyline(getSimplePolyline(start, end));
|
||||||
|
}
|
||||||
|
const sPoint = getExpandedBBoxPoint(sxBBox, start);
|
||||||
|
const tPoint = getExpandedBBoxPoint(txBBox, end);
|
||||||
|
const lineBBox = getBBoxFromPoints([ sPoint, tPoint ]);
|
||||||
|
const outerBBox = mergeBBox(sxBBox, txBBox);
|
||||||
|
const sMixBBox = mergeBBox(sxBBox, lineBBox);
|
||||||
|
const tMixBBox = mergeBBox(txBBox, lineBBox);
|
||||||
|
let connectPoints = [];
|
||||||
|
connectPoints = connectPoints.concat(
|
||||||
|
getPointsFromBBox(sMixBBox)// .filter(p => !isPointIntersectBBox(p, txBBox))
|
||||||
|
);
|
||||||
|
connectPoints = connectPoints.concat(
|
||||||
|
getPointsFromBBox(tMixBBox)// .filter(p => !isPointIntersectBBox(p, sxBBox))
|
||||||
|
);
|
||||||
|
const centerPoint = { x: (start.x + end.x) / 2, y: (start.y + end.y) / 2 };
|
||||||
|
[
|
||||||
|
lineBBox,
|
||||||
|
sMixBBox,
|
||||||
|
tMixBBox
|
||||||
|
].forEach(bbox => {
|
||||||
|
connectPoints = connectPoints.concat(
|
||||||
|
getBBoxCrossPointsByPoint(bbox, centerPoint).filter(
|
||||||
|
p => isPointOutsideBBox(p, sxBBox) && isPointOutsideBBox(p, txBBox)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
[
|
||||||
|
{ x: sPoint.x, y: tPoint.y },
|
||||||
|
{ x: tPoint.x, y: sPoint.y }
|
||||||
|
].forEach(p => {
|
||||||
|
if (
|
||||||
|
isPointOutsideBBox(p, sxBBox) && isPointOutsideBBox(p, txBBox)// &&
|
||||||
|
// isPointInsideBBox(p, sMixBBox) && isPointInsideBBox(p, tMixBBox)
|
||||||
|
) {
|
||||||
|
connectPoints.push(p);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
connectPoints.unshift(sPoint);
|
||||||
|
connectPoints.push(tPoint);
|
||||||
|
connectPoints = filterConnectPoints(connectPoints, sxBBox, txBBox, outerBBox);
|
||||||
|
const pathPoints = pathFinder(connectPoints, sPoint, tPoint, sBBox, tBBox, start, end);
|
||||||
|
pathPoints.unshift(start);
|
||||||
|
pathPoints.push(end);
|
||||||
|
|
||||||
|
return simplifyPolyline(pathPoints);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 折线
|
||||||
|
Shape.registerEdge('polyline', {
|
||||||
|
// 自定义边时的配置
|
||||||
|
options: {
|
||||||
|
// 默认配置
|
||||||
|
default: {
|
||||||
|
stroke: '#333',
|
||||||
|
lineWidth: 1,
|
||||||
|
radius: 0,
|
||||||
|
offset: 5,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
// 文本样式配置
|
||||||
|
labelCfg: {
|
||||||
|
style: {
|
||||||
|
fill: '#595959'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 鼠标hover状态下的配置
|
||||||
|
hover: {
|
||||||
|
lineWidth: 3
|
||||||
|
},
|
||||||
|
// 选中边状态下的配置
|
||||||
|
select: {
|
||||||
|
lineWidth: 5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
shapeType: 'polyline',
|
||||||
|
// 文本位置
|
||||||
|
labelPosition: 'center',
|
||||||
|
drawShape(cfg, group) {
|
||||||
|
const shapeStyle = this.getShapeStyle(cfg);
|
||||||
|
const keyShape = group.addShape('path', {
|
||||||
|
className: 'edge-shape',
|
||||||
|
attrs: shapeStyle
|
||||||
|
});
|
||||||
|
return keyShape;
|
||||||
|
},
|
||||||
|
getShapeStyle(cfg) {
|
||||||
|
const customStyle = this.getCustomConfig(cfg) || {};
|
||||||
|
const defaultConfig = customStyle.default;
|
||||||
|
const style = Util.deepMix({}, this.options.default, defaultConfig, cfg.style);
|
||||||
|
cfg = this.getPathPoints(cfg);
|
||||||
|
this.radius = customStyle.radius;
|
||||||
|
this.offset = customStyle.offset;
|
||||||
|
const startPoint = cfg.startPoint;
|
||||||
|
const endPoint = cfg.endPoint;
|
||||||
|
const controlPoints = this.getControlPoints(cfg);
|
||||||
|
let points = [ startPoint ]; // 添加起始点
|
||||||
|
// 添加控制点
|
||||||
|
if (controlPoints) {
|
||||||
|
points = points.concat(controlPoints);
|
||||||
|
}
|
||||||
|
// 添加结束点
|
||||||
|
points.push(endPoint);
|
||||||
|
const source = cfg.sourceNode;
|
||||||
|
const target = cfg.targetNode;
|
||||||
|
let routeCfg = { radius: style.radius };
|
||||||
|
if (!controlPoints) {
|
||||||
|
routeCfg = { source, target, offset: style.offset, radius: style.radius };
|
||||||
|
}
|
||||||
|
const path = this.getPath(points, routeCfg);
|
||||||
|
const attrs = Util.deepMix({}, this.options.default, defaultConfig, cfg.style, { path });
|
||||||
|
return attrs;
|
||||||
|
},
|
||||||
|
getPath(points, routeCfg) {
|
||||||
|
const { source, target, offset, radius } = routeCfg;
|
||||||
|
if (!offset) {
|
||||||
|
let path = [];
|
||||||
|
if (radius) {
|
||||||
|
path = getPathWithBorderRadiusByPolyline(points, radius);
|
||||||
|
} else {
|
||||||
|
Util.each(points, (point, index) => {
|
||||||
|
if (index === 0) {
|
||||||
|
path.push([ 'M', point.x, point.y ]);
|
||||||
|
} else {
|
||||||
|
path.push([ 'L', point.x, point.y ]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
if (radius) {
|
||||||
|
const polylinePoints = simplifyPolyline(
|
||||||
|
getPolylinePoints(points[0], points[points.length - 1], source, target, offset)
|
||||||
|
);
|
||||||
|
return getPathWithBorderRadiusByPolyline(polylinePoints, radius);
|
||||||
|
}
|
||||||
|
const polylinePoints = getPolylinePoints(points[0],
|
||||||
|
points[points.length - 1], source, target, offset);
|
||||||
|
return Util.pointsToPolygon(polylinePoints);
|
||||||
|
},
|
||||||
|
|
||||||
|
update(cfg, item) {
|
||||||
|
const group = item.getContainer();
|
||||||
|
const shapeClassName = this.itemType + CLS_SHAPE_SUFFIX;
|
||||||
|
const shape = group.findByClassName(shapeClassName);
|
||||||
|
if (!cfg.style) {
|
||||||
|
cfg.style = {};
|
||||||
|
}
|
||||||
|
const oriShapeAttrs = shape.attr();
|
||||||
|
cfg.style.radius = cfg.style.radius || oriShapeAttrs.radius;
|
||||||
|
cfg.style.offset = cfg.style.offset || oriShapeAttrs.offset;
|
||||||
|
const shapeStyle = this.getShapeStyle(cfg);
|
||||||
|
shape.attr(shapeStyle);
|
||||||
|
const labelClassName = this.itemType + CLS_LABEL_SUFFIX;
|
||||||
|
const label = group.findByClassName(labelClassName);
|
||||||
|
// 此时需要考虑之前是否绘制了 label 的场景存在三种情况
|
||||||
|
// 1. 更新时不需要 label,但是原先存在 label,此时需要删除
|
||||||
|
// 2. 更新时需要 label, 但是原先不存在,创建节点
|
||||||
|
// 3. 如果两者都存在,更新
|
||||||
|
if (!cfg.label) {
|
||||||
|
label && label.remove();
|
||||||
|
} else {
|
||||||
|
if (!label) {
|
||||||
|
const newLabel = this.drawLabel(cfg, group);
|
||||||
|
newLabel.set('className', labelClassName);
|
||||||
|
} else {
|
||||||
|
const labelCfg = cfg.labelCfg || {};
|
||||||
|
const labelStyle = this.getLabelStyle(cfg, labelCfg, group);
|
||||||
|
/**
|
||||||
|
* fixme g中shape的rotate是角度累加的,不是label的rotate想要的角度
|
||||||
|
* 由于现在label只有rotate操作,所以在更新label的时候如果style中有rotate就重置一下变换
|
||||||
|
* 后续会基于g的Text复写一个Label出来处理这一类问题
|
||||||
|
*/
|
||||||
|
label.resetMatrix();
|
||||||
|
label.attr(labelStyle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 'single-line');
|
@ -8,4 +8,5 @@ const Shape = require('./shape');
|
|||||||
require('./node');
|
require('./node');
|
||||||
require('./edge');
|
require('./edge');
|
||||||
require('./nodes');
|
require('./nodes');
|
||||||
|
require('./edges');
|
||||||
module.exports = Shape;
|
module.exports = Shape;
|
||||||
|
@ -6,6 +6,25 @@ const G = require('@antv/g/lib');
|
|||||||
const BaseUtil = require('./base');
|
const BaseUtil = require('./base');
|
||||||
const vec2 = BaseUtil.vec2;
|
const vec2 = BaseUtil.vec2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 替换字符串中的字段.
|
||||||
|
* @param {String} str 模版字符串
|
||||||
|
* @param {Object} o json data
|
||||||
|
* @param {RegExp} [regexp] 匹配字符串的正则表达式
|
||||||
|
*/
|
||||||
|
|
||||||
|
function substitute(str, o) {
|
||||||
|
if (!str || !o) {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
return str.replace(/\\?\{([^{}]+)\}/g, function(match, name) {
|
||||||
|
if (match.charAt(0) === '\\') {
|
||||||
|
return match.slice(1);
|
||||||
|
}
|
||||||
|
return (o[name] === undefined) ? '' : o[name];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getSpline(points) {
|
getSpline(points) {
|
||||||
const data = [];
|
const data = [];
|
||||||
@ -37,5 +56,33 @@ module.exports = {
|
|||||||
point.x += perpendicular[0];
|
point.x += perpendicular[0];
|
||||||
point.y += perpendicular[1];
|
point.y += perpendicular[1];
|
||||||
return point;
|
return point;
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 点集转化为Path多边形
|
||||||
|
* @param {Array} points 点集
|
||||||
|
* @param {Boolen} z 是否封闭
|
||||||
|
* @return {Array} Path
|
||||||
|
*/
|
||||||
|
pointsToPolygon(points, z) {
|
||||||
|
if (!points.length) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
let path = '';
|
||||||
|
let str = '';
|
||||||
|
|
||||||
|
for (let i = 0, length = points.length; i < length; i++) {
|
||||||
|
const item = points[i];
|
||||||
|
if (i === 0) {
|
||||||
|
str = 'M{x} {y}';
|
||||||
|
} else {
|
||||||
|
str = 'L{x} {y}';
|
||||||
|
}
|
||||||
|
path += substitute(str, item);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (z) {
|
||||||
|
path += 'Z';
|
||||||
|
}
|
||||||
|
return path;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user