前端DWG文件展示及图层控制
Wucheng

问题背景

对接方表示由于 DWG 文件转换 GLB 模型文件效果不好,因为有很多不必要的信息,很容易干扰文件转换。所以我们希望能够让程序以及用户决定哪些图层能够参与GLB模型的生成。

所以我们就需要给用户做一个DWG文件展示的功能,并且能控制不同图层的显示和隐藏以表示选中该图层。

经过查找调研发现有几个库能够使用

  1. MXCAD
  2. mlightcad/cad-viewer
  3. Autodesk Forge Viewer
  4. Open Design Alliance SDK
  5. LibreDWG

第一个库是国产CAD库,但是由于不能商用故放弃。第三个是官方库,会提供api使用需上传到他的服务器并且也需要收费,第四个使用需要加入会,第五个是C语言编写的DWG解析并且第二个库是依赖于它。

依赖安装

cad-viewer 有两个版本,一个是 vue3 组件,里面不仅能够查看图片甚至可以做到画线(但无法导出)。另一个则是 simple 版本,需要自行编写功能。

由于我们仅仅需要查看和图层功能,其余功能对我们并没有任何作用,故使用 simple 版本。

1
pnpm add @mlightcad/cad-simple-viewer@1.3.3 @mlightcad/data-model@1.5.0

尽量使用example的版本确保不会错

配置与实现

首先需要配置 vite.config.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
...
import { viteStaticCopy } from "vite-plugin-static-copy";

// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
tailwindcss(),
...
// 复制 @mlightcad worker 文件到构建输出目录
viteStaticCopy({
targets: [
{
// 复制DXF解析器Worker文件
src: "./node_modules/@mlightcad/data-model/dist/dxf-parser-worker.js",
dest: "assets",
},
{
// 复制CAD查看器Worker文件
src: "./node_modules/@mlightcad/cad-simple-viewer/dist/*-worker.js",
dest: "assets",
},
],
}),
],
});

以下是组件的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
<template>
<div
class="fixed inset-0 z-50 flex items-center justify-center bg-black/70"
@click.self="closeViewer"
>
<div
class="relative w-[90vw] h-[90vh] bg-white rounded-lg shadow-2xl"
>
<div class="flex flex-col w-full h-full">
<!-- 关闭按钮 -->
<button
@click="closeViewer"
class="absolute top-2 right-4 z-10 w-8 h-8 flex items-center justify-center bg-white/90 hover:bg-white rounded-full shadow-lg transition-colors"
>
<span class="text-gray-600 text-xl font-bold">×</span>
</button>
<div
class="p-3 bg-gray-100 border-b border-gray-300 flex items-center gap-3"
>
<span v-if="isLoadingLib" class="text-sm text-blue-600"
>正在加载 CAD 引擎...</span
>
<span v-else-if="libLoadError" class="text-sm text-red-600">{{
libLoadError
}}</span>
<span v-else-if="fileName" class="text-sm text-gray-700"
>当前文件: {{ fileName }}</span
>
</div>

<div class="flex flex-1 min-h-0">
<div
class="w-64 shrink-0 bg-white border-r border-gray-300 p-3 flex flex-col overflow-hidden gap-1"
>
<h3 class="text-base font-semibold">图层管理</h3>
<!-- 图层操作区域:包含全选等 -->
<div class="shrink-0">
<ElButton size="small" @click="setAllLayersVisibility(true)">
全部显示
</ElButton>
<ElButton size="small" @click="setAllLayersVisibility(false)">
全部隐藏
</ElButton>
</div>
<ElScrollbar
class="flex-1 min-h-0"
>
<div v-if="layers.length === 0" class="text-gray-500 text-sm shrink-0">暂无图层信息</div>
<li
v-else
v-for="layer in layers"
:key="layer.name"
class="flex items-center py-1.5 gap-2"
>
<label
class="flex items-center gap-2 flex-1 min-w-0 cursor-pointer"
>
<input
type="checkbox"
:checked="layer.visible"
@change="toggleLayer(layer)"
class="cursor-pointer shrink-0"
/>
<span
class="select-none hover:text-blue-500 transition-colors truncate"
@click="selectLayerName(layer.name)"
:title="layer.name"
>
{{ layer.name }}
</span>
</label>
<span
class="w-3 h-3 shrink-0 rounded-sm border border-gray-400"
:style="{ backgroundColor: getLayerColor(layer.color) }"
></span>
</li>
</ElScrollbar>

