@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.
Files changed (58) hide show
  1. package/LICENSE +22 -0
  2. package/dist/index.cjs +2713 -0
  3. package/dist/index.cjs.map +1 -0
  4. package/dist/index.d.ts +377 -0
  5. package/dist/index.js +2705 -0
  6. package/dist/index.js.map +1 -0
  7. package/package.json +57 -0
  8. package/src/create-auto-layout-plugin.ts +23 -0
  9. package/src/dagre-layout/acyclic.ts +129 -0
  10. package/src/dagre-layout/graph.ts +71 -0
  11. package/src/dagre-layout/index.ts +9 -0
  12. package/src/dagre-layout/layout.ts +151 -0
  13. package/src/dagre-layout/order.ts +261 -0
  14. package/src/dagre-layout/rank/feasible-tree.ts +102 -0
  15. package/src/dagre-layout/rank/index.ts +9 -0
  16. package/src/dagre-layout/rank/longest-path.ts +79 -0
  17. package/src/dagre-layout/rank/network-simplex.ts +235 -0
  18. package/src/dagre-layout/rank/normalize-ranks.ts +26 -0
  19. package/src/dagre-layout/type.ts +43 -0
  20. package/src/dagre-lib/acyclic.js +72 -0
  21. package/src/dagre-lib/add-border-segments.js +41 -0
  22. package/src/dagre-lib/coordinate-system.js +77 -0
  23. package/src/dagre-lib/data/list.js +63 -0
  24. package/src/dagre-lib/debug.js +34 -0
  25. package/src/dagre-lib/greedy-fas.js +134 -0
  26. package/src/dagre-lib/index.js +75 -0
  27. package/src/dagre-lib/layout.js +449 -0
  28. package/src/dagre-lib/nesting-graph.js +133 -0
  29. package/src/dagre-lib/normalize.js +98 -0
  30. package/src/dagre-lib/order/add-subgraph-constraints.js +57 -0
  31. package/src/dagre-lib/order/barycenter.js +34 -0
  32. package/src/dagre-lib/order/build-layer-graph.js +80 -0
  33. package/src/dagre-lib/order/cross-count.js +78 -0
  34. package/src/dagre-lib/order/index.js +86 -0
  35. package/src/dagre-lib/order/init-order.js +43 -0
  36. package/src/dagre-lib/order/resolve-conflicts.js +128 -0
  37. package/src/dagre-lib/order/sort-subgraph.js +79 -0
  38. package/src/dagre-lib/order/sort.js +62 -0
  39. package/src/dagre-lib/parent-dummy-chains.js +90 -0
  40. package/src/dagre-lib/position/bk.js +431 -0
  41. package/src/dagre-lib/position/index.js +37 -0
  42. package/src/dagre-lib/rank/feasible-tree.js +104 -0
  43. package/src/dagre-lib/rank/index.js +61 -0
  44. package/src/dagre-lib/rank/network-simplex.js +243 -0
  45. package/src/dagre-lib/rank/util.js +70 -0
  46. package/src/dagre-lib/util.js +365 -0
  47. package/src/dagre-lib/version.js +6 -0
  48. package/src/env.d.ts +10 -0
  49. package/src/index.ts +10 -0
  50. package/src/layout/constant.ts +26 -0
  51. package/src/layout/dagre.ts +245 -0
  52. package/src/layout/index.ts +9 -0
  53. package/src/layout/layout.ts +41 -0
  54. package/src/layout/position.ts +72 -0
  55. package/src/layout/store.ts +262 -0
  56. package/src/layout/type.ts +162 -0
  57. package/src/services.ts +174 -0
  58. package/src/type.ts +10 -0
