@mx-sose-front/mx-sose-graph 1.1.4 → 1.1.6
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 +120 -4
- package/dist/index.esm.js +989 -616
- 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 +2 -2
- package/src/components/DiagramListTooltip/DiagramListTooltip.vue +1 -1
- package/src/components/InteractionLayer.vue +93 -3
- package/src/constants/edgeShapeKeys.ts +4 -2
- package/src/constants/index.ts +306 -295
- package/src/render/shape-renderer.ts +13 -6
- package/src/store/graphStore.ts +256 -92
- package/src/utils/containers.ts +4 -2
- package/src/utils/contextMenuUtils.ts +40 -9
- package/src/utils/drag.ts +48 -8
- package/src/utils/graphDragService.ts +10 -9
- package/src/utils/keyboardUtils.ts +25 -11
- package/src/utils/shapeOps/shapeOps.ts +104 -32
- package/src/view/graph.vue +4 -2
|
@@ -1,20 +1,27 @@
|
|
|
1
1
|
import type { CSSProperties } from "vue";
|
|
2
2
|
import type { Shape } from "@/types";
|
|
3
|
-
import {
|
|
3
|
+
import { ShapeKeyMap, DiagramKeyMap, PinKeyMap, EdgeKeyMap } from "../constants";
|
|
4
4
|
import { getShapeComponentByKey } from "./shape-registry";
|
|
5
|
-
import { PinKeyMap } from "../constants";
|
|
6
5
|
|
|
7
|
-
// 根据 shapeKey 匹配具体组件
|
|
6
|
+
// 根据 shapeKey 匹配具体组件 (shapeType: 'shape')
|
|
8
7
|
function getComponentByShapeKey(shapeKey: string) {
|
|
9
|
-
const componentName = (
|
|
8
|
+
const componentName = (ShapeKeyMap as any)[shapeKey] || shapeKey;
|
|
10
9
|
return getShapeComponentByKey(componentName) || componentName;
|
|
11
10
|
}
|
|
11
|
+
|
|
12
|
+
// 根据 shapeKey 匹配 Pin 组件 (shapeType: 'pin')
|
|
12
13
|
function getComponentByPinKey(shapeKey: string) {
|
|
13
14
|
const componentName = (PinKeyMap as any)[shapeKey] || shapeKey;
|
|
14
15
|
return getShapeComponentByKey(componentName) || componentName;
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
// 根据 shapeKey
|
|
18
|
+
// 根据 shapeKey 匹配 Edge 组件 (shapeType: 'edge')
|
|
19
|
+
function getComponentByEdgeKey(shapeKey: string) {
|
|
20
|
+
const componentName = (EdgeKeyMap as any)[shapeKey] || shapeKey;
|
|
21
|
+
return getShapeComponentByKey(componentName) || componentName;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// 根据 shapeKey 匹配 Edge 组件 (shapeType: 'diagram')
|
|
18
25
|
export function getComponentByDiagramKey(shapeKey: string) {
|
|
19
26
|
const componentName = (DiagramKeyMap as any)[shapeKey] || shapeKey;
|
|
20
27
|
return getShapeComponentByKey(componentName) || componentName;
|
|
@@ -31,7 +38,7 @@ export const getShapeComponent = (shape: Shape) => {
|
|
|
31
38
|
case "diagram":
|
|
32
39
|
return getComponentByDiagramKey(shapeKey);
|
|
33
40
|
case "edge":
|
|
34
|
-
return
|
|
41
|
+
return getComponentByEdgeKey(shapeKey);
|
|
35
42
|
default:
|
|
36
43
|
return "ShapeComponent";
|
|
37
44
|
}
|
package/src/store/graphStore.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { defineStore } from 'pinia'
|
|
2
|
-
import { ref, computed, shallowReactive, nextTick, type Ref } from 'vue'
|
|
1
|
+
import { defineStore } from 'pinia'
|
|
2
|
+
import { ref, computed, shallowReactive, shallowRef, triggerRef, nextTick, type Ref } from 'vue'
|
|
3
3
|
import type { Shape, OwnerPayload, ShapeSizeUpdatePayload } from '../types'
|
|
4
4
|
import { eventBus } from './eventBus'
|
|
5
5
|
import { getPolicy, checkNestViaFront, } from '../utils/policy'
|
|
@@ -74,44 +74,132 @@ export const useGraphStore = defineStore('graph', () => {
|
|
|
74
74
|
const cutShapeIds = ref<Set<string>>(new Set())
|
|
75
75
|
// 剪贴板中图元的数量(用于工具栏按钮状态同步)
|
|
76
76
|
const copiedShapesCount = ref<number>(0)
|
|
77
|
-
|
|
77
|
+
// 拖动时的后代坐标快照(只在 startDrag 时计算一次,避免每帧重算)
|
|
78
|
+
// key: 被拖动的父元素 ID, value: 所有后代的坐标快照
|
|
79
|
+
type DescendantSnapshot = { id: string; x: number; y: number; width: number; height: number };
|
|
80
|
+
const dragDescendantsSnapshot = ref<Map<string, DescendantSnapshot[]>>(new Map())
|
|
78
81
|
// 计算属性
|
|
79
82
|
const shapeCount = computed(() => shapes.value.length)
|
|
80
83
|
const hasSelectedShape = computed(() => selectedShape.value !== null)
|
|
81
84
|
|
|
85
|
+
// ========== 增量索引:手动维护,避免 computed 全量重建 ==========
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 图元ID索引映射(增量维护版本)
|
|
89
|
+
* 用于快速通过ID查找图元对象
|
|
90
|
+
* key: 图元 ID, value: 图元对象
|
|
91
|
+
* 性能优化: O(1) 增删改查,避免每次 shapes 变化都 O(n) 重建
|
|
92
|
+
*/
|
|
93
|
+
const _shapeMapRef = shallowRef(new Map<string, Shape>())
|
|
94
|
+
// 使用 computed 包装,确保 Pinia 正确解包
|
|
95
|
+
const shapeMap = computed(() => _shapeMapRef.value)
|
|
96
|
+
|
|
82
97
|
/**
|
|
83
|
-
*
|
|
98
|
+
* 父子关系索引映射(增量维护版本)
|
|
84
99
|
* 用于快速查找某个图元的所有直接子图元
|
|
85
100
|
* key: 父图元 ID, value: 子图元 ID 数组
|
|
86
|
-
* 性能优化: O(
|
|
101
|
+
* 性能优化: O(1) 查询,增删时只更新受影响的条目
|
|
87
102
|
*/
|
|
88
|
-
const
|
|
89
|
-
|
|
103
|
+
const _parentChildMapRef = shallowRef(new Map<string, string[]>())
|
|
104
|
+
// 使用 computed 包装,确保 Pinia 正确解包
|
|
105
|
+
const parentChildMap = computed(() => _parentChildMapRef.value)
|
|
106
|
+
|
|
107
|
+
// ========== 索引维护内部方法 ==========
|
|
108
|
+
|
|
109
|
+
/** 添加图元到索引 */
|
|
110
|
+
function _indexAdd(shape: Shape) {
|
|
111
|
+
_shapeMapRef.value.set(shape.id, shape)
|
|
112
|
+
if (shape.parenShapeId) {
|
|
113
|
+
const children = _parentChildMapRef.value.get(shape.parenShapeId)
|
|
114
|
+
if (children) {
|
|
115
|
+
if (!children.includes(shape.id)) {
|
|
116
|
+
children.push(shape.id)
|
|
117
|
+
}
|
|
118
|
+
} else {
|
|
119
|
+
_parentChildMapRef.value.set(shape.parenShapeId, [shape.id])
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** 从索引中移除图元 */
|
|
125
|
+
function _indexRemove(shapeId: string) {
|
|
126
|
+
const shape = _shapeMapRef.value.get(shapeId)
|
|
127
|
+
if (!shape) return
|
|
128
|
+
_shapeMapRef.value.delete(shapeId)
|
|
129
|
+
if (shape.parenShapeId) {
|
|
130
|
+
const children = _parentChildMapRef.value.get(shape.parenShapeId)
|
|
131
|
+
if (children) {
|
|
132
|
+
const idx = children.indexOf(shapeId)
|
|
133
|
+
if (idx !== -1) children.splice(idx, 1)
|
|
134
|
+
if (children.length === 0) {
|
|
135
|
+
_parentChildMapRef.value.delete(shape.parenShapeId)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// 如果这个图元是父节点,清理它的子节点列表
|
|
140
|
+
_parentChildMapRef.value.delete(shapeId)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** 更新图元索引(处理父节点变更) */
|
|
144
|
+
function _indexUpdate(shape: Shape, oldParentId: string | null | undefined) {
|
|
145
|
+
_shapeMapRef.value.set(shape.id, shape)
|
|
146
|
+
const newParentId = shape.parenShapeId ?? null
|
|
147
|
+
const normalizedOldParent = oldParentId ?? null
|
|
148
|
+
|
|
149
|
+
if (normalizedOldParent !== newParentId) {
|
|
150
|
+
// 从旧父节点移除
|
|
151
|
+
if (normalizedOldParent) {
|
|
152
|
+
const oldChildren = _parentChildMapRef.value.get(normalizedOldParent)
|
|
153
|
+
if (oldChildren) {
|
|
154
|
+
const idx = oldChildren.indexOf(shape.id)
|
|
155
|
+
if (idx !== -1) oldChildren.splice(idx, 1)
|
|
156
|
+
if (oldChildren.length === 0) {
|
|
157
|
+
_parentChildMapRef.value.delete(normalizedOldParent)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// 添加到新父节点
|
|
162
|
+
if (newParentId) {
|
|
163
|
+
const newChildren = _parentChildMapRef.value.get(newParentId)
|
|
164
|
+
if (newChildren) {
|
|
165
|
+
if (!newChildren.includes(shape.id)) {
|
|
166
|
+
newChildren.push(shape.id)
|
|
167
|
+
}
|
|
168
|
+
} else {
|
|
169
|
+
_parentChildMapRef.value.set(newParentId, [shape.id])
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** 全量重建索引(用于 replace 操作或初始化) */
|
|
176
|
+
function _rebuildIndex() {
|
|
177
|
+
const newShapeMap = new Map<string, Shape>()
|
|
178
|
+
const newParentChildMap = new Map<string, string[]>()
|
|
179
|
+
|
|
90
180
|
shapes.value.forEach(shape => {
|
|
181
|
+
newShapeMap.set(shape.id, shape)
|
|
91
182
|
if (shape.parenShapeId) {
|
|
92
|
-
|
|
93
|
-
|
|
183
|
+
const children = newParentChildMap.get(shape.parenShapeId)
|
|
184
|
+
if (children) {
|
|
185
|
+
children.push(shape.id)
|
|
186
|
+
} else {
|
|
187
|
+
newParentChildMap.set(shape.parenShapeId, [shape.id])
|
|
94
188
|
}
|
|
95
|
-
map.get(shape.parenShapeId)!.push(shape.id);
|
|
96
189
|
}
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
_shapeMapRef.value = newShapeMap
|
|
193
|
+
_parentChildMapRef.value = newParentChildMap
|
|
194
|
+
triggerRef(_shapeMapRef)
|
|
195
|
+
triggerRef(_parentChildMapRef)
|
|
196
|
+
}
|
|
100
197
|
|
|
101
|
-
/**
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
* 解决嵌套图元场景下大量 find() 调用导致的性能问题
|
|
107
|
-
*/
|
|
108
|
-
const shapeMap = computed(() => {
|
|
109
|
-
const map = new Map<string, Shape>();
|
|
110
|
-
shapes.value.forEach(shape => {
|
|
111
|
-
map.set(shape.id, shape);
|
|
112
|
-
});
|
|
113
|
-
return map;
|
|
114
|
-
})
|
|
198
|
+
/** 触发索引的响应式更新 */
|
|
199
|
+
function _triggerIndexUpdate() {
|
|
200
|
+
triggerRef(_shapeMapRef)
|
|
201
|
+
triggerRef(_parentChildMapRef)
|
|
202
|
+
}
|
|
115
203
|
|
|
116
204
|
//图元shapeKey
|
|
117
205
|
const taggedValueLabels = ref<string[]>([]) // 隔间组件的图元类型
|
|
@@ -148,8 +236,11 @@ export const useGraphStore = defineStore('graph', () => {
|
|
|
148
236
|
}
|
|
149
237
|
|
|
150
238
|
shapes.value.push(shape)
|
|
239
|
+
// 增量更新索引
|
|
240
|
+
_indexAdd(shape)
|
|
241
|
+
_triggerIndexUpdate()
|
|
151
242
|
// 通过事件总线发送事件
|
|
152
|
-
eventBus.emit('shape-added', shape)
|
|
243
|
+
// eventBus.emit('shape-added', shape)
|
|
153
244
|
|
|
154
245
|
// 自动扩父逻辑:如果图元有父元素且需要自动扩父,触发扩父逻辑
|
|
155
246
|
const shouldAutoExpand = options?.autoExpandParent !== false // 默认为 true
|
|
@@ -164,16 +255,10 @@ export const useGraphStore = defineStore('graph', () => {
|
|
|
164
255
|
const removeShape = (shapeId: string) => {
|
|
165
256
|
const index = shapes.value.findIndex(s => s.id == shapeId)
|
|
166
257
|
if (index > -1) {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
// if (removedShape.parenShapeId) {
|
|
172
|
-
// const parent = shapes.value.find(s => s.id == removedShape.parenShapeId)
|
|
173
|
-
// if (parent && isShapeTypeNotInPackageTypes(parent)) {
|
|
174
|
-
// removeChildShapeFromCompartment(parent, removedShape, updateShape)
|
|
175
|
-
// }
|
|
176
|
-
// }
|
|
258
|
+
// 增量更新索引(在删除数组元素之前)
|
|
259
|
+
_indexRemove(shapeId)
|
|
260
|
+
shapes.value.splice(index, 1)
|
|
261
|
+
_triggerIndexUpdate()
|
|
177
262
|
// 如果删除的是当前选中的形状,清除选中状态
|
|
178
263
|
if (selectedShape.value?.id === shapeId) {
|
|
179
264
|
selectedShape.value = null
|
|
@@ -208,6 +293,8 @@ export const useGraphStore = defineStore('graph', () => {
|
|
|
208
293
|
const updateShapeRaw = (shapeId: string, updates: Partial<Shape>, id: 'id' | 'modelId' = 'id') => {
|
|
209
294
|
const shape = shapes.value.find(s => s[id] === shapeId)
|
|
210
295
|
if (shape) {
|
|
296
|
+
// 记录旧的父节点ID,用于索引更新
|
|
297
|
+
const oldParentId = shape.parenShapeId
|
|
211
298
|
// 对 Pin 做强保护:永不允许把 parenShapeId 置空/undefined
|
|
212
299
|
if (shape.shapeType === 'pin' && 'parenShapeId' in updates) {
|
|
213
300
|
const v: any = (updates as any).parenShapeId
|
|
@@ -224,6 +311,9 @@ export const useGraphStore = defineStore('graph', () => {
|
|
|
224
311
|
const nextBounds = updates.bounds ? { ...shape.bounds, ...updates.bounds } : shape.bounds
|
|
225
312
|
const nextStyle = updates.style ? { ...(shape.style || {}), ...(updates.style as any) } : shape.style
|
|
226
313
|
Object.assign(shape, updates, { bounds: nextBounds, style: nextStyle })
|
|
314
|
+
// 增量更新索引(处理父节点变更)
|
|
315
|
+
_indexUpdate(shape, oldParentId)
|
|
316
|
+
_triggerIndexUpdate()
|
|
227
317
|
eventBus.emit('shape-updated', shape, updates)
|
|
228
318
|
}
|
|
229
319
|
}
|
|
@@ -282,32 +372,46 @@ export const useGraphStore = defineStore('graph', () => {
|
|
|
282
372
|
}
|
|
283
373
|
// 被框选中的多个 shape/映射 selectedIds.value中的数据
|
|
284
374
|
const marqueeShapes = computed(() => {
|
|
285
|
-
const byId = (id: string) => shapes.value.find(s => s.id === id)
|
|
286
|
-
const isShape = (x: Shape | undefined): x is Shape => !!x
|
|
287
375
|
const pending = new Set(pendingNestedIds.value)
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
376
|
+
const result: Shape[] = []
|
|
377
|
+
// 使用 shapeMap 进行 O(1) 查找,避免 O(n²) 的 find
|
|
378
|
+
for (const id of selectedIds.value) {
|
|
379
|
+
if (pending.has(id)) continue
|
|
380
|
+
const shape = shapeMap.value.get(id)
|
|
381
|
+
if (shape) result.push(shape)
|
|
382
|
+
}
|
|
383
|
+
return result
|
|
292
384
|
})
|
|
293
385
|
//获取元素拖动预览框
|
|
386
|
+
// 性能优化:只返回"直接被拖动"的图元的 ghost,不包含后代
|
|
387
|
+
// 后代图元会跟随父元素移动,不需要单独显示 ghost
|
|
294
388
|
const ghostShadow = computed<Shape[]>(() => {
|
|
389
|
+
// 如果没有在拖动,直接返回空数组
|
|
390
|
+
if (!isDragging.value || draggingIds.value.length === 0) return []
|
|
391
|
+
|
|
295
392
|
const out: Shape[] = []
|
|
296
393
|
// 兜底:防止被持久化还原成非数组
|
|
297
394
|
const pendingList = Array.isArray(pendingNestedIds.value)
|
|
298
395
|
? pendingNestedIds.value
|
|
299
396
|
: []
|
|
300
397
|
const pendingSet = new Set(pendingList)
|
|
301
|
-
//
|
|
398
|
+
// 只为"直接被选中拖动"的图元创建 ghost,不包含后代
|
|
399
|
+
// 这样可以大幅减少 ghost 数量(从 3000 减少到用户实际选中的数量)
|
|
400
|
+
const directDragSet = new Set(dragSelectionSnapshot.value.length > 0
|
|
401
|
+
? dragSelectionSnapshot.value
|
|
402
|
+
: draggingIds.value.slice(0, 1)) // 至少保留主拖动项
|
|
403
|
+
|
|
302
404
|
const map = shapeMap.value
|
|
303
|
-
for (const id
|
|
405
|
+
for (const id of directDragSet) {
|
|
304
406
|
if (pendingSet.has(id)) continue
|
|
407
|
+
const ghostRect = dragGhost[id]
|
|
408
|
+
if (!ghostRect) continue
|
|
305
409
|
const orig = map.get(id)
|
|
306
410
|
if (!orig) continue
|
|
307
411
|
|
|
308
412
|
out.push({
|
|
309
413
|
...orig,
|
|
310
|
-
bounds: { ...
|
|
414
|
+
bounds: { ...ghostRect },
|
|
311
415
|
meta: { ...(orig as any).meta, isGhost: true } as any,
|
|
312
416
|
} as Shape)
|
|
313
417
|
}
|
|
@@ -316,18 +420,22 @@ export const useGraphStore = defineStore('graph', () => {
|
|
|
316
420
|
// 选择一组(用于框选/Shift 叠加)
|
|
317
421
|
const selectMany = (ids: string[]) => {
|
|
318
422
|
selectedIds.value = Array.from(new Set(ids))
|
|
423
|
+
// 使用 shapeMap 进行 O(1) 查找,避免 O(n) 的 find
|
|
319
424
|
selectedShape.value = ids.length
|
|
320
|
-
? (
|
|
425
|
+
? (shapeMap.value.get(ids[0]) ?? null)
|
|
321
426
|
: null
|
|
322
427
|
}
|
|
323
428
|
// 清空选择
|
|
324
429
|
const clearSelection = () => selectShape(null)
|
|
325
430
|
// 全选图元
|
|
326
431
|
const selectAll = () => {
|
|
327
|
-
//
|
|
328
|
-
const allShapeIds =
|
|
329
|
-
|
|
330
|
-
.
|
|
432
|
+
// 优化:直接遍历一次收集 id,避免 filter + map 两次遍历
|
|
433
|
+
const allShapeIds: string[] = []
|
|
434
|
+
for (const shape of shapes.value) {
|
|
435
|
+
if (shape.shapeType?.toLowerCase() !== 'diagram') {
|
|
436
|
+
allShapeIds.push(shape.id)
|
|
437
|
+
}
|
|
438
|
+
}
|
|
331
439
|
selectMany(allShapeIds)
|
|
332
440
|
}
|
|
333
441
|
// 设置图表标题
|
|
@@ -341,7 +449,7 @@ export const useGraphStore = defineStore('graph', () => {
|
|
|
341
449
|
/**
|
|
342
450
|
* 批量操作图元(新增 / 修改 / 删除)
|
|
343
451
|
* @param payload - 新增/修改时传 Shape[];删除时可传 Shape[](只需要 id)或 id 数组
|
|
344
|
-
* @param op - 操作类型:add | update | delete | upsert
|
|
452
|
+
* @param op - 操作类型:add | update | delete | upsert(默认)| replace
|
|
345
453
|
* @param options - 额外选项(保持你原来的 autoExpandParents)
|
|
346
454
|
*/
|
|
347
455
|
const updateShapes = (
|
|
@@ -351,11 +459,44 @@ export const useGraphStore = defineStore('graph', () => {
|
|
|
351
459
|
) => {
|
|
352
460
|
// 先把 comparents[*].comparentShapes 铺平同步到运行时
|
|
353
461
|
// const expanded = syncComparentShapesIntoRuntime(newShapes)
|
|
354
|
-
const { changedShapes } = applyShapeOp({
|
|
462
|
+
const { changedShapes, changeDetails } = applyShapeOp({
|
|
355
463
|
list: shapes.value,
|
|
356
464
|
payload: payload as any, // delete 时是 id[],其余是 Shape[]
|
|
357
465
|
op,
|
|
358
466
|
})
|
|
467
|
+
|
|
468
|
+
// 根据操作类型选择索引更新策略
|
|
469
|
+
if (op === 'replace' || changeDetails.length === 0) {
|
|
470
|
+
// replace 操作或无变更详情时,全量重建索引
|
|
471
|
+
_rebuildIndex()
|
|
472
|
+
} else {
|
|
473
|
+
// 增量更新索引
|
|
474
|
+
for (const detail of changeDetails) {
|
|
475
|
+
if (detail.type === 'add') {
|
|
476
|
+
_indexAdd(detail.shape)
|
|
477
|
+
} else if (detail.type === 'delete') {
|
|
478
|
+
// 删除时 shape 已经从数组移除,需要手动清理索引
|
|
479
|
+
_shapeMapRef.value.delete(detail.shape.id)
|
|
480
|
+
const parentId = (detail.shape as any).parenShapeId
|
|
481
|
+
if (parentId) {
|
|
482
|
+
const children = _parentChildMapRef.value.get(parentId)
|
|
483
|
+
if (children) {
|
|
484
|
+
const idx = children.indexOf(detail.shape.id)
|
|
485
|
+
if (idx !== -1) children.splice(idx, 1)
|
|
486
|
+
if (children.length === 0) {
|
|
487
|
+
_parentChildMapRef.value.delete(parentId)
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
_parentChildMapRef.value.delete(detail.shape.id)
|
|
492
|
+
} else if (detail.type === 'update') {
|
|
493
|
+
const oldParentId = (detail.oldShape as any)?.parenShapeId
|
|
494
|
+
_indexUpdate(detail.shape, oldParentId)
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
_triggerIndexUpdate()
|
|
498
|
+
}
|
|
499
|
+
|
|
359
500
|
pendingNestedIds.value = []
|
|
360
501
|
// eventBus.emit('shapes-updated', newShapes)
|
|
361
502
|
// hydrateAllComparents()
|
|
@@ -375,33 +516,16 @@ export const useGraphStore = defineStore('graph', () => {
|
|
|
375
516
|
//清空图元数据
|
|
376
517
|
const clearAll = () => {
|
|
377
518
|
shapes.value = []
|
|
519
|
+
// 清空索引
|
|
520
|
+
_shapeMapRef.value = new Map()
|
|
521
|
+
_parentChildMapRef.value = new Map()
|
|
522
|
+
triggerRef(_shapeMapRef)
|
|
523
|
+
triggerRef(_parentChildMapRef)
|
|
378
524
|
diagrams.value = []
|
|
379
525
|
selectedShape.value = null
|
|
380
526
|
pendingNestedIds.value = []
|
|
381
527
|
eventBus.emit('shapes-cleared')
|
|
382
528
|
}
|
|
383
|
-
|
|
384
|
-
/** 把所有隔间父的 comparents 从 comparentsJSON 还原;若没有 JSON 就按 parenShapeId 兜底重建 */
|
|
385
|
-
// const hydrateAllComparents = () => {
|
|
386
|
-
// if (comparentsHydratedOnce.value) return
|
|
387
|
-
// if (!shapes.value.length) return
|
|
388
|
-
// let hadAnyJSON = false
|
|
389
|
-
// for (const p of shapes.value) {
|
|
390
|
-
// if (isShapeTypeNotInPackageTypes(p)) {
|
|
391
|
-
// const any = p as any
|
|
392
|
-
// if (typeof any.comparentsJSON === 'string' && any.comparentsJSON.length) {
|
|
393
|
-
// hydrateComparentsFromJSON(p, updateShape)
|
|
394
|
-
// hadAnyJSON = true
|
|
395
|
-
// }
|
|
396
|
-
// }
|
|
397
|
-
// }
|
|
398
|
-
// // 历史数据可能还没有 comparentsJSON:按父子关系兜底重建一次
|
|
399
|
-
// if (!hadAnyJSON) {
|
|
400
|
-
// rebuildAllCompartmentChildrenAsShapes(shapes.value, updateShape)
|
|
401
|
-
// }
|
|
402
|
-
|
|
403
|
-
// comparentsHydratedOnce.value = true
|
|
404
|
-
// }
|
|
405
529
|
//拖动元素相关操作
|
|
406
530
|
// 开始拖动已有元素(仅对 shape 生效)
|
|
407
531
|
const byId = (id: string) => shapes.value.find(s => s.id === id) || null
|
|
@@ -409,17 +533,13 @@ export const useGraphStore = defineStore('graph', () => {
|
|
|
409
533
|
startDrag([shapeId], pointer)
|
|
410
534
|
}
|
|
411
535
|
const startDrag = (ids: string[], pointer: { x: number; y: number }) => {
|
|
412
|
-
//
|
|
536
|
+
// 记录"当前真正被用户选中"的快照 结束时恢复
|
|
413
537
|
dragSelectionSnapshot.value = selectedIds.value.length ? [...selectedIds.value] : [ids[0]]
|
|
414
538
|
primaryDragId.value = ids[0] || null
|
|
415
539
|
|
|
416
|
-
//
|
|
417
|
-
|
|
418
|
-
ids.
|
|
419
|
-
expanded.push(id)
|
|
420
|
-
collectDescendantIds(shapes.value, id).forEach(cid => expanded.push(cid))
|
|
421
|
-
})
|
|
422
|
-
const validIds = Array.from(new Set(expanded)).filter(id => {
|
|
540
|
+
// 性能优化:只对"直接选中"的图元建立 dragBase/dragGhost
|
|
541
|
+
// 后代图元会跟随父元素移动,不需要单独创建 ghost,大幅减少渲染开销
|
|
542
|
+
const validIds = ids.filter(id => {
|
|
423
543
|
const s = byId(id); return s && getPolicy(s!).draggable
|
|
424
544
|
})
|
|
425
545
|
if (!validIds.length) return
|
|
@@ -433,6 +553,32 @@ export const useGraphStore = defineStore('graph', () => {
|
|
|
433
553
|
draggingIds.value = validIds
|
|
434
554
|
dragAnchor.value = { x: pointer.x, y: pointer.y }
|
|
435
555
|
|
|
556
|
+
// 构建后代坐标快照(只计算一次,供预览框渲染使用)
|
|
557
|
+
const newSnapshot = new Map<string, { id: string; x: number; y: number; width: number; height: number }[]>()
|
|
558
|
+
for (const parentId of validIds) {
|
|
559
|
+
const descendants: { id: string; x: number; y: number; width: number; height: number }[] = []
|
|
560
|
+
// 递归收集所有后代
|
|
561
|
+
const collectDescendants = (pid: string) => {
|
|
562
|
+
const childIds = _parentChildMapRef.value.get(pid) || []
|
|
563
|
+
for (const childId of childIds) {
|
|
564
|
+
const childShape = _shapeMapRef.value.get(childId)
|
|
565
|
+
if (childShape?.bounds) {
|
|
566
|
+
descendants.push({
|
|
567
|
+
id: childId,
|
|
568
|
+
x: childShape.bounds.x ?? 0,
|
|
569
|
+
y: childShape.bounds.y ?? 0,
|
|
570
|
+
width: childShape.bounds.width ?? 0,
|
|
571
|
+
height: childShape.bounds.height ?? 0,
|
|
572
|
+
})
|
|
573
|
+
}
|
|
574
|
+
collectDescendants(childId) // 递归
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
collectDescendants(parentId)
|
|
578
|
+
newSnapshot.set(parentId, descendants)
|
|
579
|
+
}
|
|
580
|
+
dragDescendantsSnapshot.value = newSnapshot
|
|
581
|
+
|
|
436
582
|
const currentDiagram = shapes.value.find(s => s.shapeType === 'diagram')
|
|
437
583
|
if (currentDiagram) {
|
|
438
584
|
dragBaseCanvasSize.value = {
|
|
@@ -589,6 +735,7 @@ export const useGraphStore = defineStore('graph', () => {
|
|
|
589
735
|
}
|
|
590
736
|
// 结束拖动:提交位置/尺寸 + 处理归属 + 规范层级
|
|
591
737
|
const endDragShape = async (source?: string) => {
|
|
738
|
+
// 防并发:同一次拖拽只允许进入一次
|
|
592
739
|
if (!isDragging.value) return
|
|
593
740
|
// 提交拖动几何(commitDrag 只是“位置/大小”的更新)
|
|
594
741
|
const changedIds = commitDrag(
|
|
@@ -632,7 +779,6 @@ export const useGraphStore = defineStore('graph', () => {
|
|
|
632
779
|
pendingNestedIds,
|
|
633
780
|
addShape,
|
|
634
781
|
})
|
|
635
|
-
|
|
636
782
|
if (nestResult.cancelled) {
|
|
637
783
|
// 嵌套校验失败,已经在 graphDragService 里回滚了位置/父级,这里只需要收尾
|
|
638
784
|
cleanupAfterDrag()
|
|
@@ -652,7 +798,6 @@ export const useGraphStore = defineStore('graph', () => {
|
|
|
652
798
|
1,
|
|
653
799
|
)
|
|
654
800
|
}
|
|
655
|
-
|
|
656
801
|
// —— Pin 终极兜底:不允许在本次拖拽后变成“无父” ——
|
|
657
802
|
for (const id of changedIds) {
|
|
658
803
|
const node = shapes.value.find(s => s.id === id)
|
|
@@ -671,13 +816,33 @@ export const useGraphStore = defineStore('graph', () => {
|
|
|
671
816
|
})
|
|
672
817
|
//对“本次直接拖动的隔间”做一次自动扩容检查
|
|
673
818
|
autoExpandMovedCompartmentsAfterDrag(shapes.value, changedIds, updateShape)
|
|
819
|
+
// 把 clone 也并入“需要同步 comparents 的变更集合”
|
|
820
|
+
const allReparentIds = [...changedIds, ...clonedIds]
|
|
821
|
+
// prevParentById 要补齐 clone 的“拖动前父”,因为 clone 之前不存在
|
|
822
|
+
const prevMapForComparents = { ...prevParentById }
|
|
823
|
+
for (const cid of clonedIds) {
|
|
824
|
+
// clone 之前不存在 => 认为 beforePid = null / ''
|
|
825
|
+
prevMapForComparents[cid] = null
|
|
826
|
+
}
|
|
674
827
|
// 处理comparents字段
|
|
675
828
|
syncShowComparentsByReparent(
|
|
676
829
|
shapes.value,
|
|
677
|
-
|
|
678
|
-
|
|
830
|
+
allReparentIds,
|
|
831
|
+
prevMapForComparents,
|
|
679
832
|
(id, u) => updateShapeRaw(id, u)
|
|
680
833
|
)
|
|
834
|
+
// 在生成 payloads 之前,把父扩容先做掉
|
|
835
|
+
const shapeId = ownerPayload?.shapeId
|
|
836
|
+
if (shapeId != null) {
|
|
837
|
+
const child = shapes.value.find(s => s.id == shapeId)
|
|
838
|
+
if (child) {
|
|
839
|
+
expandParentByChild({
|
|
840
|
+
shapes: shapes.value,
|
|
841
|
+
child,
|
|
842
|
+
updateShape,
|
|
843
|
+
})
|
|
844
|
+
}
|
|
845
|
+
}
|
|
681
846
|
// 收集受影响的所有 shape(自身 + 父 + 子树 + 祖先)
|
|
682
847
|
const affectedIds = collectAffectedShapeIds(shapes.value, changedIds, clonedIds)
|
|
683
848
|
//组装 payloads(深拷贝),给更新接口用
|
|
@@ -688,7 +853,6 @@ export const useGraphStore = defineStore('graph', () => {
|
|
|
688
853
|
ownerPayload,
|
|
689
854
|
updateShape,
|
|
690
855
|
})
|
|
691
|
-
|
|
692
856
|
// 先同步调整画布大小,确保获取到最新的画布数据
|
|
693
857
|
adjustCanvasToFitAllShapes()
|
|
694
858
|
|
|
@@ -705,11 +869,8 @@ export const useGraphStore = defineStore('graph', () => {
|
|
|
705
869
|
}
|
|
706
870
|
}
|
|
707
871
|
}
|
|
708
|
-
console.log("drag end payloads:", payloads, ownerPayload);
|
|
709
|
-
|
|
710
872
|
//对外只暴露一个事件:shape-drag-end
|
|
711
873
|
eventBus.emit('shape-drag-end', payloads, ownerPayload, onNestDone)
|
|
712
|
-
|
|
713
874
|
// 清理拖拽状态
|
|
714
875
|
cleanupAfterDrag()
|
|
715
876
|
}
|
|
@@ -770,6 +931,7 @@ export const useGraphStore = defineStore('graph', () => {
|
|
|
770
931
|
dragSelectionSnapshot.value = []
|
|
771
932
|
primaryDragId.value = null
|
|
772
933
|
dragBaseCanvasSize.value = null
|
|
934
|
+
dragDescendantsSnapshot.value = new Map() // 清理后代快照
|
|
773
935
|
for (const k in dragGhost) delete dragGhost[k]
|
|
774
936
|
}
|
|
775
937
|
//设置当前打开的画布id
|
|
@@ -925,6 +1087,8 @@ export const useGraphStore = defineStore('graph', () => {
|
|
|
925
1087
|
shapeMap, // 图元ID索引映射 (性能优化)
|
|
926
1088
|
marqueeShapes,
|
|
927
1089
|
ghostShadow,
|
|
1090
|
+
dragDescendantsSnapshot, // 拖动时的后代坐标快照
|
|
1091
|
+
dragBase, // 拖动开始时的坐标快照
|
|
928
1092
|
scales,
|
|
929
1093
|
activeDiagramId,
|
|
930
1094
|
// 当前活动画布的缩放比例
|
package/src/utils/containers.ts
CHANGED
|
@@ -709,9 +709,11 @@ const pointInRect = (pt: { x: number; y: number }, r: Rect, margin = 0) =>
|
|
|
709
709
|
pt.y <= r.y + r.height - margin
|
|
710
710
|
|
|
711
711
|
const depthOf = (shapes: Shape[], id: string): number => {
|
|
712
|
+
const graphStore = useGraphStore();
|
|
713
|
+
const byId = graphStore.shapeMap;
|
|
712
714
|
let d = 0
|
|
713
|
-
let p =
|
|
714
|
-
while (p) { d++; p =
|
|
715
|
+
let p = byId.get(id)?.parenShapeId ?? null
|
|
716
|
+
while (p) { d++; p = byId.get(p)?.parenShapeId ?? null }
|
|
715
717
|
return d
|
|
716
718
|
}
|
|
717
719
|
|
|
@@ -163,10 +163,43 @@ export class ContextMenuUtils {
|
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
/**
|
|
166
|
-
*
|
|
167
|
-
|
|
166
|
+
* 检查剪贴板是否有内容
|
|
167
|
+
*/
|
|
168
|
+
static hasClipboardContent(): boolean {
|
|
169
|
+
return this.copiedShapes.length > 0;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* 清空剪贴板(粘贴完成后调用)
|
|
174
|
+
*/
|
|
175
|
+
static clearClipboard() {
|
|
176
|
+
this.copiedShapes = [];
|
|
177
|
+
this.operationType = 'copy';
|
|
178
|
+
|
|
179
|
+
// 清空 GraphStore 中的剪贴板数量
|
|
180
|
+
const graphStore = useGraphStore();
|
|
181
|
+
graphStore.setCopiedShapesCount(0);
|
|
182
|
+
console.log('剪贴板已清空');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* 清除剪切状态(只清除SVG遮盖层,不清除剪贴板数据)
|
|
187
|
+
* 当点击空白处时调用
|
|
168
188
|
*/
|
|
169
189
|
static clearCutState() {
|
|
190
|
+
// 只清除剪切遮盖层
|
|
191
|
+
if (this.operationType === 'cut') {
|
|
192
|
+
const graphStore = useGraphStore();
|
|
193
|
+
graphStore.clearCutShapeIds();
|
|
194
|
+
}
|
|
195
|
+
// 注意:不清除 copiedShapes 和 operationType,保留剪贴板数据
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* 完全清除剪切状态和剪贴板数据
|
|
200
|
+
* 当需要完全重置时调用(如按ESC取消操作)
|
|
201
|
+
*/
|
|
202
|
+
static clearAll() {
|
|
170
203
|
if (this.operationType === 'cut') {
|
|
171
204
|
const graphStore = useGraphStore();
|
|
172
205
|
graphStore.clearCutShapeIds();
|
|
@@ -224,7 +257,7 @@ export class ContextMenuUtils {
|
|
|
224
257
|
/**
|
|
225
258
|
* 处理粘贴
|
|
226
259
|
*/
|
|
227
|
-
static handlePaste(
|
|
260
|
+
static handlePaste(_target?: any) {
|
|
228
261
|
if (this.copiedShapes.length === 0) {
|
|
229
262
|
return;
|
|
230
263
|
}
|
|
@@ -237,17 +270,15 @@ export class ContextMenuUtils {
|
|
|
237
270
|
operationType: this.operationType
|
|
238
271
|
});
|
|
239
272
|
|
|
240
|
-
//
|
|
273
|
+
// 如果是剪切操作,粘贴后清除剪切状态遮盖层
|
|
241
274
|
if (this.operationType === 'cut') {
|
|
242
275
|
const graphStore = useGraphStore();
|
|
243
276
|
graphStore.clearCutShapeIds();
|
|
244
|
-
|
|
245
|
-
// 剪切粘贴后清空剪贴板数量
|
|
246
|
-
graphStore.setCopiedShapesCount(0);
|
|
247
|
-
this.copiedShapes = [];
|
|
248
|
-
this.operationType = 'copy';
|
|
249
277
|
}
|
|
250
278
|
|
|
279
|
+
// 粘贴完成后清空剪贴板(无论是复制还是剪切)
|
|
280
|
+
this.clearClipboard();
|
|
281
|
+
|
|
251
282
|
console.log('已粘贴的图元:', pastedShapes, '操作类型:', this.operationType);
|
|
252
283
|
}
|
|
253
284
|
|