@flowgram-vue/free-layout-core 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 (68) hide show
  1. package/LICENSE +22 -0
  2. package/dist/index.cjs +0 -0
  3. package/dist/index.cjs.map +1 -0
  4. package/dist/index.d.ts +1 -0
  5. package/dist/index.js +0 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/typings.cjs +46 -0
  8. package/dist/typings.cjs.map +1 -0
  9. package/dist/typings.d.ts +1 -0
  10. package/dist/typings.js +40 -0
  11. package/dist/typings.js.map +1 -0
  12. package/package.json +78 -0
  13. package/src/composables/index.ts +24 -0
  14. package/src/composables/typings.ts +97 -0
  15. package/src/composables/use-current-dom-node.ts +18 -0
  16. package/src/composables/use-current-entity.ts +15 -0
  17. package/src/composables/use-node-render-context.ts +18 -0
  18. package/src/composables/use-node-render.ts +192 -0
  19. package/src/composables/use-playground-readonly-state.ts +25 -0
  20. package/src/composables/use-workflow-document.ts +12 -0
  21. package/src/constants.ts +17 -0
  22. package/src/entities/index.ts +8 -0
  23. package/src/entities/workflow-line-entity.ts +640 -0
  24. package/src/entities/workflow-node-entity.ts +17 -0
  25. package/src/entities/workflow-port-entity.ts +340 -0
  26. package/src/entity-datas/index.ts +8 -0
  27. package/src/entity-datas/workflow-line-render-data.ts +136 -0
  28. package/src/entity-datas/workflow-node-lines-data.ts +166 -0
  29. package/src/entity-datas/workflow-node-ports-data.ts +253 -0
  30. package/src/env.d.ts +10 -0
  31. package/src/index.ts +18 -0
  32. package/src/layout/free-layout.ts +166 -0
  33. package/src/layout/index.ts +6 -0
  34. package/src/service/index.ts +10 -0
  35. package/src/service/workflow-drag-service.ts +969 -0
  36. package/src/service/workflow-hover-service.ts +106 -0
  37. package/src/service/workflow-operation-base-service.ts +118 -0
  38. package/src/service/workflow-reset-layout-service.ts +87 -0
  39. package/src/service/workflow-select-service.ts +128 -0
  40. package/src/typings/index.ts +19 -0
  41. package/src/typings/workflow-drag.ts +51 -0
  42. package/src/typings/workflow-edge.ts +15 -0
  43. package/src/typings/workflow-json.ts +64 -0
  44. package/src/typings/workflow-line.ts +71 -0
  45. package/src/typings/workflow-node.ts +52 -0
  46. package/src/typings/workflow-operation.ts +40 -0
  47. package/src/typings/workflow-registry.ts +34 -0
  48. package/src/typings/workflow-sub-canvas.ts +15 -0
  49. package/src/utils/build-group-json.ts +47 -0
  50. package/src/utils/compose.ts +6 -0
  51. package/src/utils/fit-view.ts +21 -0
  52. package/src/utils/flow-node-form-data.ts +45 -0
  53. package/src/utils/get-anti-overlap-position.ts +44 -0
  54. package/src/utils/get-line-center.ts +22 -0
  55. package/src/utils/get-url-params.ts +50 -0
  56. package/src/utils/index.ts +29 -0
  57. package/src/utils/layout-to-positions.ts +65 -0
  58. package/src/utils/location-config-to-point.ts +40 -0
  59. package/src/utils/nanoid.ts +10 -0
  60. package/src/utils/statics.ts +27 -0
  61. package/src/vue/index.ts +6 -0
  62. package/src/vue/node-render-provider.vue +16 -0
  63. package/src/workflow-commands.ts +14 -0
  64. package/src/workflow-document-container-module.ts +50 -0
  65. package/src/workflow-document-contribution.ts +31 -0
  66. package/src/workflow-document-option.ts +148 -0
  67. package/src/workflow-document.ts +873 -0
  68. package/src/workflow-lines-manager.ts +596 -0
