@logicflow/layout 2.1.0-alpha.2 → 2.1.0-alpha.4

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 (47) hide show
  1. package/.turbo/turbo-build.log +7 -6
  2. package/CHANGELOG.md +15 -0
  3. package/README.md +9 -2
  4. package/dist/index.min.js +1 -1
  5. package/dist/index.min.js.map +1 -1
  6. package/es/{dagre.d.ts → dagre/index.d.ts} +18 -20
  7. package/es/dagre/index.js +126 -0
  8. package/es/dagre/index.js.map +1 -0
  9. package/es/elkLayout/config.d.ts +26 -0
  10. package/es/elkLayout/config.js +27 -0
  11. package/es/elkLayout/config.js.map +1 -0
  12. package/es/elkLayout/index.d.ts +107 -0
  13. package/es/elkLayout/index.js +187 -0
  14. package/es/elkLayout/index.js.map +1 -0
  15. package/es/index.d.ts +1 -0
  16. package/es/index.js +1 -0
  17. package/es/index.js.map +1 -1
  18. package/es/utils/processEdge.d.ts +3 -0
  19. package/es/utils/processEdge.js +479 -0
  20. package/es/utils/processEdge.js.map +1 -0
  21. package/lib/{dagre.d.ts → dagre/index.d.ts} +18 -20
  22. package/lib/dagre/index.js +152 -0
  23. package/lib/dagre/index.js.map +1 -0
  24. package/lib/elkLayout/config.d.ts +26 -0
  25. package/lib/elkLayout/config.js +30 -0
  26. package/lib/elkLayout/config.js.map +1 -0
  27. package/lib/elkLayout/index.d.ts +107 -0
  28. package/lib/elkLayout/index.js +193 -0
  29. package/lib/elkLayout/index.js.map +1 -0
  30. package/lib/index.d.ts +1 -0
  31. package/lib/index.js +1 -0
  32. package/lib/index.js.map +1 -1
  33. package/lib/utils/processEdge.d.ts +3 -0
  34. package/lib/utils/processEdge.js +483 -0
  35. package/lib/utils/processEdge.js.map +1 -0
  36. package/package.json +3 -2
  37. package/src/dagre/index.ts +177 -0
  38. package/src/elkLayout/config.ts +26 -0
  39. package/src/elkLayout/index.ts +255 -0
  40. package/src/index.ts +2 -0
  41. package/src/utils/processEdge.ts +585 -0
  42. package/stats.html +1 -1
  43. package/es/dagre.js +0 -376
  44. package/es/dagre.js.map +0 -1
  45. package/lib/dagre.js +0 -402
  46. package/lib/dagre.js.map +0 -1
  47. package/src/dagre.ts +0 -438
