@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.
- package/LICENSE +22 -0
- package/dist/index.cjs +0 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +0 -0
- package/dist/index.js.map +1 -0
- package/dist/typings.cjs +46 -0
- package/dist/typings.cjs.map +1 -0
- package/dist/typings.d.ts +1 -0
- package/dist/typings.js +40 -0
- package/dist/typings.js.map +1 -0
- package/package.json +78 -0
- package/src/composables/index.ts +24 -0
- package/src/composables/typings.ts +97 -0
- package/src/composables/use-current-dom-node.ts +18 -0
- package/src/composables/use-current-entity.ts +15 -0
- package/src/composables/use-node-render-context.ts +18 -0
- package/src/composables/use-node-render.ts +192 -0
- package/src/composables/use-playground-readonly-state.ts +25 -0
- package/src/composables/use-workflow-document.ts +12 -0
- package/src/constants.ts +17 -0
- package/src/entities/index.ts +8 -0
- package/src/entities/workflow-line-entity.ts +640 -0
- package/src/entities/workflow-node-entity.ts +17 -0
- package/src/entities/workflow-port-entity.ts +340 -0
- package/src/entity-datas/index.ts +8 -0
- package/src/entity-datas/workflow-line-render-data.ts +136 -0
- package/src/entity-datas/workflow-node-lines-data.ts +166 -0
- package/src/entity-datas/workflow-node-ports-data.ts +253 -0
- package/src/env.d.ts +10 -0
- package/src/index.ts +18 -0
- package/src/layout/free-layout.ts +166 -0
- package/src/layout/index.ts +6 -0
- package/src/service/index.ts +10 -0
- package/src/service/workflow-drag-service.ts +969 -0
- package/src/service/workflow-hover-service.ts +106 -0
- package/src/service/workflow-operation-base-service.ts +118 -0
- package/src/service/workflow-reset-layout-service.ts +87 -0
- package/src/service/workflow-select-service.ts +128 -0
- package/src/typings/index.ts +19 -0
- package/src/typings/workflow-drag.ts +51 -0
- package/src/typings/workflow-edge.ts +15 -0
- package/src/typings/workflow-json.ts +64 -0
- package/src/typings/workflow-line.ts +71 -0
- package/src/typings/workflow-node.ts +52 -0
- package/src/typings/workflow-operation.ts +40 -0
- package/src/typings/workflow-registry.ts +34 -0
- package/src/typings/workflow-sub-canvas.ts +15 -0
- package/src/utils/build-group-json.ts +47 -0
- package/src/utils/compose.ts +6 -0
- package/src/utils/fit-view.ts +21 -0
- package/src/utils/flow-node-form-data.ts +45 -0
- package/src/utils/get-anti-overlap-position.ts +44 -0
- package/src/utils/get-line-center.ts +22 -0
- package/src/utils/get-url-params.ts +50 -0
- package/src/utils/index.ts +29 -0
- package/src/utils/layout-to-positions.ts +65 -0
- package/src/utils/location-config-to-point.ts +40 -0
- package/src/utils/nanoid.ts +10 -0
- package/src/utils/statics.ts +27 -0
- package/src/vue/index.ts +6 -0
- package/src/vue/node-render-provider.vue +16 -0
- package/src/workflow-commands.ts +14 -0
- package/src/workflow-document-container-module.ts +50 -0
- package/src/workflow-document-contribution.ts +31 -0
- package/src/workflow-document-option.ts +148 -0
- package/src/workflow-document.ts +873 -0
- package/src/workflow-lines-manager.ts +596 -0
|
@@ -0,0 +1,873 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { customAlphabet } from 'nanoid';
|
|
7
|
+
import { inject, injectable, optional, postConstruct } from 'inversify';
|
|
8
|
+
import { Emitter, type IPoint } from '@flowgram-vue/utils';
|
|
9
|
+
import { NodeEngineContext } from '@flowgram-vue/form-core';
|
|
10
|
+
import {
|
|
11
|
+
AddNodeData,
|
|
12
|
+
FlowDocument,
|
|
13
|
+
FlowNodeBaseType,
|
|
14
|
+
FlowNodeTransformData,
|
|
15
|
+
} from '@flowgram-vue/document';
|
|
16
|
+
import {
|
|
17
|
+
injectPlaygroundContext,
|
|
18
|
+
PlaygroundConfigEntity,
|
|
19
|
+
PlaygroundContext,
|
|
20
|
+
PositionData,
|
|
21
|
+
TransformData,
|
|
22
|
+
} from '@flowgram-vue/core';
|
|
23
|
+
|
|
24
|
+
import { WorkflowLinesManager } from './workflow-lines-manager';
|
|
25
|
+
import {
|
|
26
|
+
WorkflowDocumentOptions,
|
|
27
|
+
WorkflowDocumentOptionsDefault,
|
|
28
|
+
} from './workflow-document-option';
|
|
29
|
+
import { getFlowNodeFormData } from './utils/flow-node-form-data';
|
|
30
|
+
import { buildGroupJSON, delay, fitView, getAntiOverlapPosition } from './utils';
|
|
31
|
+
import {
|
|
32
|
+
type WorkflowContentChangeEvent,
|
|
33
|
+
WorkflowContentChangeType,
|
|
34
|
+
WorkflowEdgeJSON,
|
|
35
|
+
type WorkflowJSON,
|
|
36
|
+
type WorkflowNodeJSON,
|
|
37
|
+
type WorkflowNodeMeta,
|
|
38
|
+
type WorkflowNodeRegistry,
|
|
39
|
+
WorkflowSubCanvas,
|
|
40
|
+
} from './typings';
|
|
41
|
+
import { WorkflowSelectService } from './service/workflow-select-service';
|
|
42
|
+
import { FREE_LAYOUT_KEY, type FreeLayout } from './layout';
|
|
43
|
+
import { WorkflowNodeLinesData, WorkflowNodePortsData } from './entity-datas';
|
|
44
|
+
import {
|
|
45
|
+
WorkflowLineEntity,
|
|
46
|
+
WorkflowLinePortInfo,
|
|
47
|
+
WorkflowNodeEntity,
|
|
48
|
+
WorkflowPortEntity,
|
|
49
|
+
} from './entities';
|
|
50
|
+
|
|
51
|
+
const nanoid = customAlphabet('1234567890', 5);
|
|
52
|
+
|
|
53
|
+
export const WorkflowDocumentProvider = Symbol('WorkflowDocumentProvider');
|
|
54
|
+
export type WorkflowDocumentProvider = () => WorkflowDocument;
|
|
55
|
+
|
|
56
|
+
@injectable()
|
|
57
|
+
export class WorkflowDocument extends FlowDocument {
|
|
58
|
+
private _onContentChangeEmitter = new Emitter<WorkflowContentChangeEvent>();
|
|
59
|
+
|
|
60
|
+
protected readonly onLoadedEmitter = new Emitter<void>();
|
|
61
|
+
|
|
62
|
+
readonly onContentChange = this._onContentChangeEmitter.event;
|
|
63
|
+
|
|
64
|
+
private _onReloadEmitter = new Emitter<WorkflowDocument>();
|
|
65
|
+
|
|
66
|
+
readonly onReload = this._onReloadEmitter.event;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 数据加载完成
|
|
70
|
+
*/
|
|
71
|
+
readonly onLoaded = this.onLoadedEmitter.event;
|
|
72
|
+
|
|
73
|
+
protected _loading = false;
|
|
74
|
+
|
|
75
|
+
@inject(WorkflowLinesManager) linesManager: WorkflowLinesManager;
|
|
76
|
+
|
|
77
|
+
@inject(PlaygroundConfigEntity) playgroundConfig: PlaygroundConfigEntity;
|
|
78
|
+
|
|
79
|
+
@injectPlaygroundContext() playgroundContext: PlaygroundContext;
|
|
80
|
+
|
|
81
|
+
@inject(WorkflowDocumentOptions)
|
|
82
|
+
options: WorkflowDocumentOptions = {};
|
|
83
|
+
|
|
84
|
+
@inject(NodeEngineContext) @optional() nodeEngineContext: NodeEngineContext;
|
|
85
|
+
|
|
86
|
+
@inject(WorkflowSelectService) selectServices: WorkflowSelectService;
|
|
87
|
+
|
|
88
|
+
get loading(): boolean {
|
|
89
|
+
return this._loading;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* use `ctx.tools.fitView()` instead
|
|
94
|
+
* @deprecated
|
|
95
|
+
* @param easing
|
|
96
|
+
*/
|
|
97
|
+
async fitView(easing?: boolean): Promise<void> {
|
|
98
|
+
return fitView(this, this.playgroundConfig, easing).then(() => {
|
|
99
|
+
this.linesManager.forceUpdate();
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
@postConstruct()
|
|
104
|
+
init(): void {
|
|
105
|
+
super.init();
|
|
106
|
+
this.currentLayoutKey = this.options.defaultLayout || FREE_LAYOUT_KEY;
|
|
107
|
+
this.linesManager.init(this);
|
|
108
|
+
this.playgroundConfig.getCursors = () => this.options.cursors;
|
|
109
|
+
this.linesManager.onAvailableLinesChange((e) => this.fireContentChange(e));
|
|
110
|
+
this.playgroundConfig.onReadonlyOrDisabledChange(({ readonly }) => {
|
|
111
|
+
if (this.nodeEngineContext) {
|
|
112
|
+
this.nodeEngineContext.readonly = readonly;
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async load(): Promise<void> {
|
|
118
|
+
if (this.disposed) return;
|
|
119
|
+
this._loading = true;
|
|
120
|
+
await super.load();
|
|
121
|
+
this._loading = false;
|
|
122
|
+
this.onLoadedEmitter.fire();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* @deprecated use `ctx.operation.fromJSON` instead
|
|
127
|
+
*/
|
|
128
|
+
async reload(json: WorkflowJSON, delayTime = 0): Promise<void> {
|
|
129
|
+
if (this.disposed) return;
|
|
130
|
+
this._loading = true;
|
|
131
|
+
this.clear();
|
|
132
|
+
this.fromJSON(json);
|
|
133
|
+
// loading添加delay,避免reload时触发fireContentChange的副作用
|
|
134
|
+
await delay(delayTime);
|
|
135
|
+
this._loading = false;
|
|
136
|
+
this._onReloadEmitter.fire(this);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* 从数据加载
|
|
141
|
+
* @param json
|
|
142
|
+
*/
|
|
143
|
+
fromJSON(json: Partial<WorkflowJSON>, fireRender = true): void {
|
|
144
|
+
if (this.disposed) return;
|
|
145
|
+
const workflowJSON: WorkflowJSON = {
|
|
146
|
+
nodes: json.nodes ?? [],
|
|
147
|
+
edges: json.edges ?? [],
|
|
148
|
+
};
|
|
149
|
+
// 触发画布更新
|
|
150
|
+
this.entityManager.changeEntityLocked = true;
|
|
151
|
+
|
|
152
|
+
// 逐层渲染
|
|
153
|
+
this.batchAddFromJSON(workflowJSON);
|
|
154
|
+
|
|
155
|
+
this.entityManager.changeEntityLocked = false;
|
|
156
|
+
this.transformer.loading = false;
|
|
157
|
+
// 批量触发画布更新
|
|
158
|
+
if (fireRender) {
|
|
159
|
+
this.fireRender();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* 清空画布
|
|
165
|
+
*/
|
|
166
|
+
clear(): void {
|
|
167
|
+
this.getAllNodes().map((node) => node.dispose()); // 清空节点
|
|
168
|
+
this.linesManager.getAllLines().map((line) => line.dispose()); // 清空线条
|
|
169
|
+
this.getAllPorts().map((port) => port.dispose()); // 清空端口
|
|
170
|
+
this.selectServices.clear(); // 清空选择
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* 创建流程节点
|
|
175
|
+
* @param json
|
|
176
|
+
*/
|
|
177
|
+
createWorkflowNode(
|
|
178
|
+
json: WorkflowNodeJSON,
|
|
179
|
+
/** @deprecated */
|
|
180
|
+
isClone: boolean = false,
|
|
181
|
+
parentID?: string
|
|
182
|
+
): WorkflowNodeEntity {
|
|
183
|
+
return this._createWorkflowNode(json, { parentID });
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* 创建流程节点
|
|
188
|
+
* @param json
|
|
189
|
+
*/
|
|
190
|
+
private _createWorkflowNode(
|
|
191
|
+
json: WorkflowNodeJSON,
|
|
192
|
+
options?: {
|
|
193
|
+
parentID?: string;
|
|
194
|
+
onNodeCreated?: (node: WorkflowNodeEntity) => void;
|
|
195
|
+
onEdgeCreated?: (edge: WorkflowLineEntity) => void;
|
|
196
|
+
}
|
|
197
|
+
): WorkflowNodeEntity {
|
|
198
|
+
const { parentID, onNodeCreated, onEdgeCreated } = options ?? {};
|
|
199
|
+
// 是否是一个已经存在的节点
|
|
200
|
+
const existedNode = this.getNode(json.id);
|
|
201
|
+
const isExistedNode = existedNode && existedNode.flowNodeType === json.type;
|
|
202
|
+
const parent = this.getNode(parentID ?? this.root.id) ?? this.root;
|
|
203
|
+
const node = this.addNode(
|
|
204
|
+
{
|
|
205
|
+
...json,
|
|
206
|
+
parent,
|
|
207
|
+
},
|
|
208
|
+
undefined,
|
|
209
|
+
true
|
|
210
|
+
) as WorkflowNodeEntity;
|
|
211
|
+
|
|
212
|
+
const registry = node.getNodeRegistry() as WorkflowNodeRegistry;
|
|
213
|
+
const { formMeta } = registry;
|
|
214
|
+
const meta = node.getNodeMeta<WorkflowNodeMeta>();
|
|
215
|
+
const formData = getFlowNodeFormData(node);
|
|
216
|
+
|
|
217
|
+
const transform = node.getData<FlowNodeTransformData>(FlowNodeTransformData)!;
|
|
218
|
+
const freeLayout = this.layout as FreeLayout;
|
|
219
|
+
if (!isExistedNode) {
|
|
220
|
+
transform.onDataChange(() => {
|
|
221
|
+
// TODO 这个有点难以理解,其实是为了同步size 数据
|
|
222
|
+
freeLayout.syncTransform(node);
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
let { position } = meta;
|
|
226
|
+
if (!position) {
|
|
227
|
+
// 获取默认的位置
|
|
228
|
+
position = this.getNodeDefaultPosition(json.type);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// 更新节点位置信息
|
|
232
|
+
node.getData(TransformData)!.update({
|
|
233
|
+
position,
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// 初始化表单数据
|
|
237
|
+
if (formMeta && formData) {
|
|
238
|
+
if (!formData.formModel.initialized) {
|
|
239
|
+
// 如果表单数据在前置步骤(fromJSON)内已定义,则跳过表单初始化逻辑
|
|
240
|
+
formData.createForm(formMeta, json.data);
|
|
241
|
+
|
|
242
|
+
formData.onDataChange(() => {
|
|
243
|
+
this.fireContentChange({
|
|
244
|
+
type: WorkflowContentChangeType.NODE_DATA_CHANGE,
|
|
245
|
+
toJSON: () => formData.toJSON(),
|
|
246
|
+
entity: node,
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
} else {
|
|
250
|
+
formData.updateFormValues(json.data);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
// 位置变更
|
|
254
|
+
const positionData = node.getData<PositionData>(PositionData)!;
|
|
255
|
+
if (!isExistedNode) {
|
|
256
|
+
positionData.onDataChange(() => {
|
|
257
|
+
this.fireContentChange({
|
|
258
|
+
type: WorkflowContentChangeType.MOVE_NODE,
|
|
259
|
+
toJSON: () => positionData.toJSON(),
|
|
260
|
+
entity: node,
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const subCanvas = this.getNodeSubCanvas(node);
|
|
266
|
+
|
|
267
|
+
if (!isExistedNode && !subCanvas?.isCanvas) {
|
|
268
|
+
this.fireContentChange({
|
|
269
|
+
type: WorkflowContentChangeType.ADD_NODE,
|
|
270
|
+
entity: node,
|
|
271
|
+
toJSON: () => this.toNodeJSON(node),
|
|
272
|
+
});
|
|
273
|
+
node.onDispose(() => {
|
|
274
|
+
if (!node.parent || node.parent.flowNodeType === FlowNodeBaseType.ROOT) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const parentTransform = node.parent.getData(FlowNodeTransformData);
|
|
278
|
+
parentTransform.fireChange();
|
|
279
|
+
});
|
|
280
|
+
let lastDeleteNodeData: WorkflowNodeJSON | undefined;
|
|
281
|
+
node.preDispose.onDispose(() => {
|
|
282
|
+
lastDeleteNodeData = this.toNodeJSON(node);
|
|
283
|
+
});
|
|
284
|
+
node.onDispose(() => {
|
|
285
|
+
this.fireContentChange({
|
|
286
|
+
type: WorkflowContentChangeType.DELETE_NODE,
|
|
287
|
+
entity: node,
|
|
288
|
+
toJSON: () => lastDeleteNodeData,
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// 若存在子节点,则创建子节点
|
|
294
|
+
if (json.blocks) {
|
|
295
|
+
this.batchAddFromJSON(
|
|
296
|
+
{ nodes: json.blocks, edges: json.edges ?? [] },
|
|
297
|
+
{
|
|
298
|
+
parent: node,
|
|
299
|
+
onNodeCreated,
|
|
300
|
+
onEdgeCreated,
|
|
301
|
+
}
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
// 子画布联动
|
|
305
|
+
if (subCanvas) {
|
|
306
|
+
const canvasTransform = subCanvas.canvasNode.getData<TransformData>(TransformData);
|
|
307
|
+
canvasTransform.update({
|
|
308
|
+
position: subCanvas.parentNode.getNodeMeta()?.canvasPosition,
|
|
309
|
+
});
|
|
310
|
+
if (!isExistedNode) {
|
|
311
|
+
subCanvas.parentNode.onDispose(() => {
|
|
312
|
+
subCanvas.canvasNode.dispose();
|
|
313
|
+
});
|
|
314
|
+
subCanvas.canvasNode.onDispose(() => {
|
|
315
|
+
subCanvas.parentNode.dispose();
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
if (!isExistedNode) {
|
|
320
|
+
this.onNodeCreateEmitter.fire({
|
|
321
|
+
node,
|
|
322
|
+
data: json,
|
|
323
|
+
json,
|
|
324
|
+
});
|
|
325
|
+
} else {
|
|
326
|
+
this.onNodeUpdateEmitter.fire({
|
|
327
|
+
node,
|
|
328
|
+
data: json,
|
|
329
|
+
json,
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return node;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* 添加节点,如果节点已经存在则不会重复创建
|
|
338
|
+
* @param data
|
|
339
|
+
* @param addedNodes
|
|
340
|
+
*/
|
|
341
|
+
addNode(
|
|
342
|
+
data: AddNodeData,
|
|
343
|
+
addedNodes?: WorkflowNodeEntity[],
|
|
344
|
+
ignoreCreateAndUpdateEvent?: boolean
|
|
345
|
+
): WorkflowNodeEntity {
|
|
346
|
+
const { id, type = 'block', originParent, parent, meta, hidden, index } = data;
|
|
347
|
+
let node = this.getNode(id);
|
|
348
|
+
let isNew = false;
|
|
349
|
+
const register = this.getNodeRegistry(type, data.originParent);
|
|
350
|
+
// node 类型变化则全部删除重新来
|
|
351
|
+
if (node && node.flowNodeType !== data.type) {
|
|
352
|
+
node.dispose();
|
|
353
|
+
node = undefined;
|
|
354
|
+
}
|
|
355
|
+
if (!node) {
|
|
356
|
+
const { dataRegistries } = register;
|
|
357
|
+
node = this.entityManager.createEntity<WorkflowNodeEntity>(WorkflowNodeEntity, {
|
|
358
|
+
id,
|
|
359
|
+
document: this,
|
|
360
|
+
flowNodeType: type,
|
|
361
|
+
originParent,
|
|
362
|
+
meta,
|
|
363
|
+
});
|
|
364
|
+
this.options.preNodeCreate?.(node);
|
|
365
|
+
const datas = dataRegistries
|
|
366
|
+
? this.nodeDataRegistries.concat(...dataRegistries)
|
|
367
|
+
: this.nodeDataRegistries;
|
|
368
|
+
node.addInitializeData(datas);
|
|
369
|
+
node.ports = node.getData(WorkflowNodePortsData);
|
|
370
|
+
node.lines = node.getData(WorkflowNodeLinesData);
|
|
371
|
+
node.onDispose(() => this.onNodeDisposeEmitter.fire({ node: node! }));
|
|
372
|
+
this.options.fromNodeJSON?.(node, data, true);
|
|
373
|
+
isNew = true;
|
|
374
|
+
} else {
|
|
375
|
+
this.options.fromNodeJSON?.(node, data, false);
|
|
376
|
+
}
|
|
377
|
+
// 初始化数据重制
|
|
378
|
+
node.initData({
|
|
379
|
+
originParent,
|
|
380
|
+
parent,
|
|
381
|
+
meta,
|
|
382
|
+
hidden,
|
|
383
|
+
index,
|
|
384
|
+
});
|
|
385
|
+
addedNodes?.push(node);
|
|
386
|
+
// 自定义创建逻辑
|
|
387
|
+
if (register.onCreate) {
|
|
388
|
+
const extendNodes = register.onCreate(node, data);
|
|
389
|
+
if (extendNodes && addedNodes) {
|
|
390
|
+
addedNodes.push(...extendNodes);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (!ignoreCreateAndUpdateEvent) {
|
|
395
|
+
if (isNew) {
|
|
396
|
+
this.onNodeCreateEmitter.fire({
|
|
397
|
+
node,
|
|
398
|
+
data,
|
|
399
|
+
json: data,
|
|
400
|
+
});
|
|
401
|
+
} else {
|
|
402
|
+
this.onNodeUpdateEmitter.fire({ node, data, json: data });
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return node;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
get layout(): FreeLayout {
|
|
410
|
+
const layout = this.layouts.find((layout) => layout.name == this.currentLayoutKey);
|
|
411
|
+
if (!layout) {
|
|
412
|
+
throw new Error(`Unknown flow layout: ${this.currentLayoutKey}`);
|
|
413
|
+
}
|
|
414
|
+
return layout as FreeLayout;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* 获取默认的 x y 坐标, 默认为当前画布可视区域中心
|
|
419
|
+
* @param type
|
|
420
|
+
* @protected
|
|
421
|
+
*/
|
|
422
|
+
getNodeDefaultPosition(type: string | number): IPoint {
|
|
423
|
+
const { size } = this.getNodeRegistry(type).meta || {};
|
|
424
|
+
// 当前可视区域的中心位置
|
|
425
|
+
let position = this.playgroundConfig.getViewport(true).center;
|
|
426
|
+
if (size) {
|
|
427
|
+
position = {
|
|
428
|
+
x: position.x,
|
|
429
|
+
y: position.y - size.height / 2,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
// 去掉叠加的
|
|
433
|
+
return getAntiOverlapPosition(this, position);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* 通过类型创建节点, 如果没有提供position 则直接放在画布中间
|
|
438
|
+
* @param type
|
|
439
|
+
*/
|
|
440
|
+
createWorkflowNodeByType(
|
|
441
|
+
type: string | number,
|
|
442
|
+
position?: IPoint,
|
|
443
|
+
json: Partial<WorkflowNodeJSON> = {},
|
|
444
|
+
parentID?: string
|
|
445
|
+
): WorkflowNodeEntity {
|
|
446
|
+
let id: string = json.id as string;
|
|
447
|
+
if (id === undefined) {
|
|
448
|
+
// 保证 id 不要重复
|
|
449
|
+
do {
|
|
450
|
+
id = `1${nanoid()}`;
|
|
451
|
+
} while (this.entityManager.getEntityById(id));
|
|
452
|
+
} else {
|
|
453
|
+
if (this.entityManager.getEntityById(id)) {
|
|
454
|
+
throw new Error(`[WorkflowDocument.createWorkflowNodeByType] Node Id "${id}" duplicated.`);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
return this._createWorkflowNode(
|
|
458
|
+
{
|
|
459
|
+
...json,
|
|
460
|
+
id,
|
|
461
|
+
type,
|
|
462
|
+
meta: { position, ...json?.meta }, // TODO title 和 meta 要从注册数据去拿
|
|
463
|
+
data: json?.data,
|
|
464
|
+
blocks: json?.blocks,
|
|
465
|
+
edges: json?.edges,
|
|
466
|
+
},
|
|
467
|
+
{ parentID }
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
getAllNodes(): WorkflowNodeEntity[] {
|
|
472
|
+
return this.entityManager
|
|
473
|
+
.getEntities<WorkflowNodeEntity>(WorkflowNodeEntity)
|
|
474
|
+
.filter((n) => n.id !== FlowNodeBaseType.ROOT);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
getAllEdges(): WorkflowLineEntity[] {
|
|
478
|
+
return this.entityManager.getEntities<WorkflowLineEntity>(WorkflowLineEntity);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
getAllPorts(): WorkflowPortEntity[] {
|
|
482
|
+
return this.entityManager
|
|
483
|
+
.getEntities<WorkflowPortEntity>(WorkflowPortEntity)
|
|
484
|
+
.filter((p) => p.node.id !== FlowNodeBaseType.ROOT);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* 获取画布中的非游离节点
|
|
489
|
+
* 1. 开始节点
|
|
490
|
+
* 2. 从开始节点出发能走到的节点
|
|
491
|
+
* 3. 结束节点
|
|
492
|
+
* 4. 默认所有子画布内节点为游离节点
|
|
493
|
+
*/
|
|
494
|
+
getAssociatedNodes(): WorkflowNodeEntity[] {
|
|
495
|
+
const allNode = this.getAllNodes();
|
|
496
|
+
|
|
497
|
+
const allLines = this.linesManager
|
|
498
|
+
.getAllLines()
|
|
499
|
+
.filter((line) => line.from && line.to)
|
|
500
|
+
.map((line) => ({
|
|
501
|
+
from: line.from!.id,
|
|
502
|
+
to: line.to!.id,
|
|
503
|
+
}));
|
|
504
|
+
|
|
505
|
+
const startNodeId = allNode.find((node) => node.isStart)?.id;
|
|
506
|
+
const endNodeId = allNode.find((node) => node.isNodeEnd)?.id;
|
|
507
|
+
|
|
508
|
+
// 子画布内节点无需开始/结束
|
|
509
|
+
const nodeInContainer = allNode
|
|
510
|
+
.filter((node) => node.parent?.getNodeMeta<WorkflowNodeMeta>().isContainer)
|
|
511
|
+
.map((node) => node.id);
|
|
512
|
+
|
|
513
|
+
const associatedCache = new Set(nodeInContainer);
|
|
514
|
+
if (endNodeId) {
|
|
515
|
+
associatedCache.add(endNodeId);
|
|
516
|
+
}
|
|
517
|
+
const bfs = (nodeId: string) => {
|
|
518
|
+
if (associatedCache.has(nodeId)) {
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
associatedCache.add(nodeId);
|
|
522
|
+
const nextNodes = allLines.reduce((ids, { from, to }) => {
|
|
523
|
+
if (from === nodeId && !associatedCache.has(to)) {
|
|
524
|
+
ids.push(to);
|
|
525
|
+
}
|
|
526
|
+
return ids;
|
|
527
|
+
}, [] as string[]);
|
|
528
|
+
|
|
529
|
+
nextNodes.forEach(bfs);
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
if (startNodeId) {
|
|
533
|
+
bfs(startNodeId);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const associatedNodes = allNode.filter((node) => associatedCache.has(node.id));
|
|
537
|
+
|
|
538
|
+
return associatedNodes;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* 触发渲染
|
|
543
|
+
*/
|
|
544
|
+
fireRender() {
|
|
545
|
+
this.entityManager.fireEntityChanged(WorkflowNodeEntity.type);
|
|
546
|
+
this.entityManager.fireEntityChanged(WorkflowLineEntity.type);
|
|
547
|
+
this.entityManager.fireEntityChanged(WorkflowPortEntity.type);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
fireContentChange(event: WorkflowContentChangeEvent): void {
|
|
551
|
+
if (this._loading || this.disposed || this.entityManager.changeEntityLocked) {
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
this._onContentChangeEmitter.fire(event);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
toNodeJSON(node: WorkflowNodeEntity): WorkflowNodeJSON {
|
|
558
|
+
// 如果是子画布,返回其父节点的JSON
|
|
559
|
+
const subCanvas = this.getNodeSubCanvas(node);
|
|
560
|
+
if (subCanvas?.isCanvas === true) {
|
|
561
|
+
return this.toNodeJSON(subCanvas.parentNode);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const json = this.toNodeJSONFromOptions(node);
|
|
565
|
+
const children = this.getNodeChildren(node);
|
|
566
|
+
|
|
567
|
+
// 计算子节点 JSON
|
|
568
|
+
const blocks = children.map((child) => this.toNodeJSON(child));
|
|
569
|
+
|
|
570
|
+
// 计算子线条 JSON
|
|
571
|
+
const linesMap = new Map<string, WorkflowEdgeJSON>();
|
|
572
|
+
children.forEach((child) => {
|
|
573
|
+
const childLinesData = child.getData<WorkflowNodeLinesData>(WorkflowNodeLinesData);
|
|
574
|
+
[...childLinesData.inputLines, ...childLinesData.outputLines]
|
|
575
|
+
.filter(Boolean)
|
|
576
|
+
.forEach((line) => {
|
|
577
|
+
const lineJSON = this.toLineJSON(line);
|
|
578
|
+
if (!lineJSON || linesMap.has(line.id)) {
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
linesMap.set(line.id, lineJSON);
|
|
582
|
+
});
|
|
583
|
+
});
|
|
584
|
+
const edges = Array.from(linesMap.values()); // 使用 Map 防止线条重复
|
|
585
|
+
|
|
586
|
+
// 拼接 JSON
|
|
587
|
+
if (blocks.length > 0) json.blocks = blocks;
|
|
588
|
+
if (edges.length > 0) json.edges = edges;
|
|
589
|
+
|
|
590
|
+
return json;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* 节点转换为JSON, 没有format的过程
|
|
595
|
+
* @param node
|
|
596
|
+
* @returns
|
|
597
|
+
*/
|
|
598
|
+
private toNodeJSONFromOptions(node: WorkflowNodeEntity): WorkflowNodeJSON {
|
|
599
|
+
if (this.options.toNodeJSON) {
|
|
600
|
+
return this.options.toNodeJSON(node) as WorkflowNodeJSON;
|
|
601
|
+
}
|
|
602
|
+
return WorkflowDocumentOptionsDefault.toNodeJSON!(node) as WorkflowNodeJSON;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
copyNode(
|
|
606
|
+
node: WorkflowNodeEntity,
|
|
607
|
+
newNodeId?: string | undefined,
|
|
608
|
+
format?: (json: WorkflowNodeJSON) => WorkflowNodeJSON,
|
|
609
|
+
position?: IPoint
|
|
610
|
+
): WorkflowNodeEntity {
|
|
611
|
+
let json = this.toNodeJSON(node);
|
|
612
|
+
if (format) {
|
|
613
|
+
json = format(json);
|
|
614
|
+
}
|
|
615
|
+
position = position || {
|
|
616
|
+
x: json.meta!.position!.x + 30,
|
|
617
|
+
y: json.meta!.position!.y + 30,
|
|
618
|
+
};
|
|
619
|
+
return this._createWorkflowNode(
|
|
620
|
+
{
|
|
621
|
+
id: newNodeId || `1${nanoid()}`,
|
|
622
|
+
type: node.flowNodeType,
|
|
623
|
+
meta: {
|
|
624
|
+
...json.meta,
|
|
625
|
+
position,
|
|
626
|
+
},
|
|
627
|
+
data: json.data,
|
|
628
|
+
blocks: json.blocks,
|
|
629
|
+
edges: json.edges,
|
|
630
|
+
},
|
|
631
|
+
{
|
|
632
|
+
parentID: node.parent?.id,
|
|
633
|
+
}
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
copyNodeFromJSON(
|
|
638
|
+
flowNodeType: string,
|
|
639
|
+
nodeJSON: WorkflowNodeJSON,
|
|
640
|
+
newNodeId?: string | undefined,
|
|
641
|
+
position?: IPoint,
|
|
642
|
+
parentID?: string
|
|
643
|
+
): WorkflowNodeEntity {
|
|
644
|
+
position = position || {
|
|
645
|
+
x: nodeJSON.meta!.position!.x + 30,
|
|
646
|
+
y: nodeJSON.meta!.position!.y + 30,
|
|
647
|
+
};
|
|
648
|
+
return this._createWorkflowNode(
|
|
649
|
+
{
|
|
650
|
+
id: newNodeId || `1${nanoid()}`,
|
|
651
|
+
type: flowNodeType,
|
|
652
|
+
meta: {
|
|
653
|
+
...nodeJSON.meta,
|
|
654
|
+
position,
|
|
655
|
+
},
|
|
656
|
+
data: nodeJSON.data,
|
|
657
|
+
blocks: nodeJSON.blocks,
|
|
658
|
+
edges: nodeJSON.edges,
|
|
659
|
+
},
|
|
660
|
+
{
|
|
661
|
+
parentID,
|
|
662
|
+
}
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
canRemove(node: WorkflowNodeEntity, silent?: boolean): boolean {
|
|
667
|
+
const meta = node.getNodeMeta<WorkflowNodeMeta>();
|
|
668
|
+
if (meta.deleteDisable) {
|
|
669
|
+
return false;
|
|
670
|
+
}
|
|
671
|
+
if (this.options.canDeleteNode && !this.options.canDeleteNode(node, silent)) {
|
|
672
|
+
return false;
|
|
673
|
+
}
|
|
674
|
+
return true;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* 判断端口是否为错误态
|
|
679
|
+
*/
|
|
680
|
+
isErrorPort(port: WorkflowPortEntity, defaultValue = false) {
|
|
681
|
+
if (typeof this.options.isErrorPort === 'function') {
|
|
682
|
+
return this.options.isErrorPort(port);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
return defaultValue;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* 导出数据
|
|
690
|
+
*/
|
|
691
|
+
toJSON(): WorkflowJSON {
|
|
692
|
+
if (this.disposed) {
|
|
693
|
+
throw new Error(
|
|
694
|
+
'The WorkflowDocument has been disposed and it is no longer possible to call toJSON.'
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
const rootJSON = this.toNodeJSON(this.root);
|
|
698
|
+
const json = {
|
|
699
|
+
nodes: rootJSON.blocks ?? [],
|
|
700
|
+
edges: rootJSON.edges ?? [],
|
|
701
|
+
};
|
|
702
|
+
return json;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
dispose() {
|
|
706
|
+
super.dispose();
|
|
707
|
+
this._onReloadEmitter.dispose();
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* 批量添加节点
|
|
712
|
+
* @deprecated use 'batchAddFromJSON' instead
|
|
713
|
+
* @param json
|
|
714
|
+
* @param options
|
|
715
|
+
*/
|
|
716
|
+
public renderJSON(
|
|
717
|
+
json: WorkflowJSON,
|
|
718
|
+
options?: {
|
|
719
|
+
parent?: WorkflowNodeEntity;
|
|
720
|
+
/** @deprecated useless api */
|
|
721
|
+
isClone?: boolean;
|
|
722
|
+
}
|
|
723
|
+
): {
|
|
724
|
+
nodes: WorkflowNodeEntity[];
|
|
725
|
+
edges: WorkflowLineEntity[];
|
|
726
|
+
} {
|
|
727
|
+
return this.batchAddFromJSON(json, options);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* 批量添加节点
|
|
732
|
+
*/
|
|
733
|
+
public batchAddFromJSON(
|
|
734
|
+
json: WorkflowJSON,
|
|
735
|
+
options?: {
|
|
736
|
+
parent?: WorkflowNodeEntity;
|
|
737
|
+
onNodeCreated?: (node: WorkflowNodeEntity) => void;
|
|
738
|
+
onEdgeCreated?: (edge: WorkflowLineEntity) => void;
|
|
739
|
+
}
|
|
740
|
+
): {
|
|
741
|
+
nodes: WorkflowNodeEntity[];
|
|
742
|
+
edges: WorkflowLineEntity[];
|
|
743
|
+
} {
|
|
744
|
+
const { parent = this.root, onNodeCreated, onEdgeCreated } = options ?? {};
|
|
745
|
+
// 创建节点
|
|
746
|
+
const parentID = this.getNodeSubCanvas(parent)?.canvasNode.id ?? parent.id;
|
|
747
|
+
const processedJSON = buildGroupJSON(json);
|
|
748
|
+
const nodes = processedJSON.nodes.map((nodeJSON: WorkflowNodeJSON) =>
|
|
749
|
+
this._createWorkflowNode(nodeJSON, {
|
|
750
|
+
parentID,
|
|
751
|
+
onNodeCreated,
|
|
752
|
+
onEdgeCreated,
|
|
753
|
+
})
|
|
754
|
+
);
|
|
755
|
+
// 创建线条
|
|
756
|
+
const edges = processedJSON.edges
|
|
757
|
+
.map((edge) => this.createWorkflowLine(edge, parentID))
|
|
758
|
+
.filter(Boolean) as WorkflowLineEntity[];
|
|
759
|
+
// 触发回调
|
|
760
|
+
nodes.forEach((node) => options?.onNodeCreated?.(node));
|
|
761
|
+
edges.forEach((edge) => options?.onEdgeCreated?.(edge));
|
|
762
|
+
return { nodes, edges };
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
private getNodeSubCanvas(node: WorkflowNodeEntity): WorkflowSubCanvas | undefined {
|
|
766
|
+
if (!node) return;
|
|
767
|
+
const nodeMeta = node.getNodeMeta<WorkflowNodeMeta>();
|
|
768
|
+
const subCanvas = nodeMeta.subCanvas?.(node);
|
|
769
|
+
return subCanvas;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
private getNodeChildren(node: WorkflowNodeEntity): WorkflowNodeEntity[] {
|
|
773
|
+
if (!node || node.flowNodeType === FlowNodeBaseType.GROUP) return [];
|
|
774
|
+
const subCanvas = this.getNodeSubCanvas(node);
|
|
775
|
+
// get real children
|
|
776
|
+
const realChildren = subCanvas ? subCanvas.canvasNode.blocks : node.blocks;
|
|
777
|
+
// filter sub canvas node
|
|
778
|
+
const childrenWithoutSubCanvas = realChildren
|
|
779
|
+
.filter((child) => {
|
|
780
|
+
const childMeta = child.getNodeMeta<WorkflowNodeMeta>();
|
|
781
|
+
return !childMeta.subCanvas?.(node)?.isCanvas;
|
|
782
|
+
})
|
|
783
|
+
.filter(Boolean);
|
|
784
|
+
// flat group nodes
|
|
785
|
+
const children = childrenWithoutSubCanvas
|
|
786
|
+
.map((child) => {
|
|
787
|
+
if (child.flowNodeType === FlowNodeBaseType.GROUP) {
|
|
788
|
+
return [child, ...child.blocks];
|
|
789
|
+
}
|
|
790
|
+
return child;
|
|
791
|
+
})
|
|
792
|
+
.flat();
|
|
793
|
+
return children;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
private toLineJSON(line: WorkflowLineEntity): WorkflowEdgeJSON | undefined {
|
|
797
|
+
const lineJSON = line.toJSON();
|
|
798
|
+
if (
|
|
799
|
+
!line.from ||
|
|
800
|
+
!line.info.from ||
|
|
801
|
+
!line.fromPort ||
|
|
802
|
+
!line.to ||
|
|
803
|
+
!line.info.to ||
|
|
804
|
+
!line.toPort
|
|
805
|
+
) {
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
// 父子节点之间连线,需替换子画布为父节点
|
|
809
|
+
const fromSubCanvas = this.getNodeSubCanvas(line.from);
|
|
810
|
+
const toSubCanvas = this.getNodeSubCanvas(line.to);
|
|
811
|
+
if (fromSubCanvas && !fromSubCanvas.isCanvas && toSubCanvas && toSubCanvas.isCanvas) {
|
|
812
|
+
// 忽略子画布与父节点的连线
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
if (line.from === line.to.parent && fromSubCanvas) {
|
|
816
|
+
return {
|
|
817
|
+
...lineJSON,
|
|
818
|
+
sourceNodeID: fromSubCanvas.parentNode.id,
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
if (line.to === line.from.parent && toSubCanvas) {
|
|
822
|
+
return {
|
|
823
|
+
...lineJSON,
|
|
824
|
+
targetNodeID: toSubCanvas.parentNode.id,
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
return lineJSON;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
private createWorkflowLine(
|
|
831
|
+
json: WorkflowEdgeJSON,
|
|
832
|
+
parentID?: string
|
|
833
|
+
): WorkflowLineEntity | undefined {
|
|
834
|
+
const fromNode = this.getNode(json.sourceNodeID);
|
|
835
|
+
const toNode = this.getNode(json.targetNodeID);
|
|
836
|
+
// 脏数据清除
|
|
837
|
+
if (!fromNode || !toNode) {
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
const lineInfo: WorkflowLinePortInfo = {
|
|
841
|
+
from: json.sourceNodeID,
|
|
842
|
+
fromPort: json.sourcePortID,
|
|
843
|
+
to: json.targetNodeID,
|
|
844
|
+
toPort: json.targetPortID,
|
|
845
|
+
data: json.data,
|
|
846
|
+
};
|
|
847
|
+
if (!parentID) {
|
|
848
|
+
return this.linesManager.createLine(lineInfo);
|
|
849
|
+
}
|
|
850
|
+
// 父子节点之间连线,需替换父节点为子画布
|
|
851
|
+
const canvasNode = this.getNode(parentID);
|
|
852
|
+
if (!canvasNode) {
|
|
853
|
+
return this.linesManager.createLine(lineInfo);
|
|
854
|
+
}
|
|
855
|
+
const parentSubCanvas = this.getNodeSubCanvas(canvasNode);
|
|
856
|
+
if (!parentSubCanvas) {
|
|
857
|
+
return this.linesManager.createLine(lineInfo);
|
|
858
|
+
}
|
|
859
|
+
if (lineInfo.from === parentSubCanvas.parentNode.id) {
|
|
860
|
+
return this.linesManager.createLine({
|
|
861
|
+
...lineInfo,
|
|
862
|
+
from: parentSubCanvas.canvasNode.id,
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
if (lineInfo.to === parentSubCanvas.parentNode.id) {
|
|
866
|
+
return this.linesManager.createLine({
|
|
867
|
+
...lineInfo,
|
|
868
|
+
to: parentSubCanvas.canvasNode.id,
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
return this.linesManager.createLine(lineInfo);
|
|
872
|
+
}
|
|
873
|
+
}
|