@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,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
+ }