@flowgram-vue/renderer 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 (50) hide show
  1. package/LICENSE +22 -0
  2. package/dist/index.cjs +2933 -0
  3. package/dist/index.cjs.map +1 -0
  4. package/dist/index.d.ts +721 -0
  5. package/dist/index.js +2918 -0
  6. package/dist/index.js.map +1 -0
  7. package/index.module.less +167 -0
  8. package/package.json +64 -0
  9. package/src/components/Adder.ts +107 -0
  10. package/src/components/BranchDraggableRenderer.ts +77 -0
  11. package/src/components/Collapse.ts +87 -0
  12. package/src/components/CollapseAdder.ts +94 -0
  13. package/src/components/CustomLine.ts +35 -0
  14. package/src/components/LabelsRenderer.ts +162 -0
  15. package/src/components/LinesRenderer.ts +99 -0
  16. package/src/components/MarkerActivatedArrow.ts +44 -0
  17. package/src/components/MarkerArrow.ts +44 -0
  18. package/src/components/RoundedTurningLine.ts +165 -0
  19. package/src/components/StraightLine.ts +31 -0
  20. package/src/components/utils.ts +295 -0
  21. package/src/entities/README.md +3 -0
  22. package/src/entities/flow-drag-entity.ts +267 -0
  23. package/src/entities/flow-select-config-entity.ts +114 -0
  24. package/src/entities/index.ts +8 -0
  25. package/src/entities/selector-box-config-entity.ts +88 -0
  26. package/src/env.d.ts +10 -0
  27. package/src/flow-renderer-container-module.ts +14 -0
  28. package/src/flow-renderer-contribution.ts +12 -0
  29. package/src/flow-renderer-registry.ts +155 -0
  30. package/src/flow-renderer-resize-observer.ts +56 -0
  31. package/src/hooks/use-base-color.ts +26 -0
  32. package/src/index.ts +16 -0
  33. package/src/layer-vue-provide.ts +44 -0
  34. package/src/layers/flow-context-menu-layer.ts +153 -0
  35. package/src/layers/flow-debug-layer.ts +227 -0
  36. package/src/layers/flow-drag-layer.ts +413 -0
  37. package/src/layers/flow-labels-layer.ts +100 -0
  38. package/src/layers/flow-lines-layer.ts +111 -0
  39. package/src/layers/flow-nodes-content-layer.ts +155 -0
  40. package/src/layers/flow-nodes-transform-layer.ts +151 -0
  41. package/src/layers/flow-scroll-bar-layer.ts +401 -0
  42. package/src/layers/flow-scroll-limit-layer.ts +36 -0
  43. package/src/layers/flow-selector-bounds-layer.ts +191 -0
  44. package/src/layers/flow-selector-box-layer.ts +244 -0
  45. package/src/layers/index.ts +16 -0
  46. package/src/utils/element.ts +36 -0
  47. package/src/utils/find-selected-nodes.ts +80 -0
  48. package/src/utils/index.ts +7 -0
  49. package/src/utils/scroll-bar-events.ts +13 -0
  50. package/src/utils/scroll-limit.ts +58 -0
