@mx-sose-front/mx-sose-graph 1.1.6 → 1.1.7

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/src/utils/drag.ts CHANGED
@@ -1,4 +1,4 @@
1
- // utils/drag.ts
1
+ // utils/drag.ts
2
2
  import type { Shape } from '../types'
3
3
  import { getBounds, getDiagramRect, clampPointToRect } from './geom'
4
4
  import { useGraphStore } from '../store/graphStore'
@@ -31,11 +31,9 @@ export const buildDragSnapshot = (
31
31
  shapes: Shape[],
32
32
  ids: string[],
33
33
  ) => {
34
- const graphStore = useGraphStore();
35
- const byId = graphStore.shapeMap;
36
34
  const dragBase: Record<string, Rect> = {}
37
35
  ids.forEach(id => {
38
- const s = byId.get(id); if (!s) return
36
+ const s = shapes.find(x => x.id === id); if (!s) return
39
37
  const b = getBounds(s)
40
38
  dragBase[id] = { x: b.x, y: b.y, width: b.width, height: b.height }
41
39
  })
@@ -63,8 +61,7 @@ export const stepSingleDrag = (
63
61
  constrainEdges: { left?: boolean; top?: boolean; right?: boolean; bottom?: boolean },
64
62
  hitPointer?: { x: number; y: number },
65
63
  ) => {
66
- const graphStore = useGraphStore();
67
- const s = graphStore.shapeMap.get(id); if (!s) return { ghost: {}, hover: null }
64
+ const s = shapes.find(x => x.id === id); if (!s) return { ghost: {}, hover: null }
68
65
  const base = dragBase[id]; if (!base) return { ghost: {}, hover: null }
69
66
  const container = getDiagramRect(shapes)
70
67
 
@@ -104,8 +101,6 @@ export const stepGroupDrag = (
104
101
  dragAnchor: { x: number; y: number },
105
102
  groupBaseBox: Rect,
106
103
  ) => {
107
- const graphStore = useGraphStore();
108
- const byId = graphStore.shapeMap;
109
104
  const diagramRect = getDiagramRect(shapes)
110
105
  const dxRaw = pointer.x - dragAnchor.x
111
106
  const dyRaw = pointer.y - dragAnchor.y
@@ -125,14 +120,14 @@ export const stepGroupDrag = (
125
120
  // 构建整组 ghost
126
121
  const ghost: Record<string, Rect> = {}
127
122
  for (const id of draggingIds) {
128
- const s = byId.get(id); if (!s) continue
123
+ const s = shapes.find(x => x.id === id); if (!s) continue
129
124
  const b = getBounds(s)
130
125
  ghost[id] = { x: b.x + dx, y: b.y + dy, width: b.width, height: b.height }
131
126
  }
132
127
 
133
128
  // —— hover 统一改为“指针命中 topmost 容器”
134
129
  // 用主拖动项获取 diagramId(也可以存你当前图的 id)
135
- const priShape = byId.get(primaryId)
130
+ const priShape = shapes.find(x => x.id === primaryId)
136
131
  const diagramId = priShape?.diagramId || ''
137
132
 
138
133
  const hoverShape = pickContainerByPointerTopmost(
@@ -160,22 +155,9 @@ export const commitDrag = (
160
155
  updateShape: (id: string, updates: Partial<Shape>) => void,
161
156
  baseMap?: Record<string, Rect>
162
157
  ) => {
163
- const graphStore = useGraphStore();
164
- const parentChildMap = graphStore.parentChildMap;
165
158
  const changedIds = new Set<string>()
166
159
 
167
- // 递归收集所有后代 ID
168
- const collectAllDescendants = (parentId: string): string[] => {
169
- const result: string[] = []
170
- const children = parentChildMap.get(parentId) || []
171
- for (const childId of children) {
172
- result.push(childId)
173
- result.push(...collectAllDescendants(childId))
174
- }
175
- return result
176
- }
177
-
178
- // 仅把"参与拖拽的节点"的 ghost 写回;不处理父子关系,也不扩父或夹回
160
+ // 仅把“参与拖拽的节点”的 ghost 写回;不处理父子关系,也不扩父或夹回
179
161
  for (const id of draggingIds) {
180
162
  const s = shapes.find(x => x.id === id)
181
163
  const r = ghost[id] // 参与拖拽的 id 都应当在 ghost 里
@@ -190,34 +172,12 @@ export const commitDrag = (
190
172
  }
191
173
 
192
174
  const pb = (baseMap?.[id] ?? (s.bounds as Rect))
193
- const dx = nb.x - pb.x
194
- const dy = nb.y - pb.y
195
- const same = dx === 0 && dy === 0 && nb.width === pb.width && nb.height === pb.height
175
+ const same =
176
+ pb.x === nb.x && pb.y === nb.y && pb.width === nb.width && pb.height === nb.height
196
177
 
197
178
  if (!same) {
198
179
  updateShape(id, { bounds: nb })
199
180
  changedIds.add(id)
200
-
201
- // 性能优化:同步移动所有后代元素(只在父元素位置变化时)
202
- if (dx !== 0 || dy !== 0) {
203
- const descendants = collectAllDescendants(id)
204
- for (const descId of descendants) {
205
- // 跳过已经在 draggingIds 中的元素(避免重复处理)
206
- if (draggingIds.includes(descId)) continue
207
- const descShape = shapes.find(x => x.id === descId)
208
- if (!descShape?.bounds) continue
209
- const descBounds = descShape.bounds as Rect
210
- updateShape(descId, {
211
- bounds: {
212
- x: Math.round(descBounds.x + dx),
213
- y: Math.round(descBounds.y + dy),
214
- width: descBounds.width,
215
- height: descBounds.height,
216
- }
217
- })
218
- changedIds.add(descId)
219
- }
220
- }
221
181
  }
222
182
  }
223
183
 
@@ -31,13 +31,11 @@ const KEYBOARD_MOVE_DEBOUNCE_DELAY = 1000; // 1000ms 防抖延迟
31
31
  */
32
32
  const collectAffectedShapeIds = (graphStore: any, movedIds: Set<string>): Set<string> => {
33
33
  const affectedIds = new Set<string>();
34
- const shapeMap = graphStore.shapeMap;
35
34
 
36
35
  movedIds.forEach(id => {
37
36
  affectedIds.add(id);
38
37
 
39
- // 使用 shapeMap O(1) 查找
40
- const shape = shapeMap.get(id);
38
+ const shape = graphStore.shapes.find((s: any) => s.id === id);
41
39
  if (!shape) return;
42
40
 
43
41
  // 添加父元素
@@ -60,8 +58,7 @@ const collectAffectedShapeIds = (graphStore: any, movedIds: Set<string>): Set<st
60
58
  let guard = 0;
61
59
  while (currentParentId && guard++ < 100) {
62
60
  affectedIds.add(currentParentId);
63
- // 使用 shapeMap O(1) 查找
64
- const parent = shapeMap.get(currentParentId);
61
+ const parent = graphStore.shapes.find((s: any) => s.id === currentParentId);
65
62
  currentParentId = parent?.parenShapeId;
66
63
  }
67
64
  });
@@ -77,11 +74,10 @@ const emitKeyboardMoveEnd = (graphStore: any) => {
77
74
 
78
75
  // 收集所有受影响的图元
79
76
  const affectedIds = collectAffectedShapeIds(graphStore, keyboardMovingShapeIds);
80
- const shapeMap = graphStore.shapeMap;
81
77
 
82
- // 组装 payloads(深拷贝) - 使用 shapeMap O(1) 查找
78
+ // 组装 payloads(深拷贝)
83
79
  const payloads = Array.from(affectedIds)
84
- .map(id => shapeMap.get(id))
80
+ .map(id => graphStore.shapes.find((s: any) => s.id === id))
85
81
  .filter(Boolean)
86
82
  .map(s => _.cloneDeep(s));
87
83
 
@@ -290,14 +286,8 @@ export const createKeyboardHandler = (config: KeyboardConfig): KeyboardEventHand
290
286
  e.preventDefault();
291
287
  const step = 1; // 移动距离
292
288
 
293
- // 获取画布信息(diagram类型的shape)- 遍历一次找 diagram
294
- let canvas: any = null;
295
- for (const shape of graphStore.shapes) {
296
- if (shape.shapeType === 'diagram') {
297
- canvas = shape;
298
- break;
299
- }
300
- }
289
+ // 获取画布信息(diagram类型的shape
290
+ const canvas = graphStore.shapes.find(shape => shape.shapeType === 'diagram');
301
291
  const canvasX = canvas?.bounds?.x || 0;
302
292
  const canvasY = canvas?.bounds?.y || 0;
303
293
  const canvasWidth = canvas?.bounds?.width || 300; // 最小画布宽度
@@ -309,13 +299,10 @@ export const createKeyboardHandler = (config: KeyboardConfig): KeyboardEventHand
309
299
  const canvasMaxX = canvasWidth + canvasX;
310
300
  const canvasMaxY = canvasHeight + canvasY;
311
301
 
312
- // 获取所有选中的图元 - 使用 shapeMap O(1) 查找
313
- const shapeMap = graphStore.shapeMap;
314
- const selectedShapes: any[] = [];
315
- for (const id of graphStore.selectedIds) {
316
- const shape = shapeMap.get(id);
317
- if (shape) selectedShapes.push(shape);
318
- }
302
+ // 获取所有选中的图元
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);
319
306
 
320
307
  // ==================== 防抖机制:记录初始状态 ====================
321
308
  // 在第一次移动时记录初始bounds
@@ -375,8 +362,7 @@ export const createKeyboardHandler = (config: KeyboardConfig): KeyboardEventHand
375
362
  // 查找并计算所有子图元的新位置
376
363
  const childIds = graphStore.parentChildMap.get(shape.id) || [];
377
364
  childIds.forEach(childId => {
378
- // 使用 shapeMap O(1) 查找
379
- const child = shapeMap.get(childId);
365
+ const child = graphStore.shapes.find(s => s.id === childId);
380
366
  if (child && child.bounds) {
381
367
  // 如果子图元也在选中列表中,跳过它
382
368
  // 因为它会在遍历 selectedShapes 时被单独处理
@@ -4,15 +4,6 @@
4
4
  export type ShapeId = string | number
5
5
  export type ShapeOp = "add" | "update" | "delete" | "upsert" | "replace"
6
6
 
7
- /**
8
- * 变更详情,用于增量更新外部索引
9
- */
10
- export interface ShapeChangeDetail<Shape> {
11
- type: 'add' | 'update' | 'delete'
12
- shape: Shape
13
- oldShape?: Shape // update 时提供旧值,用于检测父节点变更
14
- }
15
-
16
7
  /**
17
8
  * 创建一个图元操作器(每个操作器维护自己的 indexMap)
18
9
  * 推荐在每个 store 实例里创建一次并复用(不要每次调用都 new)
@@ -78,15 +69,13 @@ export function createShapeOperator<Shape extends { id: ShapeId }>() {
78
69
  return ids
79
70
  }
80
71
 
81
- /** O(1) 删除:swap last + pop(会改变数组顺序),返回被删除的元素 */
82
- const removeByIdFast = (list: Shape[], id: ShapeId): Shape | null => {
72
+ /** O(1) 删除:swap last + pop(会改变数组顺序) */
73
+ const removeByIdFast = (list: Shape[], id: ShapeId) => {
83
74
  const idx = indexMap.get(id)
84
- if (idx == null) return null
75
+ if (idx == null) return false
85
76
 
86
77
  const lastIdx = list.length - 1
87
- if (lastIdx < 0) return null
88
-
89
- const removed = list[idx]
78
+ if (lastIdx < 0) return false
90
79
 
91
80
  if (idx !== lastIdx) {
92
81
  const moved = list[lastIdx]
@@ -96,57 +85,52 @@ export function createShapeOperator<Shape extends { id: ShapeId }>() {
96
85
 
97
86
  list.pop()
98
87
  indexMap.delete(id)
99
- return removed
88
+ return true
100
89
  }
101
90
 
102
91
  /** upsert:有则整条替换,无则 push 新增 */
103
- const upsertOne = (list: Shape[], incoming: Shape): { changed: boolean; isAdd: boolean; oldShape?: Shape } => {
92
+ const upsertOne = (list: Shape[], incoming: Shape) => {
104
93
  const id = incoming?.id
105
- if (id == null) return { changed: false, isAdd: false }
94
+ if (id == null) return false
106
95
 
107
96
  const idx = indexMap.get(id)
108
97
  if (idx == null) {
109
98
  list.push(incoming)
110
99
  indexMap.set(id, list.length - 1)
111
- return { changed: true, isAdd: true }
112
100
  } else {
113
- const oldShape = list[idx]
114
101
  list[idx] = incoming
115
- return { changed: true, isAdd: false, oldShape }
116
102
  }
103
+ return true
117
104
  }
118
105
 
119
106
  /** update:仅存在才替换 */
120
- const updateOne = (list: Shape[], incoming: Shape): { changed: boolean; oldShape?: Shape } => {
107
+ const updateOne = (list: Shape[], incoming: Shape) => {
121
108
  const id = incoming?.id
122
- if (id == null) return { changed: false }
109
+ if (id == null) return false
123
110
 
124
111
  const idx = indexMap.get(id)
125
- if (idx == null) return { changed: false }
112
+ if (idx == null) return false
126
113
 
127
- const oldShape = list[idx]
128
114
  list[idx] = incoming
129
- return { changed: true, oldShape }
115
+ return true
130
116
  }
131
117
 
132
118
  /**
133
119
  * add:不存在才新增
134
- * - 已存在时选择"替换"保证最终一致
120
+ * - 已存在时选择“替换”保证最终一致
135
121
  */
136
- const addOne = (list: Shape[], incoming: Shape): { changed: boolean; isAdd: boolean; oldShape?: Shape } => {
122
+ const addOne = (list: Shape[], incoming: Shape) => {
137
123
  const id = incoming?.id
138
- if (id == null) return { changed: false, isAdd: false }
124
+ if (id == null) return false
139
125
 
140
126
  const idx = indexMap.get(id)
141
127
  if (idx == null) {
142
128
  list.push(incoming)
143
129
  indexMap.set(id, list.length - 1)
144
- return { changed: true, isAdd: true }
145
130
  } else {
146
- const oldShape = list[idx]
147
131
  list[idx] = incoming
148
- return { changed: true, isAdd: false, oldShape }
149
132
  }
133
+ return true
150
134
  }
151
135
 
152
136
  /**
@@ -156,9 +140,8 @@ export function createShapeOperator<Shape extends { id: ShapeId }>() {
156
140
  list: Shape[]
157
141
  payload: Shape[] | ShapeId[]
158
142
  op: ShapeOp
159
- }): { changedShapes: Shape[]; changeDetails: ShapeChangeDetail<Shape>[] } => {
143
+ }) => {
160
144
  const { list, payload, op } = args
161
- const changeDetails: ShapeChangeDetail<Shape>[] = []
162
145
 
163
146
  // ==================== replace:全量覆盖(就地覆盖数组内容) ====================
164
147
  if (op === "replace") {
@@ -175,8 +158,8 @@ export function createShapeOperator<Shape extends { id: ShapeId }>() {
175
158
  for (let i = 0; i < list.length; i++) {
176
159
  indexMap.set(list[i].id, i)
177
160
  }
178
- // replace 场景:返回空的 changeDetails,调用方应该全量重建索引
179
- return { changedShapes: incomingShapes, changeDetails: [] }
161
+ // replace 场景:可以认为所有都是“变更”,直接返回整批(方便 autoExpand)
162
+ return { changedShapes: incomingShapes }
180
163
  }
181
164
  // 轻量保证索引可用
182
165
  ensureIndex(list)
@@ -186,12 +169,9 @@ export function createShapeOperator<Shape extends { id: ShapeId }>() {
186
169
  if (op === "delete") {
187
170
  const ids = extractIds(payload as any)
188
171
  for (const id of ids) {
189
- const removed = removeByIdFast(list, id)
190
- if (removed) {
191
- changeDetails.push({ type: 'delete', shape: removed })
192
- }
172
+ removeByIdFast(list, id)
193
173
  }
194
- return { changedShapes, changeDetails }
174
+ return { changedShapes }
195
175
  }
196
176
 
197
177
  const incomingShapes = payload as Shape[]
@@ -199,39 +179,15 @@ export function createShapeOperator<Shape extends { id: ShapeId }>() {
199
179
  if (!incoming) continue
200
180
 
201
181
  if (op === "upsert") {
202
- const result = upsertOne(list, incoming)
203
- if (result.changed) {
204
- changedShapes.push(incoming)
205
- changeDetails.push({
206
- type: result.isAdd ? 'add' : 'update',
207
- shape: incoming,
208
- oldShape: result.oldShape
209
- })
210
- }
182
+ if (upsertOne(list, incoming)) changedShapes.push(incoming)
211
183
  } else if (op === "update") {
212
- const result = updateOne(list, incoming)
213
- if (result.changed) {
214
- changedShapes.push(incoming)
215
- changeDetails.push({
216
- type: 'update',
217
- shape: incoming,
218
- oldShape: result.oldShape
219
- })
220
- }
184
+ if (updateOne(list, incoming)) changedShapes.push(incoming)
221
185
  } else if (op === "add") {
222
- const result = addOne(list, incoming)
223
- if (result.changed) {
224
- changedShapes.push(incoming)
225
- changeDetails.push({
226
- type: result.isAdd ? 'add' : 'update',
227
- shape: incoming,
228
- oldShape: result.oldShape
229
- })
230
- }
186
+ if (addOne(list, incoming)) changedShapes.push(incoming)
231
187
  }
232
188
  }
233
189
 
234
- return { changedShapes, changeDetails }
190
+ return { changedShapes }
235
191
  }
236
192
 
237
193
  return {
@@ -360,8 +360,7 @@ const updateShapes = (shapes: Shape[]) => {
360
360
  })
361
361
  clearEdgeStyleCache() // 批量更新时清空 edge 样式缓存,避免旧数据残留
362
362
  // graphStore.updateShapes(shapes)
363
- // 使用 clearAll 清空数据和索引,然后用 replace 重新加载
364
- graphStore.clearAll()
363
+ graphStore.shapes=[]
365
364
  graphStore.updateShapes(shapes, 'replace');
366
365
  // 在图形批量更新后(通常是从后端加载数据),初始化所有连线的端点
367
366
  // 确保连线不会横跨图元,而是从合适的位置连接
@@ -389,8 +388,7 @@ function sweepShapesWithInert() {
389
388
  let removed = 0
390
389
  for (let i = arr.length - 1; i >= 0; i--) {
391
390
  if ('inert' in arr[i]) {
392
- // 使用 removeShape 确保索引同步
393
- graphStore.removeShape(arr[i].id)
391
+ arr.splice(i, 1) // 直接删,避免 removeShape 的事件/副作用导致回填
394
392
  removed++
395
393
  }
396
394
  }