@@ -0,0 +1,177 @@
1
+ /**
2
+ * @fileoverview Dagre布局插件 - 提供自动化图形布局功能
3
+ *
4
+ * 本插件基于dagre.js实现LogicFlow的自动化布局功能,支持多种布局方向
5
+ * 可自动计算节点位置和连线路径,实现整洁的图形展示
6
+ */
7
+ import LogicFlow, { BaseNodeModel, BaseEdgeModel } from '@logicflow/core'
8
+ import dagre, { GraphLabel, graphlib } from 'dagre'
9
+ import { processEdges } from '../utils/processEdge'
10
+
11
+ import NodeConfig = LogicFlow.NodeConfig
12
+ import EdgeConfig = LogicFlow.EdgeConfig
13
+
14
+ type BaseNodeData = {
15
+ x: number
16
+ y: number
17
+ width: number
18
+ height: number
19
+ }
20
+
21
+ /**
22
+ * Dagre布局配置选项接口
23
+ * @interface DagreOption
24
+ * @extends GraphLabel - 继承dagre原生配置
25
+ */
26
+ export interface DagreOption extends GraphLabel {
27
+ /**
28
+ * 是否是默认锚点
29
+ * true: 会根据布局方向自动计算边的路径点
30
+ */
31
+ isDefaultAnchor?: boolean
32
+ }
33
+
34
+ /**
35
+ * Dagre插件接口定义
36
+ */
37
+ export interface DagrePlugin {
38
+ /**
39
+ * 执行布局计算
40
+ * @param option - 布局配置选项
41
+ */
42
+ layout(option: DagreOption): void
43
+ }
44
+
45
+ /**
46
+ * Dagre布局类 - LogicFlow自动布局插件
47
+ * 基于dagre.js提供图的自动布局能力
48
+ */
49
+ export class Dagre {
50
+ /** 插件名称,用于在LogicFlow中注册 */
51
+ static pluginName = 'dagre'
52
+
53
+ /** LogicFlow实例引用 */
54
+ lf: LogicFlow
55
+
56
+ /** 当前布局配置 */
57
+ option: DagreOption // 使用已定义的DagreOption接口替代重复定义
58
+
59
+ /**
60
+ * 插件初始化方法,由LogicFlow自动调用
61
+ * @param lf - LogicFlow实例
62
+ */
63
+ render(lf: LogicFlow) {
64
+ this.lf = lf
65
+ }
66
+
67
+ /**
68
+ * 执行布局算法,重新排列图中的节点和边
69
+ * @param option - 布局配置选项
70
+ */
71
+ layout(option: DagreOption = {}) {
72
+ const { nodes, edges, gridSize } = this.lf.graphModel
73
+
74
+ // 根据网格大小调整节点间距
75
+ let nodesep = 100
76
+ let ranksep = 150
77
+ if (gridSize > 20) {
78
+ nodesep = gridSize * 2
79
+ ranksep = gridSize * 2
80
+ }
81
+
82
+ // 合并默认配置和用户配置
83
+ this.option = {
84
+ // 默认从左到右布局
85
+ rankdir: 'LR',
86
+ // 默认右下角对齐
87
+ align: 'UL',
88
+ // 紧凑树形排名算法
89
+ ranker: 'tight-tree',
90
+ // 层级间距
91
+ ranksep,
92
+ // 同层节点间距
93
+ nodesep,
94
+ // 图的水平边距
95
+ marginx: 120,
96
+ // 图的垂直边距
97
+ marginy: 120,
98
+ // 用户自定义选项覆盖默认值
99
+ ...option,
100
+ }
101
+ this.applyDagreLayout(nodes, edges)
102
+ }
103
+
104
+ /**
105
+ * 使用 Dagre 布局
106
+ * @param nodes - 节点数据
107
+ * @param edges - 边数据
108
+ */
109
+ applyDagreLayout(nodes: BaseNodeModel[], edges: BaseEdgeModel[]) {
110
+ // 创建dagre图实例
111
+ const g = new graphlib.Graph()
112
+ // dagre布局配置
113
+ g.setGraph(this.option)
114
+ //构造dagre布局数据
115
+ g.setDefaultEdgeLabel(() => ({}))
116
+ nodes.forEach((node: BaseNodeModel) => {
117
+ g.setNode(node.id, {
118
+ width: node.width || 150,
119
+ height: node.height || 50,
120
+ id: node.id,
121
+ })
122
+ })
123
+ edges.forEach((edge: BaseEdgeModel) => {
124
+ g.setEdge(edge.sourceNodeId, edge.targetNodeId, {
125
+ id: edge.id,
126
+ })
127
+ })
128
+ // 开始dagre布局
129
+ try {
130
+ dagre.layout(g)
131
+ const newGraphData = this.convertLayoutDataToLf(nodes, edges, g)
132
+ this.lf.renderRawData(newGraphData)
133
+ } catch (error) {
134
+ console.error('Dagre layout error:', error)
135
+ }
136
+ }
137
+ convertLayoutDataToLf(
138
+ nodes: BaseNodeModel[],
139
+ edges: BaseEdgeModel[],
140
+ layoutData: { node: (id: string) => BaseNodeData },
141
+ ) {
142
+ // 存储新的节点和边数据
143
+ const newNodes: NodeConfig[] = []
144
+
145
+ // 更新节点位置
146
+ nodes.forEach((nodeModel) => {
147
+ const lfNode = nodeModel.getData()
148
+ const newNode = layoutData.node(nodeModel.id)
149
+ if (!lfNode || !newNode) {
150
+ throw new Error(`布局错误:找不到ID为 ${nodeModel.id} 的节点`)
151
+ }
152
+ // 更新节点坐标
153
+ lfNode.x = newNode.x
154
+ lfNode.y = newNode.y
155
+
156
+ // 更新节点文本位置
157
+ if (lfNode?.text?.x) {
158
+ lfNode.text.x = newNode.x
159
+ lfNode.text.y = newNode.y
160
+ }
161
+ newNodes.push(lfNode)
162
+ })
163
+
164
+ const newEdges: EdgeConfig[] = processEdges(
165
+ this.lf,
166
+ this.option.rankdir,
167
+ this.option.isDefaultAnchor,
168
+ edges,
169
+ newNodes,
170
+ )
171
+
172
+ return {
173
+ nodes: newNodes,
174
+ edges: newEdges,
175
+ }
176
+ }
177
+ }
@@ -0,0 +1,26 @@
1
+ export const elkOptionMap = {
2
+ rankdir: {
3
+ LR: 'RIGHT',
4
+ TB: 'DOWN',
5
+ BT: 'UP',
6
+ RL: 'LEFT',
7
+ default: 'RIGHT',
8
+ },
9
+ align: {
10
+ UL: 'RIGHTDOWN',
11
+ UR: 'RIGHTUP',
12
+ DL: 'LEFTDOWN',
13
+ DR: 'LEFTUP',
14
+ default: 'BALANCED',
15
+ },
16
+ ranker: {
17
+ 'network-simplex': 'NETWORK_SIMPLEX',
18
+ 'tight-tree': 'NETWORK_SIMPLEX',
19
+ 'longest-path': 'LONGEST_PATH',
20
+ default: 'NETWORK_SIMPLEX',
21
+ },
22
+ acyclicer: {
23
+ greedy: 'GREEDY',
24
+ default: 'DEPTH_FIRST',
25
+ },
26
+ }
@@ -0,0 +1,255 @@
1
+ /**
2
+ * @fileoverview ElkLayout布局插件 - 提供自动化图形布局功能
3
+ *
4
+ * 本插件基于elkjs实现LogicFlow的自动化布局功能,支持多种布局方向
5
+ * 可自动计算节点位置和连线路径,实现整洁的图形展示
6
+ */
7
+ import LogicFlow, { BaseNodeModel, BaseEdgeModel } from '@logicflow/core'
8
+ import elkConstructor from 'elkjs/lib/elk.bundled'
9
+ import { LayoutOptions, ElkNode } from 'elkjs/lib/elk-api'
10
+ import { processEdges } from '../utils/processEdge'
11
+ import { elkOptionMap } from './config'
12
+
13
+ import NodeConfig = LogicFlow.NodeConfig
14
+ import EdgeConfig = LogicFlow.EdgeConfig
15
+
16
+ /**
17
+ * ElkLayout布局配置选项接口
18
+ * @interface ElkLayoutOption
19
+ */
20
+ export interface ElkLayoutOption {
21
+ /**
22
+ * 布局方向
23
+ * 'LR' | 'TB' | 'BT' | 'RL'
24
+ */
25
+ rankdir?: 'LR' | 'TB' | 'BT' | 'RL'
26
+ /**
27
+ * 对齐方式
28
+ * 'UL' | 'UR' | 'DL' | 'DR'
29
+ */
30
+ align?: 'UL' | 'UR' | 'DL' | 'DR'
31
+ /**
32
+ * 同层节点间距
33
+ */
34
+ nodesep?: number
35
+ /**
36
+ * 层级间距
37
+ */
38
+ ranksep?: number
39
+ /**
40
+ * 图的水平边距
41
+ */
42
+ marginx?: number
43
+ /**
44
+ * 图的垂直边距
45
+ */
46
+ marginy?: number
47
+ /**
48
+ * 排版算法
49
+ * 'network-simplex' | 'tight-tree' | 'longest-path'
50
+ */
51
+ ranker?: 'network-simplex' | 'tight-tree' | 'longest-path'
52
+ /**
53
+ * 边间距
54
+ */
55
+ edgesep?: number
56
+ /**
57
+ * 有向无环图处理算法
58
+ * 'greedy'
59
+ */
60
+ acyclicer?: 'greedy'
61
+ /**
62
+ * 是否是默认锚点
63
+ * true: 会根据布局方向自动计算边的路径点
64
+ */
65
+ isDefaultAnchor?: boolean
66
+ /**
67
+ * ELK 原生布局属性,用于覆盖默认配置
68
+ */
69
+ elkOption?: LayoutOptions
70
+ }
71
+
72
+ /**
73
+ * ElkLayout插件接口定义
74
+ */
75
+ export interface ElkLayoutPlugin {
76
+ /**
77
+ * 执行布局计算
78
+ * @param option - 布局配置选项
79
+ */
80
+ layout(option: ElkLayoutOption): void
81
+ }
82
+
83
+ /**
84
+ * ElkLayout布局类 - LogicFlow自动布局插件
85
+ * 基于elkjs提供图的自动布局能力
86
+ */
87
+ export class ElkLayout {
88
+ /** 插件名称,用于在LogicFlow中注册 */
89
+ static pluginName = 'elkLayout'
90
+
91
+ /** LogicFlow实例引用 */
92
+ lf: LogicFlow
93
+
94
+ /** 当前布局配置 */
95
+ option: ElkLayoutOption
96
+
97
+ /**
98
+ * 插件初始化方法,由LogicFlow自动调用
99
+ * @param lf - LogicFlow实例
100
+ */
101
+ render(lf: LogicFlow) {
102
+ this.lf = lf
103
+ }
104
+
105
+ /**
106
+ * 执行布局算法,重新排列图中的节点和边
107
+ * @param option - 布局配置选项
108
+ */
109
+ layout(option: ElkLayoutOption = {}) {
110
+ const { nodes, edges, gridSize } = this.lf.graphModel
111
+
112
+ // 根据网格大小调整节点间距
113
+ let nodesep = 100
114
+ let ranksep = 150
115
+ if (gridSize > 20) {
116
+ nodesep = gridSize * 2
117
+ ranksep = gridSize * 2
118
+ }
119
+
120
+ // 合并默认配置和用户配置
121
+ this.option = {
122
+ // 默认从左到右布局
123
+ rankdir: 'LR',
124
+ // 默认右下角对齐
125
+ align: 'UL',
126
+ // 紧凑树形排名算法
127
+ ranker: 'tight-tree',
128
+ // 层级间距
129
+ ranksep,
130
+ // 同层节点间距
131
+ nodesep,
132
+ // 图的水平边距
133
+ marginx: 120,
134
+ // 图的垂直边距
135
+ marginy: 120,
136
+ // 用户自定义选项覆盖默认值
137
+ ...option,
138
+ }
139
+
140
+ this.applyElkLayout(nodes, edges)
141
+ }
142
+
143
+ /**
144
+ * 使用 ELK 布局
145
+ * @param nodes - 节点数据
146
+ * @param edges - 边数据
147
+ * @param elkOption - ELK 配置选项
148
+ */
149
+ async applyElkLayout(nodes: BaseNodeModel[], edges: BaseEdgeModel[]) {
150
+ // 创建elk实例
151
+ const elk = new elkConstructor()
152
+ // elk布局配置
153
+ const layoutOptions = this.convertOptionsToElk()
154
+ // 构造elk布局数据
155
+ const elkGraph = {
156
+ id: 'root',
157
+ children: nodes.map((node) => ({
158
+ id: node.id,
159
+ width: node.width || 150,
160
+ height: node.height || 50,
161
+ })),
162
+ edges: edges.map((edge) => ({
163
+ id: edge.id,
164
+ sources: [edge.sourceNodeId],
165
+ targets: [edge.targetNodeId],
166
+ })),
167
+ }
168
+ // 开始elk布局
169
+ try {
170
+ const elkLayoutGraph = await elk.layout(elkGraph, { layoutOptions })
171
+ const newGraphData = this.convertLayoutDataToLf(
172
+ nodes,
173
+ edges,
174
+ elkLayoutGraph,
175
+ )
176
+ this.lf.renderRawData(newGraphData)
177
+ } catch (error) {
178
+ console.error('ELK layout error:', error)
179
+ }
180
+ }
181
+ convertOptionsToElk(): LayoutOptions {
182
+ // elk布局配置
183
+ const rankdir = (this.option.rankdir ||
184
+ 'default') as keyof typeof elkOptionMap.rankdir
185
+ const align = (this.option.align ||
186
+ 'default') as keyof typeof elkOptionMap.align
187
+ const ranker = (this.option.ranker ||
188
+ 'default') as keyof typeof elkOptionMap.ranker
189
+ const acyclicer = (this.option.acyclicer ||
190
+ 'default') as keyof typeof elkOptionMap.acyclicer
191
+
192
+ const layoutOptions = {
193
+ 'elk.algorithm': 'layered',
194
+ 'elk.direction':
195
+ elkOptionMap.rankdir[rankdir] || elkOptionMap.rankdir.default,
196
+ 'elk.layered.nodePlacement.bk.fixedAlignment':
197
+ elkOptionMap.align[align] || elkOptionMap.align.default,
198
+ 'elk.layered.layering.strategy':
199
+ elkOptionMap.ranker[ranker] || elkOptionMap.ranker.default,
200
+ 'elk.layered.cycleBreaking.strategy':
201
+ elkOptionMap.acyclicer[acyclicer] || elkOptionMap.acyclicer.default,
202
+ 'elk.padding': `[top=${this.option.marginx || 20}, left=${this.option.marginy || 20}, bottom=${this.option.marginx || 20}, right=${this.option.marginy || 20}]`,
203
+ 'elk.spacing.nodeNode': `${this.option.nodesep || 50}`,
204
+ 'elk.spacing.edgeEdge': `${this.option.edgesep || 10}`,
205
+ 'elk.layered.spacing.nodeNodeBetweenLayers': `${this.option.ranksep || 100}`,
206
+ 'elk.layered.considerModelOrder.strategy': 'NODES_AND_EDGES',
207
+ 'elk.layered.nodePlacement.strategy': 'BRANDES_KOEPF',
208
+ ...this.option.elkOption,
209
+ }
210
+ return layoutOptions
211
+ }
212
+
213
+ convertLayoutDataToLf(
214
+ nodes: BaseNodeModel[],
215
+ edges: BaseEdgeModel[],
216
+ layoutData: ElkNode,
217
+ ) {
218
+ // 存储新的节点和边数据
219
+ const newNodes: NodeConfig[] = []
220
+
221
+ // 更新节点位置
222
+ nodes.forEach((nodeModel) => {
223
+ const lfNode = nodeModel.getData()
224
+ const newNode = (layoutData?.children || []).find(
225
+ (n) => n.id === nodeModel.id,
226
+ )
227
+ if (!lfNode || !newNode || !newNode.x || !newNode.y) {
228
+ throw new Error(`布局错误:找不到ID为 ${nodeModel.id} 的节点`)
229
+ }
230
+ // 更新节点坐标
231
+ lfNode.x = newNode.x + nodeModel.width / 2
232
+ lfNode.y = newNode.y + nodeModel.height / 2
233
+
234
+ // 更新节点文本位置
235
+ if (lfNode?.text?.x) {
236
+ lfNode.text.x = newNode.x + nodeModel.width / 2
237
+ lfNode.text.y = newNode.y + nodeModel.height / 2
238
+ }
239
+ newNodes.push(lfNode)
240
+ })
241
+
242
+ const newEdges: EdgeConfig[] = processEdges(
243
+ this.lf,
244
+ this.option.rankdir,
245
+ this.option.isDefaultAnchor,
246
+ edges,
247
+ newNodes,
248
+ )
249
+
250
+ return {
251
+ nodes: newNodes,
252
+ edges: newEdges,
253
+ }
254
+ }
255
+ }
package/src/index.ts CHANGED
@@ -1 +1,3 @@
1
1
  export * from './dagre'
2
+
3
+ export * from './elkLayout'