@@ -0,0 +1,365 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ /* eslint "no-console": off */
7
+
8
+ 'use strict';
9
+
10
+ import { Graph } from '@dagrejs/graphlib';
11
+
12
+ const util = {
13
+ addBorderNode,
14
+ addDummyNode,
15
+ applyWithChunking,
16
+ asNonCompoundGraph,
17
+ buildLayerMatrix,
18
+ intersectRect,
19
+ mapValues,
20
+ maxRank,
21
+ normalizeRanks,
22
+ notime,
23
+ partition,
24
+ pick,
25
+ predecessorWeights,
26
+ range,
27
+ removeEmptyRanks,
28
+ simplify,
29
+ successorWeights,
30
+ time,
31
+ uniqueId,
32
+ zipObject,
33
+ };
34
+
35
+ export {
36
+ util,
37
+ addBorderNode,
38
+ addDummyNode,
39
+ applyWithChunking,
40
+ asNonCompoundGraph,
41
+ buildLayerMatrix,
42
+ intersectRect,
43
+ mapValues,
44
+ maxRank,
45
+ normalizeRanks,
46
+ notime,
47
+ partition,
48
+ pick,
49
+ predecessorWeights,
50
+ range,
51
+ removeEmptyRanks,
52
+ simplify,
53
+ successorWeights,
54
+ time,
55
+ uniqueId,
56
+ zipObject,
57
+ };
58
+
59
+ export default util;
60
+
61
+ /*
62
+ * Adds a dummy node to the graph and return v.
63
+ */
64
+ function addDummyNode(g, type, attrs, name) {
65
+ let v;
66
+ do {
67
+ v = uniqueId(name);
68
+ } while (g.hasNode(v));
69
+
70
+ attrs.dummy = type;
71
+ g.setNode(v, attrs);
72
+ return v;
73
+ }
74
+
75
+ /*
76
+ * Returns a new graph with only simple edges. Handles aggregation of data
77
+ * associated with multi-edges.
78
+ */
79
+ function simplify(g) {
80
+ let simplified = new Graph().setGraph(g.graph());
81
+ g.nodes().forEach((v) => simplified.setNode(v, g.node(v)));
82
+ g.edges().forEach((e) => {
83
+ let simpleLabel = simplified.edge(e.v, e.w) || { weight: 0, minlen: 1 };
84
+ let label = g.edge(e);
85
+ simplified.setEdge(e.v, e.w, {
86
+ weight: simpleLabel.weight + label.weight,
87
+ minlen: Math.max(simpleLabel.minlen, label.minlen),
88
+ });
89
+ });
90
+ return simplified;
91
+ }
92
+
93
+ function asNonCompoundGraph(g) {
94
+ let simplified = new Graph({ multigraph: g.isMultigraph() }).setGraph(g.graph());
95
+ g.nodes().forEach((v) => {
96
+ if (!g.children(v).length) {
97
+ simplified.setNode(v, g.node(v));
98
+ }
99
+ });
100
+ g.edges().forEach((e) => {
101
+ simplified.setEdge(e, g.edge(e));
102
+ });
103
+ return simplified;
104
+ }
105
+
106
+ function successorWeights(g) {
107
+ let weightMap = g.nodes().map((v) => {
108
+ let sucs = {};
109
+ g.outEdges(v).forEach((e) => {
110
+ sucs[e.w] = (sucs[e.w] || 0) + g.edge(e).weight;
111
+ });
112
+ return sucs;
113
+ });
114
+ return zipObject(g.nodes(), weightMap);
115
+ }
116
+
117
+ function predecessorWeights(g) {
118
+ let weightMap = g.nodes().map((v) => {
119
+ let preds = {};
120
+ g.inEdges(v).forEach((e) => {
121
+ preds[e.v] = (preds[e.v] || 0) + g.edge(e).weight;
122
+ });
123
+ return preds;
124
+ });
125
+ return zipObject(g.nodes(), weightMap);
126
+ }
127
+
128
+ /*
129
+ * Finds where a line starting at point ({x, y}) would intersect a rectangle
130
+ * ({x, y, width, height}) if it were pointing at the rectangle's center.
131
+ */
132
+ function intersectRect(rect, point) {
133
+ let x = rect.x;
134
+ let y = rect.y;
135
+
136
+ // Rectangle intersection algorithm from:
137
+ // http://math.stackexchange.com/questions/108113/find-edge-between-two-boxes
138
+ let dx = point.x - x;
139
+ let dy = point.y - y;
140
+ let w = rect.width / 2;
141
+ let h = rect.height / 2;
142
+
143
+ if (!dx && !dy) {
144
+ throw new Error('Not possible to find intersection inside of the rectangle');
145
+ }
146
+
147
+ let sx, sy;
148
+ if (Math.abs(dy) * w > Math.abs(dx) * h) {
149
+ // Intersection is top or bottom of rect.
150
+ if (dy < 0) {
151
+ h = -h;
152
+ }
153
+ sx = (h * dx) / dy;
154
+ sy = h;
155
+ } else {
156
+ // Intersection is left or right of rect.
157
+ if (dx < 0) {
158
+ w = -w;
159
+ }
160
+ sx = w;
161
+ sy = (w * dy) / dx;
162
+ }
163
+
164
+ return { x: x + sx, y: y + sy };
165
+ }
166
+
167
+ /*
168
+ * Given a DAG with each node assigned "rank" and "order" properties, this
169
+ * function will produce a matrix with the ids of each node.
170
+ */
171
+ function buildLayerMatrix(g) {
172
+ let layering = range(maxRank(g) + 1).map(() => []);
173
+ g.nodes().forEach((v) => {
174
+ let node = g.node(v);
175
+ let rank = node.rank;
176
+ if (rank !== undefined) {
177
+ layering[rank][node.order] = v;
178
+ }
179
+ });
180
+ return layering;
181
+ }
182
+
183
+ /*
184
+ * Adjusts the ranks for all nodes in the graph such that all nodes v have
185
+ * rank(v) >= 0 and at least one node w has rank(w) = 0.
186
+ */
187
+ function normalizeRanks(g) {
188
+ let nodeRanks = g.nodes().map((v) => {
189
+ let rank = g.node(v).rank;
190
+ if (rank === undefined) {
191
+ return Number.MAX_VALUE;
192
+ }
193
+
194
+ return rank;
195
+ });
196
+ let min = applyWithChunking(Math.min, nodeRanks);
197
+ g.nodes().forEach((v) => {
198
+ let node = g.node(v);
199
+ if (Object.hasOwn(node, 'rank')) {
200
+ node.rank -= min;
201
+ }
202
+ });
203
+ }
204
+
205
+ function removeEmptyRanks(g) {
206
+ // Ranks may not start at 0, so we need to offset them
207
+ let nodeRanks = g.nodes().map((v) => g.node(v).rank);
208
+ let offset = applyWithChunking(Math.min, nodeRanks);
209
+
210
+ let layers = [];
211
+ g.nodes().forEach((v) => {
212
+ let rank = g.node(v).rank - offset;
213
+ if (!layers[rank]) {
214
+ layers[rank] = [];
215
+ }
216
+ layers[rank].push(v);
217
+ });
218
+
219
+ let delta = 0;
220
+ let nodeRankFactor = g.graph().nodeRankFactor;
221
+ Array.from(layers).forEach((vs, i) => {
222
+ if (vs === undefined && i % nodeRankFactor !== 0) {
223
+ --delta;
224
+ } else if (vs !== undefined && delta) {
225
+ vs.forEach((v) => (g.node(v).rank += delta));
226
+ }
227
+ });
228
+ }
229
+
230
+ function addBorderNode(g, prefix, rank, order) {
231
+ let node = {
232
+ width: 0,
233
+ height: 0,
234
+ };
235
+ if (arguments.length >= 4) {
236
+ node.rank = rank;
237
+ node.order = order;
238
+ }
239
+ return addDummyNode(g, 'border', node, prefix);
240
+ }
241
+
242
+ function splitToChunks(array, chunkSize = CHUNKING_THRESHOLD) {
243
+ const chunks = [];
244
+ for (let i = 0; i < array.length; i += chunkSize) {
245
+ const chunk = array.slice(i, i + chunkSize);
246
+ chunks.push(chunk);
247
+ }
248
+ return chunks;
249
+ }
250
+
251
+ const CHUNKING_THRESHOLD = 65535;
252
+
253
+ function applyWithChunking(fn, argsArray) {
254
+ if (argsArray.length > CHUNKING_THRESHOLD) {
255
+ const chunks = splitToChunks(argsArray);
256
+ return fn.apply(
257
+ null,
258
+ chunks.map((chunk) => fn.apply(null, chunk))
259
+ );
260
+ } else {
261
+ return fn.apply(null, argsArray);
262
+ }
263
+ }
264
+
265
+ function maxRank(g) {
266
+ const nodes = g.nodes();
267
+ const nodeRanks = nodes.map((v) => {
268
+ let rank = g.node(v).rank;
269
+ if (rank === undefined) {
270
+ return Number.MIN_VALUE;
271
+ }
272
+ return rank;
273
+ });
274
+
275
+ return applyWithChunking(Math.max, nodeRanks);
276
+ }
277
+
278
+ /*
279
+ * Partition a collection into two groups: `lhs` and `rhs`. If the supplied
280
+ * function returns true for an entry it goes into `lhs`. Otherwise it goes
281
+ * into `rhs.
282
+ */
283
+ function partition(collection, fn) {
284
+ let result = { lhs: [], rhs: [] };
285
+ collection.forEach((value) => {
286
+ if (fn(value)) {
287
+ result.lhs.push(value);
288
+ } else {
289
+ result.rhs.push(value);
290
+ }
291
+ });
292
+ return result;
293
+ }
294
+
295
+ /*
296
+ * Returns a new function that wraps `fn` with a timer. The wrapper logs the
297
+ * time it takes to execute the function.
298
+ */
299
+ function time(name, fn) {
300
+ let start = Date.now();
301
+ try {
302
+ return fn();
303
+ } finally {
304
+ console.log(name + ' time: ' + (Date.now() - start) + 'ms');
305
+ }
306
+ }
307
+
308
+ function notime(name, fn) {
309
+ return fn();
310
+ }
311
+
312
+ let idCounter = 0;
313
+ function uniqueId(prefix) {
314
+ var id = ++idCounter;
315
+ return toString(prefix) + id;
316
+ }
317
+
318
+ function range(start, limit, step = 1) {
319
+ if (limit == null) {
320
+ limit = start;
321
+ start = 0;
322
+ }
323
+
324
+ let endCon = (i) => i < limit;
325
+ if (step < 0) {
326
+ endCon = (i) => limit < i;
327
+ }
328
+
329
+ const range = [];
330
+ for (let i = start; endCon(i); i += step) {
331
+ range.push(i);
332
+ }
333
+
334
+ return range;
335
+ }
336
+
337
+ function pick(source, keys) {
338
+ const dest = {};
339
+ for (const key of keys) {
340
+ if (source[key] !== undefined) {
341
+ dest[key] = source[key];
342
+ }
343
+ }
344
+
345
+ return dest;
346
+ }
347
+
348
+ function mapValues(obj, funcOrProp) {
349
+ let func = funcOrProp;
350
+ if (typeof funcOrProp === 'string') {
351
+ func = (val) => val[funcOrProp];
352
+ }
353
+
354
+ return Object.entries(obj).reduce((acc, [k, v]) => {
355
+ acc[k] = func(v, k);
356
+ return acc;
357
+ }, {});
358
+ }
359
+
360
+ function zipObject(props, values) {
361
+ return props.reduce((acc, key, i) => {
362
+ acc[key] = values[i];
363
+ return acc;
364
+ }, {});
365
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ export default '1.1.5-pre';
package/src/env.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ declare module '*.vue' {
7
+ import type { DefineComponent } from 'vue';
8
+ const component: DefineComponent<object, object, unknown>;
9
+ export default component;
10
+ }
package/src/index.ts ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ export { dagreLib } from './dagre-lib';
7
+ export { createFreeAutoLayoutPlugin } from './create-auto-layout-plugin';
8
+ export { AutoLayoutService } from './services';
9
+ export { Graph as DagreGraph } from '@dagrejs/graphlib';
10
+ export * from './layout';
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import { LayoutConfig, LayoutOptions } from './type';
7
+
8
+ export const DefaultLayoutConfig: LayoutConfig = {
9
+ rankdir: 'LR',
10
+ align: undefined,
11
+ nodesep: 100,
12
+ edgesep: 10,
13
+ ranksep: 100,
14
+ marginx: 0,
15
+ marginy: 0,
16
+ acyclicer: undefined,
17
+ ranker: 'network-simplex',
18
+ };
19
+
20
+ export const DefaultLayoutOptions: LayoutOptions = {
21
+ filterNode: undefined,
22
+ getFollowNode: undefined,
23
+ disableFitView: false,
24
+ enableAnimation: false,
25
+ animationDuration: 300,
26
+ };
@@ -0,0 +1,245 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import { Graph as DagreGraph } from '@dagrejs/graphlib';
7
+
8
+ import { dagreLib } from '../dagre-lib/index';
9
+ import { DagreNode, LayoutNode } from './type';
10
+ import { LayoutStore } from './store';
11
+
12
+ export class DagreLayout {
13
+ private readonly graph: DagreGraph;
14
+
15
+ constructor(private readonly store: LayoutStore) {
16
+ this.graph = this.createGraph();
17
+ }
18
+
19
+ public layout(): void {
20
+ this.graphSetData();
21
+ this.dagreLayout();
22
+ this.alignTopEdgeIfNeeded();
23
+ this.layoutSetPosition();
24
+ }
25
+
26
+ private dagreLayout(): void {
27
+ let layoutGraph = dagreLib.buildLayoutGraph(this.graph);
28
+ this.runLayout(layoutGraph);
29
+ dagreLib.updateInputGraph(this.graph, layoutGraph);
30
+ }
31
+
32
+ private runLayout(graph: DagreGraph): void {
33
+ dagreLib.makeSpaceForEdgeLabels(graph);
34
+ dagreLib.removeSelfEdges(graph);
35
+ dagreLib.acyclic.run(graph);
36
+ dagreLib.nestingGraph.run(graph);
37
+ dagreLib.rank(dagreLib.util.asNonCompoundGraph(graph));
38
+ dagreLib.injectEdgeLabelProxies(graph);
39
+ dagreLib.removeEmptyRanks(graph);
40
+ dagreLib.nestingGraph.cleanup(graph);
41
+ dagreLib.normalizeRanks(graph);
42
+ dagreLib.assignRankMinMax(graph);
43
+ dagreLib.removeEdgeLabelProxies(graph);
44
+ dagreLib.normalize.run(graph);
45
+ dagreLib.parentDummyChains(graph);
46
+ dagreLib.addBorderSegments(graph);
47
+ dagreLib.order(graph);
48
+ this.setOrderAndRank(graph);
49
+ dagreLib.insertSelfEdges(graph);
50
+ dagreLib.coordinateSystem.adjust(graph);
51
+ dagreLib.position(graph);
52
+ dagreLib.positionSelfEdges(graph);
53
+ dagreLib.removeBorderNodes(graph);
54
+ dagreLib.normalize.undo(graph);
55
+ dagreLib.fixupEdgeLabelCoords(graph);
56
+ dagreLib.coordinateSystem.undo(graph);
57
+ dagreLib.translateGraph(graph);
58
+ dagreLib.assignNodeIntersects(graph);
59
+ dagreLib.reversePointsForReversedEdges(graph);
60
+ dagreLib.acyclic.undo(graph);
61
+ }
62
+
63
+ private createGraph(): DagreGraph {
64
+ const graph = new DagreGraph({ multigraph: true });
65
+ graph.setDefaultEdgeLabel(() => ({}));
66
+ graph.setGraph(this.store.config);
67
+ return graph;
68
+ }
69
+
70
+ private graphSetData(): void {
71
+ const { nodes, edges } = this.store;
72
+ nodes.forEach((layoutNode) => {
73
+ this.graph.setNode(layoutNode.index, {
74
+ originID: layoutNode.id,
75
+ width: layoutNode.size.width,
76
+ height: layoutNode.size.height,
77
+ });
78
+ });
79
+ edges
80
+ .sort((next, prev) => {
81
+ if (next.fromIndex === prev.fromIndex) {
82
+ return next.toIndex! < prev.toIndex! ? -1 : 1;
83
+ }
84
+ return next.fromIndex < prev.fromIndex ? -1 : 1;
85
+ })
86
+ .forEach((layoutEdge) => {
87
+ this.graph.setEdge({
88
+ v: layoutEdge.fromIndex,
89
+ w: layoutEdge.toIndex,
90
+ name: layoutEdge.name,
91
+ });
92
+ });
93
+ }
94
+
95
+ private layoutSetPosition(): void {
96
+ this.store.nodes.forEach((layoutNode) => {
97
+ const offsetX = this.getOffsetX(layoutNode);
98
+ const graphNode = this.graph.node(layoutNode.index);
99
+ if (!graphNode) {
100
+ // 异常兜底,一般不会出现
101
+ layoutNode.rank = -1;
102
+ layoutNode.position = {
103
+ x: layoutNode.position.x + offsetX,
104
+ y: layoutNode.position.y,
105
+ };
106
+ return;
107
+ }
108
+ layoutNode.rank = graphNode.rank ?? -1;
109
+ layoutNode.position = {
110
+ x: this.normalizeNumber(graphNode.x) + offsetX,
111
+ y: this.normalizeNumber(graphNode.y),
112
+ };
113
+ });
114
+ }
115
+
116
+ private alignTopEdgeIfNeeded(): void {
117
+ const { alignTopEdge } = this.store.options;
118
+ const { rankdir, marginy = 0 } = this.store.config;
119
+
120
+ if (!alignTopEdge || (rankdir !== 'LR' && rankdir !== 'RL')) {
121
+ return;
122
+ }
123
+
124
+ const rankGroups = this.rankGroup(this.graph);
125
+
126
+ rankGroups.forEach((indexSet) => {
127
+ const graphNodes = Array.from(indexSet)
128
+ .map((id) => this.graph.node(id) as DagreNode | undefined)
129
+ .filter(Boolean) as DagreNode[];
130
+
131
+ if (graphNodes.length === 0) {
132
+ return;
133
+ }
134
+
135
+ const minTop = Math.min(...graphNodes.map((node) => node.y - node.height / 2));
136
+ const deltaY = marginy - minTop;
137
+
138
+ if (deltaY === 0) {
139
+ return;
140
+ }
141
+
142
+ graphNodes.forEach((node) => {
143
+ node.y += deltaY;
144
+ });
145
+ });
146
+ }
147
+
148
+ private normalizeNumber(number: number): number {
149
+ // NaN 转为 0,异常兜底,一般不会出现
150
+ return Number.isNaN(number) ? 0 : number;
151
+ }
152
+
153
+ private getOffsetX(layoutNode: LayoutNode): number {
154
+ if (layoutNode.layoutNodes.length === 0) {
155
+ return 0;
156
+ }
157
+ // 存在子节点才需计算padding带来的偏移
158
+ const { padding } = layoutNode;
159
+ const leftOffset = -layoutNode.size.width / 2 + padding.left;
160
+ return leftOffset;
161
+ }
162
+
163
+ private setOrderAndRank(g: DagreGraph): DagreGraph {
164
+ // 跟随调整
165
+ this.followAdjust(g);
166
+ // 重新排序
167
+ this.normalizeOrder(g);
168
+ return g;
169
+ }
170
+
171
+ /** 跟随调整 */
172
+ private followAdjust(g: DagreGraph): void {
173
+ const rankGroup = this.rankGroup(g);
174
+ g.nodes().forEach((i) => {
175
+ const graphNode: DagreNode = g.node(i);
176
+ const layoutNode = this.store.getNodeByIndex(i);
177
+
178
+ // 没有跟随节点,则不调整
179
+ if (!graphNode || !layoutNode?.followedBy) return;
180
+ const { followedBy } = layoutNode;
181
+ const { rank: targetRank, order: targetOrder } = graphNode;
182
+
183
+ // 跟随节点索引
184
+ const followIndexes = followedBy
185
+ .map((id) => this.store.getNode(id)?.index)
186
+ .filter(Boolean) as string[];
187
+ const followSet = new Set(followIndexes);
188
+
189
+ // 目标节点之后的节点
190
+ const rankIndexes = rankGroup.get(targetRank);
191
+ if (!rankIndexes) return;
192
+ const afterIndexes = Array.from(rankIndexes).filter((index) => {
193
+ if (followSet.has(index)) return false;
194
+ const graphNode = g.node(index);
195
+ return graphNode.order > targetOrder;
196
+ });
197
+
198
+ // 目标节点之后的节点 order 增加跟随节点数量
199
+ afterIndexes.forEach((index) => {
200
+ const graphNode = g.node(index);
201
+ graphNode.order = graphNode.order + followedBy.length;
202
+ });
203
+
204
+ // 跟随节点 order 增加
205
+ followIndexes.forEach((followIndex, index) => {
206
+ const graphNode = g.node(followIndex);
207
+ graphNode.order = targetOrder + index + 1;
208
+ // 更新 rank 分组缓存
209
+ const originRank = graphNode.rank;
210
+ graphNode.rank = targetRank;
211
+ rankGroup.get(originRank)?.delete(followIndex);
212
+ rankGroup.get(targetRank)?.add(followIndex);
213
+ });
214
+ });
215
+ }
216
+
217
+ /** rank 内 order 可能不连续,需要重新排序 */
218
+ private normalizeOrder(g: DagreGraph): void {
219
+ const rankGroup = this.rankGroup(g);
220
+ rankGroup.forEach((indexSet, rank) => {
221
+ const graphNodes: DagreNode[] = Array.from(indexSet).map((id) => g.node(id));
222
+ graphNodes.sort((a, b) => a.order - b.order);
223
+ graphNodes.forEach((node, index) => {
224
+ node.order = index;
225
+ });
226
+ });
227
+ }
228
+
229
+ /** 获取 rank 分组 */
230
+ private rankGroup(g: DagreGraph): Map<number, Set<string>> {
231
+ const rankGroup = new Map<number, Set<string>>();
232
+ g.nodes().forEach((i) => {
233
+ const graphNode = g.node(i) as DagreNode | undefined;
234
+ if (!graphNode || typeof graphNode.rank !== 'number') {
235
+ return;
236
+ }
237
+ const rank = graphNode.rank;
238
+ if (!rankGroup.has(rank)) {
239
+ rankGroup.set(rank, new Set());
240
+ }
241
+ rankGroup.get(rank)?.add(i);
242
+ });
243
+ return rankGroup;
244
+ }
245
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ export { Layout } from './layout';
7
+ export type { LayoutNode, LayoutEdge, GetFollowNode, LayoutOptions } from './type';
8
+ export type { LayoutStore } from './store';
9
+ export { DefaultLayoutConfig } from './constant';
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import { ILayout, LayoutConfig, LayoutOptions, LayoutParams } from './type';
7
+ import { LayoutStore } from './store';
8
+ import { LayoutPosition } from './position';
9
+ import { DagreLayout } from './dagre';
10
+
11
+ export class Layout implements ILayout {
12
+ private readonly _store: LayoutStore;
13
+
14
+ private readonly _layout: DagreLayout;
15
+
16
+ private readonly _position: LayoutPosition;
17
+
18
+ constructor(config: LayoutConfig) {
19
+ this._store = new LayoutStore(config);
20
+ this._layout = new DagreLayout(this._store);
21
+ this._position = new LayoutPosition(this._store);
22
+ }
23
+
24
+ public init(params: LayoutParams, options: LayoutOptions): void {
25
+ this._store.create(params, options);
26
+ }
27
+
28
+ public layout(): void {
29
+ if (!this._store.initialized) {
30
+ return;
31
+ }
32
+ this._layout.layout();
33
+ }
34
+
35
+ public async position(): Promise<void> {
36
+ if (!this._store.initialized) {
37
+ return;
38
+ }
39
+ return await this._position.position();
40
+ }
41
+ }