mirror of
https://github.com/projectstorm/react-diagrams.git
synced 2026-03-13 09:50:09 +08:00
Merge branch 'Reroute-(redistribute)-links-only,-based-on-current-nodes-positions-#731' of https://github.com/h0111in/react-diagrams into h0111in-Reroute-(redistribute)-links-only,-based-on-current-nodes-positions-#731
This commit is contained in:
@@ -45,7 +45,8 @@ class DemoWidget extends React.Component<{ model: DiagramModel; engine: DiagramE
|
||||
marginx: 25,
|
||||
marginy: 25
|
||||
},
|
||||
includeLinks: true
|
||||
includeLinks: true,
|
||||
nodeMargin: 25
|
||||
});
|
||||
}
|
||||
|
||||
@@ -57,6 +58,13 @@ class DemoWidget extends React.Component<{ model: DiagramModel; engine: DiagramE
|
||||
this.props.engine.repaintCanvas();
|
||||
};
|
||||
|
||||
autoRefreshLinks = () => {
|
||||
this.engine.refreshLinks(this.props.model);
|
||||
|
||||
// only happens if pathfing is enabled (check line 25)
|
||||
this.reroute();
|
||||
this.props.engine.repaintCanvas();
|
||||
};
|
||||
componentDidMount(): void {
|
||||
setTimeout(() => {
|
||||
this.autoDistribute();
|
||||
@@ -72,7 +80,12 @@ class DemoWidget extends React.Component<{ model: DiagramModel; engine: DiagramE
|
||||
|
||||
render() {
|
||||
return (
|
||||
<DemoWorkspaceWidget buttons={<DemoButton onClick={this.autoDistribute}>Re-distribute</DemoButton>}>
|
||||
<DemoWorkspaceWidget buttons={
|
||||
<div>
|
||||
<DemoButton onClick={this.autoDistribute}>Re-distribute</DemoButton>
|
||||
<DemoButton onClick={this.autoRefreshLinks}>Refresh Links</DemoButton>
|
||||
</div>
|
||||
}>
|
||||
<DemoCanvasWidget>
|
||||
<CanvasWidget engine={this.props.engine} />
|
||||
</DemoCanvasWidget>
|
||||
@@ -106,9 +119,11 @@ export default () => {
|
||||
});
|
||||
|
||||
// more links for more complicated diagram
|
||||
links.push(connectNodes(nodesFrom[0], nodesTo[1], engine));
|
||||
links.push(connectNodes(nodesTo[0], nodesFrom[1], engine));
|
||||
links.push(connectNodes(nodesFrom[1], nodesTo[2], engine));
|
||||
links.push(connectNodes(nodesTo[0], nodesTo[1], engine));
|
||||
links.push(connectNodes(nodesTo[1], nodesTo[2], engine));
|
||||
links.push(connectNodes(nodesTo[0], nodesTo[2], engine));
|
||||
links.push(connectNodes(nodesFrom[0], nodesFrom[2], engine));
|
||||
links.push(connectNodes(nodesFrom[0], nodesTo[2], engine));
|
||||
|
||||
// initial random position
|
||||
nodesFrom.forEach((node, index) => {
|
||||
|
||||
@@ -3,6 +3,7 @@ import * as dagre from 'dagre';
|
||||
import * as _ from 'lodash';
|
||||
import { GraphLabel } from 'dagre';
|
||||
import { Point } from '@projectstorm/geometry';
|
||||
import { link } from 'fs/promises';
|
||||
|
||||
export interface DagreEngineOptions {
|
||||
graph?: GraphLabel;
|
||||
@@ -10,6 +11,7 @@ export interface DagreEngineOptions {
|
||||
* Will also layout links
|
||||
*/
|
||||
includeLinks?: boolean;
|
||||
nodeMargin?: number;
|
||||
}
|
||||
|
||||
export class DagreEngine {
|
||||
@@ -46,9 +48,24 @@ export class DagreEngine {
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
g.nodes().forEach(function (v) {
|
||||
console.log("Node " + v + ": " + JSON.stringify(g.node(v)));
|
||||
});
|
||||
g.edges().forEach(function (e) {
|
||||
console.log("Edge " + e.v + " -> " + e.w + ": " + JSON.stringify(g.edge(e)));
|
||||
});
|
||||
|
||||
// layout the graph
|
||||
dagre.layout(g);
|
||||
|
||||
g.nodes().forEach(function (v) {
|
||||
console.log("Node " + v + ": " + JSON.stringify(g.node(v)));
|
||||
});
|
||||
g.edges().forEach(function (e) {
|
||||
console.log("Edge " + e.v + " -> " + e.w + ": " + JSON.stringify(g.edge(e)));
|
||||
});
|
||||
|
||||
g.nodes().forEach((v) => {
|
||||
const node = g.node(v);
|
||||
model.getNode(v).setPosition(node.x - node.width / 2, node.y - node.height / 2);
|
||||
@@ -68,4 +85,144 @@ export class DagreEngine {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public refreshLinks(diagram: DiagramModel) {
|
||||
const { nodeMargin } = this.options;
|
||||
const nodes = diagram.getNodes();
|
||||
const links = diagram.getLinks();
|
||||
let maxChunkRowIndex = -1;
|
||||
// build the chunk matrix
|
||||
const chunks: { [id: number]: { [id: number]: boolean } } = {}; // true: occupied, false: blank
|
||||
const NodeXColumnIndexDictionary: { [id: number]: number } = {};
|
||||
let verticalLines: number[] = [];
|
||||
_.forEach(nodes, (node) => {
|
||||
// find vertical lines. vertical lines go through maximum number of nodes located under each other.
|
||||
const nodeColumnCenter = node.getX() + node.width / 2;
|
||||
if (_.every(verticalLines, (vLine) => {
|
||||
return Math.abs(nodeColumnCenter - vLine) > nodeMargin
|
||||
})) {
|
||||
verticalLines.push(nodeColumnCenter);
|
||||
}
|
||||
});
|
||||
|
||||
// sort chunk columns
|
||||
verticalLines = verticalLines.sort((a, b) => a - b);
|
||||
_.forEach(verticalLines, (line, index) => {
|
||||
chunks[index] = {};
|
||||
chunks[index + 0.5] = {};
|
||||
});
|
||||
|
||||
// set occupied chunks
|
||||
_.forEach(nodes, (node) => {
|
||||
const nodeColumnCenter = node.getX() + node.width / 2;
|
||||
const startChunkIndex = Math.floor(node.getY() / (nodeMargin));
|
||||
const endChunkIndex = Math.floor((node.getY() + node.height) / (nodeMargin));
|
||||
// find max ChunkRowIndex
|
||||
if (endChunkIndex > maxChunkRowIndex) maxChunkRowIndex = endChunkIndex;
|
||||
const nodeColumnIndex = _.findIndex(verticalLines, (vLine) => {
|
||||
return Math.abs(nodeColumnCenter - vLine) <= nodeMargin;
|
||||
})
|
||||
_.forEach(_.range(startChunkIndex, endChunkIndex + 1), (chunkIndex) => {
|
||||
chunks[nodeColumnIndex][chunkIndex] = true;
|
||||
});
|
||||
NodeXColumnIndexDictionary[node.getX()] = nodeColumnIndex;
|
||||
});
|
||||
|
||||
// sort links based on their distances
|
||||
const edges = _.map(links, (link) => {
|
||||
if (link.getSourcePort() && link.getTargetPort()) {
|
||||
const source = link.getSourcePort().getNode();
|
||||
const target = link.getTargetPort().getNode();
|
||||
const sourceIndex = NodeXColumnIndexDictionary[source.getX()];
|
||||
const targetIndex = NodeXColumnIndexDictionary[target.getX()];
|
||||
|
||||
return sourceIndex > targetIndex ? {
|
||||
link,
|
||||
sourceIndex,
|
||||
sourceY: source.getY() + source.height / 2,
|
||||
source,
|
||||
targetIndex,
|
||||
targetY: target.getY() + source.height / 2,
|
||||
target
|
||||
} : {
|
||||
link,
|
||||
sourceIndex: targetIndex,
|
||||
sourceY: target.getY() + target.height / 2,
|
||||
source: target,
|
||||
targetIndex: sourceIndex,
|
||||
targetY: source.getY() + source.height / 2,
|
||||
target: source
|
||||
};
|
||||
}
|
||||
});
|
||||
const sortedEdges = _.sortBy(edges, (link) => {
|
||||
return Math.abs(link.targetIndex - link.sourceIndex);
|
||||
})
|
||||
// set link points
|
||||
|
||||
if (this.options.includeLinks) {
|
||||
|
||||
_.forEach(sortedEdges, (edge) => {
|
||||
|
||||
const link = diagram.getLink(edge.link.getID())
|
||||
// re-draw
|
||||
if (Math.abs(edge.sourceIndex - edge.targetIndex) > 1) {
|
||||
// get the length of link in column
|
||||
const columns = _.range(edge.sourceIndex - 1, edge.targetIndex);
|
||||
|
||||
const chunkIndex = Math.floor(edge.sourceY / nodeMargin);
|
||||
const targetChunkIndex = Math.floor(edge.targetY / nodeMargin);
|
||||
|
||||
// check upper paths
|
||||
let northCost = 1; let aboveRowIndex = chunkIndex;
|
||||
for (; aboveRowIndex >= 0; aboveRowIndex--, northCost++) {
|
||||
if (_.every(columns, (columnIndex) => {
|
||||
return !(chunks[columnIndex][aboveRowIndex] || chunks[columnIndex + 0.5][aboveRowIndex] || chunks[columnIndex - 0.5][aboveRowIndex]) ;
|
||||
})) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// check lower paths
|
||||
let southCost = 0;
|
||||
let belowRowIndex = chunkIndex;
|
||||
for (; belowRowIndex <= maxChunkRowIndex; belowRowIndex++, southCost++) {
|
||||
if (_.every(columns, (columnIndex) => {
|
||||
return !(chunks[columnIndex][belowRowIndex] || chunks[columnIndex + 0.5][belowRowIndex] || chunks[columnIndex - 0.5][belowRowIndex]) ;
|
||||
})) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// pick the cheapest path
|
||||
const pathRowIndex = (southCost + (belowRowIndex - targetChunkIndex)) < (northCost + (targetChunkIndex - aboveRowIndex)) ? belowRowIndex + 1 : aboveRowIndex - 1;
|
||||
|
||||
// Finally update the link points
|
||||
|
||||
const points = [link.getFirstPoint()];
|
||||
points.push(new PointModel({ link: link, position: new Point((verticalLines[columns[0]] + verticalLines[columns[0] + 1]) / 2, (pathRowIndex + 0.5) * nodeMargin) }));
|
||||
|
||||
_.forEach(columns, (column) => {
|
||||
points.push(new PointModel({ link: link, position: new Point(verticalLines[column], (pathRowIndex + 0.5) * nodeMargin) }));
|
||||
points.push(new PointModel({ link: link, position: new Point((verticalLines[column] + verticalLines[column - 1]) / 2, (pathRowIndex + 0.5) * nodeMargin) }));
|
||||
chunks[column][pathRowIndex] = true;
|
||||
chunks[column][pathRowIndex + 1] = true;
|
||||
chunks[column + 0.5][pathRowIndex] = true;
|
||||
chunks[column + 0.5][pathRowIndex + 1] = true;
|
||||
})
|
||||
link.setPoints(points.concat(link.getLastPoint()));
|
||||
|
||||
} else { // refresh
|
||||
link.setPoints([link.getFirstPoint(), link.getLastPoint()]);
|
||||
const columnIndex = (edge.sourceIndex + edge.targetIndex)/2;
|
||||
if (!chunks[columnIndex]) {
|
||||
chunks[columnIndex] = {};
|
||||
}
|
||||
const rowIndex = Math.floor(((edge.sourceY + edge.targetY)/2)/ nodeMargin);
|
||||
chunks[columnIndex][rowIndex] = true
|
||||
chunks[columnIndex][rowIndex + 1] = true
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user