@flowgram-vue/free-snap-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 +1036 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +112 -0
- package/dist/index.js +1034 -0
- package/dist/index.js.map +1 -0
- package/package.json +57 -0
- package/src/constant.ts +22 -0
- package/src/create-plugin.ts +34 -0
- package/src/env.d.ts +10 -0
- package/src/index.ts +7 -0
- package/src/layer.ts +518 -0
- package/src/service.ts +703 -0
- package/src/type.ts +98 -0
- package/src/utils.ts +44 -0
package/src/layer.ts
ADDED
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Fragment, h, type VNode } from 'vue';
|
|
7
|
+
import { inject, injectable } from 'inversify';
|
|
8
|
+
import { domUtils } from '@flowgram-vue/utils';
|
|
9
|
+
import { Rectangle } from '@flowgram-vue/utils';
|
|
10
|
+
import { WorkflowDocument } from '@flowgram-vue/free-layout-core';
|
|
11
|
+
import { FlowNodeTransformData } from '@flowgram-vue/document';
|
|
12
|
+
import { Layer } from '@flowgram-vue/core';
|
|
13
|
+
|
|
14
|
+
import { isEqual, isGreaterThan, isNumber } from './utils';
|
|
15
|
+
import { AlignRect, SnapEvent, WorkflowSnapLayerOptions } from './type';
|
|
16
|
+
import { WorkflowSnapService } from './service';
|
|
17
|
+
|
|
18
|
+
interface SnapRenderLine {
|
|
19
|
+
className: string;
|
|
20
|
+
sourceNode: string;
|
|
21
|
+
top: number;
|
|
22
|
+
left: number;
|
|
23
|
+
width: number;
|
|
24
|
+
height: number;
|
|
25
|
+
dashed?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@injectable()
|
|
29
|
+
export class WorkflowSnapLayer extends Layer<WorkflowSnapLayerOptions> {
|
|
30
|
+
public static type = 'WorkflowSnapLayer';
|
|
31
|
+
|
|
32
|
+
@inject(WorkflowDocument) private readonly document: WorkflowDocument;
|
|
33
|
+
|
|
34
|
+
@inject(WorkflowSnapService) private readonly service: WorkflowSnapService;
|
|
35
|
+
|
|
36
|
+
public readonly node = domUtils.createDivWithClass(
|
|
37
|
+
'gedit-playground-layer gedit-flow-snap-layer'
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
private edgeLines: SnapRenderLine[] = [];
|
|
41
|
+
|
|
42
|
+
private alignLines: SnapRenderLine[] = [];
|
|
43
|
+
|
|
44
|
+
public onReady(): void {
|
|
45
|
+
this.node.style.zIndex = '9999';
|
|
46
|
+
this.toDispose.pushAll([
|
|
47
|
+
this.service.onSnap((event: SnapEvent) => {
|
|
48
|
+
this.edgeLines = this.calcEdgeLines(event);
|
|
49
|
+
this.alignLines = this.calcAlignLines(event);
|
|
50
|
+
this.render();
|
|
51
|
+
}),
|
|
52
|
+
]);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
public render(): VNode {
|
|
56
|
+
const children: VNode[] = [];
|
|
57
|
+
if (this.alignLines.length > 0) {
|
|
58
|
+
children.push(
|
|
59
|
+
h('div', { class: 'workflow-snap-align-lines' }, this.renderAlignLines())
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
if (this.edgeLines.length > 0) {
|
|
63
|
+
children.push(h('div', { class: 'workflow-snap-edge-lines' }, this.renderEdgeLines()));
|
|
64
|
+
}
|
|
65
|
+
return h(Fragment, null, children);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
public onZoom(scale: number): void {
|
|
69
|
+
this.node.style.transform = `scale(${scale})`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private renderEdgeLines(): VNode[] {
|
|
73
|
+
return this.edgeLines.map((renderLine: SnapRenderLine) => {
|
|
74
|
+
const { className, sourceNode, top, left, width, height, dashed } = renderLine;
|
|
75
|
+
const id = `${className}-${sourceNode}-${top}-${left}-${width}-${height}`;
|
|
76
|
+
const isHorizontal = width < height;
|
|
77
|
+
const border = `${this.options.edgeLineWidth}px ${dashed ? 'dashed' : 'solid'} ${
|
|
78
|
+
this.options.edgeColor
|
|
79
|
+
}`;
|
|
80
|
+
return h('div', {
|
|
81
|
+
class: `workflow-snap-edge-line ${className}`,
|
|
82
|
+
'data-testid': 'sdk.workflow.canvas.snap.edgeLine',
|
|
83
|
+
'data-snap-line-id': id,
|
|
84
|
+
'data-snap-line-source-node': sourceNode,
|
|
85
|
+
key: id,
|
|
86
|
+
style: {
|
|
87
|
+
top: `${top}px`,
|
|
88
|
+
left: `${left}px`,
|
|
89
|
+
width: `${width}px`,
|
|
90
|
+
height: `${height}px`,
|
|
91
|
+
position: 'absolute',
|
|
92
|
+
borderLeft: isHorizontal ? border : 'none',
|
|
93
|
+
borderTop: !isHorizontal ? border : 'none',
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private renderAlignLines(): VNode[] {
|
|
100
|
+
return this.alignLines.map((renderLine: SnapRenderLine) => {
|
|
101
|
+
const id = `${renderLine.className}-${renderLine.sourceNode}-${renderLine.top}-${renderLine.left}-${renderLine.width}-${renderLine.height}`;
|
|
102
|
+
const isHorizontal = isGreaterThan(renderLine.width, renderLine.height);
|
|
103
|
+
const alignLineWidth = this.options.alignLineWidth; // 整体线条粗细
|
|
104
|
+
const alignCrossWidth = this.options.alignCrossWidth; // 工字形横线的长度
|
|
105
|
+
|
|
106
|
+
// 调整渲染位置以保持居中
|
|
107
|
+
const adjustedTop = isHorizontal ? renderLine.top - alignLineWidth / 2 : renderLine.top;
|
|
108
|
+
const adjustedLeft = isHorizontal ? renderLine.left : renderLine.left - alignLineWidth / 2;
|
|
109
|
+
|
|
110
|
+
return h(
|
|
111
|
+
'div',
|
|
112
|
+
{
|
|
113
|
+
class: `workflow-snap-align-line ${renderLine.className}`,
|
|
114
|
+
'data-testid': 'sdk.workflow.canvas.snap.alignLine',
|
|
115
|
+
'data-snap-line-id': id,
|
|
116
|
+
'data-snap-line-source-node': renderLine.sourceNode,
|
|
117
|
+
key: id,
|
|
118
|
+
style: {
|
|
119
|
+
position: 'absolute',
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
[
|
|
123
|
+
h('div', {
|
|
124
|
+
style: {
|
|
125
|
+
position: 'absolute',
|
|
126
|
+
top: `${adjustedTop}px`,
|
|
127
|
+
left: `${adjustedLeft}px`,
|
|
128
|
+
width: `${isHorizontal ? renderLine.width : alignLineWidth}px`,
|
|
129
|
+
height: `${isHorizontal ? alignLineWidth : renderLine.height}px`,
|
|
130
|
+
backgroundColor: this.options.alignColor,
|
|
131
|
+
},
|
|
132
|
+
}),
|
|
133
|
+
h('div', {
|
|
134
|
+
style: {
|
|
135
|
+
position: 'absolute',
|
|
136
|
+
top: `${
|
|
137
|
+
isHorizontal
|
|
138
|
+
? adjustedTop - (alignCrossWidth - alignLineWidth) / 2
|
|
139
|
+
: adjustedTop
|
|
140
|
+
}px`,
|
|
141
|
+
left: `${
|
|
142
|
+
isHorizontal
|
|
143
|
+
? adjustedLeft
|
|
144
|
+
: adjustedLeft - (alignCrossWidth - alignLineWidth) / 2
|
|
145
|
+
}px`,
|
|
146
|
+
width: `${isHorizontal ? alignLineWidth : alignCrossWidth}px`,
|
|
147
|
+
height: `${isHorizontal ? alignCrossWidth : alignLineWidth}px`,
|
|
148
|
+
backgroundColor: this.options.alignColor,
|
|
149
|
+
},
|
|
150
|
+
}),
|
|
151
|
+
h('div', {
|
|
152
|
+
style: {
|
|
153
|
+
position: 'absolute',
|
|
154
|
+
top: `${
|
|
155
|
+
isHorizontal
|
|
156
|
+
? adjustedTop - (alignCrossWidth - alignLineWidth) / 2
|
|
157
|
+
: adjustedTop + renderLine.height - alignLineWidth
|
|
158
|
+
}px`,
|
|
159
|
+
left: `${
|
|
160
|
+
isHorizontal
|
|
161
|
+
? adjustedLeft + renderLine.width - alignLineWidth
|
|
162
|
+
: adjustedLeft - (alignCrossWidth - alignLineWidth) / 2
|
|
163
|
+
}px`,
|
|
164
|
+
width: `${isHorizontal ? alignLineWidth : alignCrossWidth}px`,
|
|
165
|
+
height: `${isHorizontal ? alignCrossWidth : alignLineWidth}px`,
|
|
166
|
+
backgroundColor: this.options.alignColor,
|
|
167
|
+
},
|
|
168
|
+
}),
|
|
169
|
+
]
|
|
170
|
+
);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private calcEdgeLines(event: SnapEvent): SnapRenderLine[] {
|
|
175
|
+
const { alignRects, snapRect, snapEdgeLines } = event;
|
|
176
|
+
const edgeLines: SnapRenderLine[] = [];
|
|
177
|
+
|
|
178
|
+
const topFullAlign = this.directionFullAlign({
|
|
179
|
+
alignRects: alignRects.top,
|
|
180
|
+
targetRect: snapRect,
|
|
181
|
+
isVertical: true,
|
|
182
|
+
});
|
|
183
|
+
const bottomFullAlign = this.directionFullAlign({
|
|
184
|
+
alignRects: alignRects.bottom,
|
|
185
|
+
targetRect: snapRect,
|
|
186
|
+
isVertical: true,
|
|
187
|
+
});
|
|
188
|
+
const leftFullAlign = this.directionFullAlign({
|
|
189
|
+
alignRects: alignRects.left,
|
|
190
|
+
targetRect: snapRect,
|
|
191
|
+
isVertical: false,
|
|
192
|
+
});
|
|
193
|
+
const rightFullAlign = this.directionFullAlign({
|
|
194
|
+
alignRects: alignRects.right,
|
|
195
|
+
targetRect: snapRect,
|
|
196
|
+
isVertical: false,
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// 处理顶部对齐
|
|
200
|
+
if (topFullAlign) {
|
|
201
|
+
const top = topFullAlign.rect.top;
|
|
202
|
+
const height = bottomFullAlign
|
|
203
|
+
? snapRect.bottom - snapRect.height / 2 - top
|
|
204
|
+
: snapRect.bottom - top;
|
|
205
|
+
const width = this.options.edgeLineWidth;
|
|
206
|
+
const lineData = {
|
|
207
|
+
top,
|
|
208
|
+
width,
|
|
209
|
+
height,
|
|
210
|
+
};
|
|
211
|
+
edgeLines.push({
|
|
212
|
+
className: 'edge-full-top-left',
|
|
213
|
+
sourceNode: topFullAlign.sourceNodeId,
|
|
214
|
+
left: snapRect.left,
|
|
215
|
+
...lineData,
|
|
216
|
+
});
|
|
217
|
+
edgeLines.push({
|
|
218
|
+
className: 'edge-full-top-right',
|
|
219
|
+
sourceNode: topFullAlign.sourceNodeId,
|
|
220
|
+
left: snapRect.right - 1,
|
|
221
|
+
...lineData,
|
|
222
|
+
});
|
|
223
|
+
edgeLines.push({
|
|
224
|
+
className: 'edge-full-top-mid',
|
|
225
|
+
sourceNode: topFullAlign.sourceNodeId,
|
|
226
|
+
left: snapRect.left + snapRect.width / 2,
|
|
227
|
+
dashed: true,
|
|
228
|
+
...lineData,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// 处理底部对齐
|
|
233
|
+
if (bottomFullAlign) {
|
|
234
|
+
const top = topFullAlign ? snapRect.top + snapRect.height / 2 : snapRect.top;
|
|
235
|
+
const height = bottomFullAlign.rect.bottom - top;
|
|
236
|
+
const width = this.options.edgeLineWidth;
|
|
237
|
+
const lineData = {
|
|
238
|
+
top,
|
|
239
|
+
width,
|
|
240
|
+
height,
|
|
241
|
+
};
|
|
242
|
+
edgeLines.push({
|
|
243
|
+
className: 'edge-full-bottom-left',
|
|
244
|
+
sourceNode: bottomFullAlign.sourceNodeId,
|
|
245
|
+
left: snapRect.left,
|
|
246
|
+
...lineData,
|
|
247
|
+
});
|
|
248
|
+
edgeLines.push({
|
|
249
|
+
className: 'edge-full-bottom-right',
|
|
250
|
+
sourceNode: bottomFullAlign.sourceNodeId,
|
|
251
|
+
left: snapRect.right - 1,
|
|
252
|
+
...lineData,
|
|
253
|
+
});
|
|
254
|
+
edgeLines.push({
|
|
255
|
+
className: 'edge-full-bottom-mid',
|
|
256
|
+
sourceNode: bottomFullAlign.sourceNodeId,
|
|
257
|
+
left: snapRect.left + snapRect.width / 2,
|
|
258
|
+
dashed: true,
|
|
259
|
+
...lineData,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// 处理左侧对齐
|
|
264
|
+
if (leftFullAlign) {
|
|
265
|
+
const left = leftFullAlign.rect.left;
|
|
266
|
+
const width = rightFullAlign
|
|
267
|
+
? snapRect.right - snapRect.width / 2 - left
|
|
268
|
+
: snapRect.right - left;
|
|
269
|
+
const height = this.options.edgeLineWidth;
|
|
270
|
+
const lineData = {
|
|
271
|
+
left,
|
|
272
|
+
width,
|
|
273
|
+
height,
|
|
274
|
+
};
|
|
275
|
+
edgeLines.push({
|
|
276
|
+
className: 'edge-full-left-top',
|
|
277
|
+
sourceNode: leftFullAlign.sourceNodeId,
|
|
278
|
+
top: snapRect.top,
|
|
279
|
+
...lineData,
|
|
280
|
+
});
|
|
281
|
+
edgeLines.push({
|
|
282
|
+
className: 'edge-full-left-bottom',
|
|
283
|
+
sourceNode: leftFullAlign.sourceNodeId,
|
|
284
|
+
top: snapRect.bottom - 1,
|
|
285
|
+
...lineData,
|
|
286
|
+
});
|
|
287
|
+
edgeLines.push({
|
|
288
|
+
className: 'edge-full-left-mid',
|
|
289
|
+
sourceNode: leftFullAlign.sourceNodeId,
|
|
290
|
+
top: snapRect.top + snapRect.height / 2,
|
|
291
|
+
dashed: true,
|
|
292
|
+
...lineData,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// 处理右侧对齐
|
|
297
|
+
if (rightFullAlign) {
|
|
298
|
+
const left = leftFullAlign ? snapRect.left + snapRect.width / 2 : snapRect.left;
|
|
299
|
+
const width = rightFullAlign.rect.right - left;
|
|
300
|
+
const height = this.options.edgeLineWidth;
|
|
301
|
+
const lineData = {
|
|
302
|
+
left,
|
|
303
|
+
width,
|
|
304
|
+
height,
|
|
305
|
+
};
|
|
306
|
+
edgeLines.push({
|
|
307
|
+
className: 'edge-full-right-top',
|
|
308
|
+
sourceNode: rightFullAlign.sourceNodeId,
|
|
309
|
+
top: snapRect.top,
|
|
310
|
+
...lineData,
|
|
311
|
+
});
|
|
312
|
+
edgeLines.push({
|
|
313
|
+
className: 'edge-full-right-bottom',
|
|
314
|
+
sourceNode: rightFullAlign.sourceNodeId,
|
|
315
|
+
top: snapRect.bottom - 1,
|
|
316
|
+
...lineData,
|
|
317
|
+
});
|
|
318
|
+
edgeLines.push({
|
|
319
|
+
className: 'edge-full-right-mid',
|
|
320
|
+
sourceNode: rightFullAlign.sourceNodeId,
|
|
321
|
+
top: snapRect.top + snapRect.height / 2,
|
|
322
|
+
dashed: true,
|
|
323
|
+
...lineData,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const snappedEdgeLines = Object.entries(snapEdgeLines)
|
|
328
|
+
.map(([direction, snapLine]) => {
|
|
329
|
+
if (!snapLine) {
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
const sourceNode = this.document.getNode(snapLine.sourceNodeId);
|
|
333
|
+
if (!sourceNode) {
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
const nodeRect = sourceNode.getData(FlowNodeTransformData).bounds;
|
|
337
|
+
if (isNumber(snapLine.x)) {
|
|
338
|
+
// 垂直
|
|
339
|
+
const top = Math.min(nodeRect.top, snapRect.top);
|
|
340
|
+
const bottom = Math.max(nodeRect.bottom, snapRect.bottom);
|
|
341
|
+
const height = bottom - top;
|
|
342
|
+
const left = direction === 'right' ? snapLine.x - 1 : snapLine.x;
|
|
343
|
+
const width = this.options.edgeLineWidth;
|
|
344
|
+
const isMidX = direction === 'midVertical';
|
|
345
|
+
const lineData: SnapRenderLine = {
|
|
346
|
+
className: `edge-snapped-${direction}`,
|
|
347
|
+
sourceNode: snapLine.sourceNodeId,
|
|
348
|
+
top,
|
|
349
|
+
left,
|
|
350
|
+
width,
|
|
351
|
+
height,
|
|
352
|
+
dashed: isMidX,
|
|
353
|
+
};
|
|
354
|
+
const onTop = top === nodeRect.top;
|
|
355
|
+
if (onTop && topFullAlign) {
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
if (!onTop && bottomFullAlign) {
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
return lineData;
|
|
362
|
+
} else if (isNumber(snapLine.y)) {
|
|
363
|
+
// 水平
|
|
364
|
+
const left = Math.min(nodeRect.left, snapRect.left);
|
|
365
|
+
const right = Math.max(nodeRect.right, snapRect.right);
|
|
366
|
+
const width = right - left;
|
|
367
|
+
const top = direction === 'bottom' ? snapLine.y - 1 : snapLine.y;
|
|
368
|
+
const height = this.options.edgeLineWidth;
|
|
369
|
+
const isMidY = direction === 'midHorizontal';
|
|
370
|
+
const lineData: SnapRenderLine = {
|
|
371
|
+
className: `edge-snapped-${direction}`,
|
|
372
|
+
sourceNode: snapLine.sourceNodeId,
|
|
373
|
+
top,
|
|
374
|
+
left,
|
|
375
|
+
width,
|
|
376
|
+
height,
|
|
377
|
+
dashed: isMidY,
|
|
378
|
+
};
|
|
379
|
+
const onLeft = left === nodeRect.left;
|
|
380
|
+
if (onLeft && leftFullAlign) {
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
if (!onLeft && rightFullAlign) {
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
return lineData;
|
|
387
|
+
}
|
|
388
|
+
})
|
|
389
|
+
.filter(Boolean) as SnapRenderLine[];
|
|
390
|
+
|
|
391
|
+
edgeLines.push(...snappedEdgeLines);
|
|
392
|
+
|
|
393
|
+
return edgeLines;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
private directionFullAlign(params: {
|
|
397
|
+
alignRects: AlignRect[];
|
|
398
|
+
targetRect: Rectangle;
|
|
399
|
+
isVertical: boolean;
|
|
400
|
+
}): AlignRect | undefined {
|
|
401
|
+
const { alignRects, targetRect, isVertical } = params;
|
|
402
|
+
let fullAlignIndex = -1;
|
|
403
|
+
for (let i = 0; i < alignRects.length; i++) {
|
|
404
|
+
const alignRect = alignRects[i];
|
|
405
|
+
const prevRect = alignRects[i - 1]?.rect ?? targetRect;
|
|
406
|
+
const isFullAlign = this.rectFullAlign(alignRect.rect, prevRect, isVertical);
|
|
407
|
+
if (!isFullAlign) {
|
|
408
|
+
// 未对齐则中断
|
|
409
|
+
break; // 用 for 循环 + break 反而比 Array.findIndex 实现可读性更好
|
|
410
|
+
}
|
|
411
|
+
fullAlignIndex = i;
|
|
412
|
+
}
|
|
413
|
+
const fullAlignRect = alignRects[fullAlignIndex];
|
|
414
|
+
return fullAlignRect;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
private rectFullAlign(rectA: Rectangle, rectB: Rectangle, isVertical: boolean): boolean {
|
|
418
|
+
if (isVertical) {
|
|
419
|
+
return isEqual(rectA.left, rectB.left) && isEqual(rectA.right, rectB.right);
|
|
420
|
+
} else {
|
|
421
|
+
return isEqual(rectA.top, rectB.top) && isEqual(rectA.bottom, rectB.bottom);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
private calcAlignLines(event: SnapEvent): SnapRenderLine[] {
|
|
426
|
+
const { alignRects, alignSpacing, snapRect } = event;
|
|
427
|
+
|
|
428
|
+
const topAlignLines = this.calcDirectionAlignLines({
|
|
429
|
+
alignRects: alignRects.top,
|
|
430
|
+
targetRect: snapRect,
|
|
431
|
+
isVertical: true,
|
|
432
|
+
spacing: alignSpacing.midVertical ?? alignSpacing.top,
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
const bottomAlignLines = this.calcDirectionAlignLines({
|
|
436
|
+
alignRects: alignRects.bottom,
|
|
437
|
+
targetRect: snapRect,
|
|
438
|
+
isVertical: true,
|
|
439
|
+
spacing: alignSpacing.midVertical ?? alignSpacing.bottom,
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
const leftAlignLines = this.calcDirectionAlignLines({
|
|
443
|
+
alignRects: alignRects.left,
|
|
444
|
+
targetRect: snapRect,
|
|
445
|
+
isVertical: false,
|
|
446
|
+
spacing: alignSpacing.midHorizontal ?? alignSpacing.left,
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
const rightAlignLines = this.calcDirectionAlignLines({
|
|
450
|
+
alignRects: alignRects.right,
|
|
451
|
+
targetRect: snapRect,
|
|
452
|
+
isVertical: false,
|
|
453
|
+
spacing: alignSpacing.midHorizontal ?? alignSpacing.right,
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
return [...topAlignLines, ...bottomAlignLines, ...leftAlignLines, ...rightAlignLines];
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
private calcDirectionAlignLines(params: {
|
|
460
|
+
alignRects: AlignRect[];
|
|
461
|
+
targetRect: Rectangle;
|
|
462
|
+
isVertical: boolean;
|
|
463
|
+
spacing?: number;
|
|
464
|
+
}) {
|
|
465
|
+
const { alignRects, targetRect, isVertical, spacing } = params;
|
|
466
|
+
const alignLines: SnapRenderLine[] = [];
|
|
467
|
+
if (!spacing) {
|
|
468
|
+
return alignLines;
|
|
469
|
+
}
|
|
470
|
+
for (let i = 0; i < alignRects.length; i++) {
|
|
471
|
+
const alignRect = alignRects[i];
|
|
472
|
+
const rect = alignRect.rect;
|
|
473
|
+
const prevRect = alignRects[i - 1]?.rect ?? targetRect;
|
|
474
|
+
|
|
475
|
+
const betweenSpacing = isVertical
|
|
476
|
+
? Math.min(Math.abs(prevRect.top - rect.bottom), Math.abs(prevRect.bottom - rect.top))
|
|
477
|
+
: Math.min(Math.abs(prevRect.left - rect.right), Math.abs(prevRect.right - rect.left));
|
|
478
|
+
if (!isEqual(betweenSpacing, spacing)) {
|
|
479
|
+
// 不连续,需要中断
|
|
480
|
+
break; // 因为要用到 break,所以不能用 Array.map()
|
|
481
|
+
}
|
|
482
|
+
if (isVertical) {
|
|
483
|
+
const centerX = this.calcHorizontalIntersectionCenter(rect, targetRect);
|
|
484
|
+
alignLines.push({
|
|
485
|
+
className: 'align-vertical',
|
|
486
|
+
sourceNode: alignRect.sourceNodeId,
|
|
487
|
+
top: Math.min(rect.bottom, prevRect.bottom),
|
|
488
|
+
left: centerX,
|
|
489
|
+
width: 1,
|
|
490
|
+
height: spacing,
|
|
491
|
+
});
|
|
492
|
+
} else {
|
|
493
|
+
const centerY = this.calcVerticalIntersectionCenter(rect, targetRect);
|
|
494
|
+
alignLines.push({
|
|
495
|
+
className: 'align-horizontal',
|
|
496
|
+
sourceNode: alignRect.sourceNodeId,
|
|
497
|
+
top: centerY,
|
|
498
|
+
left: Math.min(rect.right, prevRect.right),
|
|
499
|
+
width: spacing,
|
|
500
|
+
height: 1,
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
return alignLines;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
private calcVerticalIntersectionCenter(rectA: Rectangle, rectB: Rectangle): number {
|
|
508
|
+
const top = Math.max(rectA.top, rectB.top);
|
|
509
|
+
const bottom = Math.min(rectA.bottom, rectB.bottom);
|
|
510
|
+
return (top + bottom) / 2;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
private calcHorizontalIntersectionCenter(rectA: Rectangle, rectB: Rectangle): number {
|
|
514
|
+
const left = Math.max(rectA.left, rectB.left);
|
|
515
|
+
const right = Math.min(rectA.right, rectB.right);
|
|
516
|
+
return (left + right) / 2;
|
|
517
|
+
}
|
|
518
|
+
}
|