@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.
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 +6 -11
  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
@@ -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
+ }
@@ -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" :is="getShapeComponent(shape)" :shape="shape"
7
- :style="getShapeStyle(shape)" @name-click="handleNameClick" @shape-click="handleShapeClick"
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'" ref="shapeComponents"
10
- class="shape-element" :data-shape-id="shape.id"
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
- @update-shape="handleUpdateConnectShape" @diagramDoubleClick="handleDiagramDoubleClick"
24
- @connect-end="handleConnectEnd" @action-button-click="handleActionButtonClick"
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
- @close="tooltipVisible = false" :diagram-location-data="chartLocationData || []" />
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
- const handleScaleChange = (scale: number) => {
144
- graphStore.setScale(scale)
145
- emit('scale-changed', scale)
146
- }
148
+ // ========== 视口裁剪优化 ==========
149
+ import { useViewportCulling } from '../utils/viewportCulling'
147
150
 
148
- // 处理放大按钮点击
149
- const handleZoomIn = () => {
150
- // ZoomSlider组件已经处理了内部逻辑,这里只需要触发外部事件
151
- const newScale = Math.min(currentScale.value + 0.1, 3)
152
- graphStore.setScale(newScale)
153
- emit('scale-changed', newScale)
154
- }
151
+ // 创建视口裁剪实例
152
+ const viewportCulling = useViewportCulling({
153
+ bufferSize: 200, // 缓冲区大小
154
+ ensureEdgeIntegrity: true // 确保连线完整性
155
+ })
155
156
 
156
- // 处理缩小按钮点击
157
- const handleZoomOut = () => {
158
- // ZoomSlider组件已经处理了内部逻辑,这里只需要触发外部事件
159
- const newScale = Math.max(currentScale.value - 0.1, 0.1)
160
- graphStore.setScale(newScale)
161
- emit('scale-changed', newScale)
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 shapes = computed(() =>
167
+ // 所有图元(应用原有过滤逻辑)
168
+ const allShapes = computed(() =>
166
169
  (graphStore.visibleShapes || []).filter(s => {
167
- // 外部拖拽创建中的形状应该被隐藏,只显示 ghost
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 才检查;inert===false 才被过滤掉
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
- graphStore.updateShapes(shapes)
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: 30px;
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
- }