@mx-sose-front/mx-sose-graph 1.1.3 → 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 +177 -9
- package/dist/index.esm.js +4569 -63119
- package/dist/index.esm.js.map +1 -1
- package/dist/index.umd.js +1 -39
- 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 +10 -10
- package/src/components/DiagramListTooltip/DiagramListTooltip.vue +7 -12
- package/src/components/InteractionLayer.vue +323 -157
- package/src/components/LineStyle/LineStyleMarker.vue +1 -1
- package/src/components/{NameEditor.vue → NameEditor/NameEditor.vue} +4 -4
- package/src/components/{SelectionBox.vue → SelectionBox/SelectionBox.vue} +5 -5
- package/src/components/Shape/Block.vue +1 -1
- package/src/constants/edgeShapeKeys.ts +43 -3
- package/src/constants/index.ts +19 -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 +106 -147
- 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 +195 -32
- 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 -137
- /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
|
|
|
@@ -11,22 +15,120 @@ interface KeyboardConfig {
|
|
|
11
15
|
onShapesRemove?: (items: Array<{ modelId: string; shapeId: string; shapeType: string; isRemoveModelTree: boolean }>) => void;
|
|
12
16
|
isEditingName?: () => boolean;
|
|
13
17
|
onCopy?: () => void;
|
|
18
|
+
onCut?: () => void;
|
|
14
19
|
onPaste?: () => void;
|
|
20
|
+
isTextareaDialogOpen?: () => boolean; // 检查对话框是否打开
|
|
15
21
|
}
|
|
16
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
|
+
|
|
17
107
|
/**
|
|
18
108
|
* 创建键盘事件处理器
|
|
19
109
|
* @param config 快捷键配置
|
|
20
110
|
* @returns 键盘事件处理函数
|
|
21
111
|
*/
|
|
22
|
-
export const createKeyboardHandler = (config: KeyboardConfig): KeyboardEventHandler => {
|
|
112
|
+
export const createKeyboardHandler = (config: KeyboardConfig): KeyboardEventHandler | any => {
|
|
23
113
|
const graphStore = useGraphStore();
|
|
24
114
|
|
|
25
|
-
return (e: KeyboardEvent) => {
|
|
26
|
-
|
|
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且正在连线时,取消连线
|
|
27
124
|
if (e.key === 'Escape' && config.onCancelConnection) {
|
|
28
125
|
e.preventDefault();
|
|
29
126
|
config.onCancelConnection();
|
|
127
|
+
// 同时清理键盘移动状态
|
|
128
|
+
cleanupKeyboardMoveState();
|
|
129
|
+
|
|
130
|
+
// 清除所有剪切状态(SVG 遮盖层 + 剪切缓存)
|
|
131
|
+
ContextMenuUtils.clearCutState();
|
|
30
132
|
}
|
|
31
133
|
|
|
32
134
|
// 处理Delete键移除图元
|
|
@@ -85,7 +187,7 @@ export const createKeyboardHandler = (config: KeyboardConfig): KeyboardEventHand
|
|
|
85
187
|
if (config.isEditingName && config.isEditingName()) {
|
|
86
188
|
return;
|
|
87
189
|
}
|
|
88
|
-
|
|
190
|
+
|
|
89
191
|
e.preventDefault();
|
|
90
192
|
if (graphStore.museInGraphView) {
|
|
91
193
|
graphStore.selectAll();
|
|
@@ -102,7 +204,7 @@ export const createKeyboardHandler = (config: KeyboardConfig): KeyboardEventHand
|
|
|
102
204
|
if (config.isEditingName && config.isEditingName()) {
|
|
103
205
|
return;
|
|
104
206
|
}
|
|
105
|
-
|
|
207
|
+
|
|
106
208
|
e.preventDefault();
|
|
107
209
|
if (graphStore.museInGraphView && config.onEditProperty) {
|
|
108
210
|
config.onEditProperty();
|
|
@@ -115,11 +217,20 @@ export const createKeyboardHandler = (config: KeyboardConfig): KeyboardEventHand
|
|
|
115
217
|
if (config.isEditingName && config.isEditingName()) {
|
|
116
218
|
return;
|
|
117
219
|
}
|
|
118
|
-
|
|
220
|
+
|
|
119
221
|
e.preventDefault();
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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);
|
|
123
234
|
if (config.onDelete) {
|
|
124
235
|
config.onDelete();
|
|
125
236
|
}
|
|
@@ -132,7 +243,7 @@ export const createKeyboardHandler = (config: KeyboardConfig): KeyboardEventHand
|
|
|
132
243
|
if (config.isEditingName && config.isEditingName()) {
|
|
133
244
|
return;
|
|
134
245
|
}
|
|
135
|
-
|
|
246
|
+
|
|
136
247
|
e.preventDefault();
|
|
137
248
|
if (config.onCopy) {
|
|
138
249
|
config.onCopy();
|
|
@@ -145,20 +256,33 @@ export const createKeyboardHandler = (config: KeyboardConfig): KeyboardEventHand
|
|
|
145
256
|
if (config.isEditingName && config.isEditingName()) {
|
|
146
257
|
return;
|
|
147
258
|
}
|
|
148
|
-
|
|
259
|
+
|
|
149
260
|
e.preventDefault();
|
|
150
261
|
if (config.onPaste) {
|
|
151
262
|
config.onPaste();
|
|
152
263
|
}
|
|
153
264
|
}
|
|
154
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
|
+
|
|
155
279
|
// 按下方向键时移动选中的图元(不在输入框或文本域中时)
|
|
156
280
|
if ((e.key === 'ArrowUp' || e.key === 'ArrowDown' || e.key === 'ArrowLeft' || e.key === 'ArrowRight') && graphStore.museInGraphView && !isInputElement) {
|
|
157
281
|
// 如果正在编辑名称,不执行移动功能
|
|
158
282
|
if (config.isEditingName && config.isEditingName()) {
|
|
159
283
|
return;
|
|
160
284
|
}
|
|
161
|
-
|
|
285
|
+
|
|
162
286
|
e.preventDefault();
|
|
163
287
|
const step = 1; // 移动距离
|
|
164
288
|
|
|
@@ -176,7 +300,18 @@ export const createKeyboardHandler = (config: KeyboardConfig): KeyboardEventHand
|
|
|
176
300
|
const canvasMaxY = canvasHeight + canvasY;
|
|
177
301
|
|
|
178
302
|
// 获取所有选中的图元
|
|
179
|
-
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
|
+
});
|
|
180
315
|
|
|
181
316
|
selectedShapes.forEach(shape => {
|
|
182
317
|
if (shape && shape.bounds) {
|
|
@@ -200,18 +335,22 @@ export const createKeyboardHandler = (config: KeyboardConfig): KeyboardEventHand
|
|
|
200
335
|
break;
|
|
201
336
|
}
|
|
202
337
|
|
|
203
|
-
//
|
|
338
|
+
// 添加边界约束:确保图元不会超出画布的边界
|
|
204
339
|
newX = Math.max(newX, canvasMinX);
|
|
205
340
|
newY = Math.max(newY, canvasMinY);
|
|
206
341
|
newX = Math.min(newX, canvasMaxX - width);
|
|
207
342
|
newY = Math.min(newY, canvasMaxY - height);
|
|
208
343
|
|
|
209
|
-
//
|
|
344
|
+
// 计算实际移动的距离(考虑边界约束后的实际偏移)
|
|
210
345
|
const deltaX = newX - x;
|
|
211
346
|
const deltaY = newY - y;
|
|
212
347
|
|
|
213
|
-
//
|
|
214
|
-
|
|
348
|
+
// 先收集所有需要更新的图元(父图元 + 子图元)
|
|
349
|
+
const updates: Array<{ id: string; bounds: any }> = [];
|
|
350
|
+
|
|
351
|
+
// 添加父图元的更新
|
|
352
|
+
updates.push({
|
|
353
|
+
id: shape.id,
|
|
215
354
|
bounds: {
|
|
216
355
|
x: newX,
|
|
217
356
|
y: newY,
|
|
@@ -220,38 +359,62 @@ export const createKeyboardHandler = (config: KeyboardConfig): KeyboardEventHand
|
|
|
220
359
|
}
|
|
221
360
|
});
|
|
222
361
|
|
|
223
|
-
//
|
|
224
|
-
const
|
|
225
|
-
|
|
362
|
+
// 查找并计算所有子图元的新位置
|
|
363
|
+
const childIds = graphStore.parentChildMap.get(shape.id) || [];
|
|
364
|
+
childIds.forEach(childId => {
|
|
365
|
+
const child = graphStore.shapes.find(s => s.id === childId);
|
|
226
366
|
if (child && child.bounds) {
|
|
367
|
+
// 如果子图元也在选中列表中,跳过它
|
|
368
|
+
// 因为它会在遍历 selectedShapes 时被单独处理
|
|
369
|
+
if (graphStore.selectedIds.includes(child.id)) {
|
|
370
|
+
return; // 跳过已选中的子图元
|
|
371
|
+
}
|
|
372
|
+
|
|
227
373
|
const childX = child.bounds.x || 0;
|
|
228
374
|
const childY = child.bounds.y || 0;
|
|
229
375
|
const childWidth = child.bounds.width || 0;
|
|
230
376
|
const childHeight = child.bounds.height || 0;
|
|
231
377
|
|
|
232
|
-
// 计算子图元的新位置
|
|
233
|
-
|
|
234
|
-
|
|
378
|
+
// 计算子图元的新位置(使用父图元的实际移动距离)
|
|
379
|
+
let childNewX = childX + deltaX;
|
|
380
|
+
let childNewY = childY + deltaY;
|
|
235
381
|
|
|
236
382
|
// 为子图元添加边界约束
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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);
|
|
241
387
|
|
|
242
|
-
//
|
|
243
|
-
|
|
388
|
+
// 添加子图元的更新
|
|
389
|
+
updates.push({
|
|
390
|
+
id: child.id,
|
|
244
391
|
bounds: {
|
|
245
|
-
x:
|
|
246
|
-
y:
|
|
392
|
+
x: childNewX,
|
|
393
|
+
y: childNewY,
|
|
247
394
|
width: childWidth,
|
|
248
395
|
height: childHeight
|
|
249
396
|
}
|
|
250
397
|
});
|
|
251
398
|
}
|
|
252
399
|
});
|
|
400
|
+
|
|
401
|
+
// 统一批量更新所有图元(避免中间状态导致的累积偏移)
|
|
402
|
+
updates.forEach(update => {
|
|
403
|
+
graphStore.updateShape(update.id, { bounds: update.bounds });
|
|
404
|
+
});
|
|
253
405
|
}
|
|
254
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);
|
|
255
418
|
}
|
|
256
|
-
}
|
|
419
|
+
}
|
|
257
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
|
+
}
|