@@ -0,0 +1,295 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import {
7
+ FlowNodeTransformData,
8
+ type FlowNodeTransitionData,
9
+ type FlowTransitionLine,
10
+ FlowTransitionLineEnum,
11
+ type Vertex,
12
+ DefaultSpacingKey,
13
+ DEFAULT_SPACING,
14
+ } from '@flowgram-vue/document';
15
+
16
+ import { BASE_DEFAULT_COLOR } from '../hooks/use-base-color';
17
+
18
+ export const DEFAULT_LINE_ATTRS: Record<string, string> = {
19
+ stroke: BASE_DEFAULT_COLOR,
20
+ fill: 'transparent',
21
+ strokeLinecap: 'round',
22
+ strokeLinejoin: 'round',
23
+ };
24
+
25
+ // 默认的圆角半径
26
+ export const DEFAULT_RADIUS = DEFAULT_SPACING[DefaultSpacingKey.ROUNDED_LINE_RADIUS];
27
+
28
+ // 小圆角
29
+ export const MINI_RADIUS = 10;
30
+
31
+ // 默认 label 激活高度
32
+ export const DEFAULT_LABEL_ACTIVATE_HEIGHT = 32;
33
+
34
+ /**
35
+ * 根据椭圆方程计算 y 坐标
36
+ *
37
+ * x^2 / rx^2 + y^2 / ry^2 = 1
38
+ */
39
+ export const calcEllipseY = (x: number, rx: number, ry: number) =>
40
+ Math.sqrt(ry ** 2 * (1 - x ** 2 / rx ** 2));
41
+
42
+ /**
43
+ * 获取转弯线的转折点 (水平布局)
44
+ */
45
+ export function getHorizontalVertices(
46
+ line: FlowTransitionLine,
47
+ xRadius = 16,
48
+ yRadius = 20
49
+ ): Vertex[] {
50
+ const { from, to, type } = line || {};
51
+
52
+ // 空间可以容纳的圆角数
53
+ const deltaY = Math.abs(to.y - from.y);
54
+ const deltaX = Math.abs(to.x - from.x);
55
+
56
+ const radiusXCount = deltaX / xRadius;
57
+ const radiusYCount = deltaY / yRadius;
58
+
59
+ let res: Vertex[] = [];
60
+
61
+ // 容纳不下一个圆角,直接连线
62
+ if (radiusXCount < 1) {
63
+ return [];
64
+ }
65
+
66
+ switch (type) {
67
+ case FlowTransitionLineEnum.DIVERGE_LINE:
68
+ case FlowTransitionLineEnum.DRAGGING_LINE:
69
+ if (radiusXCount <= 1) {
70
+ return [
71
+ {
72
+ x: to.x,
73
+ y: from.y,
74
+ radiusX: deltaX,
75
+ },
76
+ ];
77
+ }
78
+ res = [
79
+ {
80
+ x: from.x + yRadius,
81
+ y: from.y,
82
+ },
83
+ {
84
+ x: from.x + yRadius,
85
+ y: to.y,
86
+ },
87
+ ];
88
+ if (radiusXCount < 2) {
89
+ const firstRadius = deltaX - yRadius;
90
+ res = [
91
+ {
92
+ x: from.x + firstRadius,
93
+ y: from.y,
94
+ // 第一个圆角收缩 y 半径
95
+ radiusX: firstRadius,
96
+ },
97
+ {
98
+ x: from.x + firstRadius,
99
+ y: to.y,
100
+ },
101
+ ];
102
+ }
103
+
104
+ // y 轴空间不足处理
105
+ if (radiusYCount < 2) {
106
+ res[0].moveY = deltaY / 2;
107
+ res[1].moveY = deltaY / 2;
108
+ }
109
+
110
+ return res;
111
+
112
+ case FlowTransitionLineEnum.MERGE_LINE:
113
+ // 聚合线 y 轴空间不足时直接连上
114
+ if (radiusXCount < 2) {
115
+ return [
116
+ {
117
+ x: to.x,
118
+ y: from.y,
119
+ },
120
+ ];
121
+ }
122
+
123
+ res = [
124
+ {
125
+ x: to.x - yRadius,
126
+ y: from.y,
127
+ },
128
+ {
129
+ x: to.x - yRadius,
130
+ y: to.y,
131
+ },
132
+ ];
133
+
134
+ // y 轴空间不足处理
135
+ if (radiusYCount < 2) {
136
+ res[0].moveY = deltaY / 2;
137
+ res[1].moveY = deltaY / 2;
138
+ }
139
+
140
+ return res;
141
+
142
+ default:
143
+ break;
144
+ }
145
+
146
+ return [];
147
+ }
148
+ /**
149
+ * 获取转弯线的转折点 (垂直布局)
150
+ */
151
+ export function getVertices(line: FlowTransitionLine, xRadius = 16, yRadius = 20): Vertex[] {
152
+ const { from, to, type } = line || {};
153
+
154
+ // 空间可以容纳的圆角数
155
+ const deltaY = Math.abs(to.y - from.y);
156
+ const deltaX = Math.abs(to.x - from.x);
157
+
158
+ const radiusYCount = deltaY / yRadius;
159
+ const radiusXCount = deltaX / xRadius;
160
+
161
+ let res: Vertex[] = [];
162
+
163
+ // 容纳不下一个圆角,直接连线
164
+ if (radiusYCount < 1) {
165
+ return [];
166
+ }
167
+
168
+ switch (type) {
169
+ case FlowTransitionLineEnum.DIVERGE_LINE:
170
+ case FlowTransitionLineEnum.DRAGGING_LINE:
171
+ if (radiusYCount <= 1) {
172
+ return [
173
+ {
174
+ x: to.x,
175
+ y: from.y,
176
+ radiusY: deltaY,
177
+ },
178
+ ];
179
+ }
180
+ res = [
181
+ {
182
+ x: from.x,
183
+ y: from.y + yRadius,
184
+ },
185
+ {
186
+ x: to.x,
187
+ y: from.y + yRadius,
188
+ },
189
+ ];
190
+ if (radiusYCount < 2) {
191
+ const firstRadius = deltaY - yRadius;
192
+ res = [
193
+ {
194
+ x: from.x,
195
+ y: from.y + firstRadius,
196
+ // 第一个圆角收缩 y 半径
197
+ radiusY: firstRadius,
198
+ },
199
+ {
200
+ x: to.x,
201
+ y: from.y + firstRadius,
202
+ },
203
+ ];
204
+ }
205
+
206
+ // x 轴空间不足处理
207
+ if (radiusXCount < 2) {
208
+ res[0].moveX = deltaX / 2;
209
+ res[1].moveX = deltaX / 2;
210
+ }
211
+
212
+ return res;
213
+
214
+ case FlowTransitionLineEnum.MERGE_LINE:
215
+ // 聚合线 y 轴空间不足时直接连上
216
+ if (radiusYCount < 2) {
217
+ return [
218
+ {
219
+ x: from.x,
220
+ y: to.y,
221
+ },
222
+ ];
223
+ }
224
+
225
+ res = [
226
+ {
227
+ x: from.x,
228
+ y: to.y - yRadius,
229
+ },
230
+ {
231
+ x: to.x,
232
+ y: to.y - yRadius,
233
+ },
234
+ ];
235
+
236
+ // x 轴空间不足处理
237
+ if (radiusXCount < 2) {
238
+ res[0].moveX = deltaX / 2;
239
+ res[1].moveX = deltaX / 2;
240
+ }
241
+
242
+ return res;
243
+
244
+ default:
245
+ break;
246
+ }
247
+
248
+ return [];
249
+ }
250
+
251
+ // 获取上一个节点和下一个节点中较宽的宽度作为 hover 热区
252
+ export function getTransitionLabelHoverWidth(data: FlowNodeTransitionData) {
253
+ const { isVertical } = data.entity;
254
+ if (isVertical) {
255
+ const nextWidth =
256
+ data.entity.next?.firstChild && !data.entity.next.isInlineBlocks
257
+ ? data.entity.next.firstChild!.getData(FlowNodeTransformData)!.size.width
258
+ : data.entity.next?.getData(FlowNodeTransformData)!.size.width;
259
+
260
+ // 获取上一个节点和下一个节点中较宽的宽度作为 hover 热区
261
+ const maxWidth = Math.max(
262
+ data.entity.getData(FlowNodeTransformData)?.size.width ??
263
+ DEFAULT_SPACING[DefaultSpacingKey.HOVER_AREA_WIDTH],
264
+ nextWidth || 0
265
+ );
266
+
267
+ return maxWidth;
268
+ }
269
+ if (data.transform.next) {
270
+ return data.transform.next.inputPoint.x - data.transform.outputPoint.x;
271
+ }
272
+ return DEFAULT_LABEL_ACTIVATE_HEIGHT;
273
+ }
274
+
275
+ export function getTransitionLabelHoverHeight(data: FlowNodeTransitionData) {
276
+ const { isVertical } = data.entity;
277
+ if (isVertical) {
278
+ if (data.transform.next) {
279
+ return data.transform.next.inputPoint.y - data.transform.outputPoint.y;
280
+ }
281
+ return DEFAULT_LABEL_ACTIVATE_HEIGHT;
282
+ }
283
+ const nextHeight =
284
+ data.entity.next?.firstChild && !data.entity.next.isInlineBlocks
285
+ ? data.entity.next.firstChild!.getData(FlowNodeTransformData)!.size.height
286
+ : data.entity.next?.getData(FlowNodeTransformData)!.size.height;
287
+
288
+ // 获取上一个节点和下一个节点中较宽的宽度作为 hover 热区
289
+ const maxHeight = Math.max(
290
+ data.entity.getData(FlowNodeTransformData)?.size.height || 280,
291
+ nextHeight || 0
292
+ );
293
+
294
+ return maxHeight;
295
+ }
@@ -0,0 +1,3 @@
1
+ ## 画布渲染相关 entity 数据
2
+
3
+ 这里存放的是和画布渲染强耦合的数据
@@ -0,0 +1,267 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import { Rectangle } from '@flowgram-vue/utils';
7
+ import {
8
+ type FlowNodeTransitionData,
9
+ FlowTransitionLabelEnum,
10
+ LABEL_SIDE_TYPE,
11
+ } from '@flowgram-vue/document';
12
+ import { ConfigEntity, type EntityOpts, PlaygroundConfigEntity } from '@flowgram-vue/core';
13
+
14
+ import { DEFAULT_LABEL_ACTIVATE_HEIGHT } from '../components/utils';
15
+
16
+ const BRANCH_HOVER_HEIGHT = 64;
17
+
18
+ interface FlowDragEntityConfig extends EntityOpts {}
19
+
20
+ enum ScrollDirection {
21
+ TOP,
22
+ BOTTOM,
23
+ LEFT,
24
+ RIGHT,
25
+ }
26
+
27
+ const SCROLL_DELTA = 4;
28
+
29
+ const SCROLL_INTERVAL = 20;
30
+
31
+ const SCROLL_BOUNDING = 20;
32
+
33
+ const EDITOR_LEFT_BAR_WIDTH = 60;
34
+
35
+ export interface CollisionRetType {
36
+ hasCollision: boolean;
37
+ labelOffsetType?: LABEL_SIDE_TYPE;
38
+ }
39
+
40
+ export class FlowDragEntity extends ConfigEntity<FlowDragEntityConfig> {
41
+ private playgroundConfigEntity: PlaygroundConfigEntity;
42
+
43
+ static type = 'FlowDragEntity';
44
+
45
+ private containerX = 0;
46
+
47
+ private containerY = 0;
48
+
49
+ private _scrollXInterval: { interval: number; origin: number } | undefined;
50
+
51
+ private _scrollYInterval: { interval: number; origin: number } | undefined;
52
+
53
+ get hasScroll(): boolean {
54
+ return Boolean(this._scrollXInterval || this._scrollYInterval);
55
+ }
56
+
57
+ constructor(conf: any) {
58
+ super(conf);
59
+ this.playgroundConfigEntity = this.entityManager.getEntity<PlaygroundConfigEntity>(
60
+ PlaygroundConfigEntity,
61
+ true
62
+ )!;
63
+ }
64
+
65
+ isCollision(
66
+ transition: FlowNodeTransitionData,
67
+ rect: Rectangle,
68
+ isBranch: boolean
69
+ ): CollisionRetType {
70
+ const scale = this.playgroundConfigEntity.finalScale || 0;
71
+ if (isBranch) {
72
+ return this.isBranchCollision(transition, rect, scale);
73
+ }
74
+ return this.isNodeCollision(transition, rect, scale);
75
+ }
76
+
77
+ // 检测节点维度碰撞方法
78
+ isNodeCollision(
79
+ transition: FlowNodeTransitionData,
80
+ rect: Rectangle,
81
+ scale: number
82
+ ): CollisionRetType {
83
+ const { labels } = transition;
84
+ const { isVertical } = transition.entity;
85
+
86
+ const hasCollision = labels.some((label) => {
87
+ if (
88
+ !label ||
89
+ ![
90
+ FlowTransitionLabelEnum.ADDER_LABEL,
91
+ FlowTransitionLabelEnum.COLLAPSE_ADDER_LABEL,
92
+ ].includes(label.type)
93
+ ) {
94
+ return false;
95
+ }
96
+
97
+ const hoverWidth = isVertical
98
+ ? transition.transform.bounds.width
99
+ : DEFAULT_LABEL_ACTIVATE_HEIGHT;
100
+ const hoverHeight = isVertical
101
+ ? DEFAULT_LABEL_ACTIVATE_HEIGHT
102
+ : transition.transform.bounds.height;
103
+
104
+ const labelRect = new Rectangle(
105
+ (label.offset.x - hoverWidth / 2) * scale,
106
+ (label.offset.y - hoverHeight / 2) * scale,
107
+ hoverWidth * scale,
108
+ hoverHeight * scale
109
+ );
110
+ // 检测两个正方形是否相互碰撞
111
+ return Rectangle.intersects(labelRect, rect);
112
+ });
113
+
114
+ return {
115
+ hasCollision,
116
+ // 节点不关心 offsetType
117
+ labelOffsetType: undefined,
118
+ };
119
+ }
120
+
121
+ // 检测分支维度碰撞
122
+ isBranchCollision(
123
+ transition: FlowNodeTransitionData,
124
+ rect: Rectangle,
125
+ scale: number
126
+ ): CollisionRetType {
127
+ const { labels } = transition;
128
+ const { isVertical } = transition.entity;
129
+
130
+ let labelOffsetType: LABEL_SIDE_TYPE = LABEL_SIDE_TYPE.NORMAL_BRANCH;
131
+ const hasCollision = labels.some((label) => {
132
+ if (!label || label.type !== FlowTransitionLabelEnum.BRANCH_DRAGGING_LABEL) {
133
+ return false;
134
+ }
135
+ const hoverHeight = isVertical ? BRANCH_HOVER_HEIGHT : label.width || 0;
136
+ // BRANCH_DRAGGING_LABEL 类型的 label 一定存在 width 属性
137
+ const hoverWidth = isVertical ? label.width || 0 : BRANCH_HOVER_HEIGHT;
138
+
139
+ const labelRect = new Rectangle(
140
+ (label.offset.x - hoverWidth / 2) * scale,
141
+ (label.offset.y - hoverHeight / 2) * scale,
142
+ hoverWidth * scale,
143
+ hoverHeight * scale
144
+ );
145
+ // 检测两个正方形是否相互碰撞
146
+ const collision = Rectangle.intersects(labelRect, rect);
147
+ if (collision) {
148
+ labelOffsetType = label.props!.side;
149
+ }
150
+ return collision;
151
+ });
152
+
153
+ return {
154
+ hasCollision,
155
+ labelOffsetType,
156
+ };
157
+ }
158
+
159
+ private _startScrollX(origin: number, added: boolean): void {
160
+ if (this._scrollXInterval) {
161
+ return;
162
+ }
163
+ const interval = window.setInterval(() => {
164
+ const current = this._scrollXInterval;
165
+ if (!current) return;
166
+ // eslint-disable-next-line no-multi-assign
167
+ const scrollX = (current.origin = added
168
+ ? current.origin + SCROLL_DELTA
169
+ : current.origin - SCROLL_DELTA);
170
+ this.playgroundConfigEntity.updateConfig({
171
+ scrollX,
172
+ });
173
+ const playgroundConfig = this.playgroundConfigEntity.config;
174
+ if (playgroundConfig?.scrollX === scrollX) {
175
+ if (added) {
176
+ this.containerX += SCROLL_DELTA;
177
+ } else {
178
+ this.containerX -= SCROLL_DELTA;
179
+ }
180
+ }
181
+ }, SCROLL_INTERVAL);
182
+ this._scrollXInterval = { interval, origin };
183
+ }
184
+
185
+ private _stopScrollX(): void {
186
+ if (this._scrollXInterval) {
187
+ clearInterval(this._scrollXInterval.interval);
188
+ this._scrollXInterval = undefined;
189
+ }
190
+ }
191
+
192
+ private _startScrollY(origin: number, added: boolean): void {
193
+ if (this._scrollYInterval) {
194
+ return;
195
+ }
196
+ const interval = window.setInterval(() => {
197
+ const current = this._scrollYInterval;
198
+ if (!current) return;
199
+ // eslint-disable-next-line no-multi-assign
200
+ const scrollY = (current.origin = added
201
+ ? current.origin + SCROLL_DELTA
202
+ : current.origin - SCROLL_DELTA);
203
+ this.playgroundConfigEntity.updateConfig({
204
+ scrollY,
205
+ });
206
+ const playgroundConfig = this.playgroundConfigEntity.config;
207
+ if (playgroundConfig?.scrollY === scrollY) {
208
+ if (added) {
209
+ this.containerY += SCROLL_DELTA;
210
+ } else {
211
+ this.containerY -= SCROLL_DELTA;
212
+ }
213
+ }
214
+ }, SCROLL_INTERVAL);
215
+ this._scrollYInterval = { interval, origin };
216
+ }
217
+
218
+ private _stopScrollY(): void {
219
+ if (this._scrollYInterval) {
220
+ clearInterval(this._scrollYInterval.interval);
221
+ this._scrollYInterval = undefined;
222
+ }
223
+ }
224
+
225
+ stopAllScroll(): void {
226
+ this._stopScrollX();
227
+ this._stopScrollY();
228
+ }
229
+
230
+ scrollDirection(e: MouseEvent, x: number, y: number): ScrollDirection | undefined {
231
+ const playgroundConfig = this.playgroundConfigEntity.config;
232
+ const currentScrollX = playgroundConfig.scrollX;
233
+ const currentScrollY = playgroundConfig.scrollY;
234
+ this.containerX = x;
235
+ this.containerY = y;
236
+ const clientRect = this.playgroundConfigEntity.playgroundDomNode.getBoundingClientRect();
237
+
238
+ const mouseToBottom = playgroundConfig.height + clientRect.y - e.clientY;
239
+ if (mouseToBottom < SCROLL_BOUNDING) {
240
+ this._startScrollY(currentScrollY, true);
241
+ return ScrollDirection.BOTTOM;
242
+ }
243
+ const mouseToTop = e.clientY - clientRect.y;
244
+ if (mouseToTop < SCROLL_BOUNDING) {
245
+ this._startScrollY(currentScrollY, false);
246
+ return ScrollDirection.TOP;
247
+ }
248
+ this._stopScrollY();
249
+ const mouseToRight = playgroundConfig.width + clientRect.x - e.clientX;
250
+ if (mouseToRight < SCROLL_BOUNDING) {
251
+ this._startScrollX(currentScrollX, true);
252
+ return ScrollDirection.RIGHT;
253
+ }
254
+ const mouseToLeft = e.clientX - clientRect.x;
255
+ if (mouseToLeft < SCROLL_BOUNDING + EDITOR_LEFT_BAR_WIDTH) {
256
+ this._startScrollX(currentScrollX, false);
257
+ return ScrollDirection.LEFT;
258
+ }
259
+ this._stopScrollX();
260
+
261
+ return undefined;
262
+ }
263
+
264
+ dispose(): void {
265
+ this.toDispose.dispose();
266
+ }
267
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import {
7
+ type FlowNodeEntity,
8
+ FlowNodeRenderData,
9
+ FlowNodeTransformData,
10
+ } from '@flowgram-vue/document';
11
+ import { ConfigEntity } from '@flowgram-vue/core';
12
+ import { Compare, Rectangle } from '@flowgram-vue/utils';
13
+
14
+ import { findSelectedNodes } from '../utils/find-selected-nodes';
15
+
16
+ interface FlowSelectConfigEntityData {
17
+ selectedNodes: FlowNodeEntity[];
18
+ }
19
+
20
+ const BOUNDS_PADDING_DEFAULT = 10;
21
+
22
+ /**
23
+ * 圈选节点相关数据存储
24
+ */
25
+ export class FlowSelectConfigEntity extends ConfigEntity<FlowSelectConfigEntityData> {
26
+ static type = 'FlowSelectConfigEntity';
27
+
28
+ boundsPadding = BOUNDS_PADDING_DEFAULT;
29
+
30
+ getDefaultConfig(): FlowSelectConfigEntityData {
31
+ return {
32
+ selectedNodes: [],
33
+ };
34
+ }
35
+
36
+ get selectedNodes(): FlowNodeEntity[] {
37
+ return this.config.selectedNodes;
38
+ }
39
+
40
+ /**
41
+ * 选中节点
42
+ * @param nodes
43
+ */
44
+ set selectedNodes(nodes: FlowNodeEntity[]) {
45
+ nodes = findSelectedNodes(nodes);
46
+ // if (nodes.length === 1 && nodes[0].flowNodeType === FlowNodeBaseType.END) {
47
+ // nodes = [];
48
+ // }
49
+ if (
50
+ nodes.length !== this.config.selectedNodes.length ||
51
+ nodes.some(n => !this.config.selectedNodes.includes(n))
52
+ ) {
53
+ this.config.selectedNodes.forEach(oldNode => {
54
+ if (!nodes.includes(oldNode)) {
55
+ oldNode.getData(FlowNodeRenderData)!.activated = false;
56
+ }
57
+ });
58
+ // 高亮选中的节点
59
+ nodes.forEach(node => {
60
+ node.getData(FlowNodeRenderData)!.activated = true;
61
+ });
62
+ if (Compare.isArrayShallowChanged(this.config.selectedNodes, nodes)) {
63
+ this.updateConfig({
64
+ selectedNodes: nodes,
65
+ });
66
+ }
67
+ }
68
+ }
69
+
70
+ /**
71
+ * 清除选中节点
72
+ */
73
+ clearSelectedNodes() {
74
+ if (this.config.selectedNodes.length === 0) return;
75
+ this.config.selectedNodes.forEach(node => {
76
+ node.getData(FlowNodeRenderData)!.activated = false;
77
+ });
78
+ this.updateConfig({
79
+ selectedNodes: [],
80
+ });
81
+ }
82
+
83
+ /**
84
+ * 通过选择框选中节点
85
+ * @param rect
86
+ * @param transforms
87
+ */
88
+ selectFromBounds(rect: Rectangle, transforms: FlowNodeTransformData[]): void {
89
+ const selectedNodes: FlowNodeEntity[] = [];
90
+ transforms.forEach(transform => {
91
+ if (Rectangle.intersects(rect, transform.bounds)) {
92
+ if (transform.entity.originParent) {
93
+ selectedNodes.push(transform.entity.originParent);
94
+ } else {
95
+ selectedNodes.push(transform.entity);
96
+ }
97
+ }
98
+ });
99
+ this.selectedNodes = selectedNodes;
100
+ }
101
+
102
+ /**
103
+ * 获取选中节点外围的最大边框
104
+ */
105
+ getSelectedBounds(): Rectangle {
106
+ const nodes = this.selectedNodes;
107
+ if (nodes.length === 0) {
108
+ return Rectangle.EMPTY;
109
+ }
110
+ return Rectangle.enlarge(nodes.map(n => n.getData(FlowNodeTransformData)!.bounds)).pad(
111
+ this.boundsPadding,
112
+ );
113
+ }
114
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ export * from './flow-drag-entity';
7
+ export * from './flow-select-config-entity';
8
+ export * from './selector-box-config-entity';