@mx-sose-front/mx-sose-graph 1.1.2 → 1.1.4
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/dist/index.d.ts +178 -10
- package/dist/index.esm.js +4944 -63227
- package/dist/index.esm.js.map +1 -1
- package/dist/index.umd.js +1 -38
- package/dist/index.umd.js.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +10 -1
- package/src/components/ContextMenu/ContextMenu.vue +27 -13
- package/src/components/DiagramListTooltip/DiagramListTooltip.vue +7 -12
- package/src/components/InteractionLayer.vue +656 -496
- package/src/components/LineStyle/LineStyleMarker.vue +1 -1
- package/src/components/NameEditor/NameEditor.vue +212 -0
- package/src/components/SelectionBox/SelectionBox.vue +189 -0
- package/src/components/Shape/Block.vue +1 -1
- package/src/constants/edgeShapeKeys.ts +43 -3
- package/src/constants/index.ts +21 -4
- package/src/hooks/index.ts +3 -0
- package/src/hooks/useHighlight.ts +223 -0
- package/src/hooks/useNameEdit.ts +234 -0
- package/src/{utils/resizeUtils.ts → hooks/useResize.ts} +55 -155
- package/src/index.ts +4 -1
- package/src/render/shape-renderer.ts +59 -46
- package/src/statics/icons/createMenu/show.png +0 -0
- package/src/statics/icons/createMenu/tree.png +0 -0
- package/src/statics/icons/createMenu//345/261/225/347/244/272/347/253/257/345/217/243/345/261/236/346/200/247@3x.png +0 -0
- package/src/statics/icons/createMenu//345/261/225/347/244/272/350/277/236/347/272/277@3x.png +0 -0
- package/src/statics/icons/createMenu//346/211/200/345/234/250/345/233/276/350/241/250@3x.png +0 -0
- package/src/store/graphStore.ts +185 -65
- package/src/types/index.ts +4 -2
- package/src/types/interactionLayer.ts +1 -0
- package/src/utils/batchAutoExpand.ts +65 -0
- package/src/utils/compartment.ts +78 -4
- package/src/utils/containers.ts +24 -10
- package/src/utils/contextMenuUtils.ts +126 -110
- package/src/utils/diagram.ts +19 -15
- package/src/utils/drag.ts +10 -5
- package/src/utils/edgeUtils.ts +3 -4
- package/src/utils/graphDragService.ts +27 -23
- package/src/utils/iconLoader.ts +7 -7
- package/src/utils/keyboardUtils.ts +221 -30
- package/src/utils/pinUtils.ts +1 -2
- package/src/utils/shapeOps/shapeOps.ts +168 -0
- package/src/utils/viewportCulling.ts +193 -0
- package/src/view/graph.vue +115 -60
- package/src/utils/highlightUtils.ts +0 -162
- package/src/utils/nameEditUtils.ts +0 -132
- package/src/utils/packgeMap.ts +0 -1
- /package/src/statics/icons/createMenu/{scissors.png → cut.png} +0 -0
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { useGraphStore } from '../store/graphStore';
|
|
2
2
|
import { eventBus } from '../store';
|
|
3
|
+
import _ from 'lodash';
|
|
4
|
+
import { guardOperate } from './license-guard';
|
|
5
|
+
import { ContextMenuUtils } from './contextMenuUtils';
|
|
6
|
+
|
|
3
7
|
// 定义快捷键处理函数类型
|
|
4
8
|
type KeyboardEventHandler = (e: KeyboardEvent) => void;
|
|
5
9
|
|
|
@@ -10,21 +14,121 @@ interface KeyboardConfig {
|
|
|
10
14
|
onCancelConnection?: () => void;
|
|
11
15
|
onShapesRemove?: (items: Array<{ modelId: string; shapeId: string; shapeType: string; isRemoveModelTree: boolean }>) => void;
|
|
12
16
|
isEditingName?: () => boolean;
|
|
17
|
+
onCopy?: () => void;
|
|
18
|
+
onCut?: () => void;
|
|
19
|
+
onPaste?: () => void;
|
|
20
|
+
isTextareaDialogOpen?: () => boolean; // 检查对话框是否打开
|
|
13
21
|
}
|
|
14
22
|
|
|
23
|
+
// ==================== 键盘移动防抖状态管理 ====================
|
|
24
|
+
let keyboardMoveTimer: number | null = null;
|
|
25
|
+
let keyboardMoveInitialBounds: Map<string, any> = new Map();
|
|
26
|
+
let keyboardMovingShapeIds: Set<string> = new Set();
|
|
27
|
+
const KEYBOARD_MOVE_DEBOUNCE_DELAY = 1000; // 1000ms 防抖延迟
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 收集受影响的图元ID(包括自身、父元素、子元素、祖先链)
|
|
31
|
+
*/
|
|
32
|
+
const collectAffectedShapeIds = (graphStore: any, movedIds: Set<string>): Set<string> => {
|
|
33
|
+
const affectedIds = new Set<string>();
|
|
34
|
+
|
|
35
|
+
movedIds.forEach(id => {
|
|
36
|
+
affectedIds.add(id);
|
|
37
|
+
|
|
38
|
+
const shape = graphStore.shapes.find((s: any) => s.id === id);
|
|
39
|
+
if (!shape) return;
|
|
40
|
+
|
|
41
|
+
// 添加父元素
|
|
42
|
+
if (shape.parenShapeId) {
|
|
43
|
+
affectedIds.add(shape.parenShapeId);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 添加所有子元素(递归)
|
|
47
|
+
const collectChildren = (parentId: string) => {
|
|
48
|
+
const childIds = graphStore.parentChildMap.get(parentId) || [];
|
|
49
|
+
childIds.forEach((childId: string) => {
|
|
50
|
+
affectedIds.add(childId);
|
|
51
|
+
collectChildren(childId); // 递归收集子元素的子元素
|
|
52
|
+
});
|
|
53
|
+
};
|
|
54
|
+
collectChildren(id);
|
|
55
|
+
|
|
56
|
+
// 添加祖先链
|
|
57
|
+
let currentParentId = shape.parenShapeId;
|
|
58
|
+
let guard = 0;
|
|
59
|
+
while (currentParentId && guard++ < 100) {
|
|
60
|
+
affectedIds.add(currentParentId);
|
|
61
|
+
const parent = graphStore.shapes.find((s: any) => s.id === currentParentId);
|
|
62
|
+
currentParentId = parent?.parenShapeId;
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
return affectedIds;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 触发键盘移动结束事件,发射 shape-drag-end 进行数据同步
|
|
71
|
+
*/
|
|
72
|
+
const emitKeyboardMoveEnd = (graphStore: any) => {
|
|
73
|
+
if (keyboardMovingShapeIds.size === 0) return;
|
|
74
|
+
|
|
75
|
+
// 收集所有受影响的图元
|
|
76
|
+
const affectedIds = collectAffectedShapeIds(graphStore, keyboardMovingShapeIds);
|
|
77
|
+
|
|
78
|
+
// 组装 payloads(深拷贝)
|
|
79
|
+
const payloads = Array.from(affectedIds)
|
|
80
|
+
.map(id => graphStore.shapes.find((s: any) => s.id === id))
|
|
81
|
+
.filter(Boolean)
|
|
82
|
+
.map(s => _.cloneDeep(s));
|
|
83
|
+
|
|
84
|
+
// 发射事件(与拖拽结束使用相同的事件)
|
|
85
|
+
if (payloads.length > 0) {
|
|
86
|
+
eventBus.emit('shape-drag-end', payloads);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// 清理状态
|
|
90
|
+
keyboardMoveTimer = null;
|
|
91
|
+
keyboardMoveInitialBounds.clear();
|
|
92
|
+
keyboardMovingShapeIds.clear();
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 清理键盘移动状态(用于取消或切换选中时)
|
|
97
|
+
*/
|
|
98
|
+
const cleanupKeyboardMoveState = () => {
|
|
99
|
+
if (keyboardMoveTimer !== null) {
|
|
100
|
+
window.clearTimeout(keyboardMoveTimer);
|
|
101
|
+
keyboardMoveTimer = null;
|
|
102
|
+
}
|
|
103
|
+
keyboardMoveInitialBounds.clear();
|
|
104
|
+
keyboardMovingShapeIds.clear();
|
|
105
|
+
};
|
|
106
|
+
|
|
15
107
|
/**
|
|
16
108
|
* 创建键盘事件处理器
|
|
17
109
|
* @param config 快捷键配置
|
|
18
110
|
* @returns 键盘事件处理函数
|
|
19
111
|
*/
|
|
20
|
-
export const createKeyboardHandler = (config: KeyboardConfig): KeyboardEventHandler => {
|
|
112
|
+
export const createKeyboardHandler = (config: KeyboardConfig): KeyboardEventHandler | any => {
|
|
21
113
|
const graphStore = useGraphStore();
|
|
22
114
|
|
|
23
|
-
return (e: KeyboardEvent) => {
|
|
24
|
-
|
|
115
|
+
return async (e: KeyboardEvent) => {
|
|
116
|
+
const result = await guardOperate(() => true)
|
|
117
|
+
if (!result) return // 未授权就直接结束
|
|
118
|
+
// 🚫 如果对话框打开,禁用所有快捷键
|
|
119
|
+
if (config.isTextareaDialogOpen && config.isTextareaDialogOpen()) {
|
|
120
|
+
return; // 直接返回,不处理任何快捷键
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 按下Esc且正在连线时,取消连线
|
|
25
124
|
if (e.key === 'Escape' && config.onCancelConnection) {
|
|
26
125
|
e.preventDefault();
|
|
27
126
|
config.onCancelConnection();
|
|
127
|
+
// 同时清理键盘移动状态
|
|
128
|
+
cleanupKeyboardMoveState();
|
|
129
|
+
|
|
130
|
+
// 清除所有剪切状态(SVG 遮盖层 + 剪切缓存)
|
|
131
|
+
ContextMenuUtils.clearCutState();
|
|
28
132
|
}
|
|
29
133
|
|
|
30
134
|
// 处理Delete键移除图元
|
|
@@ -83,7 +187,7 @@ export const createKeyboardHandler = (config: KeyboardConfig): KeyboardEventHand
|
|
|
83
187
|
if (config.isEditingName && config.isEditingName()) {
|
|
84
188
|
return;
|
|
85
189
|
}
|
|
86
|
-
|
|
190
|
+
|
|
87
191
|
e.preventDefault();
|
|
88
192
|
if (graphStore.museInGraphView) {
|
|
89
193
|
graphStore.selectAll();
|
|
@@ -100,7 +204,7 @@ export const createKeyboardHandler = (config: KeyboardConfig): KeyboardEventHand
|
|
|
100
204
|
if (config.isEditingName && config.isEditingName()) {
|
|
101
205
|
return;
|
|
102
206
|
}
|
|
103
|
-
|
|
207
|
+
|
|
104
208
|
e.preventDefault();
|
|
105
209
|
if (graphStore.museInGraphView && config.onEditProperty) {
|
|
106
210
|
config.onEditProperty();
|
|
@@ -113,24 +217,72 @@ export const createKeyboardHandler = (config: KeyboardConfig): KeyboardEventHand
|
|
|
113
217
|
if (config.isEditingName && config.isEditingName()) {
|
|
114
218
|
return;
|
|
115
219
|
}
|
|
116
|
-
|
|
220
|
+
|
|
117
221
|
e.preventDefault();
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
222
|
+
|
|
223
|
+
// 获取所有选中的图元
|
|
224
|
+
const selectedShapes = graphStore.marqueeShapes ?? [];
|
|
225
|
+
|
|
226
|
+
// 过滤掉画布类型的图元
|
|
227
|
+
const shapesToDelete = selectedShapes.filter(s =>
|
|
228
|
+
s.shapeType?.toLowerCase?.() !== 'diagram'
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
// 如果有可删除的图元,发送删除事件
|
|
232
|
+
if (shapesToDelete.length > 0) {
|
|
233
|
+
eventBus.emit('shapes-delete', shapesToDelete);
|
|
121
234
|
if (config.onDelete) {
|
|
122
235
|
config.onDelete();
|
|
123
236
|
}
|
|
124
237
|
}
|
|
125
238
|
}
|
|
126
239
|
|
|
240
|
+
// 按下Ctrl+C时复制选中的图元
|
|
241
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 'c' && graphStore.museInGraphView && !isInputElement) {
|
|
242
|
+
// 如果正在编辑名称,不执行复制功能
|
|
243
|
+
if (config.isEditingName && config.isEditingName()) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
e.preventDefault();
|
|
248
|
+
if (config.onCopy) {
|
|
249
|
+
config.onCopy();
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// 按下Ctrl+V时粘贴选中的图元
|
|
254
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 'v' && graphStore.museInGraphView && !isInputElement) {
|
|
255
|
+
// 如果正在编辑名称,不执行粘贴功能
|
|
256
|
+
if (config.isEditingName && config.isEditingName()) {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
e.preventDefault();
|
|
261
|
+
if (config.onPaste) {
|
|
262
|
+
config.onPaste();
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// 按下Ctrl+X时剪切选中的图元
|
|
267
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 'x' && graphStore.museInGraphView && !isInputElement) {
|
|
268
|
+
// 如果正在编辑名称,不执行剪切功能
|
|
269
|
+
if (config.isEditingName && config.isEditingName()) {
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
e.preventDefault();
|
|
274
|
+
if (config.onCut) {
|
|
275
|
+
config.onCut();
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
127
279
|
// 按下方向键时移动选中的图元(不在输入框或文本域中时)
|
|
128
280
|
if ((e.key === 'ArrowUp' || e.key === 'ArrowDown' || e.key === 'ArrowLeft' || e.key === 'ArrowRight') && graphStore.museInGraphView && !isInputElement) {
|
|
129
281
|
// 如果正在编辑名称,不执行移动功能
|
|
130
282
|
if (config.isEditingName && config.isEditingName()) {
|
|
131
283
|
return;
|
|
132
284
|
}
|
|
133
|
-
|
|
285
|
+
|
|
134
286
|
e.preventDefault();
|
|
135
287
|
const step = 1; // 移动距离
|
|
136
288
|
|
|
@@ -148,7 +300,18 @@ export const createKeyboardHandler = (config: KeyboardConfig): KeyboardEventHand
|
|
|
148
300
|
const canvasMaxY = canvasHeight + canvasY;
|
|
149
301
|
|
|
150
302
|
// 获取所有选中的图元
|
|
151
|
-
const selectedShapes = graphStore.selectedIds
|
|
303
|
+
const selectedShapes = graphStore.selectedIds
|
|
304
|
+
.map(id => graphStore.shapes.find(s => s.id === id))
|
|
305
|
+
.filter((shape): shape is NonNullable<typeof shape> => shape != null);
|
|
306
|
+
|
|
307
|
+
// ==================== 防抖机制:记录初始状态 ====================
|
|
308
|
+
// 在第一次移动时记录初始bounds
|
|
309
|
+
selectedShapes.forEach(shape => {
|
|
310
|
+
if (!keyboardMoveInitialBounds.has(shape.id)) {
|
|
311
|
+
keyboardMoveInitialBounds.set(shape.id, { ...shape.bounds });
|
|
312
|
+
}
|
|
313
|
+
keyboardMovingShapeIds.add(shape.id);
|
|
314
|
+
});
|
|
152
315
|
|
|
153
316
|
selectedShapes.forEach(shape => {
|
|
154
317
|
if (shape && shape.bounds) {
|
|
@@ -172,18 +335,22 @@ export const createKeyboardHandler = (config: KeyboardConfig): KeyboardEventHand
|
|
|
172
335
|
break;
|
|
173
336
|
}
|
|
174
337
|
|
|
175
|
-
//
|
|
338
|
+
// 添加边界约束:确保图元不会超出画布的边界
|
|
176
339
|
newX = Math.max(newX, canvasMinX);
|
|
177
340
|
newY = Math.max(newY, canvasMinY);
|
|
178
341
|
newX = Math.min(newX, canvasMaxX - width);
|
|
179
342
|
newY = Math.min(newY, canvasMaxY - height);
|
|
180
343
|
|
|
181
|
-
//
|
|
344
|
+
// 计算实际移动的距离(考虑边界约束后的实际偏移)
|
|
182
345
|
const deltaX = newX - x;
|
|
183
346
|
const deltaY = newY - y;
|
|
184
347
|
|
|
185
|
-
//
|
|
186
|
-
|
|
348
|
+
// 先收集所有需要更新的图元(父图元 + 子图元)
|
|
349
|
+
const updates: Array<{ id: string; bounds: any }> = [];
|
|
350
|
+
|
|
351
|
+
// 添加父图元的更新
|
|
352
|
+
updates.push({
|
|
353
|
+
id: shape.id,
|
|
187
354
|
bounds: {
|
|
188
355
|
x: newX,
|
|
189
356
|
y: newY,
|
|
@@ -192,38 +359,62 @@ export const createKeyboardHandler = (config: KeyboardConfig): KeyboardEventHand
|
|
|
192
359
|
}
|
|
193
360
|
});
|
|
194
361
|
|
|
195
|
-
//
|
|
196
|
-
const
|
|
197
|
-
|
|
362
|
+
// 查找并计算所有子图元的新位置
|
|
363
|
+
const childIds = graphStore.parentChildMap.get(shape.id) || [];
|
|
364
|
+
childIds.forEach(childId => {
|
|
365
|
+
const child = graphStore.shapes.find(s => s.id === childId);
|
|
198
366
|
if (child && child.bounds) {
|
|
367
|
+
// 如果子图元也在选中列表中,跳过它
|
|
368
|
+
// 因为它会在遍历 selectedShapes 时被单独处理
|
|
369
|
+
if (graphStore.selectedIds.includes(child.id)) {
|
|
370
|
+
return; // 跳过已选中的子图元
|
|
371
|
+
}
|
|
372
|
+
|
|
199
373
|
const childX = child.bounds.x || 0;
|
|
200
374
|
const childY = child.bounds.y || 0;
|
|
201
375
|
const childWidth = child.bounds.width || 0;
|
|
202
376
|
const childHeight = child.bounds.height || 0;
|
|
203
377
|
|
|
204
|
-
// 计算子图元的新位置
|
|
205
|
-
|
|
206
|
-
|
|
378
|
+
// 计算子图元的新位置(使用父图元的实际移动距离)
|
|
379
|
+
let childNewX = childX + deltaX;
|
|
380
|
+
let childNewY = childY + deltaY;
|
|
207
381
|
|
|
208
382
|
// 为子图元添加边界约束
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
383
|
+
childNewX = Math.max(childNewX, canvasMinX);
|
|
384
|
+
childNewY = Math.max(childNewY, canvasMinY);
|
|
385
|
+
childNewX = Math.min(childNewX, canvasMaxX - childWidth);
|
|
386
|
+
childNewY = Math.min(childNewY, canvasMaxY - childHeight);
|
|
213
387
|
|
|
214
|
-
//
|
|
215
|
-
|
|
388
|
+
// 添加子图元的更新
|
|
389
|
+
updates.push({
|
|
390
|
+
id: child.id,
|
|
216
391
|
bounds: {
|
|
217
|
-
x:
|
|
218
|
-
y:
|
|
392
|
+
x: childNewX,
|
|
393
|
+
y: childNewY,
|
|
219
394
|
width: childWidth,
|
|
220
395
|
height: childHeight
|
|
221
396
|
}
|
|
222
397
|
});
|
|
223
398
|
}
|
|
224
399
|
});
|
|
400
|
+
|
|
401
|
+
// 统一批量更新所有图元(避免中间状态导致的累积偏移)
|
|
402
|
+
updates.forEach(update => {
|
|
403
|
+
graphStore.updateShape(update.id, { bounds: update.bounds });
|
|
404
|
+
});
|
|
225
405
|
}
|
|
226
406
|
});
|
|
407
|
+
|
|
408
|
+
// ==================== 防抖机制:重置定时器 ====================
|
|
409
|
+
// 清除之前的定时器
|
|
410
|
+
if (keyboardMoveTimer !== null) {
|
|
411
|
+
window.clearTimeout(keyboardMoveTimer);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// 设置新的防抖定时器,1000ms后触发数据同步
|
|
415
|
+
keyboardMoveTimer = window.setTimeout(() => {
|
|
416
|
+
emitKeyboardMoveEnd(graphStore);
|
|
417
|
+
}, KEYBOARD_MOVE_DEBOUNCE_DELAY);
|
|
227
418
|
}
|
|
228
|
-
}
|
|
419
|
+
}
|
|
229
420
|
};
|
package/src/utils/pinUtils.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Shape, Bounds } from '../types'
|
|
2
2
|
import { getBounds } from './geom'
|
|
3
3
|
|
|
4
|
-
const PortKeys = ['OperationalPort']
|
|
4
|
+
const PortKeys = ['OperationalPort', 'ServicePort', 'ResourcePort']
|
|
5
5
|
/**
|
|
6
6
|
* 计算点到矩形边的距离
|
|
7
7
|
*/
|
|
@@ -329,7 +329,6 @@ export function snapPinToParentEdge(
|
|
|
329
329
|
dominantBaseline: baseline,
|
|
330
330
|
}
|
|
331
331
|
} catch {}
|
|
332
|
-
console.log(dropPoint, attachedBounds)
|
|
333
332
|
|
|
334
333
|
// 直接返回“完全贴边并在外侧”的坐标(已在 calculatePinAttachmentPosition 中确定)
|
|
335
334
|
return { x: Math.round(attachedBounds.x || 0), y: Math.round(attachedBounds.y || 0) };
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 图元批量操作工具(新增/更新/删除/upsert)
|
|
3
|
+
*/
|
|
4
|
+
export type ShapeId = string | number
|
|
5
|
+
export type ShapeOp = "add" | "update" | "delete" | "upsert" | "replace"
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 创建一个图元操作器(每个操作器维护自己的 indexMap)
|
|
9
|
+
* 推荐在每个 store 实例里创建一次并复用(不要每次调用都 new)
|
|
10
|
+
*/
|
|
11
|
+
export function createShapeOperator<Shape extends { id: ShapeId }>() {
|
|
12
|
+
const indexMap = new Map<ShapeId, number>()
|
|
13
|
+
/**
|
|
14
|
+
* 重建索引
|
|
15
|
+
*/
|
|
16
|
+
const resetIndex = (list: Shape[]) => {
|
|
17
|
+
indexMap.clear()
|
|
18
|
+
for (let i = 0; i < list.length; i++) {
|
|
19
|
+
indexMap.set(list[i].id, i)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const ensureIndex = (list: Shape[]) => {
|
|
23
|
+
if (indexMap.size === list.length) return
|
|
24
|
+
indexMap.clear()
|
|
25
|
+
for (let i = 0; i < list.length; i++) {
|
|
26
|
+
indexMap.set(list[i].id, i)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** delete 支持 payload 传 id[] 或 Shape[](只要有 id 即可) */
|
|
31
|
+
const extractIds = (payload: Shape[] | ShapeId[]) => {
|
|
32
|
+
const ids: ShapeId[] = []
|
|
33
|
+
for (const item of payload as any[]) {
|
|
34
|
+
if (item == null) continue
|
|
35
|
+
if (typeof item === "string" || typeof item === "number") {
|
|
36
|
+
ids.push(item)
|
|
37
|
+
} else if (item.id != null) {
|
|
38
|
+
ids.push(item.id)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return ids
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** O(1) 删除:swap last + pop(会改变数组顺序) */
|
|
45
|
+
const removeByIdFast = (list: Shape[], id: ShapeId) => {
|
|
46
|
+
const idx = indexMap.get(id)
|
|
47
|
+
if (idx == null) return false
|
|
48
|
+
|
|
49
|
+
const lastIdx = list.length - 1
|
|
50
|
+
if (lastIdx < 0) return false
|
|
51
|
+
|
|
52
|
+
if (idx !== lastIdx) {
|
|
53
|
+
const moved = list[lastIdx]
|
|
54
|
+
list[idx] = moved
|
|
55
|
+
indexMap.set(moved.id, idx)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
list.pop()
|
|
59
|
+
indexMap.delete(id)
|
|
60
|
+
return true
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** upsert:有则整条替换,无则 push 新增 */
|
|
64
|
+
const upsertOne = (list: Shape[], incoming: Shape) => {
|
|
65
|
+
const id = incoming?.id
|
|
66
|
+
if (id == null) return false
|
|
67
|
+
|
|
68
|
+
const idx = indexMap.get(id)
|
|
69
|
+
if (idx == null) {
|
|
70
|
+
list.push(incoming)
|
|
71
|
+
indexMap.set(id, list.length - 1)
|
|
72
|
+
} else {
|
|
73
|
+
list[idx] = incoming
|
|
74
|
+
}
|
|
75
|
+
return true
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** update:仅存在才替换 */
|
|
79
|
+
const updateOne = (list: Shape[], incoming: Shape) => {
|
|
80
|
+
const id = incoming?.id
|
|
81
|
+
if (id == null) return false
|
|
82
|
+
|
|
83
|
+
const idx = indexMap.get(id)
|
|
84
|
+
if (idx == null) return false
|
|
85
|
+
|
|
86
|
+
list[idx] = incoming
|
|
87
|
+
return true
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* add:不存在才新增
|
|
92
|
+
* - 已存在时选择“替换”保证最终一致
|
|
93
|
+
*/
|
|
94
|
+
const addOne = (list: Shape[], incoming: Shape) => {
|
|
95
|
+
const id = incoming?.id
|
|
96
|
+
if (id == null) return false
|
|
97
|
+
|
|
98
|
+
const idx = indexMap.get(id)
|
|
99
|
+
if (idx == null) {
|
|
100
|
+
list.push(incoming)
|
|
101
|
+
indexMap.set(id, list.length - 1)
|
|
102
|
+
} else {
|
|
103
|
+
list[idx] = incoming
|
|
104
|
+
}
|
|
105
|
+
return true
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 对外统一入口
|
|
110
|
+
*/
|
|
111
|
+
const applyShapeOp = (args: {
|
|
112
|
+
list: Shape[]
|
|
113
|
+
payload: Shape[] | ShapeId[]
|
|
114
|
+
op: ShapeOp
|
|
115
|
+
}) => {
|
|
116
|
+
const { list, payload, op } = args
|
|
117
|
+
|
|
118
|
+
// ==================== replace:全量覆盖(就地覆盖数组内容) ====================
|
|
119
|
+
if (op === "replace") {
|
|
120
|
+
const incomingShapes = payload as Shape[]
|
|
121
|
+
// 清空原数组(保持 list 引用不变,即 shapes.value 引用不变)
|
|
122
|
+
list.length = 0
|
|
123
|
+
// 写入新数据
|
|
124
|
+
for (const s of incomingShapes) {
|
|
125
|
+
if (!s) continue
|
|
126
|
+
list.push(s)
|
|
127
|
+
}
|
|
128
|
+
// 重建索引
|
|
129
|
+
indexMap.clear()
|
|
130
|
+
for (let i = 0; i < list.length; i++) {
|
|
131
|
+
indexMap.set(list[i].id, i)
|
|
132
|
+
}
|
|
133
|
+
// replace 场景:可以认为所有都是“变更”,直接返回整批(方便 autoExpand)
|
|
134
|
+
return { changedShapes: incomingShapes }
|
|
135
|
+
}
|
|
136
|
+
// 轻量保证索引可用
|
|
137
|
+
ensureIndex(list)
|
|
138
|
+
|
|
139
|
+
const changedShapes: Shape[] = []
|
|
140
|
+
|
|
141
|
+
if (op === "delete") {
|
|
142
|
+
const ids = extractIds(payload as any)
|
|
143
|
+
for (const id of ids) {
|
|
144
|
+
removeByIdFast(list, id)
|
|
145
|
+
}
|
|
146
|
+
return { changedShapes }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const incomingShapes = payload as Shape[]
|
|
150
|
+
for (const incoming of incomingShapes) {
|
|
151
|
+
if (!incoming) continue
|
|
152
|
+
|
|
153
|
+
if (op === "upsert") {
|
|
154
|
+
if (upsertOne(list, incoming)) changedShapes.push(incoming)
|
|
155
|
+
} else if (op === "update") {
|
|
156
|
+
if (updateOne(list, incoming)) changedShapes.push(incoming)
|
|
157
|
+
} else if (op === "add") {
|
|
158
|
+
if (addOne(list, incoming)) changedShapes.push(incoming)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return { changedShapes }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
applyShapeOp,
|
|
167
|
+
}
|
|
168
|
+
}
|