@mx-sose-front/mx-sose-graph 1.1.3 → 1.1.5
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 +177 -9
- package/dist/index.esm.js +4569 -63119
- package/dist/index.esm.js.map +1 -1
- package/dist/index.umd.js +1 -39
- package/dist/index.umd.js.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +10 -1
- package/src/components/ContextMenu/ContextMenu.vue +10 -10
- package/src/components/DiagramListTooltip/DiagramListTooltip.vue +6 -11
- package/src/components/InteractionLayer.vue +323 -157
- package/src/components/LineStyle/LineStyleMarker.vue +1 -1
- package/src/components/{NameEditor.vue → NameEditor/NameEditor.vue} +4 -4
- package/src/components/{SelectionBox.vue → SelectionBox/SelectionBox.vue} +5 -5
- package/src/components/Shape/Block.vue +1 -1
- package/src/constants/edgeShapeKeys.ts +43 -3
- package/src/constants/index.ts +19 -4
- package/src/hooks/index.ts +3 -0
- package/src/hooks/useHighlight.ts +223 -0
- package/src/hooks/useNameEdit.ts +234 -0
- package/src/{utils/resizeUtils.ts → hooks/useResize.ts} +55 -155
- package/src/index.ts +4 -1
- package/src/render/shape-renderer.ts +59 -46
- package/src/statics/icons/createMenu/show.png +0 -0
- package/src/statics/icons/createMenu/tree.png +0 -0
- 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
- package/src/statics/icons/createMenu//345/261/225/347/244/272/350/277/236/347/272/277@3x.png +0 -0
- package/src/statics/icons/createMenu//346/211/200/345/234/250/345/233/276/350/241/250@3x.png +0 -0
- package/src/store/graphStore.ts +185 -65
- package/src/types/index.ts +4 -2
- package/src/types/interactionLayer.ts +1 -0
- package/src/utils/batchAutoExpand.ts +65 -0
- package/src/utils/compartment.ts +78 -4
- package/src/utils/containers.ts +24 -10
- package/src/utils/contextMenuUtils.ts +106 -147
- package/src/utils/drag.ts +10 -5
- package/src/utils/edgeUtils.ts +3 -4
- package/src/utils/graphDragService.ts +27 -23
- package/src/utils/iconLoader.ts +7 -7
- package/src/utils/keyboardUtils.ts +195 -32
- package/src/utils/pinUtils.ts +1 -2
- package/src/utils/shapeOps/shapeOps.ts +168 -0
- package/src/utils/viewportCulling.ts +193 -0
- package/src/view/graph.vue +115 -60
- package/src/utils/highlightUtils.ts +0 -162
- package/src/utils/nameEditUtils.ts +0 -137
- /package/src/statics/icons/createMenu/{scissors.png → cut.png} +0 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { ref, computed, type Ref, type ComputedRef } from 'vue'
|
|
2
|
+
import type { Shape } from '../types'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 视口边界类型
|
|
6
|
+
*/
|
|
7
|
+
export interface ViewportBounds {
|
|
8
|
+
left: number
|
|
9
|
+
top: number
|
|
10
|
+
right: number
|
|
11
|
+
bottom: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 视口裁剪配置
|
|
16
|
+
*/
|
|
17
|
+
export interface ViewportCullingOptions {
|
|
18
|
+
/** 缓冲区大小(像素),默认200 */
|
|
19
|
+
bufferSize?: number
|
|
20
|
+
/** 是否启用连线完整性保证,默认true */
|
|
21
|
+
ensureEdgeIntegrity?: boolean
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 视口裁剪工具类
|
|
26
|
+
* 用于优化大数据量场景下的渲染性能,只渲染可见区域内的图元
|
|
27
|
+
*/
|
|
28
|
+
export class ViewportCulling {
|
|
29
|
+
private viewportBounds: Ref<ViewportBounds>
|
|
30
|
+
private bufferSize: number
|
|
31
|
+
private ensureEdgeIntegrity: boolean
|
|
32
|
+
|
|
33
|
+
constructor(options: ViewportCullingOptions = {}) {
|
|
34
|
+
this.bufferSize = options.bufferSize ?? 200
|
|
35
|
+
this.ensureEdgeIntegrity = options.ensureEdgeIntegrity ?? true
|
|
36
|
+
|
|
37
|
+
// 初始化视口边界(默认无限大,渲染所有图元)
|
|
38
|
+
this.viewportBounds = ref({
|
|
39
|
+
left: -Infinity,
|
|
40
|
+
top: -Infinity,
|
|
41
|
+
right: Infinity,
|
|
42
|
+
bottom: Infinity
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 更新视口边界
|
|
48
|
+
* @param container 画布容器元素
|
|
49
|
+
* @param scale 当前缩放比例
|
|
50
|
+
*/
|
|
51
|
+
updateViewport(container: HTMLElement | null, scale: number): void {
|
|
52
|
+
if (!container) return
|
|
53
|
+
|
|
54
|
+
const rect = container.getBoundingClientRect()
|
|
55
|
+
|
|
56
|
+
// 缓冲区:根据缩放比例调整,提前加载即将进入视口的图元
|
|
57
|
+
const buffer = this.bufferSize / scale
|
|
58
|
+
|
|
59
|
+
this.viewportBounds.value = {
|
|
60
|
+
left: (container.scrollLeft / scale) - buffer,
|
|
61
|
+
top: (container.scrollTop / scale) - buffer,
|
|
62
|
+
right: (container.scrollLeft + rect.width) / scale + buffer,
|
|
63
|
+
bottom: (container.scrollTop + rect.height) / scale + buffer
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 判断图元是否在视口内(AABB碰撞检测)
|
|
69
|
+
* @param shape 图元对象
|
|
70
|
+
* @returns 是否在视口内
|
|
71
|
+
*/
|
|
72
|
+
isShapeInViewport(shape: Shape): boolean {
|
|
73
|
+
const bounds = shape.bounds
|
|
74
|
+
const viewport = this.viewportBounds.value
|
|
75
|
+
|
|
76
|
+
// 没有完整bounds的图元默认渲染
|
|
77
|
+
if (!bounds || bounds.x === undefined || bounds.y === undefined ||
|
|
78
|
+
bounds.width === undefined || bounds.height === undefined) {
|
|
79
|
+
return true
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// AABB碰撞检测:判断矩形是否相交
|
|
83
|
+
// 如果图元完全在视口外的任一方向,则返回false
|
|
84
|
+
return !(
|
|
85
|
+
bounds.x + bounds.width < viewport.left || // 完全在左边
|
|
86
|
+
bounds.x > viewport.right || // 完全在右边
|
|
87
|
+
bounds.y + bounds.height < viewport.top || // 完全在上边
|
|
88
|
+
bounds.y > viewport.bottom // 完全在下边
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 过滤出可见的图元
|
|
94
|
+
* @param shapes 所有图元数组
|
|
95
|
+
* @returns 可见图元数组
|
|
96
|
+
*/
|
|
97
|
+
filterVisibleShapes(shapes: Shape[]): Shape[] {
|
|
98
|
+
if (!shapes.length) return []
|
|
99
|
+
|
|
100
|
+
// 创建图元索引,优化查找性能
|
|
101
|
+
const shapeMap = new Map<string, Shape>()
|
|
102
|
+
shapes.forEach(s => shapeMap.set(s.id, s))
|
|
103
|
+
|
|
104
|
+
// 收集需要渲染的图元ID
|
|
105
|
+
const visibleIds = new Set<string>()
|
|
106
|
+
|
|
107
|
+
// 1. 添加视口内的图元
|
|
108
|
+
shapes.forEach(shape => {
|
|
109
|
+
if (this.isShapeInViewport(shape)) {
|
|
110
|
+
visibleIds.add(shape.id)
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
// 2. 添加可见图元的所有父级(嵌套支持)
|
|
115
|
+
// 确保嵌套图元的父容器也被渲染
|
|
116
|
+
const shapesToAdd = new Set<string>()
|
|
117
|
+
shapes.forEach(shape => {
|
|
118
|
+
if (visibleIds.has(shape.id) && shape.parenShapeId) {
|
|
119
|
+
let parentId: string | undefined = shape.parenShapeId
|
|
120
|
+
while (parentId) {
|
|
121
|
+
if (!visibleIds.has(parentId)) {
|
|
122
|
+
shapesToAdd.add(parentId)
|
|
123
|
+
}
|
|
124
|
+
const parent = shapeMap.get(parentId) // ✅ O(1) 查找
|
|
125
|
+
parentId = parent?.parenShapeId
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
})
|
|
129
|
+
shapesToAdd.forEach(id => visibleIds.add(id))
|
|
130
|
+
|
|
131
|
+
// 3. 处理连线:如果连线的任一端点在视口内,则渲染整条连线及两个端点
|
|
132
|
+
if (this.ensureEdgeIntegrity) {
|
|
133
|
+
shapes.forEach(shape => {
|
|
134
|
+
if (shape.shapeType === 'edge' && shape.sourceId && shape.targetId) {
|
|
135
|
+
// 检查端点图元是否存在 (使用 shapeMap, O(1))
|
|
136
|
+
const hasSource = shapeMap.has(shape.sourceId)
|
|
137
|
+
const hasTarget = shapeMap.has(shape.targetId)
|
|
138
|
+
|
|
139
|
+
// 只有当两个端点都存在时才处理连线
|
|
140
|
+
if (hasSource && hasTarget) {
|
|
141
|
+
// 如果连线本身在视口内,或任一端点在视口内
|
|
142
|
+
if (visibleIds.has(shape.id) ||
|
|
143
|
+
visibleIds.has(shape.sourceId) ||
|
|
144
|
+
visibleIds.has(shape.targetId)) {
|
|
145
|
+
visibleIds.add(shape.id)
|
|
146
|
+
visibleIds.add(shape.sourceId)
|
|
147
|
+
visibleIds.add(shape.targetId)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 4. 返回需要渲染的图元
|
|
155
|
+
return shapes.filter(s => visibleIds.has(s.id))
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* 创建可见图元的计算属性
|
|
160
|
+
* @param allShapes 所有图元的Ref
|
|
161
|
+
* @returns 可见图元的ComputedRef
|
|
162
|
+
*/
|
|
163
|
+
createVisibleShapesComputed(allShapes: Ref<Shape[]>): ComputedRef<Shape[]> {
|
|
164
|
+
return computed(() => this.filterVisibleShapes(allShapes.value))
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* 获取当前视口边界
|
|
169
|
+
*/
|
|
170
|
+
getViewportBounds(): ViewportBounds {
|
|
171
|
+
return this.viewportBounds.value
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* 计算性能提升百分比
|
|
176
|
+
* @param totalCount 总图元数量
|
|
177
|
+
* @param visibleCount 可见图元数量
|
|
178
|
+
* @returns 性能提升百分比
|
|
179
|
+
*/
|
|
180
|
+
static calculatePerformanceImprovement(totalCount: number, visibleCount: number): number {
|
|
181
|
+
if (totalCount === 0) return 0
|
|
182
|
+
return Math.round(((totalCount - visibleCount) / totalCount) * 100)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* 创建视口裁剪实例的便捷函数
|
|
188
|
+
* @param options 配置选项
|
|
189
|
+
* @returns ViewportCulling实例
|
|
190
|
+
*/
|
|
191
|
+
export function useViewportCulling(options?: ViewportCullingOptions): ViewportCulling {
|
|
192
|
+
return new ViewportCulling(options)
|
|
193
|
+
}
|
package/src/view/graph.vue
CHANGED
|
@@ -3,14 +3,25 @@
|
|
|
3
3
|
<!-- 画布内容区域 -->
|
|
4
4
|
<div class="diagram-content" ref="diagramContentRef" @wheel="handleWheel">
|
|
5
5
|
<div class="shapes-container" v-if="shapes.length > 0">
|
|
6
|
-
<component v-for="shape in shapes" :key="shape.id"
|
|
7
|
-
|
|
6
|
+
<component v-for="shape in shapes" :key="shape.id"
|
|
7
|
+
v-memo="[shape.id, shape.bounds, graphStore.selectedShape?.id === shape.id]" :is="getShapeComponent(shape)"
|
|
8
|
+
:shape="shape" :style="getShapeStyle(shape)" @name-click="handleNameClick" @shape-click="handleShapeClick"
|
|
8
9
|
@edge-click="handleEdgeClick"
|
|
9
|
-
:is-selected="graphStore.selectedShape?.id === shape.id && shape.shapeType === 'edge'"
|
|
10
|
-
|
|
10
|
+
:is-selected="graphStore.selectedShape?.id === shape.id && shape.shapeType === 'edge'" class="shape-element"
|
|
11
|
+
:data-shape-id="shape.id"
|
|
11
12
|
@compartment-metrics="(m: any, shape: any) => onCompartmentMetrics(m, shape)" />
|
|
12
13
|
</div>
|
|
13
14
|
|
|
15
|
+
<!-- 剪切状态遮盖层 - 使用 SVG 渲染,性能更优 -->
|
|
16
|
+
<svg v-if="cutShapeBounds.length > 0" class="cut-overlay-svg">
|
|
17
|
+
<rect v-for="bounds in cutShapeBounds" :key="bounds.id"
|
|
18
|
+
:x="bounds.x"
|
|
19
|
+
:y="bounds.y"
|
|
20
|
+
:width="bounds.width"
|
|
21
|
+
:height="bounds.height"
|
|
22
|
+
fill="rgba(255, 255, 255, 0.6)" />
|
|
23
|
+
</svg>
|
|
24
|
+
|
|
14
25
|
<!-- 交互层 - 整合了连接层逻辑 -->
|
|
15
26
|
<InteractionLayer v-if="diagramBounds" ref="interactionLayerRef" :connect-shape-data="connectShapeData"
|
|
16
27
|
:diagram-bounds="diagramBounds" :style="{
|
|
@@ -20,9 +31,9 @@
|
|
|
20
31
|
top: `${diagramBounds.y}px`
|
|
21
32
|
}" :resShape="resShape" :edgeCheck="edgeCheck" @property-panel="handlePropertyPanel"
|
|
22
33
|
:actionButtonShapeDataId="actionButtonShapeId" @edit-name="handleEditName"
|
|
23
|
-
|
|
24
|
-
@
|
|
25
|
-
@object-flow-connect-end="handleObjectFlowConnectEnd"
|
|
34
|
+
:is-textarea-dialog-open="props.isTextareaDialogOpen" @update-shape="handleUpdateConnectShape"
|
|
35
|
+
@diagramDoubleClick="handleDiagramDoubleClick" @connect-end="handleConnectEnd"
|
|
36
|
+
@action-button-click="handleActionButtonClick" @object-flow-connect-end="handleObjectFlowConnectEnd"
|
|
26
37
|
@model-type-property-id-button-click="handleModelTypePropertyIdButtonClick" :lines="props.lines"
|
|
27
38
|
:packages="props.packages" :diagram="props.diagram" :tagged-value-labels="props.taggedValueLabels"
|
|
28
39
|
@action-button-add="handleActionButtonAdd" />
|
|
@@ -30,19 +41,7 @@
|
|
|
30
41
|
|
|
31
42
|
<!-- 所在图表 -->
|
|
32
43
|
<DiagramListTooltip :visible="tooltipVisible" :x="tooltipX" :y="tooltipY" :current-diagram-id="currentDiagramId"
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
<!-- 缩放条 -->
|
|
36
|
-
<ZoomSlider
|
|
37
|
-
v-model="zoomValue"
|
|
38
|
-
:min="0.1"
|
|
39
|
-
:max="3"
|
|
40
|
-
:step="0.1"
|
|
41
|
-
@zoom-in="handleZoomIn"
|
|
42
|
-
@zoom-out="handleZoomOut"
|
|
43
|
-
@scale-change="handleScaleChange"
|
|
44
|
-
v-if="graphStore.canOperate"
|
|
45
|
-
/>
|
|
44
|
+
@close="tooltipVisible = false" :diagram-location-data="chartLocationData || []" />
|
|
46
45
|
</div>
|
|
47
46
|
</template>
|
|
48
47
|
|
|
@@ -66,13 +65,12 @@ import ActivityAction from '../components/Shape/ActivityAction.vue'
|
|
|
66
65
|
import Pin from '../components/Pin/Pin.vue'
|
|
67
66
|
import Port from '../components/Pin/Port.vue'
|
|
68
67
|
import { registerShapes } from "../render/shape-registry";
|
|
69
|
-
import { getShapeComponent, getShapeStyle } from "../render/shape-renderer";
|
|
68
|
+
import { getShapeComponent, getShapeStyle, clearEdgeStyleCache } from "../render/shape-renderer";
|
|
70
69
|
import { ShapeConfig } from '../utils/diagram'
|
|
71
70
|
import { setCompartmentZones, buildZones, setTaggedValueLabelsCache, setPackageTypesCache } from '../utils/compartment'
|
|
72
71
|
import { eventBus } from "../store";
|
|
73
72
|
import { guardOperate } from "../utils/license-guard"
|
|
74
73
|
import DiagramListTooltip from '../components/DiagramListTooltip/DiagramListTooltip.vue';
|
|
75
|
-
import ZoomSlider from '../components/ZoomSlider/ZoomSlider.vue'
|
|
76
74
|
|
|
77
75
|
registerShapes({
|
|
78
76
|
StrategicTaxonomyDiagram,
|
|
@@ -89,6 +87,7 @@ registerShapes({
|
|
|
89
87
|
Port
|
|
90
88
|
})
|
|
91
89
|
const interactionLayerRef = ref<InstanceType<typeof InteractionLayer> | null>(null)
|
|
90
|
+
const diagramContentRef = ref<HTMLDivElement | null>(null) // 画布内容区域ref
|
|
92
91
|
|
|
93
92
|
// 所在图表弹窗相关变量
|
|
94
93
|
const tooltipVisible = ref(false)
|
|
@@ -111,6 +110,7 @@ const props = defineProps<{
|
|
|
111
110
|
ports: string[],
|
|
112
111
|
canOperate: boolean
|
|
113
112
|
chartLocationData?: locationChart[] | null,
|
|
113
|
+
isTextareaDialogOpen: boolean
|
|
114
114
|
}>()
|
|
115
115
|
|
|
116
116
|
const emit = defineEmits<{
|
|
@@ -134,45 +134,78 @@ const currentScale = computed(() => graphStore.getScale())
|
|
|
134
134
|
// 缩放值,用于双向绑定滑块
|
|
135
135
|
const zoomValue = ref(currentScale.value)
|
|
136
136
|
|
|
137
|
-
//
|
|
137
|
+
// 监听当前缩放比例变化,更新滑块值
|
|
138
138
|
watch(currentScale, (newScale) => {
|
|
139
139
|
zoomValue.value = newScale
|
|
140
|
+
|
|
141
|
+
// ========== 视口裁剪优化:缩放时更新视口 ==========
|
|
142
|
+
nextTick(() => {
|
|
143
|
+
updateViewport()
|
|
144
|
+
})
|
|
145
|
+
// ========== 视口裁剪优化结束 ==========
|
|
140
146
|
})
|
|
141
147
|
|
|
142
|
-
//
|
|
143
|
-
|
|
144
|
-
graphStore.setScale(scale)
|
|
145
|
-
emit('scale-changed', scale)
|
|
146
|
-
}
|
|
148
|
+
// ========== 视口裁剪优化 ==========
|
|
149
|
+
import { useViewportCulling } from '../utils/viewportCulling'
|
|
147
150
|
|
|
148
|
-
//
|
|
149
|
-
const
|
|
150
|
-
//
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
emit('scale-changed', newScale)
|
|
154
|
-
}
|
|
151
|
+
// 创建视口裁剪实例
|
|
152
|
+
const viewportCulling = useViewportCulling({
|
|
153
|
+
bufferSize: 200, // 缓冲区大小
|
|
154
|
+
ensureEdgeIntegrity: true // 确保连线完整性
|
|
155
|
+
})
|
|
155
156
|
|
|
156
|
-
//
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
157
|
+
// 更新视口边界(封装原有逻辑),使用 rAF 节流避免滚动时每帧多次重算
|
|
158
|
+
let viewportRafId: number | null = null
|
|
159
|
+
const updateViewport = () => {
|
|
160
|
+
if (viewportRafId != null) return
|
|
161
|
+
viewportRafId = requestAnimationFrame(() => {
|
|
162
|
+
viewportRafId = null
|
|
163
|
+
viewportCulling.updateViewport(diagramContentRef.value, currentScale.value)
|
|
164
|
+
})
|
|
162
165
|
}
|
|
163
166
|
|
|
164
|
-
//
|
|
165
|
-
const
|
|
167
|
+
// 所有图元(应用原有过滤逻辑)
|
|
168
|
+
const allShapes = computed(() =>
|
|
166
169
|
(graphStore.visibleShapes || []).filter(s => {
|
|
167
|
-
//
|
|
170
|
+
// 外部拖拽创建中的形状应该被隐藏,只显示 ghost
|
|
168
171
|
if (graphStore.externalCreatingId && s.id === graphStore.externalCreatingId) {
|
|
169
172
|
return false
|
|
170
173
|
}
|
|
171
|
-
return (s?.shapeType !== 'shape' && s?.shapeType !== 'pin') // 非 shape
|
|
172
|
-
|| s?.inert !== false // 仅当是 shape
|
|
174
|
+
return (s?.shapeType !== 'shape' && s?.shapeType !== 'pin') // 非 shape:一律渲染
|
|
175
|
+
|| s?.inert !== false // 仅当是 shape 才检查;inert===false 才被过滤掉
|
|
173
176
|
})
|
|
174
177
|
)
|
|
175
178
|
|
|
179
|
+
// 可见图元(基于视口裁剪) - 使用工具类
|
|
180
|
+
const visibleShapes = viewportCulling.createVisibleShapesComputed(allShapes)
|
|
181
|
+
|
|
182
|
+
// 使用visibleShapes替代shapes进行渲染
|
|
183
|
+
const shapes = visibleShapes
|
|
184
|
+
// ========== 视口裁剪优化结束 ==========
|
|
185
|
+
|
|
186
|
+
// 剪切状态图元的边界信息(用于 SVG 遮盖层渲染)
|
|
187
|
+
const cutShapeBounds = computed(() => {
|
|
188
|
+
const cutIdsSet = graphStore.cutShapeIds as Set<string>
|
|
189
|
+
|
|
190
|
+
if (!cutIdsSet || !(cutIdsSet instanceof Set) || cutIdsSet.size === 0) return []
|
|
191
|
+
|
|
192
|
+
const idsArray = Array.from(cutIdsSet)
|
|
193
|
+
|
|
194
|
+
return idsArray
|
|
195
|
+
.map(id => {
|
|
196
|
+
const shape = graphStore.shapeMap.get(id)
|
|
197
|
+
if (!shape?.bounds) return null
|
|
198
|
+
return {
|
|
199
|
+
id,
|
|
200
|
+
x: shape.bounds.x ?? 0,
|
|
201
|
+
y: shape.bounds.y ?? 0,
|
|
202
|
+
width: shape.bounds.width ?? 0,
|
|
203
|
+
height: shape.bounds.height ?? 0,
|
|
204
|
+
}
|
|
205
|
+
})
|
|
206
|
+
.filter(Boolean) as Array<{ id: string; x: number; y: number; width: number; height: number }>
|
|
207
|
+
})
|
|
208
|
+
|
|
176
209
|
// 将隔间组件从 DOM 量测得到的头/内容高度写回 shape.meta,
|
|
177
210
|
const onCompartmentMetrics = (
|
|
178
211
|
m: { headerH: number; contentH: number },
|
|
@@ -325,8 +358,10 @@ const updateShapes = (shapes: Shape[]) => {
|
|
|
325
358
|
if (b.shapeType === 'diagram') return 1
|
|
326
359
|
return 0
|
|
327
360
|
})
|
|
328
|
-
|
|
329
|
-
|
|
361
|
+
clearEdgeStyleCache() // 批量更新时清空 edge 样式缓存,避免旧数据残留
|
|
362
|
+
// graphStore.updateShapes(shapes)
|
|
363
|
+
graphStore.shapes=[]
|
|
364
|
+
graphStore.updateShapes(shapes, 'replace');
|
|
330
365
|
// 在图形批量更新后(通常是从后端加载数据),初始化所有连线的端点
|
|
331
366
|
// 确保连线不会横跨图元,而是从合适的位置连接
|
|
332
367
|
nextTick(() => {
|
|
@@ -369,16 +404,7 @@ const continueExternalCreateDrag = (payload: { clientX: number; clientY: number
|
|
|
369
404
|
const finishExternalCreateDrag = (payload: { clientX: number; clientY: number }) => {
|
|
370
405
|
interactionLayerRef.value?.finishExternalCreateDrag(payload)
|
|
371
406
|
}
|
|
372
|
-
// 监听shapes变化,确保InteractionLayer位置正确
|
|
373
|
-
watch(shapes, () => {
|
|
374
|
-
nextTick(() => {
|
|
375
|
-
// 可以在这里添加额外的位置调整逻辑
|
|
376
|
-
})
|
|
377
|
-
}, { deep: true })
|
|
378
407
|
|
|
379
|
-
watch(() => props.actionButtonShapeId, (newVal) => {
|
|
380
|
-
console.log('newVal', newVal);
|
|
381
|
-
})
|
|
382
408
|
// 监听 taggedValueLabels,立即同步到 graphStore
|
|
383
409
|
watch(
|
|
384
410
|
() => props.taggedValueLabels,
|
|
@@ -445,6 +471,18 @@ onMounted(() => {
|
|
|
445
471
|
|
|
446
472
|
// 监听所在图表弹窗位置信息事件
|
|
447
473
|
eventBus.on('locate-chart-position', handleLocateChartPosition)
|
|
474
|
+
|
|
475
|
+
// ========== 视口裁剪优化:初始化视口 ==========
|
|
476
|
+
nextTick(() => {
|
|
477
|
+
updateViewport() // 初始化视口边界
|
|
478
|
+
|
|
479
|
+
// 监听滚动事件
|
|
480
|
+
const container = diagramContentRef.value
|
|
481
|
+
if (container) {
|
|
482
|
+
container.addEventListener('scroll', updateViewport)
|
|
483
|
+
}
|
|
484
|
+
})
|
|
485
|
+
// ========== 视口裁剪优化结束 ==========
|
|
448
486
|
})
|
|
449
487
|
|
|
450
488
|
onUnmounted(() => {
|
|
@@ -457,6 +495,13 @@ onUnmounted(() => {
|
|
|
457
495
|
|
|
458
496
|
// 移除所在图表弹窗位置信息事件监听器
|
|
459
497
|
eventBus.off('locate-chart-position', handleLocateChartPosition)
|
|
498
|
+
|
|
499
|
+
// ========== 视口裁剪优化:移除滚动监听 ==========
|
|
500
|
+
const container = diagramContentRef.value
|
|
501
|
+
if (container) {
|
|
502
|
+
container.removeEventListener('scroll', updateViewport)
|
|
503
|
+
}
|
|
504
|
+
// ========== 视口裁剪优化结束 ==========
|
|
460
505
|
})
|
|
461
506
|
|
|
462
507
|
defineExpose({
|
|
@@ -484,7 +529,7 @@ defineExpose({
|
|
|
484
529
|
top: 10px;
|
|
485
530
|
left: 10px;
|
|
486
531
|
right: 0;
|
|
487
|
-
bottom:
|
|
532
|
+
bottom: 10px;
|
|
488
533
|
overflow: auto;
|
|
489
534
|
/* 防止Firefox中的默认缩放行为 */
|
|
490
535
|
touch-action: none;
|
|
@@ -500,9 +545,10 @@ defineExpose({
|
|
|
500
545
|
transform: scale(v-bind('currentScale'));
|
|
501
546
|
}
|
|
502
547
|
|
|
503
|
-
/* 所有 shape
|
|
548
|
+
/* 所有 shape 组件都使用绝对定位,启用 GPU 合成减少重绘 */
|
|
504
549
|
.shapes-container>* {
|
|
505
550
|
position: absolute;
|
|
551
|
+
will-change: transform;
|
|
506
552
|
}
|
|
507
553
|
|
|
508
554
|
/* 缩放百分比显示 */
|
|
@@ -521,5 +567,14 @@ defineExpose({
|
|
|
521
567
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
522
568
|
}
|
|
523
569
|
|
|
524
|
-
|
|
570
|
+
/* 剪切状态遮盖层 - SVG 方案 */
|
|
571
|
+
.cut-overlay-svg {
|
|
572
|
+
position: absolute;
|
|
573
|
+
top: 0;
|
|
574
|
+
left: 0;
|
|
575
|
+
width: 100%;
|
|
576
|
+
height: 100%;
|
|
577
|
+
pointer-events: none;
|
|
578
|
+
z-index: 998;
|
|
579
|
+
}
|
|
525
580
|
</style>
|
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
import type { Shape } from '../types';
|
|
2
|
-
|
|
3
|
-
// GraphStore接口定义,包含highlightUtils所需的方法和属性
|
|
4
|
-
interface GraphStore {
|
|
5
|
-
shapes: Shape[];
|
|
6
|
-
updateShape: (shapeId: string, updates: Partial<Shape>, id?: 'id' | 'modelId') => void;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* 图元高亮工具类
|
|
11
|
-
* 用于处理图元高亮状态管理和样式转换
|
|
12
|
-
*/
|
|
13
|
-
export class HighlightUtils {
|
|
14
|
-
private highlightedShapeId: string | null = null;
|
|
15
|
-
private originalShapeStyles = new Map<string, { borderColor?: string; borderWidth?: number }>();
|
|
16
|
-
private graphStore: GraphStore;
|
|
17
|
-
private highlightTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* 构造函数
|
|
21
|
-
* @param graphStore 图元存储实例
|
|
22
|
-
*/
|
|
23
|
-
constructor(graphStore: GraphStore) {
|
|
24
|
-
this.graphStore = graphStore;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* 高亮或取消高亮图元
|
|
29
|
-
* @param shape 要高亮的图元,null表示取消所有高亮
|
|
30
|
-
* @param isHighlight 是否高亮
|
|
31
|
-
* @param isValidSource 是否是有效的连接源(影响高亮颜色)
|
|
32
|
-
* @returns 高亮后的图元(如果有)
|
|
33
|
-
*/
|
|
34
|
-
public highlightShape(
|
|
35
|
-
shape: Shape | null,
|
|
36
|
-
isHighlight: boolean,
|
|
37
|
-
isValidSource: boolean = true
|
|
38
|
-
): Shape | null {
|
|
39
|
-
// 取消高亮处理
|
|
40
|
-
if (!shape && !isHighlight) {
|
|
41
|
-
this.cancelAllHighlights();
|
|
42
|
-
return null;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// 无效图元或边类型图元不处理
|
|
46
|
-
if (!shape || shape.shapeType === 'edge') return null;
|
|
47
|
-
|
|
48
|
-
// 高亮操作
|
|
49
|
-
if (isHighlight && shape) {
|
|
50
|
-
// 先取消之前可能存在的高亮
|
|
51
|
-
if (this.highlightedShapeId && this.highlightedShapeId !== shape.id) {
|
|
52
|
-
this.cancelAllHighlights();
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// 保存原始样式
|
|
56
|
-
if (!this.originalShapeStyles.has(shape.id)) {
|
|
57
|
-
// 确保borderWidth是number类型
|
|
58
|
-
let borderWidth: number | undefined;
|
|
59
|
-
if (shape.style?.borderWidth !== undefined) {
|
|
60
|
-
borderWidth = typeof shape.style.borderWidth === 'string'
|
|
61
|
-
? parseFloat(shape.style.borderWidth)
|
|
62
|
-
: shape.style.borderWidth;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
this.originalShapeStyles.set(shape.id, {
|
|
66
|
-
borderColor: shape.style?.borderColor,
|
|
67
|
-
borderWidth: borderWidth
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// 设置高亮样式(蓝色表示可以连接,红色表示不可以)
|
|
72
|
-
const highlightStyle = {
|
|
73
|
-
...shape.style,
|
|
74
|
-
borderColor: isValidSource ? '#1890ff' : '#f56c6c', // 蓝色或红色
|
|
75
|
-
borderWidth: 3,
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
// 更新图元样式
|
|
79
|
-
this.graphStore.updateShape(shape.id, { style: highlightStyle });
|
|
80
|
-
this.highlightedShapeId = shape.id;
|
|
81
|
-
|
|
82
|
-
return shape;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
return null;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* 取消所有图元的高亮状态
|
|
90
|
-
*/
|
|
91
|
-
public cancelAllHighlights(): void {
|
|
92
|
-
if (this.highlightedShapeId) {
|
|
93
|
-
// 找到当前高亮的图元
|
|
94
|
-
const currentHighlighted = this.graphStore.shapes.find(s => s.id === this.highlightedShapeId);
|
|
95
|
-
if (currentHighlighted) {
|
|
96
|
-
// 恢复原始样式
|
|
97
|
-
const originalStyle = this.originalShapeStyles.get(this.highlightedShapeId);
|
|
98
|
-
if (originalStyle) {
|
|
99
|
-
const restoreStyle = {
|
|
100
|
-
...currentHighlighted.style,
|
|
101
|
-
borderColor: originalStyle.borderColor !== undefined ? originalStyle.borderColor : undefined,
|
|
102
|
-
borderWidth: originalStyle.borderWidth !== undefined ? Number(originalStyle.borderWidth) : undefined
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
// 更新图元恢复原始样式
|
|
106
|
-
this.graphStore.updateShape(this.highlightedShapeId, { style: restoreStyle });
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
this.originalShapeStyles.delete(this.highlightedShapeId);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// 重置状态
|
|
114
|
-
this.highlightedShapeId = null;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* 获取当前高亮的图元ID
|
|
119
|
-
*/
|
|
120
|
-
public getHighlightedShapeId(): string | null {
|
|
121
|
-
return this.highlightedShapeId;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* 获取当前高亮的图元
|
|
126
|
-
*/
|
|
127
|
-
public getHighlightedShape(): Shape | null {
|
|
128
|
-
if (!this.highlightedShapeId) return null;
|
|
129
|
-
return this.graphStore.shapes.find(s => s.id === this.highlightedShapeId) || null;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* 设置高亮定时器
|
|
134
|
-
* @param callback 回调函数
|
|
135
|
-
* @param delay 延迟时间(毫秒)
|
|
136
|
-
*/
|
|
137
|
-
public setHighlightTimeout(callback: () => void, delay: number): void {
|
|
138
|
-
// 先清除已存在的定时器
|
|
139
|
-
this.clearHighlightTimeout();
|
|
140
|
-
// 设置新的定时器
|
|
141
|
-
this.highlightTimeout = setTimeout(callback, delay);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* 清除高亮定时器
|
|
146
|
-
*/
|
|
147
|
-
public clearHighlightTimeout(): void {
|
|
148
|
-
if (this.highlightTimeout) {
|
|
149
|
-
clearTimeout(this.highlightTimeout);
|
|
150
|
-
this.highlightTimeout = null;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* 清理所有高亮状态和数据
|
|
156
|
-
*/
|
|
157
|
-
public dispose(): void {
|
|
158
|
-
this.cancelAllHighlights();
|
|
159
|
-
this.clearHighlightTimeout();
|
|
160
|
-
this.originalShapeStyles.clear();
|
|
161
|
-
}
|
|
162
|
-
}
|