@flowgram-vue/free-auto-layout-plugin 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/dist/index.cjs +2713 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +377 -0
- package/dist/index.js +2705 -0
- package/dist/index.js.map +1 -0
- package/package.json +57 -0
- package/src/create-auto-layout-plugin.ts +23 -0
- package/src/dagre-layout/acyclic.ts +129 -0
- package/src/dagre-layout/graph.ts +71 -0
- package/src/dagre-layout/index.ts +9 -0
- package/src/dagre-layout/layout.ts +151 -0
- package/src/dagre-layout/order.ts +261 -0
- package/src/dagre-layout/rank/feasible-tree.ts +102 -0
- package/src/dagre-layout/rank/index.ts +9 -0
- package/src/dagre-layout/rank/longest-path.ts +79 -0
- package/src/dagre-layout/rank/network-simplex.ts +235 -0
- package/src/dagre-layout/rank/normalize-ranks.ts +26 -0
- package/src/dagre-layout/type.ts +43 -0
- package/src/dagre-lib/acyclic.js +72 -0
- package/src/dagre-lib/add-border-segments.js +41 -0
- package/src/dagre-lib/coordinate-system.js +77 -0
- package/src/dagre-lib/data/list.js +63 -0
- package/src/dagre-lib/debug.js +34 -0
- package/src/dagre-lib/greedy-fas.js +134 -0
- package/src/dagre-lib/index.js +75 -0
- package/src/dagre-lib/layout.js +449 -0
- package/src/dagre-lib/nesting-graph.js +133 -0
- package/src/dagre-lib/normalize.js +98 -0
- package/src/dagre-lib/order/add-subgraph-constraints.js +57 -0
- package/src/dagre-lib/order/barycenter.js +34 -0
- package/src/dagre-lib/order/build-layer-graph.js +80 -0
- package/src/dagre-lib/order/cross-count.js +78 -0
- package/src/dagre-lib/order/index.js +86 -0
- package/src/dagre-lib/order/init-order.js +43 -0
- package/src/dagre-lib/order/resolve-conflicts.js +128 -0
- package/src/dagre-lib/order/sort-subgraph.js +79 -0
- package/src/dagre-lib/order/sort.js +62 -0
- package/src/dagre-lib/parent-dummy-chains.js +90 -0
- package/src/dagre-lib/position/bk.js +431 -0
- package/src/dagre-lib/position/index.js +37 -0
- package/src/dagre-lib/rank/feasible-tree.js +104 -0
- package/src/dagre-lib/rank/index.js +61 -0
- package/src/dagre-lib/rank/network-simplex.js +243 -0
- package/src/dagre-lib/rank/util.js +70 -0
- package/src/dagre-lib/util.js +365 -0
- package/src/dagre-lib/version.js +6 -0
- package/src/env.d.ts +10 -0
- package/src/index.ts +10 -0
- package/src/layout/constant.ts +26 -0
- package/src/layout/dagre.ts +245 -0
- package/src/layout/index.ts +9 -0
- package/src/layout/layout.ts +41 -0
- package/src/layout/position.ts +72 -0
- package/src/layout/store.ts +262 -0
- package/src/layout/type.ts +162 -0
- package/src/services.ts +174 -0
- package/src/type.ts +10 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { LayoutEdge } from '../type';
|
|
7
|
+
import { LayoutGraph } from '../graph';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 计算边的松弛度
|
|
11
|
+
*/
|
|
12
|
+
const calculateSlack = (graph: LayoutGraph, edge: LayoutEdge): number => {
|
|
13
|
+
const sourceNode = graph.getNode(edge.from);
|
|
14
|
+
const targetNode = graph.getNode(edge.to);
|
|
15
|
+
if (!sourceNode || !targetNode) {
|
|
16
|
+
return Number.POSITIVE_INFINITY;
|
|
17
|
+
}
|
|
18
|
+
return targetNode.rank - sourceNode.rank - (edge.minlen || 1);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 深度优先搜索构建紧致树
|
|
23
|
+
*/
|
|
24
|
+
const dfs = (graph: LayoutGraph, tightTree: LayoutGraph, nodeId: string): void => {
|
|
25
|
+
const edges = graph.edges.filter((e) => e.from === nodeId || e.to === nodeId);
|
|
26
|
+
edges.forEach((edge) => {
|
|
27
|
+
const neighborId = edge.from === nodeId ? edge.to : edge.from;
|
|
28
|
+
if (!tightTree.hasNode(neighborId) && calculateSlack(graph, edge) === 0) {
|
|
29
|
+
const neighborNode = graph.getNode(neighborId);
|
|
30
|
+
if (neighborNode) {
|
|
31
|
+
tightTree.addLayoutNode({ ...neighborNode });
|
|
32
|
+
tightTree.addLayoutEdge({ ...edge });
|
|
33
|
+
dfs(graph, tightTree, neighborId);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 构建最大紧致树
|
|
41
|
+
*/
|
|
42
|
+
const buildTightTree = (graph: LayoutGraph, tightTree: LayoutGraph): number => {
|
|
43
|
+
tightTree.nodes.forEach((node) => dfs(graph, tightTree, node.id));
|
|
44
|
+
return tightTree.nodes.length;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 找到具有最小松弛度的边
|
|
49
|
+
*/
|
|
50
|
+
const findMinSlackEdge = (graph: LayoutGraph, tightTree: LayoutGraph): LayoutEdge | undefined => {
|
|
51
|
+
let minSlack = Number.POSITIVE_INFINITY;
|
|
52
|
+
let minSlackEdge: LayoutEdge | undefined;
|
|
53
|
+
|
|
54
|
+
graph.edges.forEach((edge) => {
|
|
55
|
+
const hasSource = tightTree.hasNode(edge.from);
|
|
56
|
+
const hasTarget = tightTree.hasNode(edge.to);
|
|
57
|
+
if (hasSource !== hasTarget) {
|
|
58
|
+
const slack = calculateSlack(graph, edge);
|
|
59
|
+
if (slack < minSlack) {
|
|
60
|
+
minSlack = slack;
|
|
61
|
+
minSlackEdge = edge;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
return minSlackEdge;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 调整rank值
|
|
71
|
+
*/
|
|
72
|
+
const shiftRanks = (graph: LayoutGraph, tightTree: LayoutGraph, delta: number): void => {
|
|
73
|
+
tightTree.nodes.forEach((node) => {
|
|
74
|
+
const graphNode = graph.getNode(node.id);
|
|
75
|
+
if (graphNode) {
|
|
76
|
+
graphNode.rank += delta;
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 构建紧致生成树
|
|
83
|
+
*/
|
|
84
|
+
export const feasibleTree = (graph: LayoutGraph): LayoutGraph => {
|
|
85
|
+
const tightTree = new LayoutGraph();
|
|
86
|
+
|
|
87
|
+
// 选择任意节点作为起始节点
|
|
88
|
+
const startNode = graph.nodes[0];
|
|
89
|
+
tightTree.addLayoutNode({ ...startNode });
|
|
90
|
+
|
|
91
|
+
while (buildTightTree(graph, tightTree) < graph.nodes.length) {
|
|
92
|
+
const minSlackEdge = findMinSlackEdge(graph, tightTree);
|
|
93
|
+
if (minSlackEdge) {
|
|
94
|
+
const delta = tightTree.hasNode(minSlackEdge.from)
|
|
95
|
+
? calculateSlack(graph, minSlackEdge)
|
|
96
|
+
: -calculateSlack(graph, minSlackEdge);
|
|
97
|
+
shiftRanks(graph, tightTree, delta);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return tightTree;
|
|
102
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export { feasibleTree } from './feasible-tree';
|
|
7
|
+
export { longestPath } from './longest-path';
|
|
8
|
+
export { networkSimplex } from './network-simplex';
|
|
9
|
+
export { normalizeRanks } from './normalize-ranks';
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { LayoutGraph } from '../graph';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 计算图中节点的最长路径
|
|
10
|
+
*/
|
|
11
|
+
export const longestPath = (graph: LayoutGraph): LayoutGraph => {
|
|
12
|
+
// 用于记录已访问的节点
|
|
13
|
+
const visited: Record<string, boolean> = {};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 深度优先搜索计算节点的层级
|
|
17
|
+
* @param nodeId 当前节点ID
|
|
18
|
+
* @returns 计算得到的层级
|
|
19
|
+
*/
|
|
20
|
+
const dfs = (nodeId: string): number => {
|
|
21
|
+
const node = graph.getNode(nodeId);
|
|
22
|
+
|
|
23
|
+
// 如果节点不存在,返回 -1
|
|
24
|
+
if (!node) {
|
|
25
|
+
return -1;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// 如果节点已访问且已经计算过rank,直接返回其rank
|
|
29
|
+
if (visited[nodeId] && node.rank !== -1) {
|
|
30
|
+
return node.rank;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 标记节点为已访问
|
|
34
|
+
visited[nodeId] = true;
|
|
35
|
+
|
|
36
|
+
// 获取所有以当前节点为起点的边
|
|
37
|
+
const outgoingEdges = graph.edges.filter((edge) => edge.from === nodeId);
|
|
38
|
+
|
|
39
|
+
// 如果没有出边,说明是叶子节点,rank为0
|
|
40
|
+
if (outgoingEdges.length === 0) {
|
|
41
|
+
node.rank = 0;
|
|
42
|
+
return 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 计算所有子节点的最大rank
|
|
46
|
+
let maxChildRank = -1;
|
|
47
|
+
outgoingEdges.forEach((edge) => {
|
|
48
|
+
const childRank = dfs(edge.to);
|
|
49
|
+
const minlen = edge.minlen || 1; // 使用默认最小长度1,如果未指定
|
|
50
|
+
maxChildRank = Math.max(maxChildRank, childRank + minlen);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// 当前节点的rank为子节点最大rank + 1
|
|
54
|
+
node.rank = maxChildRank;
|
|
55
|
+
return node.rank;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// 从每个没有入边的节点(源节点)开始DFS
|
|
59
|
+
const sourceNodes = graph.nodes.filter(
|
|
60
|
+
(node) => !graph.edges.some((edge) => edge.to === node.id)
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
sourceNodes.forEach((node) => dfs(node.id));
|
|
64
|
+
|
|
65
|
+
// 确保所有节点都被访问到
|
|
66
|
+
graph.nodes.forEach((node) => {
|
|
67
|
+
if (node.rank === -1) {
|
|
68
|
+
dfs(node.id);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// 反转rank值,使得源节点的rank最小
|
|
73
|
+
const maxRank = Math.max(...graph.nodes.map((node) => node.rank));
|
|
74
|
+
graph.nodes.forEach((node) => {
|
|
75
|
+
node.rank = maxRank - node.rank;
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
return graph;
|
|
79
|
+
};
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { LayoutEdge, LayoutNode } from '../type';
|
|
7
|
+
import { LayoutGraph } from '../graph';
|
|
8
|
+
import { longestPath } from './longest-path';
|
|
9
|
+
import { feasibleTree } from './feasible-tree';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 网络单纯形
|
|
13
|
+
* 参考 https://github.com/dagrejs/dagre/blob/master/lib/rank/network-simplex.js
|
|
14
|
+
*/
|
|
15
|
+
export const networkSimplex = (g: LayoutGraph): LayoutGraph => {
|
|
16
|
+
longestPath(g); // 初始化层级
|
|
17
|
+
const t = feasibleTree(g); // 构建紧致生成树
|
|
18
|
+
initLowLimValues(t);
|
|
19
|
+
initCutValues(t, g);
|
|
20
|
+
|
|
21
|
+
let e: LayoutEdge | undefined;
|
|
22
|
+
while ((e = leaveEdge(t))) {
|
|
23
|
+
const f = enterEdge(t, g, e);
|
|
24
|
+
if (f) {
|
|
25
|
+
exchangeEdges(t, g, e, f);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return g;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const initLowLimValues = (t: LayoutGraph): void => {
|
|
32
|
+
const root = t.nodes[0];
|
|
33
|
+
if (root) {
|
|
34
|
+
dfsAssignLowLim(t, new Map<string, boolean>(), { nextLim: 1 }, root.id);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const dfsAssignLowLim = (
|
|
39
|
+
t: LayoutGraph,
|
|
40
|
+
visited: Map<string, boolean>,
|
|
41
|
+
state: { nextLim: number },
|
|
42
|
+
v: string,
|
|
43
|
+
parent?: string
|
|
44
|
+
): void => {
|
|
45
|
+
const node = t.getNode(v);
|
|
46
|
+
if (!node) return;
|
|
47
|
+
|
|
48
|
+
const low = state.nextLim;
|
|
49
|
+
visited.set(v, true);
|
|
50
|
+
|
|
51
|
+
t.edges
|
|
52
|
+
.filter((e) => e.from === v || e.to === v)
|
|
53
|
+
.forEach((e) => {
|
|
54
|
+
const w = e.from === v ? e.to : e.from;
|
|
55
|
+
if (!visited.get(w)) {
|
|
56
|
+
dfsAssignLowLim(t, visited, state, w, v);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
node.low = low;
|
|
61
|
+
node.lim = state.nextLim++;
|
|
62
|
+
if (parent) {
|
|
63
|
+
node.parent = parent;
|
|
64
|
+
} else {
|
|
65
|
+
delete node.parent;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const initCutValues = (t: LayoutGraph, g: LayoutGraph): void => {
|
|
70
|
+
const vs = postorder(t);
|
|
71
|
+
vs.slice(0, -1).forEach((v) => assignCutValue(t, g, v));
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const postorder = (t: LayoutGraph): string[] => {
|
|
75
|
+
const visited = new Set<string>();
|
|
76
|
+
const result: string[] = [];
|
|
77
|
+
|
|
78
|
+
const dfs = (nodeId: string): void => {
|
|
79
|
+
const node = t.getNode(nodeId);
|
|
80
|
+
if (!node || visited.has(nodeId)) return;
|
|
81
|
+
|
|
82
|
+
visited.add(nodeId);
|
|
83
|
+
|
|
84
|
+
t.edges
|
|
85
|
+
.filter((e) => e.from === nodeId || e.to === nodeId)
|
|
86
|
+
.forEach((e) => {
|
|
87
|
+
const neighborId = e.from === nodeId ? e.to : e.from;
|
|
88
|
+
dfs(neighborId);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
result.push(nodeId);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
t.nodes.forEach((node) => dfs(node.id));
|
|
95
|
+
return result;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const assignCutValue = (t: LayoutGraph, g: LayoutGraph, childId: string): void => {
|
|
99
|
+
const child = t.getNode(childId);
|
|
100
|
+
if (!child || !child.parent) return;
|
|
101
|
+
|
|
102
|
+
const edge = t.edges.find((e) => e.from === childId && e.to === child.parent);
|
|
103
|
+
if (edge) {
|
|
104
|
+
edge.cutvalue = calcCutValue(t, g, childId);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const calcCutValue = (t: LayoutGraph, g: LayoutGraph, childId: string): number => {
|
|
109
|
+
const child = t.getNode(childId);
|
|
110
|
+
if (!child || !child.parent) return 0;
|
|
111
|
+
|
|
112
|
+
let cutValue = 0;
|
|
113
|
+
const graphEdge = g.edges.find(
|
|
114
|
+
(e) =>
|
|
115
|
+
(e.from === childId && e.to === child.parent) || (e.from === child.parent && e.to === childId)
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
if (graphEdge) {
|
|
119
|
+
cutValue = graphEdge.weight || 0;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
g.edges
|
|
123
|
+
.filter((e) => e.from === childId || e.to === childId)
|
|
124
|
+
.forEach((e) => {
|
|
125
|
+
const otherId = e.from === childId ? e.to : e.from;
|
|
126
|
+
if (otherId !== child.parent) {
|
|
127
|
+
const otherWeight = e.weight || 0;
|
|
128
|
+
cutValue += e.from === childId ? otherWeight : -otherWeight;
|
|
129
|
+
if (isTreeEdge(t, childId, otherId)) {
|
|
130
|
+
const treeEdge = t.edges.find(
|
|
131
|
+
(te) =>
|
|
132
|
+
(te.from === childId && te.to === otherId) ||
|
|
133
|
+
(te.from === otherId && te.to === childId)
|
|
134
|
+
);
|
|
135
|
+
if (treeEdge && treeEdge.cutvalue !== undefined) {
|
|
136
|
+
cutValue += e.from === childId ? -treeEdge.cutvalue : treeEdge.cutvalue;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
return cutValue;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const isTreeEdge = (t: LayoutGraph, u: string, v: string): boolean =>
|
|
146
|
+
t.edges.some((e) => (e.from === u && e.to === v) || (e.from === v && e.to === u));
|
|
147
|
+
|
|
148
|
+
const leaveEdge = (t: LayoutGraph): LayoutEdge | undefined =>
|
|
149
|
+
t.edges.find((e) => (e.cutvalue || 0) < 0);
|
|
150
|
+
|
|
151
|
+
const enterEdge = (t: LayoutGraph, g: LayoutGraph, edge: LayoutEdge): LayoutEdge | undefined => {
|
|
152
|
+
const vLabel = t.getNode(edge.from);
|
|
153
|
+
const wLabel = t.getNode(edge.to);
|
|
154
|
+
if (!vLabel || !wLabel) return undefined;
|
|
155
|
+
|
|
156
|
+
const tailLabel = vLabel.lim! > wLabel.lim! ? wLabel : vLabel;
|
|
157
|
+
const flip = tailLabel === wLabel;
|
|
158
|
+
|
|
159
|
+
const candidates = g.edges.filter((e) => {
|
|
160
|
+
const vNode = t.getNode(e.from);
|
|
161
|
+
const wNode = t.getNode(e.to);
|
|
162
|
+
return (
|
|
163
|
+
vNode &&
|
|
164
|
+
wNode &&
|
|
165
|
+
flip === isDescendant(t, vNode, tailLabel) &&
|
|
166
|
+
flip !== isDescendant(t, wNode, tailLabel)
|
|
167
|
+
);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
return candidates.reduce((acc, e) => {
|
|
171
|
+
if (slack(g, e) < slack(g, acc)) {
|
|
172
|
+
return e;
|
|
173
|
+
}
|
|
174
|
+
return acc;
|
|
175
|
+
});
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const isDescendant = (t: LayoutGraph, vLabel: LayoutNode, rootLabel: LayoutNode): boolean =>
|
|
179
|
+
(rootLabel.low || 0) <= (vLabel.lim || 0) && (vLabel.lim || 0) <= (rootLabel.lim || 0);
|
|
180
|
+
|
|
181
|
+
const slack = (g: LayoutGraph, edge: LayoutEdge): number => {
|
|
182
|
+
const source = g.getNode(edge.from);
|
|
183
|
+
const target = g.getNode(edge.to);
|
|
184
|
+
if (!source || !target) return Number.POSITIVE_INFINITY;
|
|
185
|
+
return Math.abs(target.rank - source.rank) - (edge.minlen || 1);
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const exchangeEdges = (t: LayoutGraph, g: LayoutGraph, e: LayoutEdge, f: LayoutEdge): void => {
|
|
189
|
+
t.removeEdge(e.id);
|
|
190
|
+
t.addLayoutEdge(f);
|
|
191
|
+
initLowLimValues(t);
|
|
192
|
+
initCutValues(t, g);
|
|
193
|
+
updateRanks(t, g);
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const updateRanks = (t: LayoutGraph, g: LayoutGraph): void => {
|
|
197
|
+
const root = t.nodes.find((v) => !v.parent);
|
|
198
|
+
if (!root) return;
|
|
199
|
+
|
|
200
|
+
const vs = preorder(t, root.id);
|
|
201
|
+
vs.slice(1).forEach((v) => {
|
|
202
|
+
const node = t.getNode(v);
|
|
203
|
+
const parent = t.getNode(node?.parent || '');
|
|
204
|
+
if (!node || !parent) return;
|
|
205
|
+
|
|
206
|
+
const edge = g.edges.find(
|
|
207
|
+
(e) => (e.from === v && e.to === node.parent) || (e.from === node.parent && e.to === v)
|
|
208
|
+
);
|
|
209
|
+
if (!edge) return;
|
|
210
|
+
|
|
211
|
+
const flipped = edge.from === node.parent;
|
|
212
|
+
node.rank = parent.rank + (flipped ? edge.minlen || 1 : -(edge.minlen || 1));
|
|
213
|
+
});
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const preorder = (t: LayoutGraph, root: string): string[] => {
|
|
217
|
+
const result: string[] = [];
|
|
218
|
+
const visited = new Set<string>();
|
|
219
|
+
|
|
220
|
+
const dfs = (nodeId: string): void => {
|
|
221
|
+
if (visited.has(nodeId)) return;
|
|
222
|
+
visited.add(nodeId);
|
|
223
|
+
result.push(nodeId);
|
|
224
|
+
|
|
225
|
+
t.edges
|
|
226
|
+
.filter((e) => e.from === nodeId || e.to === nodeId)
|
|
227
|
+
.forEach((e) => {
|
|
228
|
+
const neighborId = e.from === nodeId ? e.to : e.from;
|
|
229
|
+
dfs(neighborId);
|
|
230
|
+
});
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
dfs(root);
|
|
234
|
+
return result;
|
|
235
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { LayoutGraph } from '../graph';
|
|
7
|
+
|
|
8
|
+
export const normalizeRanks = (graph: LayoutGraph): LayoutGraph => {
|
|
9
|
+
// 获取所有节点的 rank 值
|
|
10
|
+
const nodeRanks: number[] = graph.nodes.map((node) => {
|
|
11
|
+
const rank: number = node.rank;
|
|
12
|
+
return rank === -1 ? Number.MAX_VALUE : rank;
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
// 找出最小的 rank 值
|
|
16
|
+
const minRank: number = Math.min(...nodeRanks);
|
|
17
|
+
|
|
18
|
+
// 调整所有节点的 rank 值
|
|
19
|
+
graph.nodes.forEach((node) => {
|
|
20
|
+
if (node.rank !== -1) {
|
|
21
|
+
node.rank -= minRank;
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
return graph;
|
|
26
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { WorkflowNodeEntity } from '@flowgram-vue/free-layout-core';
|
|
7
|
+
|
|
8
|
+
export interface LayoutNode {
|
|
9
|
+
id: string;
|
|
10
|
+
node: WorkflowNodeEntity;
|
|
11
|
+
/** 层级 */
|
|
12
|
+
rank: number;
|
|
13
|
+
/** 同层级索引 */
|
|
14
|
+
order: number;
|
|
15
|
+
/** 位置 */
|
|
16
|
+
position: {
|
|
17
|
+
x: number;
|
|
18
|
+
y: number;
|
|
19
|
+
};
|
|
20
|
+
/** 宽高 */
|
|
21
|
+
size: {
|
|
22
|
+
width: number;
|
|
23
|
+
height: number;
|
|
24
|
+
};
|
|
25
|
+
low?: number;
|
|
26
|
+
lim?: number;
|
|
27
|
+
parent?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface LayoutEdge {
|
|
31
|
+
id: string;
|
|
32
|
+
from: string;
|
|
33
|
+
to: string;
|
|
34
|
+
|
|
35
|
+
cutvalue?: number;
|
|
36
|
+
minlen?: number;
|
|
37
|
+
weight?: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ILayoutGraph {
|
|
41
|
+
nodes: LayoutNode[];
|
|
42
|
+
edges: LayoutEdge[];
|
|
43
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
import greedyFAS from './greedy-fas';
|
|
9
|
+
import { uniqueId } from './util';
|
|
10
|
+
|
|
11
|
+
export const acyclic = {
|
|
12
|
+
run,
|
|
13
|
+
undo,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export default acyclic;
|
|
17
|
+
|
|
18
|
+
function run(g) {
|
|
19
|
+
let fas = g.graph().acyclicer === 'greedy' ? greedyFAS(g, weightFn(g)) : dfsFAS(g);
|
|
20
|
+
fas.forEach((e) => {
|
|
21
|
+
let label = g.edge(e);
|
|
22
|
+
g.removeEdge(e);
|
|
23
|
+
label.forwardName = e.name;
|
|
24
|
+
label.reversed = true;
|
|
25
|
+
g.setEdge(e.w, e.v, label, uniqueId('rev'));
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
function weightFn(g) {
|
|
29
|
+
return (e) => {
|
|
30
|
+
return g.edge(e).weight;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function dfsFAS(g) {
|
|
36
|
+
let fas = [];
|
|
37
|
+
let stack = {};
|
|
38
|
+
let visited = {};
|
|
39
|
+
|
|
40
|
+
function dfs(v) {
|
|
41
|
+
if (Object.hasOwn(visited, v)) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
visited[v] = true;
|
|
45
|
+
stack[v] = true;
|
|
46
|
+
g.outEdges(v).forEach((e) => {
|
|
47
|
+
if (Object.hasOwn(stack, e.w)) {
|
|
48
|
+
fas.push(e);
|
|
49
|
+
} else {
|
|
50
|
+
dfs(e.w);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
delete stack[v];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
g.nodes().forEach(dfs);
|
|
57
|
+
return fas;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function undo(g) {
|
|
61
|
+
g.edges().forEach((e) => {
|
|
62
|
+
let label = g.edge(e);
|
|
63
|
+
if (label.reversed) {
|
|
64
|
+
g.removeEdge(e);
|
|
65
|
+
|
|
66
|
+
let forwardName = label.forwardName;
|
|
67
|
+
delete label.reversed;
|
|
68
|
+
delete label.forwardName;
|
|
69
|
+
g.setEdge(e.w, e.v, label, forwardName);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { util } from './util';
|
|
7
|
+
|
|
8
|
+
export { addBorderSegments };
|
|
9
|
+
export default addBorderSegments;
|
|
10
|
+
|
|
11
|
+
function addBorderSegments(g) {
|
|
12
|
+
function dfs(v) {
|
|
13
|
+
let children = g.children(v);
|
|
14
|
+
let node = g.node(v);
|
|
15
|
+
if (children.length) {
|
|
16
|
+
children.forEach(dfs);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (Object.hasOwn(node, 'minRank')) {
|
|
20
|
+
node.borderLeft = [];
|
|
21
|
+
node.borderRight = [];
|
|
22
|
+
for (let rank = node.minRank, maxRank = node.maxRank + 1; rank < maxRank; ++rank) {
|
|
23
|
+
addBorderNode(g, 'borderLeft', '_bl', v, node, rank);
|
|
24
|
+
addBorderNode(g, 'borderRight', '_br', v, node, rank);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
g.children().forEach(dfs);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function addBorderNode(g, prop, prefix, sg, sgNode, rank) {
|
|
33
|
+
let label = { width: 0, height: 0, rank: rank, borderType: prop };
|
|
34
|
+
let prev = sgNode[prop][rank - 1];
|
|
35
|
+
let curr = util.addDummyNode(g, 'border', label, prefix);
|
|
36
|
+
sgNode[prop][rank] = curr;
|
|
37
|
+
g.setParent(curr, sg);
|
|
38
|
+
if (prev) {
|
|
39
|
+
g.setEdge(prev, curr, { weight: 1 });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
export const coordinateSystem = {
|
|
9
|
+
adjust,
|
|
10
|
+
undo,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export default coordinateSystem;
|
|
14
|
+
|
|
15
|
+
function adjust(g) {
|
|
16
|
+
let rankDir = g.graph().rankdir.toLowerCase();
|
|
17
|
+
if (rankDir === 'lr' || rankDir === 'rl') {
|
|
18
|
+
swapWidthHeight(g);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function undo(g) {
|
|
23
|
+
let rankDir = g.graph().rankdir.toLowerCase();
|
|
24
|
+
if (rankDir === 'bt' || rankDir === 'rl') {
|
|
25
|
+
reverseY(g);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (rankDir === 'lr' || rankDir === 'rl') {
|
|
29
|
+
swapXY(g);
|
|
30
|
+
swapWidthHeight(g);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function swapWidthHeight(g) {
|
|
35
|
+
g.nodes().forEach((v) => swapWidthHeightOne(g.node(v)));
|
|
36
|
+
g.edges().forEach((e) => swapWidthHeightOne(g.edge(e)));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function swapWidthHeightOne(attrs) {
|
|
40
|
+
let w = attrs.width;
|
|
41
|
+
attrs.width = attrs.height;
|
|
42
|
+
attrs.height = w;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function reverseY(g) {
|
|
46
|
+
g.nodes().forEach((v) => reverseYOne(g.node(v)));
|
|
47
|
+
|
|
48
|
+
g.edges().forEach((e) => {
|
|
49
|
+
let edge = g.edge(e);
|
|
50
|
+
edge.points.forEach(reverseYOne);
|
|
51
|
+
if (Object.hasOwn(edge, 'y')) {
|
|
52
|
+
reverseYOne(edge);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function reverseYOne(attrs) {
|
|
58
|
+
attrs.y = -attrs.y;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function swapXY(g) {
|
|
62
|
+
g.nodes().forEach((v) => swapXYOne(g.node(v)));
|
|
63
|
+
|
|
64
|
+
g.edges().forEach((e) => {
|
|
65
|
+
let edge = g.edge(e);
|
|
66
|
+
edge.points.forEach(swapXYOne);
|
|
67
|
+
if (Object.hasOwn(edge, 'x')) {
|
|
68
|
+
swapXYOne(edge);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function swapXYOne(attrs) {
|
|
74
|
+
let x = attrs.x;
|
|
75
|
+
attrs.x = attrs.y;
|
|
76
|
+
attrs.y = x;
|
|
77
|
+
}
|