@@ -0,0 +1,340 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import { type IPoint, Rectangle, Emitter, Compare } from '@flowgram-vue/utils';
7
+ import { FlowNodeTransformData } from '@flowgram-vue/document';
8
+ import { Entity, type EntityOpts, PlaygroundConfigEntity, TransformData } from '@flowgram-vue/core';
9
+
10
+ import { type WorkflowDocument } from '../workflow-document';
11
+ import { type WorkflowPortType, getPortEntityId, domReactToBounds } from '../utils/statics';
12
+ import { locationConfigToPoint } from '../utils/location-config-to-point';
13
+ import { type WorkflowNodeMeta, LinePointLocation, LinePoint } from '../typings';
14
+ import { type WorkflowNodeEntity } from './workflow-node-entity';
15
+ import { type WorkflowLineEntity } from './workflow-line-entity';
16
+
17
+ // port 的宽度
18
+ export const PORT_SIZE = 24;
19
+
20
+ export interface WorkflowPort {
21
+ /**
22
+ * 没有代表 默认连接点,默认 input 类型 为最左边中心,output 类型为最右边中心
23
+ */
24
+ portID?: string | number;
25
+ /**
26
+ * 输入或者输出点
27
+ */
28
+ type: WorkflowPortType;
29
+ /**
30
+ * 端口位置
31
+ */
32
+ location?: LinePointLocation;
33
+ /**
34
+ * 端口位置配置
35
+ * @example
36
+ * // bottom-center
37
+ * {
38
+ * left: '50%',
39
+ * bottom: 0
40
+ * }
41
+ * // right-center
42
+ * {
43
+ * right: 0,
44
+ * top: '50%'
45
+ * }
46
+ */
47
+ locationConfig?: {
48
+ left?: string | number;
49
+ top?: string | number;
50
+ right?: string | number;
51
+ bottom?: string | number;
52
+ };
53
+ /**
54
+ * 相对于 location 的偏移
55
+ */
56
+ offset?: IPoint;
57
+ /**
58
+ * 端口热区大小
59
+ */
60
+ size?: { width: number; height: number };
61
+ /**
62
+ * 禁用端口
63
+ */
64
+ disabled?: boolean;
65
+ /**
66
+ * 将点位渲染到该父节点上
67
+ */
68
+ targetElement?: HTMLElement;
69
+ }
70
+
71
+ export type WorkflowPorts = WorkflowPort[];
72
+
73
+ export interface WorkflowPortEntityOpts extends EntityOpts, WorkflowPort {
74
+ /**
75
+ * port 属于哪个节点
76
+ */
77
+ node: WorkflowNodeEntity;
78
+ }
79
+
80
+ /**
81
+ * Port 抽象的 Entity
82
+ */
83
+ export class WorkflowPortEntity extends Entity<WorkflowPortEntityOpts> {
84
+ static type = 'WorkflowPortEntity';
85
+
86
+ readonly node: WorkflowNodeEntity;
87
+
88
+ readonly portID: string | number = '';
89
+
90
+ readonly portType: WorkflowPortType;
91
+
92
+ private _disabled?: boolean;
93
+
94
+ private _hasError = false;
95
+
96
+ private _location?: LinePointLocation;
97
+
98
+ private _locationConfig?: WorkflowPort['locationConfig'];
99
+
100
+ private _size?: { width: number; height: number };
101
+
102
+ private _offset?: IPoint;
103
+
104
+ protected readonly _onErrorChangedEmitter = new Emitter<void>();
105
+
106
+ onErrorChanged = this._onErrorChangedEmitter.event;
107
+
108
+ targetElement?: HTMLElement;
109
+
110
+ static getPortEntityId(
111
+ node: WorkflowNodeEntity,
112
+ portType: WorkflowPortType,
113
+ portID: string | number = ''
114
+ ): string {
115
+ return getPortEntityId(node, portType, portID);
116
+ }
117
+
118
+ get position(): LinePointLocation | undefined {
119
+ return this._location;
120
+ }
121
+
122
+ constructor(opts: WorkflowPortEntityOpts) {
123
+ super(opts);
124
+ this.portID = opts.portID || '';
125
+ this.portType = opts.type;
126
+ this._disabled = opts.disabled;
127
+ this._offset = opts.offset;
128
+ this._locationConfig = opts.locationConfig;
129
+ this._location = opts.location;
130
+ this._size = opts.size;
131
+ this.node = opts.node;
132
+ this.updateTargetElement(opts.targetElement);
133
+ this.toDispose.push(this.node.getData(TransformData)!.onDataChange(() => this.fireChange()));
134
+ this.toDispose.push(this.node.onDispose(this.dispose.bind(this)));
135
+ }
136
+
137
+ // 获取连线是否为错误态
138
+ get hasError() {
139
+ return this._hasError;
140
+ }
141
+
142
+ // 设置连线的错误态,外部应使用 validate 进行更新
143
+ set hasError(hasError: boolean) {
144
+ if (hasError !== this._hasError) {
145
+ this._hasError = hasError;
146
+ this._onErrorChangedEmitter.fire();
147
+ }
148
+ }
149
+
150
+ validate() {
151
+ // 一个端口可能连接很多线,需要保证所有的连线都不包含错误
152
+ const anyLineHasError = this.allLines.some((line) => {
153
+ // 忽略已销毁和被隐藏的线
154
+ if (line.disposed || line.isHidden) {
155
+ return false;
156
+ }
157
+
158
+ return line.hasError;
159
+ });
160
+ // 如果没有连线错误,需校验端口自身错误
161
+ const isPortHasError = (this.node.document as WorkflowDocument).isErrorPort(this);
162
+ this.hasError = anyLineHasError || isPortHasError;
163
+ }
164
+
165
+ isErrorPort() {
166
+ return (this.node.document as WorkflowDocument).isErrorPort(this, this.hasError);
167
+ }
168
+
169
+ get location(): LinePointLocation {
170
+ if (this._location) {
171
+ return this._location;
172
+ }
173
+ if (this.portType === 'input') {
174
+ return 'left';
175
+ }
176
+ return 'right';
177
+ }
178
+
179
+ get point(): LinePoint {
180
+ const { targetElement, _locationConfig } = this;
181
+ const { bounds } = this.node.getData(FlowNodeTransformData)!;
182
+ const location = this.location;
183
+ if (targetElement) {
184
+ const pos = domReactToBounds(targetElement.getBoundingClientRect()).center;
185
+ const point = this.entityManager
186
+ .getEntity<PlaygroundConfigEntity>(PlaygroundConfigEntity)!
187
+ .getPosFromMouseEvent({
188
+ clientX: pos.x,
189
+ clientY: pos.y,
190
+ });
191
+ return {
192
+ x: point.x,
193
+ y: point.y,
194
+ location,
195
+ };
196
+ }
197
+ if (_locationConfig) {
198
+ return {
199
+ ...locationConfigToPoint(bounds, _locationConfig, this._offset),
200
+ location,
201
+ };
202
+ }
203
+ const offset = this._offset || { x: 0, y: 0 };
204
+ let point = { x: 0, y: 0 };
205
+ switch (location) {
206
+ case 'left':
207
+ point = bounds.leftCenter;
208
+ break;
209
+ case 'top':
210
+ point = bounds.topCenter;
211
+ break;
212
+ case 'right':
213
+ point = bounds.rightCenter;
214
+ break;
215
+ case 'bottom':
216
+ point = bounds.bottomCenter;
217
+ break;
218
+ }
219
+ return {
220
+ x: point.x + offset.x,
221
+ y: point.y + offset.y,
222
+ location,
223
+ };
224
+ }
225
+
226
+ /**
227
+ * 端口热区
228
+ */
229
+ get bounds(): Rectangle {
230
+ const { point } = this;
231
+ const size = this._size || { width: PORT_SIZE, height: PORT_SIZE };
232
+ return new Rectangle(
233
+ point.x - size.width / 2,
234
+ point.y - size.height / 2,
235
+ size.width,
236
+ size.height
237
+ );
238
+ }
239
+
240
+ isHovered(x: number, y: number): boolean {
241
+ return this.bounds.contains(x, y);
242
+ }
243
+
244
+ /**
245
+ * 相对节点左上角的位置
246
+ */
247
+ get relativePosition(): IPoint {
248
+ const { point } = this;
249
+ const { bounds } = this.node.getData(FlowNodeTransformData)!;
250
+ return {
251
+ x: point.x - bounds.x,
252
+ y: point.y - bounds.y,
253
+ };
254
+ }
255
+
256
+ updateTargetElement(el?: HTMLElement): void {
257
+ if (el !== this.targetElement) {
258
+ this.targetElement = el;
259
+ this.fireChange();
260
+ }
261
+ }
262
+
263
+ /**
264
+ * 是否被禁用
265
+ */
266
+ get disabled(): boolean {
267
+ const document = this.node.document as WorkflowDocument;
268
+ if (typeof document.options.isDisabledPort === 'function') {
269
+ return document.options.isDisabledPort(this);
270
+ }
271
+ if (this._disabled) {
272
+ return true;
273
+ }
274
+ const meta = this.node.getNodeMeta<WorkflowNodeMeta>();
275
+ if (this.portType === 'input') {
276
+ return !!meta.inputDisable;
277
+ }
278
+ return !!meta.outputDisable;
279
+ }
280
+
281
+ /**
282
+ * 当前点位上连接的线条
283
+ * @deprecated use `availableLines` instead
284
+ */
285
+ get lines(): WorkflowLineEntity[] {
286
+ return this.allLines.filter((line) => !line.isDrawing);
287
+ }
288
+
289
+ /**
290
+ * 当前有效的线条,不包含正在画的线条和隐藏的线条(这个出现在线条重连会先把原来的线条隐藏)
291
+ */
292
+ get availableLines(): WorkflowLineEntity[] {
293
+ return this.allLines.filter((line) => !line.isDrawing && !line.isHidden);
294
+ }
295
+
296
+ /**
297
+ * 当前点位上连接的线条(包含 isDrawing === true 的线条)
298
+ */
299
+ get allLines(): WorkflowLineEntity[] {
300
+ const document = this.node.document as WorkflowDocument;
301
+ return document.linesManager.getLinesByPortId(this.id);
302
+ }
303
+
304
+ update(data: Exclude<WorkflowPort, 'portID' | 'type'>) {
305
+ let changed = false;
306
+ if (data.targetElement !== this.targetElement) {
307
+ this.targetElement = data.targetElement;
308
+ changed = true;
309
+ }
310
+ if (data.location !== this._location) {
311
+ this._location = data.location;
312
+ changed = true;
313
+ }
314
+ if (Compare.isChanged(data.offset, this._offset)) {
315
+ this._offset = data.offset;
316
+ changed = true;
317
+ }
318
+ if (Compare.isChanged(data.locationConfig, this._locationConfig)) {
319
+ this._locationConfig = data.locationConfig;
320
+ changed = true;
321
+ }
322
+ if (Compare.isChanged(data.size, this._size)) {
323
+ this._size = data.size;
324
+ changed = true;
325
+ }
326
+ if (data.disabled !== this._disabled) {
327
+ this._disabled = data.disabled;
328
+ changed = true;
329
+ }
330
+ if (changed) {
331
+ this.fireChange();
332
+ }
333
+ }
334
+
335
+ dispose(): void {
336
+ // 点位被删除,对应的线条也要删除
337
+ this.lines.forEach((l) => l.dispose());
338
+ super.dispose();
339
+ }
340
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ export * from './workflow-node-ports-data';
7
+ export * from './workflow-node-lines-data';
8
+ export * from './workflow-line-render-data';
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import { IPoint, Rectangle } from '@flowgram-vue/utils';
7
+ import { EntityData } from '@flowgram-vue/core';
8
+
9
+ import {
10
+ LineCenterPoint,
11
+ LinePosition,
12
+ LineRenderType,
13
+ WorkflowLineRenderContribution,
14
+ WorkflowLineRenderContributionFactory,
15
+ } from '../typings';
16
+ import { WorkflowLineEntity } from '../entities';
17
+
18
+ export interface WorkflowLineRenderDataSchema {
19
+ version: string;
20
+ contributions: Map<LineRenderType, WorkflowLineRenderContribution>;
21
+ position: LinePosition;
22
+ }
23
+
24
+ export class WorkflowLineRenderData extends EntityData<WorkflowLineRenderDataSchema> {
25
+ static type = 'WorkflowLineRenderData';
26
+
27
+ declare entity: WorkflowLineEntity;
28
+
29
+ constructor(entity: WorkflowLineEntity) {
30
+ super(entity);
31
+ this.syncContributions();
32
+ }
33
+
34
+ public getDefaultData(): WorkflowLineRenderDataSchema {
35
+ return {
36
+ version: '',
37
+ contributions: new Map(),
38
+ position: {
39
+ from: { x: 0, y: 0, location: 'right' },
40
+ to: { x: 0, y: 0, location: 'left' },
41
+ },
42
+ };
43
+ }
44
+
45
+ public get renderVersion(): string {
46
+ return this.data.version;
47
+ }
48
+
49
+ public get position(): LinePosition {
50
+ return this.data.position;
51
+ }
52
+
53
+ public get path(): string {
54
+ return this.currentLine?.path ?? '';
55
+ }
56
+
57
+ public calcDistance(pos: IPoint): number {
58
+ return this.currentLine?.calcDistance(pos) ?? Number.MAX_SAFE_INTEGER;
59
+ }
60
+
61
+ public get bounds(): Rectangle {
62
+ return this.currentLine?.bounds ?? new Rectangle();
63
+ }
64
+
65
+ /**
66
+ * 更新数据
67
+ * WARNING: 这个方法,必须在 requestAnimationFrame / useLayoutEffect 中调用,否则会引起浏览器强制重排
68
+ */
69
+ public update(): void {
70
+ this.syncContributions();
71
+ const oldVersion = this.data.version;
72
+ this.updatePosition();
73
+ const newVersion = this.data.version;
74
+ if (oldVersion === newVersion) {
75
+ return;
76
+ }
77
+ this.data.version = newVersion;
78
+ this.currentLine?.update({
79
+ fromPos: this.data.position.from,
80
+ toPos: this.data.position.to,
81
+ });
82
+ }
83
+
84
+ private get lineType(): LineRenderType {
85
+ return this.entity.renderType ?? this.entity.linesManager.lineType;
86
+ }
87
+
88
+ /**
89
+ * 获取 center 位置
90
+ */
91
+ get center(): LineCenterPoint {
92
+ return this.currentLine?.center || { x: 0, y: 0, labelX: 0, labelY: 0 };
93
+ }
94
+
95
+ /**
96
+ * 更新版本
97
+ * WARNING: 这个方法,必须在 requestAnimationFrame / useLayoutEffect 中调用,否则会引起浏览器强制重排
98
+ */
99
+ private updatePosition(): void {
100
+ this.data.position.from = this.entity.drawingFrom || this.entity.fromPort!.point;
101
+ this.data.position.to = this.entity.drawingTo || this.entity.toPort!.point;
102
+
103
+ this.data.version = [
104
+ this.lineType,
105
+ this.data.position.from.x,
106
+ this.data.position.from.y,
107
+ this.data.position.from.location,
108
+ this.data.position.to.x,
109
+ this.data.position.to.y,
110
+ this.data.position.to.location,
111
+ this.entity.uiState.shrink, // 这个会影响线条的变化
112
+ this.entity.uiState.curvature,
113
+ ].join('-');
114
+ }
115
+
116
+ private get currentLine(): WorkflowLineRenderContribution | undefined {
117
+ return this.data.contributions.get(this.lineType);
118
+ }
119
+
120
+ private syncContributions(): void {
121
+ if (this.entity.linesManager.contributionFactories.length === this.data.contributions.size) {
122
+ return;
123
+ }
124
+ this.entity.linesManager.contributionFactories.forEach((factory) => {
125
+ this.registerContribution(factory);
126
+ });
127
+ }
128
+
129
+ private registerContribution(contributionFactory: WorkflowLineRenderContributionFactory): void {
130
+ if (this.data.contributions.has(contributionFactory.type)) {
131
+ return;
132
+ }
133
+ const contribution = new contributionFactory(this.entity);
134
+ this.data.contributions.set(contributionFactory.type, contribution);
135
+ }
136
+ }
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import { Disposable } from '@flowgram-vue/utils';
7
+ import { EntityData } from '@flowgram-vue/core';
8
+
9
+ import { type WorkflowLineEntity, type WorkflowNodeEntity } from '../entities';
10
+
11
+ export interface WorkflowNodeLines {
12
+ inputLines: WorkflowLineEntity[];
13
+ outputLines: WorkflowLineEntity[];
14
+ }
15
+
16
+ /**
17
+ * 节点的关联的线条
18
+ */
19
+ export class WorkflowNodeLinesData extends EntityData<WorkflowNodeLines> {
20
+ static type = 'WorkflowNodeLinesData';
21
+
22
+ entity: WorkflowNodeEntity;
23
+
24
+ getDefaultData(): WorkflowNodeLines {
25
+ return {
26
+ inputLines: [],
27
+ outputLines: [],
28
+ };
29
+ }
30
+
31
+ constructor(entity: WorkflowNodeEntity) {
32
+ super(entity);
33
+ this.entity = entity;
34
+ this.entity.preDispose.push(
35
+ Disposable.create(() => {
36
+ this.inputLines.slice().forEach((line) => line.dispose());
37
+ this.outputLines.slice().forEach((line) => line.dispose());
38
+ })
39
+ );
40
+ }
41
+
42
+ /**
43
+ * 输入线条
44
+ */
45
+ get inputLines(): WorkflowLineEntity[] {
46
+ return this.data.inputLines;
47
+ }
48
+
49
+ /**
50
+ * 输出线条
51
+ */
52
+ get outputLines(): WorkflowLineEntity[] {
53
+ return this.data.outputLines;
54
+ }
55
+
56
+ get allLines(): WorkflowLineEntity[] {
57
+ return this.data.inputLines.concat(this.data.outputLines);
58
+ }
59
+
60
+ get availableLines(): WorkflowLineEntity[] {
61
+ return this.allLines.filter((line) => !line.isDrawing && !line.isHidden);
62
+ }
63
+
64
+ /**
65
+ * 输入节点
66
+ */
67
+ get inputNodes(): WorkflowNodeEntity[] {
68
+ return this.inputLines.map((l) => l.from!).filter(Boolean);
69
+ }
70
+
71
+ /**
72
+ * 所有输入节点
73
+ */
74
+ get allInputNodes(): WorkflowNodeEntity[] {
75
+ const nodeSet: Set<WorkflowNodeEntity> = new Set();
76
+
77
+ const handleNode = (node: WorkflowNodeEntity): void => {
78
+ if (nodeSet.has(node)) {
79
+ return;
80
+ }
81
+
82
+ nodeSet.add(node);
83
+
84
+ const { inputNodes } = node.getData<WorkflowNodeLinesData>(WorkflowNodeLinesData)!;
85
+ if (!inputNodes || !inputNodes.length) {
86
+ return;
87
+ }
88
+
89
+ inputNodes.forEach((inputNode: WorkflowNodeEntity) => {
90
+ // 如果 outputNode 和当前 node 是父子节点,则不向下遍历
91
+ if (inputNode?.parent === node || node?.parent === inputNode) {
92
+ return;
93
+ }
94
+ handleNode(inputNode);
95
+ });
96
+ };
97
+
98
+ handleNode(this.entity);
99
+ nodeSet.delete(this.entity);
100
+
101
+ return Array.from(nodeSet);
102
+ }
103
+
104
+ /**
105
+ * 输出节点
106
+ */
107
+ get outputNodes(): WorkflowNodeEntity[] {
108
+ return this.outputLines.map((l) => l.to!).filter(Boolean);
109
+ }
110
+
111
+ /**
112
+ * 输入输出节点
113
+ */
114
+ get allOutputNodes(): WorkflowNodeEntity[] {
115
+ const nodeSet: Set<WorkflowNodeEntity> = new Set();
116
+
117
+ const handleNode = (node: WorkflowNodeEntity): void => {
118
+ if (nodeSet.has(node)) {
119
+ return;
120
+ }
121
+
122
+ nodeSet.add(node);
123
+
124
+ const { outputNodes } = node.getData<WorkflowNodeLinesData>(WorkflowNodeLinesData)!;
125
+ if (!outputNodes || !outputNodes.length) {
126
+ return;
127
+ }
128
+
129
+ outputNodes.forEach((outputNode: WorkflowNodeEntity) => {
130
+ // 如果 outputNode 和当前 node 是父子节点,则不向下遍历
131
+ if (outputNode?.parent === node || node?.parent === outputNode) {
132
+ return;
133
+ }
134
+ handleNode(outputNode);
135
+ });
136
+ };
137
+
138
+ handleNode(this.entity);
139
+ nodeSet.delete(this.entity);
140
+
141
+ return Array.from(nodeSet);
142
+ }
143
+
144
+ addLine(line: WorkflowLineEntity): void {
145
+ if (line.from === this.entity) {
146
+ this.outputLines.push(line);
147
+ } else {
148
+ this.inputLines.push(line);
149
+ }
150
+ this.fireChange();
151
+ }
152
+
153
+ removeLine(line: WorkflowLineEntity): void {
154
+ const { inputLines, outputLines } = this;
155
+ const inputIndex = inputLines.indexOf(line);
156
+ const outputIndex = outputLines.indexOf(line);
157
+ if (inputIndex !== -1) {
158
+ inputLines.splice(inputIndex, 1);
159
+ this.fireChange();
160
+ }
161
+ if (outputIndex !== -1) {
162
+ outputLines.splice(outputIndex, 1);
163
+ this.fireChange();
164
+ }
165
+ }
166
+ }