@mx-sose-front/mx-sose-graph 1.1.6 → 1.1.8
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 +1 -104
- package/dist/index.esm.js +72 -372
- package/dist/index.esm.js.map +1 -1
- package/dist/index.umd.js +1 -1
- package/dist/index.umd.js.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/components/InteractionLayer.vue +3 -93
- package/src/store/graphStore.ts +50 -232
- package/src/utils/containers.ts +2 -4
- package/src/utils/drag.ts +8 -48
- package/src/utils/keyboardUtils.ts +11 -25
- package/src/utils/shapeOps/shapeOps.ts +25 -69
- package/src/view/graph.vue +2 -4
package/src/utils/drag.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
-
//
|
|
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
|
|
194
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(深拷贝)
|
|
78
|
+
// 组装 payloads(深拷贝)
|
|
83
79
|
const payloads = Array.from(affectedIds)
|
|
84
|
-
.map(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
|
|
294
|
-
|
|
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
|
-
// 获取所有选中的图元
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
|
75
|
+
if (idx == null) return false
|
|
85
76
|
|
|
86
77
|
const lastIdx = list.length - 1
|
|
87
|
-
if (lastIdx < 0) return
|
|
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
|
|
88
|
+
return true
|
|
100
89
|
}
|
|
101
90
|
|
|
102
91
|
/** upsert:有则整条替换,无则 push 新增 */
|
|
103
|
-
const upsertOne = (list: Shape[], incoming: Shape)
|
|
92
|
+
const upsertOne = (list: Shape[], incoming: Shape) => {
|
|
104
93
|
const id = incoming?.id
|
|
105
|
-
if (id == null) return
|
|
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)
|
|
107
|
+
const updateOne = (list: Shape[], incoming: Shape) => {
|
|
121
108
|
const id = incoming?.id
|
|
122
|
-
if (id == null) return
|
|
109
|
+
if (id == null) return false
|
|
123
110
|
|
|
124
111
|
const idx = indexMap.get(id)
|
|
125
|
-
if (idx == null) return
|
|
112
|
+
if (idx == null) return false
|
|
126
113
|
|
|
127
|
-
const oldShape = list[idx]
|
|
128
114
|
list[idx] = incoming
|
|
129
|
-
return
|
|
115
|
+
return true
|
|
130
116
|
}
|
|
131
117
|
|
|
132
118
|
/**
|
|
133
119
|
* add:不存在才新增
|
|
134
|
-
* -
|
|
120
|
+
* - 已存在时选择“替换”保证最终一致
|
|
135
121
|
*/
|
|
136
|
-
const addOne = (list: Shape[], incoming: Shape)
|
|
122
|
+
const addOne = (list: Shape[], incoming: Shape) => {
|
|
137
123
|
const id = incoming?.id
|
|
138
|
-
if (id == null) return
|
|
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
|
-
})
|
|
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
|
|
179
|
-
return { changedShapes: incomingShapes
|
|
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
|
-
|
|
190
|
-
if (removed) {
|
|
191
|
-
changeDetails.push({ type: 'delete', shape: removed })
|
|
192
|
-
}
|
|
172
|
+
removeByIdFast(list, id)
|
|
193
173
|
}
|
|
194
|
-
return { changedShapes
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
190
|
+
return { changedShapes }
|
|
235
191
|
}
|
|
236
192
|
|
|
237
193
|
return {
|
package/src/view/graph.vue
CHANGED
|
@@ -360,8 +360,7 @@ const updateShapes = (shapes: Shape[]) => {
|
|
|
360
360
|
})
|
|
361
361
|
clearEdgeStyleCache() // 批量更新时清空 edge 样式缓存,避免旧数据残留
|
|
362
362
|
// graphStore.updateShapes(shapes)
|
|
363
|
-
|
|
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
|
-
//
|
|
393
|
-
graphStore.removeShape(arr[i].id)
|
|
391
|
+
arr.splice(i, 1) // 直接删,避免 removeShape 的事件/副作用导致回填
|
|
394
392
|
removed++
|
|
395
393
|
}
|
|
396
394
|
}
|