fix: conflict from polyline

This commit is contained in:
shiwu.wyy 2019-09-29 14:09:20 +08:00
commit 036cf1acd8
7 changed files with 699 additions and 4 deletions

152
demos/default-edges.html Normal file
View 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>

View File

@ -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",

View File

@ -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
View File

@ -0,0 +1 @@
require('./polyline');

483
src/shape/edges/polyline.js Normal file
View 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');

View File

@ -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;

View File

@ -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;
} }
}; };