<ElButton type="primary" @click="handleConfirm">确认</ElButton>
</div>

<!-- CAD显示区域 -->
<div
class="flex-1 overflow-hidden"
@mousedown.middle.stop.prevent
@mouseup.middle.stop.prevent
@auxclick.stop.prevent
@contextmenu.middle.stop.prevent
>
<canvas
ref="canvasRef"
class="w-full h-full relative bg-gray-800"
></canvas>
</div>
</div>
</div>
</div>
</div>
</template>

<script setup lang="ts">
import { ref, onMounted, watch, nextTick } from "vue";
import type {
AcDbOpenDatabaseOptions,
AcDbObjectId,
} from "@mlightcad/data-model";
import { ElButton, ElScrollbar } from "element-plus";
import { AcApDocManager } from "@mlightcad/cad-simple-viewer";

// 定义 Props 和 Emits
interface Props {
file?: File | null;
}

const props = defineProps<Props>();

const emit = defineEmits<{
(e: "layersUpdated", layerList: typeof layers.value): void;
(e: "layer-selected", layerName: string[]): void;
(e: "close"): void;
}>();

// 状态定义
const canvasRef = ref<HTMLCanvasElement | null>(null);
const fileName = ref<string>("");
const isViewerReady = ref(false); // 新增:标记 Viewer 是否已准备好
const isLoadingLib = ref(false); // CAD 库加载状态
const libLoadError = ref<string | null>(null); // 库加载错误信息
// 图层信息
const layers = ref<
Array<{ name: string; visible: boolean; id: AcDbObjectId; color: number }>
>([]);

const closeViewer = () => {
emit("close");
};

// 初始化 Viewer
onMounted(async () => {
await nextTick();

if (!canvasRef.value) {
console.error("Canvas element not found during mount");
return;
}
// 保存原始标题
const originalTitle = document.title;

try {
isLoadingLib.value = true;

// 初始化 DocManager,绑定 Canvas
AcApDocManager.createInstance({ canvas: canvasRef.value });
console.log("CAD viewer initialized successfully");

// 标记初始化完成
isViewerReady.value = true;

document.title = originalTitle;

// 如果 Props 中已有文件,初始化完成后立即加载
if (props.file) {
loadDwgFile(props.file);
}

isLoadingLib.value = false;
} catch (error) {
console.error("Failed to initialize CAD viewer:", error);
libLoadError.value = "初始化 CAD 查看器失败";
}
});

// 加载DWG文件
const loadDwgFile = async (file: File) => {
// 双重保险:如果 Viewer 没准备好,绝对不执行
if (!isViewerReady.value || !AcApDocManager) {
console.warn("Viewer is not ready yet.");
return;
}
// 保存原始标题
const originalTitle = document.title;

fileName.value = file.name;

try {
const arrayBuffer = await file.arrayBuffer();

const options: AcDbOpenDatabaseOptions = {
minimumChunkSize: 1000,
readOnly: false,
};

// 此时访问 instance 是安全的
const success = await AcApDocManager.instance.openDocument(
file.name,
arrayBuffer,
options
);

if (success) {
console.log("DWG 文件加载成功");
fetchLayers();
} else {
console.error("DWG 文件加载失败");
}
} catch (error) {
console.error("读取文件出错:", error);
} finally {
// 恢复原始标题
document.title = originalTitle;
}
};

/**
* 获取图层信息
*/
const fetchLayers = () => {
if (!AcApDocManager) return;
const doc = AcApDocManager.instance.curDocument;
if (!doc) return;

const db = doc.database;
const layerTable = db.tables.layerTable;
const layerList: typeof layers.value = [];

const layerIterator = layerTable.newIterator();
for (const layer of layerIterator) {
layerList.push({
id: layer.objectId,
name: layer.name,
visible: !layer.isOff,
color: layer.color.colorIndex || 7,
});
}

layers.value = layerList;
emit("layersUpdated", layerList);
};

// 监听file prop变化
watch(
() => props.file,
(newFile) => {
// 只有当有新文件 且 Viewer 已经准备好时才加载
if (newFile && isViewerReady.value) {
loadDwgFile(newFile);
}
}
);

// 切换图层可见性
const toggleLayer = (layerItem: (typeof layers.value)[0]) => {
if (!AcApDocManager) return;
const doc = AcApDocManager.instance.curDocument;
if (!doc) return;

const db = doc.database;
const layer = db.tables.layerTable.getAt(layerItem.name);

if (layer) {
layer.isOff = layerItem.visible;
layerItem.visible = !layerItem.visible; // 更新本地响应式状态
}
};

