@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.
Files changed (48) hide show
  1. package/dist/index.d.ts +178 -10
  2. package/dist/index.esm.js +4944 -63227
  3. package/dist/index.esm.js.map +1 -1
  4. package/dist/index.umd.js +1 -38
  5. package/dist/index.umd.js.map +1 -1
  6. package/dist/style.css +1 -1
  7. package/package.json +10 -1
  8. package/src/components/ContextMenu/ContextMenu.vue +27 -13
  9. package/src/components/DiagramListTooltip/DiagramListTooltip.vue +7 -12
  10. package/src/components/InteractionLayer.vue +656 -496
  11. package/src/components/LineStyle/LineStyleMarker.vue +1 -1
  12. package/src/components/NameEditor/NameEditor.vue +212 -0
  13. package/src/components/SelectionBox/SelectionBox.vue +189 -0
  14. package/src/components/Shape/Block.vue +1 -1
  15. package/src/constants/edgeShapeKeys.ts +43 -3
  16. package/src/constants/index.ts +21 -4
  17. package/src/hooks/index.ts +3 -0
  18. package/src/hooks/useHighlight.ts +223 -0
  19. package/src/hooks/useNameEdit.ts +234 -0
  20. package/src/{utils/resizeUtils.ts → hooks/useResize.ts} +55 -155
  21. package/src/index.ts +4 -1
  22. package/src/render/shape-renderer.ts +59 -46
  23. package/src/statics/icons/createMenu/show.png +0 -0
  24. package/src/statics/icons/createMenu/tree.png +0 -0
  25. 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
  26. package/src/statics/icons/createMenu//345/261/225/347/244/272/350/277/236/347/272/277@3x.png +0 -0
  27. package/src/statics/icons/createMenu//346/211/200/345/234/250/345/233/276/350/241/250@3x.png +0 -0
  28. package/src/store/graphStore.ts +185 -65
  29. package/src/types/index.ts +4 -2
  30. package/src/types/interactionLayer.ts +1 -0
  31. package/src/utils/batchAutoExpand.ts +65 -0
  32. package/src/utils/compartment.ts +78 -4
  33. package/src/utils/containers.ts +24 -10
  34. package/src/utils/contextMenuUtils.ts +126 -110
  35. package/src/utils/diagram.ts +19 -15
  36. package/src/utils/drag.ts +10 -5
  37. package/src/utils/edgeUtils.ts +3 -4
  38. package/src/utils/graphDragService.ts +27 -23
  39. package/src/utils/iconLoader.ts +7 -7
  40. package/src/utils/keyboardUtils.ts +221 -30
  41. package/src/utils/pinUtils.ts +1 -2
  42. package/src/utils/shapeOps/shapeOps.ts +168 -0
  43. package/src/utils/viewportCulling.ts +193 -0
  44. package/src/view/graph.vue +115 -60
  45. package/src/utils/highlightUtils.ts +0 -162
  46. package/src/utils/nameEditUtils.ts +0 -132
  47. package/src/utils/packgeMap.ts +0 -1
  48. /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
- // 按下Esc且正在连线时,取消连线
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
- if (graphStore.selectedIds.length === 1 && graphStore.selectedShape && graphStore.selectedShape.shapeType?.toLowerCase?.() !== 'diagram' && graphStore.museInGraphView) {
119
- // 单选情况下发送删除事件
120
- eventBus.emit('shapes-delete', graphStore.selectedShape);
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.map(id => graphStore.shapes.find(s => s.id === id)).filter(Boolean);
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
- graphStore.updateShape(shape.id, {
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 childShapes = graphStore.shapes.filter(child => child.parenShapeId === shape.id);
197
- childShapes.forEach(child => {
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
- const childNewX = childX + deltaX;
206
- const childNewY = childY + deltaY;
378
+ // 计算子图元的新位置(使用父图元的实际移动距离)
379
+ let childNewX = childX + deltaX;
380
+ let childNewY = childY + deltaY;
207
381
 
208
382
  // 为子图元添加边界约束
209
- const constrainedChildX = Math.max(childNewX, canvasMinX);
210
- const constrainedChildY = Math.max(childNewY, canvasMinY);
211
- const constrainedChildXWithWidth = Math.min(constrainedChildX, canvasMaxX - childWidth);
212
- const constrainedChildYWithHeight = Math.min(constrainedChildY, canvasMaxY - childHeight);
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
- graphStore.updateShape(child.id, {
388
+ // 添加子图元的更新
389
+ updates.push({
390
+ id: child.id,
216
391
  bounds: {
217
- x: constrainedChildXWithWidth,
218
- y: constrainedChildYWithHeight,
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
  };
@@ -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
+ }