@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.
Files changed (46) hide show
  1. package/dist/index.d.ts +177 -9
  2. package/dist/index.esm.js +4569 -63119
  3. package/dist/index.esm.js.map +1 -1
  4. package/dist/index.umd.js +1 -39
  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 +10 -10
  9. package/src/components/DiagramListTooltip/DiagramListTooltip.vue +7 -12
  10. package/src/components/InteractionLayer.vue +323 -157
  11. package/src/components/LineStyle/LineStyleMarker.vue +1 -1
  12. package/src/components/{NameEditor.vue → NameEditor/NameEditor.vue} +4 -4
  13. package/src/components/{SelectionBox.vue → SelectionBox/SelectionBox.vue} +5 -5
  14. package/src/components/Shape/Block.vue +1 -1
  15. package/src/constants/edgeShapeKeys.ts +43 -3
  16. package/src/constants/index.ts +19 -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 +106 -147
  35. package/src/utils/drag.ts +10 -5
  36. package/src/utils/edgeUtils.ts +3 -4
  37. package/src/utils/graphDragService.ts +27 -23
  38. package/src/utils/iconLoader.ts +7 -7
  39. package/src/utils/keyboardUtils.ts +195 -32
  40. package/src/utils/pinUtils.ts +1 -2
  41. package/src/utils/shapeOps/shapeOps.ts +168 -0
  42. package/src/utils/viewportCulling.ts +193 -0
  43. package/src/view/graph.vue +115 -60
  44. package/src/utils/highlightUtils.ts +0 -162
  45. package/src/utils/nameEditUtils.ts +0 -137
  46. /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
- // 按下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且正在连线时,取消连线
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
- if (graphStore.selectedIds.length === 1 && graphStore.selectedShape && graphStore.selectedShape.shapeType?.toLowerCase?.() !== 'diagram' && graphStore.museInGraphView) {
121
- // 单选情况下发送删除事件
122
- 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);
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.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
+ });
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
- graphStore.updateShape(shape.id, {
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 childShapes = graphStore.shapes.filter(child => child.parenShapeId === shape.id);
225
- 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);
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
- const childNewX = childX + deltaX;
234
- const childNewY = childY + deltaY;
378
+ // 计算子图元的新位置(使用父图元的实际移动距离)
379
+ let childNewX = childX + deltaX;
380
+ let childNewY = childY + deltaY;
235
381
 
236
382
  // 为子图元添加边界约束
237
- const constrainedChildX = Math.max(childNewX, canvasMinX);
238
- const constrainedChildY = Math.max(childNewY, canvasMinY);
239
- const constrainedChildXWithWidth = Math.min(constrainedChildX, canvasMaxX - childWidth);
240
- 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);
241
387
 
242
- // 更新子图元位置
243
- graphStore.updateShape(child.id, {
388
+ // 添加子图元的更新
389
+ updates.push({
390
+ id: child.id,
244
391
  bounds: {
245
- x: constrainedChildXWithWidth,
246
- y: constrainedChildYWithHeight,
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
  };
@@ -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
+ }