mirror of
https://gitee.com/ByteDance/flowgram.ai.git
synced 2025-07-07 17:43:29 +08:00
feat(snap): node snapping and snap line rending can be disabled
This commit is contained in:
parent
2607ec93d3
commit
5f1f2a6a03
@ -1,11 +1,11 @@
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { Disposable, Emitter, Rectangle } from '@flowgram.ai/utils';
|
||||
import { IPoint } from '@flowgram.ai/utils';
|
||||
import { WorkflowNodeEntity, WorkflowDocument } from '@flowgram.ai/free-layout-core';
|
||||
import { WorkflowDragService } from '@flowgram.ai/free-layout-core';
|
||||
import { FlowNodeTransformData } from '@flowgram.ai/document';
|
||||
import { FlowNodeBaseType } from '@flowgram.ai/document';
|
||||
import { EntityManager, PlaygroundConfigEntity, TransformData } from '@flowgram.ai/core';
|
||||
import { WorkflowNodeEntity, WorkflowDocument } from '@flowgram.ai/free-layout-core';
|
||||
import { WorkflowDragService } from '@flowgram.ai/free-layout-core';
|
||||
import { Disposable, Emitter, Rectangle } from '@flowgram.ai/utils';
|
||||
import { IPoint } from '@flowgram.ai/utils';
|
||||
|
||||
import { isEqual, isGreaterThan, isLessThan, isLessThanOrEqual, isNumber } from './utils';
|
||||
import type {
|
||||
@ -44,6 +44,8 @@ export class WorkflowSnapService {
|
||||
|
||||
public readonly onSnap = this.snapEmitter.event;
|
||||
|
||||
private _disabled = false;
|
||||
|
||||
public init(params: Partial<WorkflowSnapServiceOptions> = {}): void {
|
||||
this.options = {
|
||||
...SnapDefaultOptions,
|
||||
@ -53,18 +55,46 @@ export class WorkflowSnapService {
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.disposers.forEach(disposer => disposer.dispose());
|
||||
this.disposers.forEach((disposer) => disposer.dispose());
|
||||
}
|
||||
|
||||
public get disabled(): boolean {
|
||||
return this._disabled;
|
||||
}
|
||||
|
||||
public disable(): void {
|
||||
if (this._disabled) {
|
||||
return;
|
||||
}
|
||||
this._disabled = true;
|
||||
this.clear();
|
||||
}
|
||||
|
||||
public enable(): void {
|
||||
if (!this._disabled) {
|
||||
return;
|
||||
}
|
||||
this._disabled = false;
|
||||
this.clear();
|
||||
}
|
||||
|
||||
private mountListener(): void {
|
||||
const dragAdjusterDisposer = this.dragService.registerPosAdjuster(params =>
|
||||
this.snapping({
|
||||
targetNodes: params.selectedNodes,
|
||||
position: params.position,
|
||||
}),
|
||||
);
|
||||
const dragEndDisposer = this.dragService.onNodesDrag(event => {
|
||||
if (event.type !== 'onDragEnd') {
|
||||
const dragAdjusterDisposer = this.dragService.registerPosAdjuster((params) => {
|
||||
const { selectedNodes: targetNodes, position } = params;
|
||||
const isMultiSnapping = this.options.enableMultiSnapping ? false : targetNodes.length !== 1;
|
||||
if (this._disabled || !this.options.enableEdgeSnapping || isMultiSnapping) {
|
||||
return {
|
||||
x: 0,
|
||||
y: 0,
|
||||
};
|
||||
}
|
||||
return this.snapping({
|
||||
targetNodes,
|
||||
position,
|
||||
});
|
||||
});
|
||||
const dragEndDisposer = this.dragService.onNodesDrag((event) => {
|
||||
if (event.type !== 'onDragEnd' || this._disabled) {
|
||||
return;
|
||||
}
|
||||
if (this.options.enableGridSnapping) {
|
||||
@ -81,24 +111,15 @@ export class WorkflowSnapService {
|
||||
}
|
||||
|
||||
private snapping(params: { targetNodes: WorkflowNodeEntity[]; position: IPoint }): IPoint {
|
||||
const { targetNodes: targetNodes, position } = params;
|
||||
const { targetNodes, position } = params;
|
||||
|
||||
const isMultiSnapping = this.options.enableMultiSnapping ? false : targetNodes.length !== 1;
|
||||
|
||||
if (!this.options.enableEdgeSnapping || isMultiSnapping) {
|
||||
return {
|
||||
x: 0,
|
||||
y: 0,
|
||||
};
|
||||
}
|
||||
|
||||
const selectedBounds = this.getBounds(targetNodes);
|
||||
const targetBounds = this.getBounds(targetNodes);
|
||||
|
||||
const targetRect = new Rectangle(
|
||||
position.x,
|
||||
position.y,
|
||||
selectedBounds.width,
|
||||
selectedBounds.height,
|
||||
targetBounds.width,
|
||||
targetBounds.height
|
||||
);
|
||||
|
||||
const snapNodeRects = this.getSnapNodeRects({
|
||||
@ -127,7 +148,7 @@ export class WorkflowSnapService {
|
||||
position.x + offset.x,
|
||||
position.y + offset.y,
|
||||
targetRect.width,
|
||||
targetRect.height,
|
||||
targetRect.height
|
||||
);
|
||||
|
||||
this.snapEmitter.fire({
|
||||
@ -155,23 +176,23 @@ export class WorkflowSnapService {
|
||||
});
|
||||
|
||||
// 找到最近的线条
|
||||
const topYClosestLine = snapLines.horizontal.find(line =>
|
||||
isLessThanOrEqual(Math.abs(line.y - targetRect.top), edgeThreshold),
|
||||
const topYClosestLine = snapLines.horizontal.find((line) =>
|
||||
isLessThanOrEqual(Math.abs(line.y - targetRect.top), edgeThreshold)
|
||||
);
|
||||
const bottomYClosestLine = snapLines.horizontal.find(line =>
|
||||
isLessThanOrEqual(Math.abs(line.y - targetRect.bottom), edgeThreshold),
|
||||
const bottomYClosestLine = snapLines.horizontal.find((line) =>
|
||||
isLessThanOrEqual(Math.abs(line.y - targetRect.bottom), edgeThreshold)
|
||||
);
|
||||
const leftXClosestLine = snapLines.vertical.find(line =>
|
||||
isLessThanOrEqual(Math.abs(line.x - targetRect.left), edgeThreshold),
|
||||
const leftXClosestLine = snapLines.vertical.find((line) =>
|
||||
isLessThanOrEqual(Math.abs(line.x - targetRect.left), edgeThreshold)
|
||||
);
|
||||
const rightXClosestLine = snapLines.vertical.find(line =>
|
||||
isLessThanOrEqual(Math.abs(line.x - targetRect.right), edgeThreshold),
|
||||
const rightXClosestLine = snapLines.vertical.find((line) =>
|
||||
isLessThanOrEqual(Math.abs(line.x - targetRect.right), edgeThreshold)
|
||||
);
|
||||
const midYClosestLine = snapLines.midHorizontal.find(line =>
|
||||
isLessThanOrEqual(Math.abs(line.y - targetRect.center.y), edgeThreshold),
|
||||
const midYClosestLine = snapLines.midHorizontal.find((line) =>
|
||||
isLessThanOrEqual(Math.abs(line.y - targetRect.center.y), edgeThreshold)
|
||||
);
|
||||
const midXClosestLine = snapLines.midVertical.find(line =>
|
||||
isLessThanOrEqual(Math.abs(line.x - targetRect.center.x), edgeThreshold),
|
||||
const midXClosestLine = snapLines.midVertical.find((line) =>
|
||||
isLessThanOrEqual(Math.abs(line.x - targetRect.center.x), edgeThreshold)
|
||||
);
|
||||
|
||||
// 计算最近坐标
|
||||
@ -227,11 +248,11 @@ export class WorkflowSnapService {
|
||||
x: snappedPosition.x - rect.x,
|
||||
y: snappedPosition.y - rect.y,
|
||||
};
|
||||
targetNodes.forEach(node =>
|
||||
targetNodes.forEach((node) =>
|
||||
this.updateNodePositionWithOffset({
|
||||
node,
|
||||
offset,
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@ -256,7 +277,7 @@ export class WorkflowSnapService {
|
||||
const midHorizontalLines: SnapMidHorizontalLine[] = [];
|
||||
const midVerticalLines: SnapMidVerticalLine[] = [];
|
||||
|
||||
snapNodeRects.forEach(snapNodeRect => {
|
||||
snapNodeRects.forEach((snapNodeRect) => {
|
||||
const nodeBounds = snapNodeRect.rect;
|
||||
const nodeCenter = nodeBounds.center;
|
||||
// 边缘横线
|
||||
@ -310,11 +331,11 @@ export class WorkflowSnapService {
|
||||
const targetCenter = targetRect.center;
|
||||
const targetContainerId = targetNodes[0].parent?.id ?? this.document.root.id;
|
||||
|
||||
const disabledNodeIds = targetNodes.map(n => n.id);
|
||||
const disabledNodeIds = targetNodes.map((n) => n.id);
|
||||
disabledNodeIds.push(FlowNodeBaseType.ROOT);
|
||||
const availableNodes = this.nodes
|
||||
.filter(n => n.parent?.id === targetContainerId)
|
||||
.filter(n => !disabledNodeIds.includes(n.id))
|
||||
.filter((n) => n.parent?.id === targetContainerId)
|
||||
.filter((n) => !disabledNodeIds.includes(n.id))
|
||||
.sort((nodeA, nodeB) => {
|
||||
const nodeCenterA = nodeA.getData(FlowNodeTransformData)!.bounds.center;
|
||||
const nodeCenterB = nodeB.getData(FlowNodeTransformData)!.bounds.center;
|
||||
@ -340,7 +361,7 @@ export class WorkflowSnapService {
|
||||
const availableNodes = this.getAvailableNodes(params);
|
||||
const viewRect = this.viewRect();
|
||||
return availableNodes
|
||||
.map(node => {
|
||||
.map((node) => {
|
||||
const snapNodeRect: SnapNodeRect = {
|
||||
id: node.id,
|
||||
rect: node.getData(FlowNodeTransformData).bounds,
|
||||
@ -367,7 +388,7 @@ export class WorkflowSnapService {
|
||||
if (nodes.length === 0) {
|
||||
return Rectangle.EMPTY;
|
||||
}
|
||||
return Rectangle.enlarge(nodes.map(n => n.getData(FlowNodeTransformData)!.bounds));
|
||||
return Rectangle.enlarge(nodes.map((n) => n.getData(FlowNodeTransformData)!.bounds));
|
||||
}
|
||||
|
||||
private updateNodePositionWithOffset(params: { node: WorkflowNodeEntity; offset: IPoint }): void {
|
||||
@ -379,7 +400,7 @@ export class WorkflowSnapService {
|
||||
};
|
||||
if (node.collapsedChildren?.length > 0) {
|
||||
// 嵌套情况下需将子节点 transform 设为 dirty
|
||||
node.collapsedChildren.forEach(childNode => {
|
||||
node.collapsedChildren.forEach((childNode) => {
|
||||
const childNodeTransformData =
|
||||
childNode.getData<FlowNodeTransformData>(FlowNodeTransformData);
|
||||
childNodeTransformData.fireChange();
|
||||
@ -451,7 +472,7 @@ export class WorkflowSnapService {
|
||||
const rightAlignX = alignRects.right[0].rect.left - alignSpacing.right;
|
||||
const isAlignRight = isLessThanOrEqual(
|
||||
Math.abs(targetRect.right - rightAlignX),
|
||||
alignThreshold,
|
||||
alignThreshold
|
||||
);
|
||||
if (isAlignRight) {
|
||||
rightX = rightAlignX - targetRect.width;
|
||||
@ -463,7 +484,7 @@ export class WorkflowSnapService {
|
||||
const leftAlignX = alignRects.left[0].rect.right + alignSpacing.midHorizontal;
|
||||
const isAlignMidHorizontal = isLessThanOrEqual(
|
||||
Math.abs(targetRect.left - leftAlignX),
|
||||
alignThreshold,
|
||||
alignThreshold
|
||||
);
|
||||
if (isAlignMidHorizontal) {
|
||||
midX = leftAlignX;
|
||||
@ -475,7 +496,7 @@ export class WorkflowSnapService {
|
||||
const topAlignY = alignRects.top[0].rect.bottom + alignSpacing.midVertical;
|
||||
const isAlignMidVertical = isLessThanOrEqual(
|
||||
Math.abs(targetRect.top - topAlignY),
|
||||
alignThreshold,
|
||||
alignThreshold
|
||||
);
|
||||
if (isAlignMidVertical) {
|
||||
midY = topAlignY;
|
||||
@ -552,7 +573,7 @@ export class WorkflowSnapService {
|
||||
const leftHorizontalRects: AlignRect[] = [];
|
||||
const rightHorizontalRects: AlignRect[] = [];
|
||||
|
||||
snapNodeRects.forEach(snapNodeRect => {
|
||||
snapNodeRects.forEach((snapNodeRect) => {
|
||||
const nodeRect = snapNodeRect.rect;
|
||||
const { isVerticalIntersection, isHorizontalIntersection, isIntersection } =
|
||||
this.intersection(nodeRect, targetRect);
|
||||
@ -612,7 +633,7 @@ export class WorkflowSnapService {
|
||||
}
|
||||
const { isVerticalIntersection, isHorizontalIntersection, isIntersection } = this.intersection(
|
||||
rectA,
|
||||
rectB,
|
||||
rectB
|
||||
);
|
||||
if (isIntersection) {
|
||||
return;
|
||||
@ -620,13 +641,13 @@ export class WorkflowSnapService {
|
||||
if (isHorizontal && isHorizontalIntersection && !isVerticalIntersection) {
|
||||
const betweenSpacing = Math.min(
|
||||
Math.abs(rectA.left - rectB.right),
|
||||
Math.abs(rectA.right - rectB.left),
|
||||
Math.abs(rectA.right - rectB.left)
|
||||
);
|
||||
return (betweenSpacing - targetRect.width) / 2;
|
||||
} else if (!isHorizontal && isVerticalIntersection && !isHorizontalIntersection) {
|
||||
const betweenSpacing = Math.min(
|
||||
Math.abs(rectA.top - rectB.bottom),
|
||||
Math.abs(rectA.bottom - rectB.top),
|
||||
Math.abs(rectA.bottom - rectB.top)
|
||||
);
|
||||
return (betweenSpacing - targetRect.height) / 2;
|
||||
}
|
||||
@ -646,7 +667,7 @@ export class WorkflowSnapService {
|
||||
|
||||
const { isVerticalIntersection, isHorizontalIntersection, isIntersection } = this.intersection(
|
||||
rectA,
|
||||
rectB,
|
||||
rectB
|
||||
);
|
||||
|
||||
if (isIntersection) {
|
||||
@ -663,7 +684,7 @@ export class WorkflowSnapService {
|
||||
|
||||
private intersection(
|
||||
rectA: Rectangle,
|
||||
rectB: Rectangle,
|
||||
rectB: Rectangle
|
||||
): {
|
||||
isHorizontalIntersection: boolean;
|
||||
isVerticalIntersection: boolean;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user