@flowgram-vue/free-lines-plugin 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 +1026 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.css +120 -0
- package/dist/index.css.map +1 -0
- package/dist/index.d.ts +269 -0
- package/dist/index.js +1013 -0
- package/dist/index.js.map +1 -0
- package/package.json +63 -0
- package/src/__tests__/__snapshots__/bezier-controls.spec.ts.snap +20 -0
- package/src/__tests__/bezier-controls.spec.ts +25 -0
- package/src/components/index.ts +8 -0
- package/src/components/workflow-line-render/arrow.ts +60 -0
- package/src/components/workflow-line-render/index.css +22 -0
- package/src/components/workflow-line-render/index.ts +6 -0
- package/src/components/workflow-line-render/line-svg.ts +131 -0
- package/src/components/workflow-port-render/cross-hair.ts +16 -0
- package/src/components/workflow-port-render/index.css +121 -0
- package/src/components/workflow-port-render/index.ts +183 -0
- package/src/constants/lines.ts +9 -0
- package/src/constants/points.ts +12 -0
- package/src/contributions/bezier/bezier-controls.ts +105 -0
- package/src/contributions/bezier/index.ts +121 -0
- package/src/contributions/fold/fold-line.ts +293 -0
- package/src/contributions/fold/index.ts +97 -0
- package/src/contributions/index.ts +8 -0
- package/src/contributions/straight/index.ts +89 -0
- package/src/contributions/straight/point-on-line.ts +37 -0
- package/src/contributions/utils.ts +37 -0
- package/src/create-free-lines-plugin.ts +44 -0
- package/src/css.d.ts +6 -0
- package/src/env.d.ts +10 -0
- package/src/index.ts +12 -0
- package/src/layer/index.ts +8 -0
- package/src/layer/workflow-lines-layer.ts +201 -0
- package/src/type.ts +40 -0
- package/src/types/arrow-renderer.ts +32 -0
- package/src/wrap-layer-render.ts +38 -0
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { type IPoint, Point, Rectangle } from '@flowgram-vue/utils';
|
|
7
|
+
import { LinePoint } from '@flowgram-vue/free-layout-core';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 计算点到线段的距离
|
|
11
|
+
* @param point 待测试点
|
|
12
|
+
* @param segStart 线段起点
|
|
13
|
+
* @param segEnd 线段终点
|
|
14
|
+
*/
|
|
15
|
+
const getPointToSegmentDistance = (point: IPoint, segStart: IPoint, segEnd: IPoint): number => {
|
|
16
|
+
const { x: px, y: py } = point;
|
|
17
|
+
const { x: x1, y: y1 } = segStart;
|
|
18
|
+
const { x: x2, y: y2 } = segEnd;
|
|
19
|
+
|
|
20
|
+
const A = px - x1;
|
|
21
|
+
const B = py - y1;
|
|
22
|
+
const C = x2 - x1;
|
|
23
|
+
const D = y2 - y1;
|
|
24
|
+
|
|
25
|
+
const dot = A * C + B * D;
|
|
26
|
+
const lenSq = C * C + D * D;
|
|
27
|
+
|
|
28
|
+
// 参数方程中的t参数
|
|
29
|
+
const param = lenSq === 0 ? -1 : dot / lenSq;
|
|
30
|
+
|
|
31
|
+
let xx: number;
|
|
32
|
+
let yy: number;
|
|
33
|
+
|
|
34
|
+
if (param < 0) {
|
|
35
|
+
xx = x1;
|
|
36
|
+
yy = y1;
|
|
37
|
+
} else if (param > 1) {
|
|
38
|
+
xx = x2;
|
|
39
|
+
yy = y2;
|
|
40
|
+
} else {
|
|
41
|
+
xx = x1 + param * C;
|
|
42
|
+
yy = y1 + param * D;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const dx = px - xx;
|
|
46
|
+
const dy = py - yy;
|
|
47
|
+
|
|
48
|
+
return Math.sqrt(dx * dx + dy * dy);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Fork from: https://github.com/xyflow/xyflow/blob/main/packages/system/src/utils/edges/smoothstep-edge.ts
|
|
53
|
+
* MIT License
|
|
54
|
+
* Copyright (c) 2019-2024 webkid GmbH
|
|
55
|
+
*/
|
|
56
|
+
export namespace FoldLine {
|
|
57
|
+
const EDGE_RADIUS = 5;
|
|
58
|
+
const OFFSET = 20;
|
|
59
|
+
|
|
60
|
+
function getEdgeCenter({ source, target }: { source: IPoint; target: IPoint }): [number, number] {
|
|
61
|
+
const xOffset = Math.abs(target.x - source.x) / 2;
|
|
62
|
+
const centerX = target.x < source.x ? target.x + xOffset : target.x - xOffset;
|
|
63
|
+
|
|
64
|
+
const yOffset = Math.abs(target.y - source.y) / 2;
|
|
65
|
+
const centerY = target.y < source.y ? target.y + yOffset : target.y - yOffset;
|
|
66
|
+
|
|
67
|
+
return [centerX, centerY];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const getDirection = ({ source, target }: { source: LinePoint; target: LinePoint }): IPoint => {
|
|
71
|
+
if (source.location === 'left' || source.location === 'right') {
|
|
72
|
+
return source.x < target.x ? { x: 1, y: 0 } : { x: -1, y: 0 };
|
|
73
|
+
}
|
|
74
|
+
return source.y < target.y ? { x: 0, y: 1 } : { x: 0, y: -1 };
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const handleDirections = {
|
|
78
|
+
left: { x: -1, y: 0 },
|
|
79
|
+
right: { x: 1, y: 0 },
|
|
80
|
+
top: { x: 0, y: -1 },
|
|
81
|
+
bottom: { x: 0, y: 1 },
|
|
82
|
+
};
|
|
83
|
+
// eslint-disable-next-line complexity
|
|
84
|
+
export function getPoints({ source, target }: { source: LinePoint; target: LinePoint }): {
|
|
85
|
+
points: IPoint[];
|
|
86
|
+
center: IPoint;
|
|
87
|
+
} {
|
|
88
|
+
const sourceDir = handleDirections[source.location];
|
|
89
|
+
const targetDir = handleDirections[target.location];
|
|
90
|
+
const sourceGapped: LinePoint = {
|
|
91
|
+
x: source.x + sourceDir.x * OFFSET,
|
|
92
|
+
y: source.y + sourceDir.y * OFFSET,
|
|
93
|
+
location: source.location,
|
|
94
|
+
};
|
|
95
|
+
const targetGapped: LinePoint = {
|
|
96
|
+
x: target.x + targetDir.x * OFFSET,
|
|
97
|
+
y: target.y + targetDir.y * OFFSET,
|
|
98
|
+
location: target.location,
|
|
99
|
+
};
|
|
100
|
+
const dir = getDirection({
|
|
101
|
+
source: sourceGapped,
|
|
102
|
+
target: targetGapped,
|
|
103
|
+
});
|
|
104
|
+
const dirAccessor = dir.x !== 0 ? 'x' : 'y';
|
|
105
|
+
const currDir = dir[dirAccessor];
|
|
106
|
+
|
|
107
|
+
let points: IPoint[] = [];
|
|
108
|
+
let centerX, centerY;
|
|
109
|
+
|
|
110
|
+
const [defaultCenterX, defaultCenterY] = getEdgeCenter({
|
|
111
|
+
source,
|
|
112
|
+
target,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// 计算向量乘积
|
|
116
|
+
if (sourceDir[dirAccessor] * targetDir[dirAccessor] === -1) {
|
|
117
|
+
centerX = defaultCenterX;
|
|
118
|
+
centerY = defaultCenterY;
|
|
119
|
+
|
|
120
|
+
const verticalSplit: IPoint[] = [
|
|
121
|
+
{ x: centerX, y: sourceGapped.y },
|
|
122
|
+
{ x: centerX, y: targetGapped.y },
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
const horizontalSplit: IPoint[] = [
|
|
126
|
+
{ x: sourceGapped.x, y: centerY },
|
|
127
|
+
{ x: targetGapped.x, y: centerY },
|
|
128
|
+
];
|
|
129
|
+
|
|
130
|
+
if (sourceDir[dirAccessor] === currDir) {
|
|
131
|
+
points = dirAccessor === 'x' ? verticalSplit : horizontalSplit;
|
|
132
|
+
} else {
|
|
133
|
+
points = dirAccessor === 'x' ? horizontalSplit : verticalSplit;
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
// sourceTarget means we take x from source and y from target, targetSource is the opposite
|
|
137
|
+
const sourceTarget: IPoint[] = [{ x: sourceGapped.x, y: targetGapped.y }];
|
|
138
|
+
const targetSource: IPoint[] = [{ x: targetGapped.x, y: sourceGapped.y }];
|
|
139
|
+
// this handles edges with same handle positions
|
|
140
|
+
if (dirAccessor === 'x') {
|
|
141
|
+
points = sourceDir.x === currDir ? targetSource : sourceTarget;
|
|
142
|
+
} else {
|
|
143
|
+
points = sourceDir.y === currDir ? sourceTarget : targetSource;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// these are conditions for handling mixed handle positions like Right -> Bottom for example
|
|
147
|
+
const dirAccessorOpposite = dirAccessor === 'x' ? 'y' : 'x';
|
|
148
|
+
const isSameDir = sourceDir[dirAccessor] === targetDir[dirAccessorOpposite];
|
|
149
|
+
const sourceGtTargetOppo =
|
|
150
|
+
sourceGapped[dirAccessorOpposite] > targetGapped[dirAccessorOpposite];
|
|
151
|
+
const sourceLtTargetOppo =
|
|
152
|
+
sourceGapped[dirAccessorOpposite] < targetGapped[dirAccessorOpposite];
|
|
153
|
+
const flipSourceTarget =
|
|
154
|
+
(sourceDir[dirAccessor] === 1 &&
|
|
155
|
+
((!isSameDir && sourceGtTargetOppo) || (isSameDir && sourceLtTargetOppo))) ||
|
|
156
|
+
(sourceDir[dirAccessor] !== 1 &&
|
|
157
|
+
((!isSameDir && sourceLtTargetOppo) || (isSameDir && sourceGtTargetOppo)));
|
|
158
|
+
|
|
159
|
+
if (flipSourceTarget) {
|
|
160
|
+
points = dirAccessor === 'x' ? sourceTarget : targetSource;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const sourceGapPoint = { x: sourceGapped.x, y: sourceGapped.y };
|
|
164
|
+
const targetGapPoint = { x: targetGapped.x, y: targetGapped.y };
|
|
165
|
+
const maxXDistance = Math.max(
|
|
166
|
+
Math.abs(sourceGapPoint.x - points[0].x),
|
|
167
|
+
Math.abs(targetGapPoint.x - points[0].x)
|
|
168
|
+
);
|
|
169
|
+
const maxYDistance = Math.max(
|
|
170
|
+
Math.abs(sourceGapPoint.y - points[0].y),
|
|
171
|
+
Math.abs(targetGapPoint.y - points[0].y)
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
if (maxXDistance >= maxYDistance) {
|
|
175
|
+
centerX = (sourceGapPoint.x + targetGapPoint.x) / 2;
|
|
176
|
+
centerY = points[0].y;
|
|
177
|
+
} else {
|
|
178
|
+
centerX = points[0].x;
|
|
179
|
+
centerY = (sourceGapPoint.y + targetGapPoint.y) / 2;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const pathPoints = [
|
|
184
|
+
source,
|
|
185
|
+
{ x: sourceGapped.x, y: sourceGapped.y },
|
|
186
|
+
...points,
|
|
187
|
+
{ x: targetGapped.x, y: targetGapped.y },
|
|
188
|
+
target,
|
|
189
|
+
];
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
points: pathPoints,
|
|
193
|
+
center: {
|
|
194
|
+
x: centerX,
|
|
195
|
+
y: centerY,
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function getBend(a: IPoint, b: IPoint, c: IPoint): string {
|
|
201
|
+
const bendSize = Math.min(
|
|
202
|
+
Point.getDistance(a, b) / 2,
|
|
203
|
+
Point.getDistance(b, c) / 2,
|
|
204
|
+
EDGE_RADIUS
|
|
205
|
+
);
|
|
206
|
+
const { x, y } = b;
|
|
207
|
+
|
|
208
|
+
// no bend
|
|
209
|
+
if ((a.x === x && x === c.x) || (a.y === y && y === c.y)) {
|
|
210
|
+
return `L${x} ${y}`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// first segment is horizontal
|
|
214
|
+
if (a.y === y) {
|
|
215
|
+
const xDir = a.x < c.x ? -1 : 1;
|
|
216
|
+
const yDir = a.y < c.y ? 1 : -1;
|
|
217
|
+
return `L ${x + bendSize * xDir},${y}Q ${x},${y} ${x},${y + bendSize * yDir}`;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const xDir = a.x < c.x ? 1 : -1;
|
|
221
|
+
const yDir = a.y < c.y ? -1 : 1;
|
|
222
|
+
return `L ${x},${y + bendSize * yDir}Q ${x},${y} ${x + bendSize * xDir},${y}`;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* 实现 reactFlow 原本的折叠线交互
|
|
227
|
+
*/
|
|
228
|
+
export function getSmoothStepPath(points: IPoint[]): string {
|
|
229
|
+
const path = points.reduce<string>((res, p, i) => {
|
|
230
|
+
let segment = '';
|
|
231
|
+
|
|
232
|
+
if (i > 0 && i < points.length - 1) {
|
|
233
|
+
segment = getBend(points[i - 1], p, points[i + 1]);
|
|
234
|
+
} else {
|
|
235
|
+
segment = `${i === 0 ? 'M' : 'L'}${p.x} ${p.y}`;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
res += segment;
|
|
239
|
+
|
|
240
|
+
return res;
|
|
241
|
+
}, '');
|
|
242
|
+
|
|
243
|
+
return path;
|
|
244
|
+
}
|
|
245
|
+
export function getBounds(points: IPoint[]): Rectangle {
|
|
246
|
+
const xList = points.map((p) => p.x);
|
|
247
|
+
const yList = points.map((p) => p.y);
|
|
248
|
+
const left = Math.min(...xList);
|
|
249
|
+
const right = Math.max(...xList);
|
|
250
|
+
const top = Math.min(...yList);
|
|
251
|
+
const bottom = Math.max(...yList);
|
|
252
|
+
return Rectangle.createRectangleWithTwoPoints(
|
|
253
|
+
{
|
|
254
|
+
x: left,
|
|
255
|
+
y: top,
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
x: right,
|
|
259
|
+
y: bottom,
|
|
260
|
+
}
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* 计算点到折线的最短距离
|
|
265
|
+
* @param points 折线的所有端点
|
|
266
|
+
* @param pos 待测试点
|
|
267
|
+
* @returns 最短距离
|
|
268
|
+
*/
|
|
269
|
+
export const getFoldLineToPointDistance = (points: IPoint[], pos: IPoint): number => {
|
|
270
|
+
// 特殊情况处理
|
|
271
|
+
if (points.length === 0) {
|
|
272
|
+
return Infinity;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (points.length === 1) {
|
|
276
|
+
return Point.getDistance(points[0]!, pos);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// 构建线段数组
|
|
280
|
+
const lines: [IPoint, IPoint][] = [];
|
|
281
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
282
|
+
lines.push([points[i]!, points[i + 1]!]);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// 计算点到每个线段的最短距离
|
|
286
|
+
const distances = lines.map((line) => {
|
|
287
|
+
const [p1, p2] = line;
|
|
288
|
+
return getPointToSegmentDistance(pos, p1, p2);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
return Math.min(...distances);
|
|
292
|
+
};
|
|
293
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { IPoint, Rectangle } from '@flowgram-vue/utils';
|
|
7
|
+
import {
|
|
8
|
+
WorkflowLineEntity,
|
|
9
|
+
WorkflowLineRenderContribution,
|
|
10
|
+
LinePoint,
|
|
11
|
+
LineCenterPoint,
|
|
12
|
+
} from '@flowgram-vue/free-layout-core';
|
|
13
|
+
import { LineType } from '@flowgram-vue/free-layout-core';
|
|
14
|
+
|
|
15
|
+
import { posWithShrink, toRelative } from '../utils';
|
|
16
|
+
import { FoldLine } from './fold-line';
|
|
17
|
+
|
|
18
|
+
export interface FoldData {
|
|
19
|
+
points: IPoint[];
|
|
20
|
+
path: string;
|
|
21
|
+
bbox: Rectangle;
|
|
22
|
+
center: LineCenterPoint;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class WorkflowFoldLineContribution implements WorkflowLineRenderContribution {
|
|
26
|
+
public static type = LineType.LINE_CHART;
|
|
27
|
+
|
|
28
|
+
public entity: WorkflowLineEntity;
|
|
29
|
+
|
|
30
|
+
constructor(entity: WorkflowLineEntity) {
|
|
31
|
+
this.entity = entity;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private data?: FoldData;
|
|
35
|
+
|
|
36
|
+
public get path(): string {
|
|
37
|
+
return this.data?.path ?? '';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
public calcDistance(pos: IPoint): number {
|
|
41
|
+
if (!this.data) {
|
|
42
|
+
return Number.MAX_SAFE_INTEGER;
|
|
43
|
+
}
|
|
44
|
+
return FoldLine.getFoldLineToPointDistance(this.data.points, pos);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public get bounds(): Rectangle {
|
|
48
|
+
if (!this.data) {
|
|
49
|
+
return new Rectangle();
|
|
50
|
+
}
|
|
51
|
+
return this.data.bbox;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
get center() {
|
|
55
|
+
return this.data?.center;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
public update(params: { fromPos: LinePoint; toPos: LinePoint }): void {
|
|
59
|
+
const { fromPos, toPos } = params;
|
|
60
|
+
const shrink = this.entity.uiState.shrink;
|
|
61
|
+
|
|
62
|
+
// 根据方向预先计算源点和目标点的偏移
|
|
63
|
+
const source = posWithShrink(fromPos, fromPos.location, shrink);
|
|
64
|
+
const target = posWithShrink(toPos, toPos.location, shrink);
|
|
65
|
+
|
|
66
|
+
const { points, center } = FoldLine.getPoints({
|
|
67
|
+
source: {
|
|
68
|
+
...source,
|
|
69
|
+
location: fromPos.location,
|
|
70
|
+
},
|
|
71
|
+
target: {
|
|
72
|
+
...target,
|
|
73
|
+
location: toPos.location,
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const bbox = FoldLine.getBounds(points);
|
|
78
|
+
|
|
79
|
+
// 调整所有点到 SVG 视口坐标系
|
|
80
|
+
const adjustedPoints = points.map((p) => toRelative(p, bbox));
|
|
81
|
+
|
|
82
|
+
const path = FoldLine.getSmoothStepPath(adjustedPoints);
|
|
83
|
+
|
|
84
|
+
const relativeCenter = toRelative(center, bbox);
|
|
85
|
+
this.data = {
|
|
86
|
+
points,
|
|
87
|
+
path,
|
|
88
|
+
bbox,
|
|
89
|
+
center: {
|
|
90
|
+
x: center.x,
|
|
91
|
+
y: center.y,
|
|
92
|
+
labelX: relativeCenter.x,
|
|
93
|
+
labelY: relativeCenter.y,
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { IPoint, Point, Rectangle } from '@flowgram-vue/utils';
|
|
7
|
+
import {
|
|
8
|
+
WorkflowLineEntity,
|
|
9
|
+
WorkflowLineRenderContribution,
|
|
10
|
+
LinePoint,
|
|
11
|
+
LineCenterPoint,
|
|
12
|
+
getLineCenter,
|
|
13
|
+
LineType,
|
|
14
|
+
} from '@flowgram-vue/free-layout-core';
|
|
15
|
+
|
|
16
|
+
import { LINE_PADDING } from '../../constants/lines';
|
|
17
|
+
import { projectPointOnLine } from './point-on-line';
|
|
18
|
+
import { posWithShrink } from '../utils';
|
|
19
|
+
|
|
20
|
+
export interface StraightData {
|
|
21
|
+
points: IPoint[];
|
|
22
|
+
path: string;
|
|
23
|
+
bbox: Rectangle;
|
|
24
|
+
center: LineCenterPoint;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class WorkflowStraightLineContribution implements WorkflowLineRenderContribution {
|
|
28
|
+
public static type = LineType.STRAIGHT;
|
|
29
|
+
|
|
30
|
+
public entity: WorkflowLineEntity;
|
|
31
|
+
|
|
32
|
+
constructor(entity: WorkflowLineEntity) {
|
|
33
|
+
this.entity = entity;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private data?: StraightData;
|
|
37
|
+
|
|
38
|
+
public get path(): string {
|
|
39
|
+
return this.data?.path ?? '';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public calcDistance(pos: IPoint): number {
|
|
43
|
+
if (!this.data) {
|
|
44
|
+
return Number.MAX_SAFE_INTEGER;
|
|
45
|
+
}
|
|
46
|
+
const [start, end] = this.data.points;
|
|
47
|
+
return Point.getDistance(pos, projectPointOnLine(pos, start, end));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public get bounds(): Rectangle {
|
|
51
|
+
if (!this.data) {
|
|
52
|
+
return new Rectangle();
|
|
53
|
+
}
|
|
54
|
+
return this.data.bbox;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
get center() {
|
|
58
|
+
return this.data?.center;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
public update(params: { fromPos: LinePoint; toPos: LinePoint }): void {
|
|
62
|
+
const { fromPos, toPos } = params;
|
|
63
|
+
const shrink = this.entity.uiState.shrink;
|
|
64
|
+
|
|
65
|
+
// 根据方向预先计算源点和目标点的偏移
|
|
66
|
+
const source = posWithShrink(fromPos, fromPos.location, shrink);
|
|
67
|
+
const target = posWithShrink(toPos, toPos.location, shrink);
|
|
68
|
+
|
|
69
|
+
const points = [source, target];
|
|
70
|
+
|
|
71
|
+
const bbox = Rectangle.createRectangleWithTwoPoints(points[0], points[1]);
|
|
72
|
+
|
|
73
|
+
// 调整所有点到 SVG 视口坐标系
|
|
74
|
+
const adjustedPoints = points.map((p) => ({
|
|
75
|
+
x: p.x - bbox.x + LINE_PADDING,
|
|
76
|
+
y: p.y - bbox.y + LINE_PADDING,
|
|
77
|
+
}));
|
|
78
|
+
|
|
79
|
+
// 生成直线路径
|
|
80
|
+
const path = `M ${adjustedPoints[0].x} ${adjustedPoints[0].y} L ${adjustedPoints[1].x} ${adjustedPoints[1].y}`;
|
|
81
|
+
|
|
82
|
+
this.data = {
|
|
83
|
+
points,
|
|
84
|
+
path,
|
|
85
|
+
bbox,
|
|
86
|
+
center: getLineCenter(fromPos, toPos, bbox, LINE_PADDING),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { IPoint, Rectangle } from '@flowgram-vue/utils';
|
|
7
|
+
|
|
8
|
+
export interface StraightData {
|
|
9
|
+
points: IPoint[];
|
|
10
|
+
path: string;
|
|
11
|
+
bbox: Rectangle;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 计算点到直线的投影点
|
|
16
|
+
*/
|
|
17
|
+
export function projectPointOnLine(point: IPoint, lineStart: IPoint, lineEnd: IPoint): IPoint {
|
|
18
|
+
const dx = lineEnd.x - lineStart.x;
|
|
19
|
+
const dy = lineEnd.y - lineStart.y;
|
|
20
|
+
|
|
21
|
+
// 如果是垂直线
|
|
22
|
+
if (dx === 0) {
|
|
23
|
+
return { x: lineStart.x, y: point.y };
|
|
24
|
+
}
|
|
25
|
+
// 如果是水平线
|
|
26
|
+
if (dy === 0) {
|
|
27
|
+
return { x: point.x, y: lineStart.y };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const t = ((point.x - lineStart.x) * dx + (point.y - lineStart.y) * dy) / (dx * dx + dy * dy);
|
|
31
|
+
const clampedT = Math.max(0, Math.min(1, t));
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
x: lineStart.x + clampedT * dx,
|
|
35
|
+
y: lineStart.y + clampedT * dy,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { IPoint, Rectangle } from '@flowgram-vue/utils';
|
|
7
|
+
import { LinePointLocation } from '@flowgram-vue/free-layout-core';
|
|
8
|
+
|
|
9
|
+
import { LINE_PADDING } from '../constants/lines';
|
|
10
|
+
|
|
11
|
+
export function toRelative(p: IPoint, bbox: Rectangle): IPoint {
|
|
12
|
+
return {
|
|
13
|
+
x: p.x - bbox.x + LINE_PADDING,
|
|
14
|
+
y: p.y - bbox.y + LINE_PADDING,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function getShrinkOffset(location: LinePointLocation, shrink: number): IPoint {
|
|
19
|
+
switch (location) {
|
|
20
|
+
case 'left':
|
|
21
|
+
return { x: -shrink, y: 0 };
|
|
22
|
+
case 'right':
|
|
23
|
+
return { x: shrink, y: 0 };
|
|
24
|
+
case 'bottom':
|
|
25
|
+
return { x: 0, y: shrink };
|
|
26
|
+
case 'top':
|
|
27
|
+
return { x: 0, y: -shrink };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function posWithShrink(pos: IPoint, location: LinePointLocation, shrink: number): IPoint {
|
|
32
|
+
const offset = getShrinkOffset(location, shrink);
|
|
33
|
+
return {
|
|
34
|
+
x: pos.x + offset.x,
|
|
35
|
+
y: pos.y + offset.y,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { WorkflowLinesManager } from '@flowgram-vue/free-layout-core';
|
|
7
|
+
import { definePluginCreator, PluginContext } from '@flowgram-vue/core';
|
|
8
|
+
|
|
9
|
+
import { FreeLinesPluginOptions } from './type';
|
|
10
|
+
import { WorkflowLinesLayer } from './layer';
|
|
11
|
+
import {
|
|
12
|
+
WorkflowBezierLineContribution,
|
|
13
|
+
WorkflowFoldLineContribution,
|
|
14
|
+
WorkflowStraightLineContribution,
|
|
15
|
+
} from './contributions';
|
|
16
|
+
|
|
17
|
+
export const createFreeLinesPlugin = definePluginCreator({
|
|
18
|
+
singleton: true,
|
|
19
|
+
onInit: (ctx: PluginContext, opts: FreeLinesPluginOptions) => {
|
|
20
|
+
ctx.playground.registerLayer(WorkflowLinesLayer, {
|
|
21
|
+
...opts,
|
|
22
|
+
});
|
|
23
|
+
if (opts.defaultLineUIState) {
|
|
24
|
+
ctx.container.get(WorkflowLinesManager).setDefaultUIState(opts.defaultLineUIState);
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
onReady: (ctx: PluginContext, opts: FreeLinesPluginOptions) => {
|
|
28
|
+
const linesManager = ctx.container.get(WorkflowLinesManager);
|
|
29
|
+
linesManager
|
|
30
|
+
.registerContribution(WorkflowBezierLineContribution)
|
|
31
|
+
.registerContribution(WorkflowFoldLineContribution)
|
|
32
|
+
.registerContribution(WorkflowStraightLineContribution);
|
|
33
|
+
|
|
34
|
+
if (opts.contributions) {
|
|
35
|
+
opts.contributions.forEach((contribution) => {
|
|
36
|
+
linesManager.registerContribution(contribution);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (opts.defaultLineType) {
|
|
41
|
+
linesManager.switchLineType(opts.defaultLineType);
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
});
|
package/src/css.d.ts
ADDED
package/src/env.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
declare module '*.vue' {
|
|
7
|
+
import type { DefineComponent } from 'vue';
|
|
8
|
+
const component: DefineComponent<object, object, unknown>;
|
|
9
|
+
export default component;
|
|
10
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export * from './components/workflow-port-render';
|
|
7
|
+
export * from './constants/lines';
|
|
8
|
+
export * from './create-free-lines-plugin';
|
|
9
|
+
export * from './layer';
|
|
10
|
+
export * from './type';
|
|
11
|
+
export * from './types/arrow-renderer';
|
|
12
|
+
export * from './contributions';
|