// 全部显示或全部隐藏图层
const setAllLayersVisibility = (visible: boolean) => {
if (!AcApDocManager) return;
const doc = AcApDocManager.instance.curDocument;
if (!doc) return;

layers.value.forEach((layer) => {
layer.visible = visible;
const dbLayer = doc.database.tables.layerTable.getAt(layer.name);
if (dbLayer) {
dbLayer.isOff = !visible;
}
});
};

// 发送图层名称
const selectLayerName = (name: string) => {
console.log("Selected Layer:", name);
};

// 将可见图层名字列表作为字符串返回
const handleConfirm = () => {
const visibleLayerNames = layers.value
.filter((layer) => layer.visible)
.map((layer) => layer.name)
console.log("Visible Layers:", visibleLayerNames);
emit("layer-selected", visibleLayerNames);
closeViewer();
};

// 辅助函数:将 AutoCAD 颜色索引转换为 CSS 颜色
const getLayerColor = (colorIndex: number) => {
const colors: Record<number, string> = {
1: "red",
2: "yellow",
3: "green",
4: "cyan",
5: "blue",
6: "magenta",
7: "white",
};
return colors[colorIndex] || "#ccc";
};
</script>

后续加载优化

由于DWG解析的库非常的大,会严重拖累加载速度,所以需要去做异步优化从而提升加载性能。

首先配置 vite.config.ts 的构建单独打包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export default defineConfig({
// 添加以下配置
build: {
rollupOptions: {
output: {
manualChunks: {
// CAD 相关库单独打包,支持按需加载
"cad-simple-viewer": ["@mlightcad/cad-simple-viewer"],
"cad-data-model": ["@mlightcad/data-model"],
},
},
},
},
});

然后再把需要用到组件的父组件改成异步导入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<template>
<!-- 异步组件 -->
<Suspense>
<template #default>
<!-- DWG查看器弹窗 -->
<DwgViewer
v-if="dwgViewerLoaded"
v-show="showDwgViewer"
@close="showDwgViewer = false"
:file="dwgFile"
/>
</template>
<template #fallback>
<div class="w-full h-full flex items-center justify-center bg-gray-100">
<div class="text-center">
<div class="text-blue-600 text-lg mb-2">正在加载 CAD 查看器...</div>
<div class="text-gray-500 text-sm">首次加载可能需要几秒钟</div>
</div>
</div>
</template>
</Suspense>
</template>
<script>
const DwgViewer = defineAsyncComponent(
() => import("./components/DwgViewer.vue")
);
</script>

组件内部的依赖加载也更新成延迟异步加载 cad-simple-viewer 库,双重优化保障首屏加载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// 注释掉直接import
// import { AcApDocManager } from "@mlightcad/cad-simple-viewer";

type AcApDocManagerType =
typeof import("@mlightcad/cad-simple-viewer").AcApDocManager;
let AcApDocManager: AcApDocManagerType | null = null;

// 加载库
const loadCadLibrary = async () => {
if (AcApDocManager) {
return true; // 已加载
}

isLoadingLib.value = true;
libLoadError.value = null;

try {
const module = await import("@mlightcad/cad-simple-viewer");
AcApDocManager = module.AcApDocManager;
console.log("CAD library loaded successfully");
return true;
} catch (error) {
console.error("Failed to load CAD library:", error);
libLoadError.value = "加载 CAD 库失败,请刷新页面重试";
return false;
} finally {
isLoadingLib.value = false;
}
};

onMounted(async () => {
await nextTick();

if (!canvasRef.value) {
console.error("Canvas element not found during mount");
return;
}
// 保存原始标题
const originalTitle = document.title;

try {
// 动态加载 CAD 库
const loaded = await loadCadLibrary();
if (!loaded || !AcApDocManager) {
console.error("Failed to load CAD library");
return;
}

// 初始化 DocManager,绑定 Canvas
AcApDocManager.createInstance({ canvas: canvasRef.value });
console.log("CAD viewer initialized successfully");

// 标记初始化完成
isViewerReady.value = true;

document.title = originalTitle;

// 如果 Props 中已有文件,初始化完成后立即加载
if (props.file) {
loadDwgFile(props.file);
}
} catch (error) {
console.error("Failed to initialize CAD viewer:", error);
libLoadError.value = "初始化 CAD 查看器失败";
}
});