@mx-sose-front/mx-sose-graph 1.1.8 → 1.1.9
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/assets/edgeWorker-b57ca007.js +2 -0
- package/dist/assets/edgeWorker-b57ca007.js.map +1 -0
- package/dist/index.d.ts +633 -30
- package/dist/index.esm.js +8728 -4734
- 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/Common/Tree.vue +451 -0
- package/src/components/Common/index.ts +2 -0
- package/src/components/DiagramListTooltip/DiagramListTooltip.vue +1 -2
- package/src/components/Edge/Edge.vue +172 -169
- package/src/components/Gantt/Gantt.vue +1544 -0
- package/src/components/GanttContextMenu/GanttContextMenu.vue +304 -0
- package/src/components/InteractionLayer.vue +343 -147
- package/src/components/Matrix/Matrix.vue +828 -0
- package/src/components/Matrix/index.ts +168 -0
- package/src/components/Shape/ConceptualRole.vue +2 -34
- package/src/components/Table/Table.vue +970 -0
- package/src/constants/edgeShapeKeys.ts +8 -5
- package/src/constants/index.ts +259 -45
- package/src/hooks/index.ts +2 -0
- package/src/hooks/useChartRowSelection.ts +456 -0
- package/src/hooks/useResize.ts +2 -2
- package/src/hooks/useVirtualScroll.ts +258 -0
- package/src/index.ts +1 -1
- package/src/render/shape-renderer.ts +62 -2
- package/src/statics/icons/childIcons//345/221/275/344/273/244@3x.png +0 -0
- package/src/statics/icons/childIcons//346/210/230/347/225/245/346/246/202/345/277/265/350/241/250@3x.png +0 -0
- package/src/statics/icons/childIcons//346/216/247/345/210/266@3x.png +0 -0
- package/src/statics/icons/createMenu/down.png +0 -0
- package/src/statics/icons/createMenu/remove.png +0 -0
- package/src/statics/icons/createMenu/up.png +0 -0
- package/src/store/graphStore.ts +217 -44
- package/src/types/index.ts +86 -4
- package/src/utils/batchAutoExpand.ts +9 -10
- package/src/utils/containers.ts +72 -17
- package/src/utils/contextMenuUtils.ts +7 -7
- package/src/utils/dateUtils.ts +160 -0
- package/src/utils/diagram.ts +10 -8
- package/src/utils/drag.ts +6 -5
- package/src/utils/edgeUtils.ts +344 -427
- package/src/utils/edgeWorker.ts +471 -0
- package/src/utils/hittest.ts +37 -38
- package/src/utils/index.ts +3 -0
- package/src/utils/keyboardUtils.ts +5 -5
- package/src/utils/packageOutline.ts +96 -0
- package/src/utils/rafThrottle.ts +162 -0
- package/src/utils/workerManager.ts +335 -0
- package/src/view/graph.vue +47 -33
- /package/src/statics/icons/childIcons//346/210/230/347/225/{245@3x.png" → 245/345/261/202@3x.png"} +0 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RAF (requestAnimationFrame) 节流工具类
|
|
3
|
+
* 用于优化高频事件(如 mousemove、scroll 等)的性能
|
|
4
|
+
* 确保在浏览器重绘前只执行一次回调,避免不必要的计算
|
|
5
|
+
*/
|
|
6
|
+
export class RAFThrottle {
|
|
7
|
+
private rafId: number | null = null
|
|
8
|
+
private lastArgs: any[] = []
|
|
9
|
+
private isPending = false
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 节流执行函数
|
|
13
|
+
* @param fn 要执行的函数
|
|
14
|
+
* @returns 节流后的函数
|
|
15
|
+
*/
|
|
16
|
+
throttle<T extends (...args: any[]) => any>(fn: T): T {
|
|
17
|
+
return ((...args: Parameters<T>) => {
|
|
18
|
+
// 保存最新的参数
|
|
19
|
+
this.lastArgs = args
|
|
20
|
+
|
|
21
|
+
// 如果已经有待执行的 RAF,直接返回
|
|
22
|
+
if (this.isPending) {
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
this.isPending = true
|
|
27
|
+
|
|
28
|
+
// 请求动画帧
|
|
29
|
+
this.rafId = requestAnimationFrame(() => {
|
|
30
|
+
this.isPending = false
|
|
31
|
+
this.rafId = null
|
|
32
|
+
|
|
33
|
+
// 使用最新的参数执行函数
|
|
34
|
+
fn(...this.lastArgs)
|
|
35
|
+
})
|
|
36
|
+
}) as T
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 取消待执行的 RAF
|
|
41
|
+
*/
|
|
42
|
+
cancel(): void {
|
|
43
|
+
if (this.rafId !== null) {
|
|
44
|
+
cancelAnimationFrame(this.rafId)
|
|
45
|
+
this.rafId = null
|
|
46
|
+
this.isPending = false
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 立即执行待处理的回调(如果有)
|
|
52
|
+
*/
|
|
53
|
+
flush(): void {
|
|
54
|
+
if (this.isPending && this.rafId !== null) {
|
|
55
|
+
cancelAnimationFrame(this.rafId)
|
|
56
|
+
this.rafId = null
|
|
57
|
+
this.isPending = false
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 创建 RAF 节流实例
|
|
64
|
+
* @returns RAFThrottle 实例
|
|
65
|
+
*/
|
|
66
|
+
export function createRAFThrottle(): RAFThrottle {
|
|
67
|
+
return new RAFThrottle()
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* RAF 防抖工具类
|
|
72
|
+
* 延迟执行函数,但在延迟期间如果有新调用,则重新计时
|
|
73
|
+
* 使用 requestAnimationFrame 确保在浏览器重绘前执行
|
|
74
|
+
*/
|
|
75
|
+
export class RAFDebounce {
|
|
76
|
+
private rafId: number | null = null
|
|
77
|
+
private delay: number
|
|
78
|
+
private timer: ReturnType<typeof setTimeout> | null = null
|
|
79
|
+
private lastArgs: any[] = []
|
|
80
|
+
private isPending = false
|
|
81
|
+
|
|
82
|
+
constructor(delay: number = 0) {
|
|
83
|
+
this.delay = delay
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 防抖执行函数
|
|
88
|
+
* @param fn 要执行的函数
|
|
89
|
+
* @returns 防抖后的函数
|
|
90
|
+
*/
|
|
91
|
+
debounce<T extends (...args: any[]) => any>(fn: T): T {
|
|
92
|
+
return ((...args: Parameters<T>) => {
|
|
93
|
+
// 保存最新的参数
|
|
94
|
+
this.lastArgs = args
|
|
95
|
+
|
|
96
|
+
// 清除之前的定时器
|
|
97
|
+
if (this.timer !== null) {
|
|
98
|
+
clearTimeout(this.timer)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 设置新的定时器
|
|
102
|
+
this.timer = setTimeout(() => {
|
|
103
|
+
this.timer = null
|
|
104
|
+
|
|
105
|
+
// 如果已经有待执行的 RAF,直接返回
|
|
106
|
+
if (this.isPending) {
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
this.isPending = true
|
|
111
|
+
|
|
112
|
+
// 请求动画帧
|
|
113
|
+
this.rafId = requestAnimationFrame(() => {
|
|
114
|
+
this.isPending = false
|
|
115
|
+
this.rafId = null
|
|
116
|
+
|
|
117
|
+
// 使用最新的参数执行函数
|
|
118
|
+
fn(...this.lastArgs)
|
|
119
|
+
})
|
|
120
|
+
}, this.delay)
|
|
121
|
+
}) as T
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 取消待执行的 RAF 和定时器
|
|
126
|
+
*/
|
|
127
|
+
cancel(): void {
|
|
128
|
+
if (this.timer !== null) {
|
|
129
|
+
clearTimeout(this.timer)
|
|
130
|
+
this.timer = null
|
|
131
|
+
}
|
|
132
|
+
if (this.rafId !== null) {
|
|
133
|
+
cancelAnimationFrame(this.rafId)
|
|
134
|
+
this.rafId = null
|
|
135
|
+
this.isPending = false
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* 立即执行待处理的回调(如果有)
|
|
141
|
+
*/
|
|
142
|
+
flush(): void {
|
|
143
|
+
if (this.timer !== null) {
|
|
144
|
+
clearTimeout(this.timer)
|
|
145
|
+
this.timer = null
|
|
146
|
+
}
|
|
147
|
+
if (this.isPending && this.rafId !== null) {
|
|
148
|
+
cancelAnimationFrame(this.rafId)
|
|
149
|
+
this.rafId = null
|
|
150
|
+
this.isPending = false
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* 创建 RAF 防抖实例
|
|
157
|
+
* @param delay 延迟时间(毫秒)
|
|
158
|
+
* @returns RAFDebounce 实例
|
|
159
|
+
*/
|
|
160
|
+
export function createRAFDebounce(delay: number = 0): RAFDebounce {
|
|
161
|
+
return new RAFDebounce(delay)
|
|
162
|
+
}
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
// workerManager.ts - Web Worker manager for handling edge calculations
|
|
2
|
+
|
|
3
|
+
import type { Shape } from '../types';
|
|
4
|
+
|
|
5
|
+
interface WorkerMessage {
|
|
6
|
+
type: string;
|
|
7
|
+
data: any;
|
|
8
|
+
id: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface WorkerResponse {
|
|
12
|
+
id: number;
|
|
13
|
+
result: any;
|
|
14
|
+
error?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface WorkerTask {
|
|
18
|
+
resolve: (value: any) => void;
|
|
19
|
+
reject: (reason: any) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class WorkerManager {
|
|
23
|
+
private worker: Worker | null = null;
|
|
24
|
+
private taskQueue: Map<number, WorkerTask> = new Map();
|
|
25
|
+
private nextTaskId = 1;
|
|
26
|
+
private isInitialized = false;
|
|
27
|
+
private initializationPromise: Promise<void> | null = null;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 各操作类型的版本号,用于竞态控制。
|
|
31
|
+
* 每次发起同类型请求时递增,回调中只采纳最新版本的结果。
|
|
32
|
+
*/
|
|
33
|
+
private versionMap: Map<string, number> = new Map();
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Initialize the Web Worker
|
|
37
|
+
*/
|
|
38
|
+
initialize(): Promise<void> {
|
|
39
|
+
if (this.isInitialized) {
|
|
40
|
+
return Promise.resolve();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (this.initializationPromise) {
|
|
44
|
+
return this.initializationPromise;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
this.initializationPromise = new Promise((resolve, reject) => {
|
|
48
|
+
try {
|
|
49
|
+
this.worker = new Worker(new URL('./edgeWorker.ts', import.meta.url), { type: 'module' });
|
|
50
|
+
|
|
51
|
+
this.worker.addEventListener('message', (event: MessageEvent<WorkerResponse>) => {
|
|
52
|
+
this.handleWorkerMessage(event.data);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
this.worker.addEventListener('error', (error: ErrorEvent) => {
|
|
56
|
+
console.error('Worker error:', error);
|
|
57
|
+
reject(error);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
this.isInitialized = true;
|
|
61
|
+
resolve();
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.error('Failed to initialize worker:', error);
|
|
64
|
+
reject(error);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
return this.initializationPromise;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Handle messages from the worker
|
|
73
|
+
*/
|
|
74
|
+
private handleWorkerMessage(response: WorkerResponse): void {
|
|
75
|
+
const { id, result, error } = response;
|
|
76
|
+
const task = this.taskQueue.get(id);
|
|
77
|
+
|
|
78
|
+
if (task) {
|
|
79
|
+
this.taskQueue.delete(id);
|
|
80
|
+
if (error) {
|
|
81
|
+
task.reject(new Error(error));
|
|
82
|
+
} else {
|
|
83
|
+
task.resolve(result);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* 递增并返回指定操作类型的版本号
|
|
90
|
+
*/
|
|
91
|
+
private bumpVersion(opType: string): number {
|
|
92
|
+
const next = (this.versionMap.get(opType) ?? 0) + 1;
|
|
93
|
+
this.versionMap.set(opType, next);
|
|
94
|
+
return next;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 检查版本号是否仍然是最新的
|
|
99
|
+
*/
|
|
100
|
+
isLatestVersion(opType: string, version: number): boolean {
|
|
101
|
+
return (this.versionMap.get(opType) ?? 0) === version;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Send a message to the worker and return a promise for the result
|
|
106
|
+
*/
|
|
107
|
+
private sendMessage(type: string, data: any): Promise<any> {
|
|
108
|
+
return new Promise((resolve, reject) => {
|
|
109
|
+
if (!this.worker || !this.isInitialized) {
|
|
110
|
+
reject(new Error('Worker not initialized'));
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const taskId = this.nextTaskId++;
|
|
115
|
+
this.taskQueue.set(taskId, { resolve, reject });
|
|
116
|
+
|
|
117
|
+
const message: WorkerMessage = {
|
|
118
|
+
type,
|
|
119
|
+
data,
|
|
120
|
+
id: taskId
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
this.worker.postMessage(message);
|
|
125
|
+
} catch (error) {
|
|
126
|
+
this.taskQueue.delete(taskId);
|
|
127
|
+
reject(error);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* 只提取 Worker 需要的最小字段,减少序列化开销
|
|
134
|
+
* 使用展开运算符确保所有嵌套对象都是纯 JS 对象(去除 Vue Proxy)
|
|
135
|
+
*/
|
|
136
|
+
private minifyShape(shape: Shape): any {
|
|
137
|
+
const bounds = shape.bounds
|
|
138
|
+
? { x: shape.bounds.x, y: shape.bounds.y, width: shape.bounds.width, height: shape.bounds.height }
|
|
139
|
+
: undefined;
|
|
140
|
+
return {
|
|
141
|
+
id: shape.id,
|
|
142
|
+
shapeType: shape.shapeType,
|
|
143
|
+
bounds,
|
|
144
|
+
sourceId: shape.sourceId,
|
|
145
|
+
targetId: shape.targetId,
|
|
146
|
+
modelId: shape.modelId,
|
|
147
|
+
shapeKey: shape.shapeKey,
|
|
148
|
+
direction: shape.direction,
|
|
149
|
+
parenShapeId: shape.parenShapeId,
|
|
150
|
+
waypointId: (shape as any).waypointId,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* 将可能来自 Vue ref/reactive 的点坐标转换为普通对象,避免 postMessage 触发 DataCloneError
|
|
156
|
+
*/
|
|
157
|
+
private minifyPoint(
|
|
158
|
+
point: { x?: number; y?: number } | null | undefined
|
|
159
|
+
): { x: number; y: number } | undefined {
|
|
160
|
+
if (!point) return undefined;
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
x: Number(point.x ?? 0),
|
|
164
|
+
y: Number(point.y ?? 0),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* 只提取与 changedIds 相关的 edge,减少传输量
|
|
170
|
+
*/
|
|
171
|
+
private extractRelatedEdges(
|
|
172
|
+
edgeIndex: Map<string, Shape[]> | undefined,
|
|
173
|
+
changedIds: string[]
|
|
174
|
+
): Shape[] | undefined {
|
|
175
|
+
if (!edgeIndex) return undefined;
|
|
176
|
+
const seen = new Set<string>();
|
|
177
|
+
const result: Shape[] = [];
|
|
178
|
+
for (const id of changedIds) {
|
|
179
|
+
const edges = edgeIndex.get(id);
|
|
180
|
+
if (edges) {
|
|
181
|
+
for (const e of edges) {
|
|
182
|
+
if (!seen.has(e.id)) {
|
|
183
|
+
seen.add(e.id);
|
|
184
|
+
result.push(e);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return result;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Update related edges endpoints
|
|
194
|
+
* 返回 { version, result } 以便调用方判断竞态
|
|
195
|
+
*/
|
|
196
|
+
async updateRelatedEdges(
|
|
197
|
+
shapes: Shape[],
|
|
198
|
+
changedIds: string[],
|
|
199
|
+
edgeIndex?: Map<string, Shape[]>,
|
|
200
|
+
shapeMapIndex?: Map<string, Shape>
|
|
201
|
+
): Promise<{ version: number; result: Shape[] }> {
|
|
202
|
+
await this.initialize();
|
|
203
|
+
|
|
204
|
+
const version = this.bumpVersion('updateRelatedEdges');
|
|
205
|
+
|
|
206
|
+
// 只传相关的 edge 和它们引用的 source/target shape,而非全量 shapes
|
|
207
|
+
const relatedEdges = this.extractRelatedEdges(edgeIndex, changedIds);
|
|
208
|
+
const neededShapeIds = new Set<string>(changedIds);
|
|
209
|
+
if (relatedEdges) {
|
|
210
|
+
for (const e of relatedEdges) {
|
|
211
|
+
if (e.sourceId) neededShapeIds.add(e.sourceId);
|
|
212
|
+
if (e.targetId) neededShapeIds.add(e.targetId);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const minShapes = relatedEdges
|
|
217
|
+
? Array.from(neededShapeIds)
|
|
218
|
+
.map(id => shapeMapIndex?.get(id))
|
|
219
|
+
.filter(Boolean)
|
|
220
|
+
.map(s => this.minifyShape(s!))
|
|
221
|
+
.concat(relatedEdges.map(e => this.minifyShape(e)))
|
|
222
|
+
: shapes.map(s => this.minifyShape(s));
|
|
223
|
+
|
|
224
|
+
// 构建精简的 shapeMap(只包含需要的)
|
|
225
|
+
const shapeMapData: Record<string, any> = {};
|
|
226
|
+
if (shapeMapIndex) {
|
|
227
|
+
for (const id of neededShapeIds) {
|
|
228
|
+
const s = shapeMapIndex.get(id);
|
|
229
|
+
if (s) shapeMapData[id] = this.minifyShape(s);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// 构建精简的 edgeIndex
|
|
234
|
+
let edgeIndexData: Record<string, any[]> | undefined;
|
|
235
|
+
if (edgeIndex) {
|
|
236
|
+
edgeIndexData = {};
|
|
237
|
+
for (const id of changedIds) {
|
|
238
|
+
const edges = edgeIndex.get(id);
|
|
239
|
+
if (edges) {
|
|
240
|
+
edgeIndexData[id] = edges.map(e => this.minifyShape(e));
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const result = await this.sendMessage('updateRelatedEdges', {
|
|
246
|
+
shapes: minShapes,
|
|
247
|
+
changedIds,
|
|
248
|
+
edgeIndex: edgeIndexData,
|
|
249
|
+
shapeMapIndex: shapeMapData
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
return { version, result };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Initialize all edge endpoints
|
|
257
|
+
*/
|
|
258
|
+
async initializeAllEdgeEndpoints(
|
|
259
|
+
shapes: Shape[],
|
|
260
|
+
shapeMapIndex?: Map<string, Shape>
|
|
261
|
+
): Promise<Shape[]> {
|
|
262
|
+
await this.initialize();
|
|
263
|
+
|
|
264
|
+
const minShapes = shapes.map(s => this.minifyShape(s));
|
|
265
|
+
const shapeMapData = shapeMapIndex
|
|
266
|
+
? Object.fromEntries(
|
|
267
|
+
Array.from(shapeMapIndex.entries()).map(([k, v]) => [k, this.minifyShape(v)])
|
|
268
|
+
)
|
|
269
|
+
: undefined;
|
|
270
|
+
|
|
271
|
+
return this.sendMessage('initializeAllEdgeEndpoints', {
|
|
272
|
+
shapes: minShapes,
|
|
273
|
+
shapeMapIndex: shapeMapData
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Complete connection operation
|
|
279
|
+
*/
|
|
280
|
+
async completeConnection(
|
|
281
|
+
sourceShape: Shape | undefined,
|
|
282
|
+
targetShape: Shape,
|
|
283
|
+
clickPoint: { x: number; y: number },
|
|
284
|
+
currentConnectPoint: { x: number; y: number },
|
|
285
|
+
shapes: Shape[] = []
|
|
286
|
+
): Promise<any> {
|
|
287
|
+
await this.initialize();
|
|
288
|
+
|
|
289
|
+
return this.sendMessage('completeConnection', {
|
|
290
|
+
sourceShape: sourceShape ? this.minifyShape(sourceShape) : undefined,
|
|
291
|
+
targetShape: this.minifyShape(targetShape),
|
|
292
|
+
clickPoint: this.minifyPoint(clickPoint),
|
|
293
|
+
currentConnectPoint: this.minifyPoint(currentConnectPoint),
|
|
294
|
+
shapes: shapes.map(s => this.minifyShape(s))
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Find nearest connect point
|
|
300
|
+
*/
|
|
301
|
+
async findNearestConnectPoint(
|
|
302
|
+
mousePos: { x: number; y: number },
|
|
303
|
+
shape: Shape | undefined
|
|
304
|
+
): Promise<any> {
|
|
305
|
+
await this.initialize();
|
|
306
|
+
|
|
307
|
+
return this.sendMessage('findNearestConnectPoint', {
|
|
308
|
+
mousePos: this.minifyPoint(mousePos),
|
|
309
|
+
shape: shape ? this.minifyShape(shape) : undefined
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Terminate the worker
|
|
315
|
+
*/
|
|
316
|
+
terminate(): void {
|
|
317
|
+
if (this.worker) {
|
|
318
|
+
this.worker.terminate();
|
|
319
|
+
this.worker = null;
|
|
320
|
+
this.isInitialized = false;
|
|
321
|
+
this.taskQueue.clear();
|
|
322
|
+
this.versionMap.clear();
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Get worker status
|
|
328
|
+
*/
|
|
329
|
+
isWorkerInitialized(): boolean {
|
|
330
|
+
return this.isInitialized;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Create a singleton instance
|
|
335
|
+
export const workerManager = new WorkerManager();
|
package/src/view/graph.vue
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<div class="diagram-content" ref="diagramContentRef" @wheel="handleWheel">
|
|
5
5
|
<div class="shapes-container" v-if="shapes.length > 0">
|
|
6
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)"
|
|
7
|
+
v-memo="[shape.id, shape.bounds?.x, shape.bounds?.y, shape.bounds?.width, shape.bounds?.height, shape.name, shape.parenShapeId, shape.style?.zIndex, shape.waypointId, graphStore.selectedShape?.id === shape.id]" :is="getShapeComponent(shape)"
|
|
8
8
|
:shape="shape" :style="getShapeStyle(shape)" @name-click="handleNameClick" @shape-click="handleShapeClick"
|
|
9
9
|
@edge-click="handleEdgeClick"
|
|
10
10
|
:is-selected="graphStore.selectedShape?.id === shape.id && shape.shapeType === 'edge'" class="shape-element"
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
</div>
|
|
14
14
|
|
|
15
15
|
<!-- 剪切状态遮盖层 - 使用 SVG 渲染,性能更优 -->
|
|
16
|
-
<svg v-if="cutShapeBounds.length > 0" class="cut-overlay-svg"
|
|
16
|
+
<svg v-if="cutShapeBounds.length > 0" class="cut-overlay-svg"
|
|
17
|
+
:style="{ width: diagramBounds ? `${diagramBounds.width}px` : '100%', height: diagramBounds ? `${diagramBounds.height}px` : '100%' }">
|
|
17
18
|
<rect v-for="bounds in cutShapeBounds" :key="bounds.id"
|
|
18
19
|
:x="bounds.x"
|
|
19
20
|
:y="bounds.y"
|
|
@@ -22,8 +23,8 @@
|
|
|
22
23
|
fill="rgba(255, 255, 255, 0.6)" />
|
|
23
24
|
</svg>
|
|
24
25
|
|
|
25
|
-
<!-- 交互层 -
|
|
26
|
-
<InteractionLayer v-if="diagramBounds" ref="interactionLayerRef" :connect-shape-data="connectShapeData"
|
|
26
|
+
<!-- 交互层 - 整合了连接层逻辑(甘特图模式下不渲染) -->
|
|
27
|
+
<InteractionLayer v-if="diagramBounds && !isGanttMode" ref="interactionLayerRef" :connect-shape-data="connectShapeData"
|
|
27
28
|
:diagram-bounds="diagramBounds" :style="{
|
|
28
29
|
width: `${(diagramBounds?.width ?? 0) - 10}px`,
|
|
29
30
|
height: `${(diagramBounds?.height ?? 0) - 10}px`,
|
|
@@ -38,7 +39,6 @@
|
|
|
38
39
|
:packages="props.packages" :diagram="props.diagram" :tagged-value-labels="props.taggedValueLabels"
|
|
39
40
|
@action-button-add="handleActionButtonAdd" />
|
|
40
41
|
</div>
|
|
41
|
-
|
|
42
42
|
<!-- 所在图表 -->
|
|
43
43
|
<DiagramListTooltip :visible="tooltipVisible" :x="tooltipX" :y="tooltipY" :current-diagram-id="currentDiagramId"
|
|
44
44
|
@close="tooltipVisible = false" :diagram-location-data="chartLocationData || []" />
|
|
@@ -64,13 +64,16 @@ import Diagram from '../components/Shape/Diagram.vue'
|
|
|
64
64
|
import ActivityAction from '../components/Shape/ActivityAction.vue'
|
|
65
65
|
import Pin from '../components/Pin/Pin.vue'
|
|
66
66
|
import Port from '../components/Pin/Port.vue'
|
|
67
|
+
import Gantt from '../components/Gantt/Gantt.vue'
|
|
67
68
|
import { registerShapes } from "../render/shape-registry";
|
|
68
69
|
import { getShapeComponent, getShapeStyle, clearEdgeStyleCache } from "../render/shape-renderer";
|
|
69
|
-
import { ShapeConfig } from '../utils/diagram'
|
|
70
|
+
import { ShapeConfig, adjustCanvasToFitAllShapes } from '../utils/diagram'
|
|
70
71
|
import { setCompartmentZones, buildZones, setTaggedValueLabelsCache, setPackageTypesCache } from '../utils/compartment'
|
|
71
72
|
import { eventBus } from "../store";
|
|
72
73
|
import { guardOperate } from "../utils/license-guard"
|
|
73
74
|
import DiagramListTooltip from '../components/DiagramListTooltip/DiagramListTooltip.vue';
|
|
75
|
+
import Table from '../components/Table/Table.vue'
|
|
76
|
+
import Matrix from '../components/Matrix/Matrix.vue';
|
|
74
77
|
|
|
75
78
|
registerShapes({
|
|
76
79
|
StrategicTaxonomyDiagram,
|
|
@@ -84,7 +87,10 @@ registerShapes({
|
|
|
84
87
|
ActivityAction,
|
|
85
88
|
Diagram,
|
|
86
89
|
Pin,
|
|
87
|
-
Port
|
|
90
|
+
Port,
|
|
91
|
+
Gantt,
|
|
92
|
+
Table,
|
|
93
|
+
Matrix
|
|
88
94
|
})
|
|
89
95
|
const interactionLayerRef = ref<InstanceType<typeof InteractionLayer> | null>(null)
|
|
90
96
|
const diagramContentRef = ref<HTMLDivElement | null>(null) // 画布内容区域ref
|
|
@@ -218,9 +224,18 @@ const onCompartmentMetrics = (
|
|
|
218
224
|
const z = buildZones(shape, m)
|
|
219
225
|
setCompartmentZones(shape.id, z)
|
|
220
226
|
}
|
|
221
|
-
|
|
227
|
+
|
|
228
|
+
// 是否为甘特图模式
|
|
229
|
+
const isGanttMode = computed(() => {
|
|
230
|
+
return shapes.value.some(s => s.shapeType === 'gantt' || s.shapeType === 'table' || s.shapeType === 'matrix')
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
// 获取画布(diagram)的 bounds,仅用于 InteractionLayer 定位和剪切遮盖层尺寸
|
|
234
|
+
// 甘特图/表格/矩阵等铺满型画布也需取对应 shape 的 bounds
|
|
222
235
|
const diagramBounds = computed(() => {
|
|
223
|
-
const diagram = shapes.value.find(s =>
|
|
236
|
+
const diagram = shapes.value.find(s =>
|
|
237
|
+
s.shapeType === ShapeConfig.SHAPE_TYPE || s.shapeType === 'gantt' || s.shapeType === 'table' || s.shapeType === 'matrix'
|
|
238
|
+
)
|
|
224
239
|
return diagram?.bounds || null
|
|
225
240
|
})
|
|
226
241
|
|
|
@@ -243,6 +258,13 @@ const handleNameClick = (shape: Shape, event: MouseEvent) => {
|
|
|
243
258
|
// 处理图形点击事件
|
|
244
259
|
const handleShapeClick = (shape: Shape, event: MouseEvent) => {
|
|
245
260
|
return guardOperate(async () => {
|
|
261
|
+
if (
|
|
262
|
+
props.connectShapeData?.sourceId &&
|
|
263
|
+
interactionLayerRef.value?.tryCompleteConnectionFromShapeClick?.(shape)
|
|
264
|
+
) {
|
|
265
|
+
return
|
|
266
|
+
}
|
|
267
|
+
|
|
246
268
|
graphStore.selectShape(shape)
|
|
247
269
|
})
|
|
248
270
|
}
|
|
@@ -353,19 +375,15 @@ const handleWheel = (event: WheelEvent) => {
|
|
|
353
375
|
}
|
|
354
376
|
|
|
355
377
|
const updateShapes = (shapes: Shape[]) => {
|
|
356
|
-
shapes.sort((a, b) => {
|
|
357
|
-
if (a.shapeType === 'diagram') return -1
|
|
358
|
-
if (b.shapeType === 'diagram') return 1
|
|
359
|
-
return 0
|
|
360
|
-
})
|
|
361
378
|
clearEdgeStyleCache() // 批量更新时清空 edge 样式缓存,避免旧数据残留
|
|
362
379
|
// graphStore.updateShapes(shapes)
|
|
363
380
|
graphStore.shapes=[]
|
|
364
381
|
graphStore.updateShapes(shapes, 'replace');
|
|
365
382
|
// 在图形批量更新后(通常是从后端加载数据),初始化所有连线的端点
|
|
366
383
|
// 确保连线不会横跨图元,而是从合适的位置连接
|
|
367
|
-
nextTick(() => {
|
|
368
|
-
graphStore.initializeAllEdgeEndpoints()
|
|
384
|
+
nextTick(async () => {
|
|
385
|
+
await graphStore.initializeAllEdgeEndpoints()
|
|
386
|
+
adjustCanvasToFitAllShapes()
|
|
369
387
|
})
|
|
370
388
|
}
|
|
371
389
|
|
|
@@ -380,8 +398,6 @@ watch(() => props.addShapeData, async (newVal, oldVal) => {
|
|
|
380
398
|
graphStore.addShape(newVal)
|
|
381
399
|
graphStore.selectShape(newVal)
|
|
382
400
|
eventBus.emit('addShape:ok')
|
|
383
|
-
// graphStore.addShape(newVal)
|
|
384
|
-
// resShape.value = newVal
|
|
385
401
|
},)
|
|
386
402
|
function sweepShapesWithInert() {
|
|
387
403
|
const arr = graphStore.shapes as any[]
|
|
@@ -460,18 +476,23 @@ watch(
|
|
|
460
476
|
immediate: true,
|
|
461
477
|
}
|
|
462
478
|
)
|
|
479
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
480
|
+
if (event.key === 'Escape') {
|
|
481
|
+
graphStore.clearSelection()
|
|
482
|
+
graphStore.selectedIds = []
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
463
486
|
onMounted(() => {
|
|
464
487
|
initShapes()
|
|
465
|
-
document.addEventListener('keydown',
|
|
466
|
-
if (e.key === 'Escape') {
|
|
467
|
-
graphStore.clearSelection()
|
|
468
|
-
graphStore.selectedIds = []
|
|
469
|
-
}
|
|
470
|
-
});
|
|
488
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
471
489
|
|
|
472
490
|
// 监听所在图表弹窗位置信息事件
|
|
473
491
|
eventBus.on('locate-chart-position', handleLocateChartPosition)
|
|
474
492
|
|
|
493
|
+
// 初始化画布大小
|
|
494
|
+
nextTick(adjustCanvasToFitAllShapes)
|
|
495
|
+
|
|
475
496
|
// ========== 视口裁剪优化:初始化视口 ==========
|
|
476
497
|
nextTick(() => {
|
|
477
498
|
updateViewport() // 初始化视口边界
|
|
@@ -486,12 +507,7 @@ onMounted(() => {
|
|
|
486
507
|
})
|
|
487
508
|
|
|
488
509
|
onUnmounted(() => {
|
|
489
|
-
document.removeEventListener('keydown',
|
|
490
|
-
if (e.key === 'Escape') {
|
|
491
|
-
graphStore.clearSelection()
|
|
492
|
-
graphStore.selectedIds = []
|
|
493
|
-
}
|
|
494
|
-
});
|
|
510
|
+
document.removeEventListener('keydown', handleKeyDown);
|
|
495
511
|
|
|
496
512
|
// 移除所在图表弹窗位置信息事件监听器
|
|
497
513
|
eventBus.off('locate-chart-position', handleLocateChartPosition)
|
|
@@ -572,9 +588,7 @@ defineExpose({
|
|
|
572
588
|
position: absolute;
|
|
573
589
|
top: 0;
|
|
574
590
|
left: 0;
|
|
575
|
-
width: 100%;
|
|
576
|
-
height: 100%;
|
|
577
591
|
pointer-events: none;
|
|
578
592
|
z-index: 998;
|
|
579
593
|
}
|
|
580
|
-
</style>
|
|
594
|
+
</style>
|
/package/src/statics/icons/childIcons//346/210/230/347/225/{245@3x.png" → 245/345/261/202@3x.png"}
RENAMED
|
File without changes
|