@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,969 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { nanoid } from 'nanoid';
|
|
7
|
+
import { inject, injectable, postConstruct } from 'inversify';
|
|
8
|
+
import {
|
|
9
|
+
domUtils,
|
|
10
|
+
type IPoint,
|
|
11
|
+
PromiseDeferred,
|
|
12
|
+
Emitter,
|
|
13
|
+
type PositionSchema,
|
|
14
|
+
DisposableCollection,
|
|
15
|
+
Rectangle,
|
|
16
|
+
delay,
|
|
17
|
+
Disposable,
|
|
18
|
+
Point,
|
|
19
|
+
} from '@flowgram-vue/utils';
|
|
20
|
+
import { FlowNodeTransformData, FlowNodeType, type FlowNodeEntity } from '@flowgram-vue/document';
|
|
21
|
+
import { FlowNodeBaseType } from '@flowgram-vue/document';
|
|
22
|
+
import {
|
|
23
|
+
CommandService,
|
|
24
|
+
MouseTouchEvent,
|
|
25
|
+
PlaygroundConfigEntity,
|
|
26
|
+
PlaygroundDrag,
|
|
27
|
+
type PlaygroundDragEvent,
|
|
28
|
+
TransformData,
|
|
29
|
+
} from '@flowgram-vue/core';
|
|
30
|
+
|
|
31
|
+
import { WorkflowLinesManager } from '../workflow-lines-manager';
|
|
32
|
+
import { WorkflowDocumentOptions } from '../workflow-document-option';
|
|
33
|
+
import { WorkflowDocument } from '../workflow-document';
|
|
34
|
+
import { LineEventProps, NodesDragEvent, OnDragLineEnd } from '../typings/workflow-drag';
|
|
35
|
+
import {
|
|
36
|
+
LinePointLocation,
|
|
37
|
+
WorkflowOperationBaseService,
|
|
38
|
+
type WorkflowNodeJSON,
|
|
39
|
+
type WorkflowNodeMeta,
|
|
40
|
+
} from '../typings';
|
|
41
|
+
import {
|
|
42
|
+
type WorkflowLineEntity,
|
|
43
|
+
type WorkflowLinePortInfo,
|
|
44
|
+
type WorkflowNodeEntity,
|
|
45
|
+
type WorkflowPortEntity,
|
|
46
|
+
} from '../entities';
|
|
47
|
+
import { WorkflowSelectService } from './workflow-select-service';
|
|
48
|
+
import { WorkflowHoverService } from './workflow-hover-service';
|
|
49
|
+
import { WorkflowPortType } from '../utils';
|
|
50
|
+
|
|
51
|
+
const DRAG_TIMEOUT = 100;
|
|
52
|
+
const DRAG_MIN_DELTA = 5;
|
|
53
|
+
function checkDragSuccess(
|
|
54
|
+
time: number,
|
|
55
|
+
e: PlaygroundDragEvent,
|
|
56
|
+
originLine?: WorkflowLineEntity
|
|
57
|
+
): boolean {
|
|
58
|
+
if (
|
|
59
|
+
!originLine ||
|
|
60
|
+
time > DRAG_TIMEOUT ||
|
|
61
|
+
Math.abs(e.endPos.x - e.startPos.x) >= DRAG_MIN_DELTA ||
|
|
62
|
+
Math.abs(e.endPos.y - e.startPos.y) >= DRAG_MIN_DELTA
|
|
63
|
+
) {
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function reverseLocation(sourceLocation: LinePointLocation): LinePointLocation {
|
|
70
|
+
switch (sourceLocation) {
|
|
71
|
+
case 'bottom':
|
|
72
|
+
return 'top';
|
|
73
|
+
case 'left':
|
|
74
|
+
return 'right';
|
|
75
|
+
case 'top':
|
|
76
|
+
return 'bottom';
|
|
77
|
+
case 'right':
|
|
78
|
+
return 'left';
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@injectable()
|
|
83
|
+
export class WorkflowDragService {
|
|
84
|
+
@inject(PlaygroundConfigEntity)
|
|
85
|
+
protected playgroundConfig: PlaygroundConfigEntity;
|
|
86
|
+
|
|
87
|
+
@inject(WorkflowHoverService) protected hoverService: WorkflowHoverService;
|
|
88
|
+
|
|
89
|
+
@inject(WorkflowDocument) protected document: WorkflowDocument;
|
|
90
|
+
|
|
91
|
+
@inject(WorkflowLinesManager) protected linesManager: WorkflowLinesManager;
|
|
92
|
+
|
|
93
|
+
@inject(CommandService) protected commandService: CommandService;
|
|
94
|
+
|
|
95
|
+
@inject(WorkflowSelectService) protected selectService: WorkflowSelectService;
|
|
96
|
+
|
|
97
|
+
@inject(WorkflowOperationBaseService)
|
|
98
|
+
protected operationService: WorkflowOperationBaseService;
|
|
99
|
+
|
|
100
|
+
@inject(WorkflowDocumentOptions)
|
|
101
|
+
readonly options: WorkflowDocumentOptions;
|
|
102
|
+
|
|
103
|
+
private _onDragLineEventEmitter = new Emitter<LineEventProps>();
|
|
104
|
+
|
|
105
|
+
readonly onDragLineEventChange = this._onDragLineEventEmitter.event;
|
|
106
|
+
|
|
107
|
+
isDragging = false;
|
|
108
|
+
|
|
109
|
+
private _nodesDragEmitter = new Emitter<NodesDragEvent>();
|
|
110
|
+
|
|
111
|
+
readonly onNodesDrag = this._nodesDragEmitter.event;
|
|
112
|
+
|
|
113
|
+
protected _toDispose = new DisposableCollection();
|
|
114
|
+
|
|
115
|
+
private _droppableTransforms: FlowNodeTransformData[] = [];
|
|
116
|
+
|
|
117
|
+
private _dropNode?: FlowNodeEntity;
|
|
118
|
+
|
|
119
|
+
private posAdjusters: Set<
|
|
120
|
+
(params: { selectedNodes: WorkflowNodeEntity[]; position: IPoint }) => IPoint
|
|
121
|
+
> = new Set();
|
|
122
|
+
|
|
123
|
+
private _onDragLineEndCallbacks: Map<string, OnDragLineEnd> = new Map();
|
|
124
|
+
|
|
125
|
+
@postConstruct()
|
|
126
|
+
init() {
|
|
127
|
+
this._toDispose.pushAll([this._onDragLineEventEmitter, this._nodesDragEmitter]);
|
|
128
|
+
if (this.options.onDragLineEnd) {
|
|
129
|
+
this._toDispose.push(this.onDragLineEnd(this.options.onDragLineEnd));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
dispose() {
|
|
134
|
+
this._toDispose.dispose();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* 拖拽选中节点
|
|
139
|
+
* @param triggerEvent
|
|
140
|
+
*/
|
|
141
|
+
async startDragSelectedNodes(triggerEvent: MouseEvent | TouchEvent): Promise<boolean> {
|
|
142
|
+
let { selectedNodes } = this.selectService;
|
|
143
|
+
if (selectedNodes.length === 0 || this.isDragging) {
|
|
144
|
+
return Promise.resolve(false);
|
|
145
|
+
}
|
|
146
|
+
if (
|
|
147
|
+
!this.document.options.enableReadonlyNodeDragging &&
|
|
148
|
+
(this.playgroundConfig.readonly || this.playgroundConfig.disabled)
|
|
149
|
+
) {
|
|
150
|
+
return Promise.resolve(false);
|
|
151
|
+
}
|
|
152
|
+
this.isDragging = true;
|
|
153
|
+
// 节点整体开始位置
|
|
154
|
+
let startPosition = this.getNodesPosition(selectedNodes);
|
|
155
|
+
// 单个节点开始位置
|
|
156
|
+
let startPositions = selectedNodes.map((node) => {
|
|
157
|
+
const transform = node.getData(TransformData);
|
|
158
|
+
return { x: transform.position.x, y: transform.position.y };
|
|
159
|
+
});
|
|
160
|
+
let dragSuccess = false;
|
|
161
|
+
const startTime = Date.now();
|
|
162
|
+
const dragger = new PlaygroundDrag({
|
|
163
|
+
onDragStart: (dragEvent) => {
|
|
164
|
+
this._nodesDragEmitter.fire({
|
|
165
|
+
type: 'onDragStart',
|
|
166
|
+
nodes: selectedNodes,
|
|
167
|
+
startPositions,
|
|
168
|
+
dragEvent,
|
|
169
|
+
triggerEvent,
|
|
170
|
+
dragger,
|
|
171
|
+
});
|
|
172
|
+
},
|
|
173
|
+
onDrag: (dragEvent) => {
|
|
174
|
+
if (!dragSuccess && checkDragSuccess(Date.now() - startTime, dragEvent)) {
|
|
175
|
+
dragSuccess = true;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// 计算拖拽偏移量
|
|
179
|
+
const offset: IPoint = this.getDragPosOffset({
|
|
180
|
+
event: dragEvent,
|
|
181
|
+
selectedNodes,
|
|
182
|
+
startPosition,
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const positions: PositionSchema[] = [];
|
|
186
|
+
|
|
187
|
+
selectedNodes.forEach((node, index) => {
|
|
188
|
+
const transform = node.getData(TransformData);
|
|
189
|
+
const nodeStartPosition = startPositions[index];
|
|
190
|
+
const newPosition = {
|
|
191
|
+
x: nodeStartPosition.x + offset.x,
|
|
192
|
+
y: nodeStartPosition.y + offset.y,
|
|
193
|
+
};
|
|
194
|
+
transform.update({
|
|
195
|
+
position: newPosition,
|
|
196
|
+
});
|
|
197
|
+
this.document.layout.updateAffectedTransform(node);
|
|
198
|
+
positions.push(newPosition);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
this._nodesDragEmitter.fire({
|
|
202
|
+
type: 'onDragging',
|
|
203
|
+
nodes: selectedNodes,
|
|
204
|
+
startPositions,
|
|
205
|
+
positions,
|
|
206
|
+
dragEvent,
|
|
207
|
+
triggerEvent,
|
|
208
|
+
dragger,
|
|
209
|
+
});
|
|
210
|
+
},
|
|
211
|
+
onDragEnd: (dragEvent) => {
|
|
212
|
+
this.isDragging = false;
|
|
213
|
+
this._nodesDragEmitter.fire({
|
|
214
|
+
type: 'onDragEnd',
|
|
215
|
+
nodes: selectedNodes,
|
|
216
|
+
startPositions,
|
|
217
|
+
dragEvent,
|
|
218
|
+
triggerEvent,
|
|
219
|
+
dragger,
|
|
220
|
+
});
|
|
221
|
+
this.resetContainerInternalPosition(selectedNodes);
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
const { clientX, clientY } = MouseTouchEvent.getEventCoord(triggerEvent);
|
|
225
|
+
return dragger.start(clientX, clientY, this.playgroundConfig)?.then(() => dragSuccess);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* 通过拖入卡片添加
|
|
230
|
+
* @param type
|
|
231
|
+
* @param event
|
|
232
|
+
* @param data 节点数据
|
|
233
|
+
*/
|
|
234
|
+
async dropCard(
|
|
235
|
+
type: string,
|
|
236
|
+
event: { clientX: number; clientY: number },
|
|
237
|
+
data?: Partial<WorkflowNodeJSON>,
|
|
238
|
+
parent?: WorkflowNodeEntity
|
|
239
|
+
): Promise<WorkflowNodeEntity | undefined> {
|
|
240
|
+
const mousePos = this.playgroundConfig.getPosFromMouseEvent(event);
|
|
241
|
+
if (!this.playgroundConfig.getViewport().contains(mousePos.x, mousePos.y)) {
|
|
242
|
+
// 鼠标范围不在画布之内
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
const position = this.adjustSubNodePosition(type, parent, mousePos);
|
|
246
|
+
|
|
247
|
+
const node: WorkflowNodeEntity = await this.document.createWorkflowNodeByType(
|
|
248
|
+
type,
|
|
249
|
+
position,
|
|
250
|
+
data,
|
|
251
|
+
parent?.id
|
|
252
|
+
);
|
|
253
|
+
return node;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* 拖拽卡片到画布
|
|
258
|
+
* 返回创建结果
|
|
259
|
+
* @param type
|
|
260
|
+
* @param event
|
|
261
|
+
*/
|
|
262
|
+
async startDragCard(
|
|
263
|
+
type: string,
|
|
264
|
+
event: MouseEvent,
|
|
265
|
+
data: Partial<WorkflowNodeJSON>,
|
|
266
|
+
cloneNode?: (e: PlaygroundDragEvent) => HTMLDivElement // 创建拖拽的dom
|
|
267
|
+
): Promise<WorkflowNodeEntity | undefined> {
|
|
268
|
+
let domNode: HTMLDivElement;
|
|
269
|
+
let startPos: IPoint = { x: 0, y: 0 };
|
|
270
|
+
const deferred = new PromiseDeferred<WorkflowNodeEntity | undefined>();
|
|
271
|
+
const dragger = new PlaygroundDrag({
|
|
272
|
+
onDragStart: (e) => {
|
|
273
|
+
const targetNode = event.currentTarget as HTMLDivElement;
|
|
274
|
+
domNode = cloneNode ? cloneNode(e) : (targetNode.cloneNode(true) as HTMLDivElement);
|
|
275
|
+
const bounds = targetNode.getBoundingClientRect();
|
|
276
|
+
startPos = { x: bounds.left + window.scrollX, y: bounds.top + window.scrollY };
|
|
277
|
+
domUtils.setStyle(domNode, {
|
|
278
|
+
zIndex: 1000,
|
|
279
|
+
position: 'absolute',
|
|
280
|
+
left: startPos.x,
|
|
281
|
+
top: startPos.y,
|
|
282
|
+
boxShadow: '0 6px 8px 0 rgba(28, 31, 35, .2)',
|
|
283
|
+
});
|
|
284
|
+
document.body.appendChild(domNode);
|
|
285
|
+
this.updateDroppableTransforms();
|
|
286
|
+
},
|
|
287
|
+
onDrag: (e) => {
|
|
288
|
+
const deltaX = e.endPos.x - e.startPos.x;
|
|
289
|
+
const deltaY = e.endPos.y - e.startPos.y;
|
|
290
|
+
const left = startPos.x + deltaX;
|
|
291
|
+
const right = startPos.y + deltaY;
|
|
292
|
+
domNode.style.left = `${left}px`;
|
|
293
|
+
domNode.style.top = `${right}px`;
|
|
294
|
+
// 节点类型拖拽碰撞检测
|
|
295
|
+
const { x, y } = this.playgroundConfig.getPosFromMouseEvent(e);
|
|
296
|
+
const draggingRect = new Rectangle(x, y, 170, 90);
|
|
297
|
+
const collisionTransform = this._droppableTransforms.find((transform) => {
|
|
298
|
+
const { bounds, entity } = transform;
|
|
299
|
+
const padding = this.document.layout.getPadding(entity);
|
|
300
|
+
const transformRect = new Rectangle(
|
|
301
|
+
bounds.x + padding.left + padding.right,
|
|
302
|
+
bounds.y,
|
|
303
|
+
bounds.width,
|
|
304
|
+
bounds.height
|
|
305
|
+
);
|
|
306
|
+
// 检测两个正方形是否相互碰撞
|
|
307
|
+
return Rectangle.intersects(draggingRect, transformRect);
|
|
308
|
+
});
|
|
309
|
+
this.updateDropNode(collisionTransform?.entity);
|
|
310
|
+
},
|
|
311
|
+
onDragEnd: async (e) => {
|
|
312
|
+
const dropNode = this._dropNode;
|
|
313
|
+
const { allowDrop } = this.canDropToNode({
|
|
314
|
+
dragNodeType: type,
|
|
315
|
+
dropNodeType: dropNode?.flowNodeType,
|
|
316
|
+
dropNode,
|
|
317
|
+
});
|
|
318
|
+
const dragNode = allowDrop ? await this.dropCard(type, e, data, dropNode) : undefined;
|
|
319
|
+
this.clearDrop();
|
|
320
|
+
if (dragNode) {
|
|
321
|
+
domNode.remove();
|
|
322
|
+
deferred.resolve(dragNode);
|
|
323
|
+
} else {
|
|
324
|
+
domNode.style.transition = 'all ease .2s';
|
|
325
|
+
domNode.style.left = `${startPos.x}px`;
|
|
326
|
+
domNode.style.top = `${startPos.y}px`;
|
|
327
|
+
const TIMEOUT = 200;
|
|
328
|
+
await delay(TIMEOUT);
|
|
329
|
+
domNode.remove();
|
|
330
|
+
deferred.resolve();
|
|
331
|
+
}
|
|
332
|
+
},
|
|
333
|
+
});
|
|
334
|
+
await dragger.start(event.clientX, event.clientY);
|
|
335
|
+
return deferred.promise;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* 如果存在容器节点,且传入鼠标坐标,需要用容器的坐标减去传入的鼠标坐标
|
|
340
|
+
*/
|
|
341
|
+
public adjustSubNodePosition(
|
|
342
|
+
subNodeType?: string,
|
|
343
|
+
containerNode?: WorkflowNodeEntity,
|
|
344
|
+
mousePos?: IPoint
|
|
345
|
+
): IPoint {
|
|
346
|
+
if (!mousePos) {
|
|
347
|
+
return { x: 0, y: 0 };
|
|
348
|
+
}
|
|
349
|
+
if (!subNodeType || !containerNode || containerNode.flowNodeType === FlowNodeBaseType.ROOT) {
|
|
350
|
+
return mousePos;
|
|
351
|
+
}
|
|
352
|
+
const isParentEmpty = !containerNode.children || containerNode.children.length === 0;
|
|
353
|
+
const parentPadding = this.document.layout.getPadding(containerNode);
|
|
354
|
+
const containerWorldTransform = containerNode.transform.transform.worldTransform;
|
|
355
|
+
if (isParentEmpty) {
|
|
356
|
+
// 确保空容器节点不偏移
|
|
357
|
+
return {
|
|
358
|
+
x: 0,
|
|
359
|
+
y: parentPadding.top,
|
|
360
|
+
};
|
|
361
|
+
} else {
|
|
362
|
+
return {
|
|
363
|
+
x: mousePos.x - containerWorldTransform.tx,
|
|
364
|
+
y: mousePos.y - containerWorldTransform.ty,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* 注册位置调整
|
|
371
|
+
*/
|
|
372
|
+
public registerPosAdjuster(
|
|
373
|
+
adjuster: (params: { selectedNodes: WorkflowNodeEntity[]; position: IPoint }) => IPoint
|
|
374
|
+
) {
|
|
375
|
+
this.posAdjusters.add(adjuster);
|
|
376
|
+
return {
|
|
377
|
+
dispose: () => this.posAdjusters.delete(adjuster),
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* 判断是否可以放置节点
|
|
383
|
+
*/
|
|
384
|
+
|
|
385
|
+
public canDropToNode(params: {
|
|
386
|
+
dragNodeType?: FlowNodeType;
|
|
387
|
+
dragNode?: WorkflowNodeEntity;
|
|
388
|
+
dropNode?: WorkflowNodeEntity;
|
|
389
|
+
dropNodeType?: FlowNodeType;
|
|
390
|
+
}): {
|
|
391
|
+
allowDrop: boolean;
|
|
392
|
+
message?: string;
|
|
393
|
+
dropNode?: WorkflowNodeEntity;
|
|
394
|
+
} {
|
|
395
|
+
const { canDropToNode } = this.document.options;
|
|
396
|
+
const { dragNodeType, dropNode } = params;
|
|
397
|
+
if (canDropToNode) {
|
|
398
|
+
const result = canDropToNode(params);
|
|
399
|
+
if (result) {
|
|
400
|
+
return {
|
|
401
|
+
allowDrop: true,
|
|
402
|
+
dropNode,
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
return {
|
|
406
|
+
allowDrop: false,
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
if (!dragNodeType) {
|
|
410
|
+
return {
|
|
411
|
+
allowDrop: false,
|
|
412
|
+
message: 'Please select a node to drop',
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
return {
|
|
416
|
+
allowDrop: true,
|
|
417
|
+
dropNode,
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* 获取拖拽偏移
|
|
423
|
+
*/
|
|
424
|
+
private getDragPosOffset(params: {
|
|
425
|
+
event: PlaygroundDragEvent;
|
|
426
|
+
selectedNodes: WorkflowNodeEntity[];
|
|
427
|
+
startPosition: IPoint;
|
|
428
|
+
}) {
|
|
429
|
+
const { event, selectedNodes, startPosition } = params;
|
|
430
|
+
const { finalScale } = this.playgroundConfig;
|
|
431
|
+
const mouseOffset: IPoint = {
|
|
432
|
+
x: (event.endPos.x - event.startPos.x) / finalScale,
|
|
433
|
+
y: (event.endPos.y - event.startPos.y) / finalScale,
|
|
434
|
+
};
|
|
435
|
+
const wholePosition: IPoint = {
|
|
436
|
+
x: startPosition.x + mouseOffset.x,
|
|
437
|
+
y: startPosition.y + mouseOffset.y,
|
|
438
|
+
};
|
|
439
|
+
const adjustedOffsets: IPoint[] = Array.from(this.posAdjusters.values()).map((adjuster) =>
|
|
440
|
+
adjuster({
|
|
441
|
+
selectedNodes,
|
|
442
|
+
position: wholePosition,
|
|
443
|
+
})
|
|
444
|
+
);
|
|
445
|
+
const offset: IPoint = adjustedOffsets.reduce(
|
|
446
|
+
(offset, adjustOffset) => ({
|
|
447
|
+
x: offset.x + adjustOffset.x,
|
|
448
|
+
y: offset.y + adjustOffset.y,
|
|
449
|
+
}),
|
|
450
|
+
mouseOffset
|
|
451
|
+
);
|
|
452
|
+
return offset;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
private updateDroppableTransforms() {
|
|
456
|
+
this._droppableTransforms = this.document
|
|
457
|
+
.getRenderDatas(FlowNodeTransformData, false)
|
|
458
|
+
.filter((transform) => {
|
|
459
|
+
const { entity } = transform;
|
|
460
|
+
if (entity.originParent) {
|
|
461
|
+
return this.nodeSelectable(entity) && this.nodeSelectable(entity.originParent);
|
|
462
|
+
}
|
|
463
|
+
return this.nodeSelectable(entity);
|
|
464
|
+
})
|
|
465
|
+
.filter((transform) => this.isContainer(transform.entity));
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/** 是否容器节点 */
|
|
469
|
+
private isContainer(node?: WorkflowNodeEntity): boolean {
|
|
470
|
+
return node?.getNodeMeta<WorkflowNodeMeta>().isContainer ?? false;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* 获取节点整体位置
|
|
475
|
+
*/
|
|
476
|
+
private getNodesPosition(nodes: WorkflowNodeEntity[]): IPoint {
|
|
477
|
+
const selectedBounds = Rectangle.enlarge(
|
|
478
|
+
nodes.map((n) => n.getData(FlowNodeTransformData)!.bounds)
|
|
479
|
+
);
|
|
480
|
+
const position: IPoint = {
|
|
481
|
+
x: selectedBounds.x,
|
|
482
|
+
y: selectedBounds.y,
|
|
483
|
+
};
|
|
484
|
+
return position;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
private nodeSelectable(node: FlowNodeEntity) {
|
|
488
|
+
const selectable = node.getNodeMeta().selectable;
|
|
489
|
+
if (typeof selectable === 'function') {
|
|
490
|
+
return selectable(node);
|
|
491
|
+
} else {
|
|
492
|
+
return selectable;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
private updateDropNode(node?: FlowNodeEntity) {
|
|
497
|
+
if (this._dropNode) {
|
|
498
|
+
if (this._dropNode.id === node?.id) {
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
this.selectService.clear();
|
|
502
|
+
}
|
|
503
|
+
if (node) {
|
|
504
|
+
this.selectService.selectNode(node);
|
|
505
|
+
}
|
|
506
|
+
this._dropNode = node;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
private clearDrop() {
|
|
510
|
+
if (this._dropNode) {
|
|
511
|
+
this.selectService.clear();
|
|
512
|
+
}
|
|
513
|
+
this._dropNode = undefined;
|
|
514
|
+
this._droppableTransforms = [];
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
private setLineColor(line: WorkflowLineEntity, color: string) {
|
|
518
|
+
line.highlightColor = color;
|
|
519
|
+
this.hoverService.clearHovered();
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
private checkDraggingPort(
|
|
523
|
+
isDrawingTo: boolean,
|
|
524
|
+
line: WorkflowLineEntity,
|
|
525
|
+
draggingNode: WorkflowNodeEntity,
|
|
526
|
+
draggingPort?: WorkflowPortEntity,
|
|
527
|
+
originLine?: WorkflowLineEntity
|
|
528
|
+
): {
|
|
529
|
+
hasError: boolean;
|
|
530
|
+
} {
|
|
531
|
+
let successDrawing = false;
|
|
532
|
+
if (isDrawingTo) {
|
|
533
|
+
successDrawing = !!(
|
|
534
|
+
draggingPort &&
|
|
535
|
+
// 同一条线条则不用在判断 canAddLine
|
|
536
|
+
(originLine?.toPort === draggingPort ||
|
|
537
|
+
(draggingPort.portType === 'input' &&
|
|
538
|
+
this.linesManager.canAddLine(line.fromPort!, draggingPort, true)))
|
|
539
|
+
);
|
|
540
|
+
} else {
|
|
541
|
+
successDrawing = !!(
|
|
542
|
+
draggingPort &&
|
|
543
|
+
// 同一条线条则不用在判断 canAddLine
|
|
544
|
+
(originLine?.fromPort === draggingPort ||
|
|
545
|
+
(draggingPort.portType === 'output' &&
|
|
546
|
+
this.linesManager.canAddLine(draggingPort, line.toPort!, true)))
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
if (successDrawing) {
|
|
551
|
+
this.hoverService.updateHoveredKey(draggingPort!.id);
|
|
552
|
+
if (isDrawingTo) {
|
|
553
|
+
line.setToPort(draggingPort!);
|
|
554
|
+
} else {
|
|
555
|
+
line.setFromPort(draggingPort!);
|
|
556
|
+
}
|
|
557
|
+
this._onDragLineEventEmitter.fire({
|
|
558
|
+
type: 'onDrag',
|
|
559
|
+
onDragNodeId: draggingNode.id,
|
|
560
|
+
});
|
|
561
|
+
return {
|
|
562
|
+
hasError: false,
|
|
563
|
+
};
|
|
564
|
+
} else if (this.isContainer(draggingNode)) {
|
|
565
|
+
// 在容器内进行连线的情况,需忽略
|
|
566
|
+
return {
|
|
567
|
+
hasError: false,
|
|
568
|
+
};
|
|
569
|
+
} else {
|
|
570
|
+
this.setLineColor(line, this.linesManager.lineColor.error);
|
|
571
|
+
return {
|
|
572
|
+
hasError: true,
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* 容器内子节点总体位置重置为0
|
|
579
|
+
*/
|
|
580
|
+
private resetContainerInternalPosition(nodes: WorkflowNodeEntity[]) {
|
|
581
|
+
const container = this.childrenOfContainer(nodes);
|
|
582
|
+
if (!container) {
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
const bounds: Rectangle = Rectangle.enlarge(
|
|
586
|
+
container.blocks.map((node) => {
|
|
587
|
+
const x = node.transform.position.x - node.transform.bounds.width / 2;
|
|
588
|
+
const y = node.transform.position.y;
|
|
589
|
+
const width = node.transform.bounds.width;
|
|
590
|
+
const height = node.transform.bounds.height;
|
|
591
|
+
return new Rectangle(x, y, width, height);
|
|
592
|
+
})
|
|
593
|
+
);
|
|
594
|
+
const containerTransform = container.getData(TransformData);
|
|
595
|
+
this.operationService.updateNodePosition(container, {
|
|
596
|
+
x: containerTransform.position.x + bounds.x,
|
|
597
|
+
y: containerTransform.position.y + bounds.y,
|
|
598
|
+
});
|
|
599
|
+
this.document.layout.updateAffectedTransform(container);
|
|
600
|
+
container.blocks.forEach((node) => {
|
|
601
|
+
const transform = node.getData(TransformData);
|
|
602
|
+
this.operationService.updateNodePosition(node, {
|
|
603
|
+
x: transform.position.x - bounds.x,
|
|
604
|
+
y: transform.position.y - bounds.y,
|
|
605
|
+
});
|
|
606
|
+
this.document.layout.updateAffectedTransform(node);
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
private childrenOfContainer(nodes: WorkflowNodeEntity[]): WorkflowNodeEntity | undefined {
|
|
611
|
+
if (nodes.length === 0) {
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
const sourceContainer = nodes[0]?.parent;
|
|
615
|
+
if (!sourceContainer || sourceContainer.flowNodeType === FlowNodeBaseType.ROOT) {
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
const valid = nodes.every((node) => node?.parent === sourceContainer);
|
|
619
|
+
if (!valid) {
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
return sourceContainer;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* 绘制线条
|
|
627
|
+
* @param opts
|
|
628
|
+
* @param event
|
|
629
|
+
*/
|
|
630
|
+
async startDrawingLine(
|
|
631
|
+
port: WorkflowPortEntity,
|
|
632
|
+
event: { clientX: number; clientY: number },
|
|
633
|
+
originLine?: WorkflowLineEntity
|
|
634
|
+
): Promise<{
|
|
635
|
+
dragSuccess?: boolean; // 是否拖拽成功,不成功则为选择节点
|
|
636
|
+
newLine?: WorkflowLineEntity; // 新的线条
|
|
637
|
+
}> {
|
|
638
|
+
const isDrawingTo = port.portType === 'output';
|
|
639
|
+
const isInActivePort = !originLine && port.isErrorPort() && port.disabled;
|
|
640
|
+
if (
|
|
641
|
+
originLine?.disabled ||
|
|
642
|
+
isInActivePort ||
|
|
643
|
+
this.playgroundConfig.readonly ||
|
|
644
|
+
this.playgroundConfig.disabled
|
|
645
|
+
) {
|
|
646
|
+
return { dragSuccess: false, newLine: undefined };
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
this.selectService.clear();
|
|
650
|
+
const config = this.playgroundConfig;
|
|
651
|
+
const deferred = new PromiseDeferred<{
|
|
652
|
+
dragSuccess?: boolean;
|
|
653
|
+
newLine?: WorkflowLineEntity; // 新的线条
|
|
654
|
+
}>();
|
|
655
|
+
const preCursor = config.cursor;
|
|
656
|
+
let line: WorkflowLineEntity | undefined;
|
|
657
|
+
let newLineInfo: {
|
|
658
|
+
fromPort?: WorkflowPortEntity;
|
|
659
|
+
toPort?: WorkflowPortEntity;
|
|
660
|
+
hasError: boolean;
|
|
661
|
+
};
|
|
662
|
+
const startTime = Date.now();
|
|
663
|
+
let dragSuccess = false;
|
|
664
|
+
const dragger = new PlaygroundDrag({
|
|
665
|
+
onDrag: (e) => {
|
|
666
|
+
if (!line && checkDragSuccess(Date.now() - startTime, e, originLine)) {
|
|
667
|
+
// 隐藏原来的线条
|
|
668
|
+
if (originLine) {
|
|
669
|
+
originLine.highlightColor = this.linesManager.lineColor.hidden;
|
|
670
|
+
}
|
|
671
|
+
dragSuccess = true;
|
|
672
|
+
const pos = config.getPosFromMouseEvent(event);
|
|
673
|
+
// 创建临时的线条
|
|
674
|
+
if (isDrawingTo) {
|
|
675
|
+
line = this.linesManager.createLine({
|
|
676
|
+
from: port.node.id,
|
|
677
|
+
fromPort: port.portID,
|
|
678
|
+
data: originLine?.lineData,
|
|
679
|
+
drawingTo: {
|
|
680
|
+
x: pos.x,
|
|
681
|
+
y: pos.y,
|
|
682
|
+
location: port.location === 'right' ? 'left' : 'top',
|
|
683
|
+
},
|
|
684
|
+
});
|
|
685
|
+
} else {
|
|
686
|
+
line = this.linesManager.createLine({
|
|
687
|
+
to: port.node.id,
|
|
688
|
+
toPort: port.portID,
|
|
689
|
+
data: originLine?.lineData,
|
|
690
|
+
drawingFrom: {
|
|
691
|
+
x: pos.x,
|
|
692
|
+
y: pos.y,
|
|
693
|
+
location: port.location === 'left' ? 'right' : 'bottom',
|
|
694
|
+
},
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
if (!line) {
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
config.updateCursor('grab');
|
|
701
|
+
line.highlightColor = originLine?.lockedColor || this.linesManager.lineColor.drawing;
|
|
702
|
+
this.hoverService.updateHoveredKey('');
|
|
703
|
+
}
|
|
704
|
+
if (!line) {
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
const dragPos = config.getPosFromMouseEvent(e);
|
|
708
|
+
newLineInfo = this.updateDrawingLine(isDrawingTo, line, dragPos, originLine);
|
|
709
|
+
},
|
|
710
|
+
|
|
711
|
+
onDragEnd: async (e) => {
|
|
712
|
+
const dragPos = config.getPosFromMouseEvent(e);
|
|
713
|
+
const onDragLineEndCallbacks = Array.from(this._onDragLineEndCallbacks.values());
|
|
714
|
+
config.updateCursor(preCursor);
|
|
715
|
+
const { fromPort, toPort, hasError } = newLineInfo || {};
|
|
716
|
+
await Promise.all(
|
|
717
|
+
onDragLineEndCallbacks.map((callback) =>
|
|
718
|
+
callback({
|
|
719
|
+
fromPort,
|
|
720
|
+
toPort,
|
|
721
|
+
mousePos: dragPos,
|
|
722
|
+
line,
|
|
723
|
+
originLine,
|
|
724
|
+
event: e,
|
|
725
|
+
})
|
|
726
|
+
)
|
|
727
|
+
);
|
|
728
|
+
line?.dispose();
|
|
729
|
+
this._onDragLineEventEmitter.fire({
|
|
730
|
+
type: 'onDragEnd',
|
|
731
|
+
});
|
|
732
|
+
// 清除选中状态
|
|
733
|
+
if (originLine) {
|
|
734
|
+
originLine.highlightColor = '';
|
|
735
|
+
}
|
|
736
|
+
const end = () => {
|
|
737
|
+
originLine?.validate();
|
|
738
|
+
deferred.resolve({ dragSuccess });
|
|
739
|
+
};
|
|
740
|
+
if (dragSuccess) {
|
|
741
|
+
// Step 1: check same line
|
|
742
|
+
if (originLine && originLine.toPort === toPort && originLine.fromPort === fromPort) {
|
|
743
|
+
// 线条没变化则直接返回,不做处理
|
|
744
|
+
return end();
|
|
745
|
+
}
|
|
746
|
+
// 非 input 节点不能连接
|
|
747
|
+
if (
|
|
748
|
+
(toPort && toPort.portType !== 'input') ||
|
|
749
|
+
(fromPort && fromPort.portType !== 'output')
|
|
750
|
+
) {
|
|
751
|
+
return end();
|
|
752
|
+
}
|
|
753
|
+
const newLinePortInfo: Required<WorkflowLinePortInfo> | undefined =
|
|
754
|
+
toPort && fromPort
|
|
755
|
+
? {
|
|
756
|
+
from: fromPort.node.id,
|
|
757
|
+
fromPort: fromPort.portID,
|
|
758
|
+
to: toPort.node.id,
|
|
759
|
+
toPort: toPort.portID,
|
|
760
|
+
data: originLine?.lineData,
|
|
761
|
+
}
|
|
762
|
+
: undefined;
|
|
763
|
+
// Step2: 检测 reset
|
|
764
|
+
const isReset = originLine && newLinePortInfo;
|
|
765
|
+
if (isReset && !this.linesManager.canReset(originLine, newLinePortInfo)) {
|
|
766
|
+
return end();
|
|
767
|
+
}
|
|
768
|
+
// Step 3: delete line
|
|
769
|
+
if (
|
|
770
|
+
originLine &&
|
|
771
|
+
(!this.linesManager.canRemove(originLine, newLinePortInfo, false) || hasError)
|
|
772
|
+
) {
|
|
773
|
+
// 线条无法删除则返回,不再触发 canAddLine
|
|
774
|
+
return end();
|
|
775
|
+
} else {
|
|
776
|
+
originLine?.dispose();
|
|
777
|
+
}
|
|
778
|
+
// Step 4: add line
|
|
779
|
+
if (!newLinePortInfo || !this.linesManager.canAddLine(fromPort!, toPort!, false)) {
|
|
780
|
+
// 无法添加成功
|
|
781
|
+
return end();
|
|
782
|
+
}
|
|
783
|
+
const newLine = this.linesManager.createLine(newLinePortInfo);
|
|
784
|
+
if (!newLine) {
|
|
785
|
+
end();
|
|
786
|
+
}
|
|
787
|
+
deferred.resolve({
|
|
788
|
+
dragSuccess,
|
|
789
|
+
newLine,
|
|
790
|
+
});
|
|
791
|
+
} else {
|
|
792
|
+
end();
|
|
793
|
+
}
|
|
794
|
+
},
|
|
795
|
+
});
|
|
796
|
+
const { clientX, clientY } = MouseTouchEvent.getEventCoord(event);
|
|
797
|
+
await dragger.start(clientX, clientY, config);
|
|
798
|
+
return deferred.promise;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
private updateDrawingLine(
|
|
802
|
+
isDrawingTo: boolean,
|
|
803
|
+
line: WorkflowLineEntity,
|
|
804
|
+
dragPos: IPoint,
|
|
805
|
+
originLine?: WorkflowLineEntity
|
|
806
|
+
): { fromPort?: WorkflowPortEntity; toPort?: WorkflowPortEntity; hasError: boolean } {
|
|
807
|
+
let hasError = false;
|
|
808
|
+
|
|
809
|
+
const mouseNode = this.linesManager.getNodeFromMousePos(dragPos);
|
|
810
|
+
let toNode: WorkflowNodeEntity | undefined;
|
|
811
|
+
let toPort: WorkflowPortEntity | undefined;
|
|
812
|
+
let fromPort: WorkflowPortEntity | undefined;
|
|
813
|
+
let fromNode: WorkflowNodeEntity | undefined;
|
|
814
|
+
|
|
815
|
+
if (isDrawingTo) {
|
|
816
|
+
fromPort = line.fromPort!;
|
|
817
|
+
toNode = mouseNode;
|
|
818
|
+
toPort = this.linesManager.getPortFromMousePos(dragPos, 'input');
|
|
819
|
+
if (toNode && this.canBuildContainerLine(toNode, dragPos)) {
|
|
820
|
+
// 如果鼠标 hover 在 node 中的时候,默认连线到这个 node 的初始位置
|
|
821
|
+
toPort = this.getNearestPort(toNode, dragPos, 'input');
|
|
822
|
+
hasError = this.checkDraggingPort(isDrawingTo, line, toNode, toPort, originLine).hasError;
|
|
823
|
+
}
|
|
824
|
+
if (!toPort) {
|
|
825
|
+
line.setToPort(undefined);
|
|
826
|
+
} else if (!this.linesManager.canAddLine(fromPort, toPort, true)) {
|
|
827
|
+
hasError = true;
|
|
828
|
+
line.setToPort(undefined);
|
|
829
|
+
} else {
|
|
830
|
+
line.setToPort(toPort);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
if (line.toPort) {
|
|
834
|
+
line.drawingTo = {
|
|
835
|
+
x: line.toPort.point.x,
|
|
836
|
+
y: line.toPort.point.y,
|
|
837
|
+
location: line.toPort.location,
|
|
838
|
+
};
|
|
839
|
+
} else {
|
|
840
|
+
line.drawingTo = {
|
|
841
|
+
x: dragPos.x,
|
|
842
|
+
y: dragPos.y,
|
|
843
|
+
location: reverseLocation(line.fromPort!.location),
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
} else {
|
|
847
|
+
toPort = line.toPort!;
|
|
848
|
+
fromNode = mouseNode;
|
|
849
|
+
fromPort = this.linesManager.getPortFromMousePos(dragPos, 'output');
|
|
850
|
+
if (fromNode && this.canBuildContainerLine(fromNode, dragPos)) {
|
|
851
|
+
// 如果鼠标 hover 在 node 中的时候,默认连线到这个 node 的初始位置
|
|
852
|
+
fromPort = this.getNearestPort(fromNode, dragPos, 'output');
|
|
853
|
+
hasError = this.checkDraggingPort(
|
|
854
|
+
isDrawingTo,
|
|
855
|
+
line,
|
|
856
|
+
fromNode,
|
|
857
|
+
fromPort,
|
|
858
|
+
originLine
|
|
859
|
+
).hasError;
|
|
860
|
+
}
|
|
861
|
+
if (!fromPort) {
|
|
862
|
+
line.setFromPort(undefined);
|
|
863
|
+
} else if (!this.linesManager.canAddLine(fromPort, toPort, true)) {
|
|
864
|
+
hasError = true;
|
|
865
|
+
line.setFromPort(undefined);
|
|
866
|
+
} else {
|
|
867
|
+
line.setFromPort(fromPort);
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
if (line.fromPort) {
|
|
871
|
+
line.drawingFrom = {
|
|
872
|
+
x: line.fromPort.point.x,
|
|
873
|
+
y: line.fromPort.point.y,
|
|
874
|
+
location: line.fromPort.location,
|
|
875
|
+
};
|
|
876
|
+
} else {
|
|
877
|
+
line.drawingFrom = {
|
|
878
|
+
x: dragPos.x,
|
|
879
|
+
y: dragPos.y,
|
|
880
|
+
location: reverseLocation(line.toPort!.location),
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
this._onDragLineEventEmitter.fire({
|
|
886
|
+
type: 'onDrag',
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
if (hasError) {
|
|
890
|
+
this.setLineColor(line, this.linesManager.lineColor.error);
|
|
891
|
+
} else {
|
|
892
|
+
this.setLineColor(line, originLine?.lockedColor || this.linesManager.lineColor.drawing);
|
|
893
|
+
}
|
|
894
|
+
// 触发原 toPort 的校验
|
|
895
|
+
originLine?.validate();
|
|
896
|
+
line.validate();
|
|
897
|
+
return {
|
|
898
|
+
fromPort: fromPort,
|
|
899
|
+
toPort: toPort,
|
|
900
|
+
hasError,
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
/**
|
|
905
|
+
* 重新连接线条
|
|
906
|
+
* @param line
|
|
907
|
+
* @param e
|
|
908
|
+
*/
|
|
909
|
+
async resetLine(line: WorkflowLineEntity, e: MouseEvent): Promise<void> {
|
|
910
|
+
const { fromPort, toPort } = line;
|
|
911
|
+
const mousePos = this.playgroundConfig.getPosFromMouseEvent(e);
|
|
912
|
+
const distanceFrom = Point.getDistance(fromPort!.point, mousePos);
|
|
913
|
+
const distanceTo = Point.getDistance(toPort!.point, mousePos);
|
|
914
|
+
const { dragSuccess } = await this.startDrawingLine(
|
|
915
|
+
distanceTo <= distanceFrom || !this.document.options.twoWayConnection ? fromPort! : toPort!,
|
|
916
|
+
e,
|
|
917
|
+
line
|
|
918
|
+
);
|
|
919
|
+
if (!dragSuccess) {
|
|
920
|
+
// 没有拖拽成功则表示为选中节点
|
|
921
|
+
this.selectService.select(line);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
/** 线条拖拽结束 */
|
|
926
|
+
public onDragLineEnd(callback: OnDragLineEnd): Disposable {
|
|
927
|
+
const id = nanoid();
|
|
928
|
+
this._onDragLineEndCallbacks.set(id, callback);
|
|
929
|
+
return {
|
|
930
|
+
dispose: () => {
|
|
931
|
+
this._onDragLineEndCallbacks.delete(id);
|
|
932
|
+
},
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
/** 能否建立容器连线 */
|
|
937
|
+
private canBuildContainerLine(node: WorkflowNodeEntity, mousePos: IPoint): boolean {
|
|
938
|
+
const isContainer = this.isContainer(node);
|
|
939
|
+
if (!isContainer) {
|
|
940
|
+
return true;
|
|
941
|
+
}
|
|
942
|
+
const { padding, bounds } = node.transform;
|
|
943
|
+
const DEFAULT_DELTA = 10;
|
|
944
|
+
const leftDelta = (padding.left * 2) / 3 || DEFAULT_DELTA;
|
|
945
|
+
const rightDelta = (padding.right * 2) / 3 || DEFAULT_DELTA;
|
|
946
|
+
const bottomDelta = (padding.bottom * 2) / 3 || DEFAULT_DELTA;
|
|
947
|
+
const topDelta = (padding.top * 2) / 3 || DEFAULT_DELTA;
|
|
948
|
+
const rectangles = [
|
|
949
|
+
new Rectangle(bounds.x, bounds.y, leftDelta, bounds.height), // left
|
|
950
|
+
new Rectangle(bounds.x, bounds.y, bounds.width, topDelta), // top
|
|
951
|
+
new Rectangle(bounds.x, bounds.y + bounds.height - bottomDelta, bounds.width, bottomDelta), // bottom
|
|
952
|
+
new Rectangle(bounds.x + bounds.width - rightDelta, bounds.y, rightDelta, bounds.height), // right
|
|
953
|
+
];
|
|
954
|
+
return rectangles.some((rect) => rect.contains(mousePos.x, mousePos.y));
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
/** 获取最近的 port */
|
|
958
|
+
private getNearestPort(
|
|
959
|
+
node: WorkflowNodeEntity,
|
|
960
|
+
mousePos: IPoint,
|
|
961
|
+
portType: WorkflowPortType = 'input'
|
|
962
|
+
): WorkflowPortEntity {
|
|
963
|
+
const portsData = node.ports!;
|
|
964
|
+
const distanceSortedPorts = (
|
|
965
|
+
portType === 'input' ? portsData.inputPorts : portsData.outputPorts
|
|
966
|
+
).sort((a, b) => Point.getDistance(mousePos, a.point) - Point.getDistance(mousePos, b.point));
|
|
967
|
+
return distanceSortedPorts[0];
|
|
968
|
+
}
|
|
969
|
+
}
|