Vue3集成dxf-parser与three-dxf实现Web端CAD图纸预览
1. 项目背景与核心需求:为什么要在Vue3里展示DXF?
最近在做一个工业设计相关的后台管理系统,前端用的是Vue3。产品经理提了个需求,希望用户上传的CAD图纸(DXF格式)能在网页里直接预览,并且能进行一些基础的交互,比如缩放、平移、高亮选中某个图层。这需求听起来挺合理,毕竟让用户下载文件再用专业软件打开,体验太割裂了。
我一开始觉得,这还不简单?找个能解析DXF的库,再找个3D渲染库画出来不就行了。但真上手才发现,这里面的水有点深。首先,DXF是AutoCAD的交换格式,结构复杂,版本众多,纯前端解析是个体力活。其次,要在网页里流畅地展示2D/3D的矢量图形,对渲染性能有要求。最后,还得无缝集成到Vue3的项目架构里,状态管理、组件化都得考虑。
网上搜了一圈,社区里提到比较多的组合是dxf-parser加three.js生态下的three-dxf。dxf-parser负责把二进制的DXF文件解析成结构化的JSON数据,而three-dxf则专门负责将这些数据转换成three.js能理解的几何体(Geometry)和场景(Scene)对象,这样就能用WebGL渲染出来了。这个方案看起来清晰,但相关的、特别是针对Vue3的完整教程很少,大多是零散的代码片段。所以,我决定把这次从零集成、踩坑、到最终实现的完整过程记录下来,希望能帮到有类似需求的同行。
2. 技术选型与工具链搭建:为什么是dxf-parser + three-dxf?
面对这个需求,技术栈的选型是关键。我们需要一个解析器和一个渲染器。
2.1 解析器:为什么选择 dxf-parser?
DXF文件本质上是文本文件(也有二进制格式),但它的结构是分段的(SECTION),包含实体(ENTITIES)、图层(LAYERS)、块(BLOCKS)等各种信息。手动解析极其繁琐。社区里有几个选择:
- dxf-parser: 一个专注于浏览器端解析的JavaScript库。它的优点是轻量、纯粹,只做解析这一件事,将DXF文件转换成一个结构清晰的JavaScript对象,不涉及渲染。这给了我们最大的灵活性去处理解析后的数据。
- 其他全功能库:有些库试图将解析和渲染打包在一起。但往往耦合度高,定制困难,且可能对Three.js版本有强依赖。
我选择dxf-parser的核心原因是“单一职责”和“可控性”。在Vue3的响应式系统中,我们更希望拿到干净的、结构化的数据,然后由我们自己去决定如何用Three.js渲染、如何与Vue组件交互。dxf-parser输出的结果正好是这样一个理想的中间态。
2.2 渲染器:为什么选择 three-dxf?
解析出数据后,我们需要将其可视化。Three.js是WebGL的绝佳封装,但直接用它去画DXF里的直线、圆、多段线(Polyline)是非常底层的重复劳动。three-dxf这个库的价值就在这里:它知道dxf-parser输出的数据结构,并内置了将各种DXF实体(Entity)转换为Three.js对象(如Line,Mesh,Group)的逻辑。
- 它做了什么:例如,它会把DXF中的
LINE实体转换成THREE.Line,把CIRCLE转换成THREE.Line或THREE.Mesh(用于填充),把复杂的POLYLINE转换成由多个顶点构成的THREE.Line或THREE.Mesh。它还处理了图层、颜色、线型等属性。 - 它不做什么:它不负责文件上传、不负责Three.js场景的相机控制、灯光、渲染循环管理。这些都需要我们自己在Vue组件中实现。
这个分工非常明确:dxf-parser负责理解文件,three-dxf负责翻译成Three.js语言,而我们(开发者)负责搭建舞台(Vue组件 + Three.js场景)并指挥演出。
2.3 项目环境准备
假设你已经有一个基于Vite的Vue3项目。如果没有,可以用以下命令快速创建一个:
npm create vue@latest my-dxf-viewer # 按照提示选择需要的特性,这里建议至少加上TypeScript和Pinia。 cd my-dxf-viewer npm install然后,安装我们需要的核心依赖:
npm install three dxf-parser three-dxf # 如果需要对3D场景进行更友好的交互控制,强烈推荐安装轨道控制器 npm install three/examples/jsm/controls/OrbitControls这里解释一下:
three: WebGL渲染库的本体。dxf-parser: DXF文件解析器。three-dxf: 连接解析数据和Three.js的桥梁。three/examples/jsm/controls/OrbitControls: Three.js官方示例中的轨道控制器,允许用户用鼠标拖拽、滚轮缩放场景,这对预览图纸至关重要。注意,从Three.js r125版本左右开始,很多示例组件需要像这样单独安装。
3. 核心实现:从文件上传到画面渲染
接下来,我们创建一个Vue组件DxfViewer.vue来承载所有功能。我会分步骤拆解,并解释每一步的意图。
3.1 组件基础结构与文件上传
首先,搭建组件的模板和基础响应式状态。
<template> <div class="dxf-viewer-container"> <div class="controls"> <input type="file" accept=".dxf" @change="handleFileUpload" /> <button @click="resetView">重置视图</button> <div v-if="loading">解析与渲染中...</div> </div> <div ref="canvasContainer" class="canvas-container"></div> </div> </template> <script setup lang="ts"> import { ref, onMounted, onUnmounted } from 'vue'; import * as THREE from 'three'; import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'; import DxfParser from 'dxf-parser'; import { ThreeDxf } from 'three-dxf'; // 响应式状态 const canvasContainer = ref<HTMLElement | null>(null); const loading = ref(false); // Three.js 核心对象引用 const scene = ref<THREE.Scene | null>(null); const camera = ref<THREE.PerspectiveCamera | null>(null); const renderer = ref<THREE.WebGLRenderer | null>(null); const controls = ref<OrbitControls | null>(null); const animationFrameId = ref<number | null>(null); // 文件上传处理函数 const handleFileUpload = async (event: Event) => { const target = event.target as HTMLInputElement; const file = target.files?.[0]; if (!file) return; loading.value = true; try { const text = await file.text(); // DXF通常是文本,用text()读取 await parseAndRenderDxf(text); } catch (error) { console.error('处理DXF文件失败:', error); alert('文件解析失败,请确认是否为有效的DXF文件。'); } finally { loading.value = false; // 清空input值,允许重复上传同一文件 target.value = ''; } }; </script> <style scoped> .dxf-viewer-container { width: 100%; height: 600px; border: 1px solid #ccc; position: relative; } .controls { padding: 10px; background: #f5f5f5; } .canvas-container { width: 100%; height: calc(100% - 50px); /* 减去控制栏高度 */ } </style>注意:这里用
file.text()读取文件,因为大多数DXF是ASCII文本格式。但如果遇到二进制DXF(.dxf文件头不同),dxf-parser可能无法直接解析。在实际生产中,你可能需要更复杂的文件类型检测逻辑,或者引导用户上传文本格式的DXF。这是第一个潜在的坑。
3.2 初始化Three.js场景
在组件挂载时,我们需要初始化Three.js的基础环境:场景、相机、渲染器和控制器。
// 在 script setup 中继续添加函数 const initThreeScene = () => { if (!canvasContainer.value) return; // 1. 创建场景 scene.value = new THREE.Scene(); scene.value.background = new THREE.Color(0xf0f0f0); // 设置浅灰色背景 // 2. 创建相机 (透视相机,适合3D,即使看2D图也有立体操作感) const width = canvasContainer.value.clientWidth; const height = canvasContainer.value.clientHeight; camera.value = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000); camera.value.position.z = 50; // 初始位置,根据图纸大小调整 // 3. 创建渲染器 renderer.value = new THREE.WebGLRenderer({ antialias: true }); // 开启抗锯齿 renderer.value.setSize(width, height); renderer.value.setPixelRatio(window.devicePixelRatio); canvasContainer.value.appendChild(renderer.value.domElement); // 4. 添加轨道控制器 if (camera.value && renderer.value) { controls.value = new OrbitControls(camera.value, renderer.value.domElement); controls.value.enableDamping = true; // 启用阻尼,产生惯性效果,操作更顺滑 controls.value.dampingFactor = 0.05; controls.value.screenSpacePanning = true; // 对于2D图纸,这样平移更符合直觉 // 可以根据需要限制旋转,如果是纯2D图纸,可以锁定绕X/Y轴旋转 // controls.value.maxPolarAngle = Math.PI / 2; // 限制不能翻到背面 } // 5. 添加基础光源(虽然2D线框可能不需要,但有些实体如果有面,需要光照) const ambientLight = new THREE.AmbientLight(0xffffff, 0.6); scene.value.add(ambientLight); const directionalLight = new THREE.DirectionalLight(0xffffff, 0.4); directionalLight.position.set(10, 20, 15); scene.value.add(directionalLight); // 6. 启动渲染循环 const animate = () => { animationFrameId.value = requestAnimationFrame(animate); if (controls.value) controls.value.update(); // 如果控制器启用了阻尼,需要每帧更新 if (renderer.value && scene.value && camera.value) { renderer.value.render(scene.value, camera.value); } }; animate(); // 7. 窗口大小变化响应 const handleResize = () => { if (!camera.value || !renderer.value || !canvasContainer.value) return; const width = canvasContainer.value.clientWidth; const height = canvasContainer.value.clientHeight; camera.value.aspect = width / height; camera.value.updateProjectionMatrix(); renderer.value.setSize(width, height); }; window.addEventListener('resize', handleResize); // 在组件卸载时移除监听器 onUnmounted(() => { window.removeEventListener('resize', handleResize); if (animationFrameId.value) cancelAnimationFrame(animationFrameId.value); // 释放Three.js资源 if (renderer.value) { renderer.value.dispose(); const gl = renderer.value.getContext(); if (gl) { const loseContext = gl.getExtension('WEBGL_lose_context'); loseContext?.loseContext(); } } }); }; onMounted(() => { initThreeScene(); });3.3 解析与渲染DXF文件的核心逻辑
这是最核心的一步,连接dxf-parser和three-dxf。
const parseAndRenderDxf = async (dxfString: string) => { if (!scene.value) { console.error('Three.js场景未初始化!'); return; } // 1. 使用 dxf-parser 解析字符串 const parser = new DxfParser(); let parsedData; try { parsedData = parser.parseSync(dxfString); // 也可以用 parseString } catch (parseError) { console.error('dxf-parser 解析错误:', parseError); throw new Error('DXF文件格式解析失败。'); } // 2. 清空场景中旧的DXF内容(如果有的话) // 这里我们需要一个方法来标记和清除之前由three-dxf添加的对象 // 一个简单的做法是遍历scene.children,移除特定类型或具有特定标记的对象 // 更稳健的做法是创建了一个专门的Group来存放DXF内容 const oldDxfGroup = scene.value.getObjectByName('dxfContent'); if (oldDxfGroup) { scene.value.remove(oldDxfGroup); // 释放几何体和材质内存 oldDxfGroup.traverse((obj) => { if (obj instanceof THREE.Mesh || obj instanceof THREE.Line) { obj.geometry?.dispose(); if (Array.isArray(obj.material)) { obj.material.forEach(m => m.dispose()); } else { obj.material?.dispose(); } } }); } // 3. 使用 three-dxf 创建Three.js对象 const threeDxf = new ThreeDxf(parsedData, { // 这里可以传入一些配置选项,例如字体文件路径(如果DXF包含文字) // font: '/path/to/your/font.json' // three-dxf 可能需要特定格式的字体文件来渲染TEXT实体 }); const dxfGroup = threeDxf.scene; // three-dxf 将整个图纸生成了一个Group dxfGroup.name = 'dxfContent'; // 给它一个名字,方便后续管理 scene.value.add(dxfGroup); // 4. 自动调整相机,使整个图纸适应视图 fitCameraToObject(dxfGroup); }; // 辅助函数:调整相机,使目标物体完整显示在视图中 const fitCameraToObject = (object: THREE.Object3D) => { if (!camera.value || !controls.value) return; const boundingBox = new THREE.Box3().setFromObject(object); const center = boundingBox.getCenter(new THREE.Vector3()); const size = boundingBox.getSize(new THREE.Vector3()); // 计算合适的相机距离 const maxDim = Math.max(size.x, size.y, size.z); const fov = camera.value.fov * (Math.PI / 180); let cameraZ = Math.abs(maxDim / (2 * Math.tan(fov / 2))); // 增加一些边距 cameraZ *= 1.5; // 更新相机位置和目标点 camera.value.position.set(center.x, center.y, cameraZ); camera.value.lookAt(center); controls.value.target.copy(center); // 将控制器目标点设置到图纸中心 controls.value.update(); // 立即更新控制器 }; // 重置视图函数 const resetView = () => { const dxfGroup = scene.value?.getObjectByName('dxfContent'); if (dxfGroup) { fitCameraToObject(dxfGroup); } };4. 深度踩坑与进阶优化
把基础功能跑通只是第一步。在实际项目中,你会遇到各种各样的问题。下面是我踩过的一些坑和对应的解决方案。
4.1 性能问题:大型DXF文件导致卡顿甚至崩溃
工业图纸可能非常复杂,包含成千上万个实体。直接全部渲染,可能会让浏览器卡死。
- 问题分析:Three.js每渲染一个物体(Mesh/Line)都有开销。上万个独立物体会产生巨大的绘制调用(draw calls),严重消耗性能。
- 解决方案:几何体合并(Geometry Merging)
three-dxf默认会为每个简单的实体(如每条直线、每个圆)创建独立的Three.js对象。对于大量重复或简单的图形,我们可以进行合并。 思路是:遍历threeDxf.scene的所有子对象,将材质相同的Mesh或Line的几何体合并成一个。这需要深入three-dxf生成的内部结构,操作起来比较复杂。一个更实用的折中方案是:- 分图层加载/隐藏:利用DXF的图层信息。
parsedData对象里包含layers信息。我们可以在渲染前,让用户选择只加载某些关键图层,或者默认只加载前几个图层,其他按需加载。 - 使用
THREE.LOD(Level of Detail):对于超大型图纸,可以创建多个细节层次的模型。当相机拉远时,显示简化版本(比如用包围盒代替);拉近时再显示完整细节。但这在2D图纸中实现成本较高。 - 后端预处理:对于极端情况,最好的办法是在后端服务器上用专业的CAD库(如Python的
ezdxf)对DXF进行预处理,将其转换为更轻量级的格式(如SVG、压缩的JSON结构或Three.js的BufferGeometry二进制格式),再传给前端。这彻底解决了前端的解析压力。
- 分图层加载/隐藏:利用DXF的图层信息。
4.2 文字(TEXT)和复杂线型(LTYPE)显示异常
文字不显示或显示为方框:
three-dxf渲染文字依赖于字体文件。它通常需要一个特定的JSON格式的字体文件(比如helvetiker_regular.typeface.json)。你需要:- 找到这个字体文件(Three.js示例中通常有)。
- 在初始化
ThreeDxf时通过font选项指定路径。 - 确保该字体文件能被正确加载(可能需要放在
public目录或配置资源加载)。
import fontData from '../assets/fonts/helvetiker_regular.typeface.json?url'; // Vite 使用 `?url` 获取路径 // ... 然后在异步函数中加载 const fontLoader = new THREE.FontLoader(); const font = await new Promise((resolve) => { fontLoader.load(fontData, resolve); }); const threeDxf = new ThreeDxf(parsedData, { font: font });如果字体问题无法解决,一个退而求其次的方案是,在解析后,将
TEXT实体的数据提取出来,用HTML的div标签叠加在Canvas上模拟显示,但这需要精确的坐标转换,非常麻烦。线型显示为实线:DXF支持虚线、点划线等线型。
three-dxf可能无法完美支持所有线型,或者需要额外的配置。如果线型支持对你很重要,可能需要:- 检查
parsedData中tables.ltypes的定义。 - 考虑在
three-dxf渲染后,遍历线条对象,根据其线型名称,手动修改其材质的dashSize、gapSize等属性(如果使用的是THREE.LineDashedMaterial)。但这需要对three-dxf的生成逻辑有深入了解。
- 检查
4.3 坐标系统与单位问题
- 图纸尺寸巨大或极小:DXF文件可能使用真实的工程单位(米、毫米),其坐标值可能非常大(如几百万)。这可能导致Three.js的相机视锥体(Frustum)无法容纳,或者数值精度问题。
- 解决:在
fitCameraToObject函数中,我们计算包围盒并据此调整相机。这通常能自适应。但如果坐标值过大,可能导致cameraZ计算为无穷大。这时需要在解析后,对dxfGroup的所有顶点坐标进行统一的缩放(例如除以1000),将其归一化到一个合理的范围内。
- 解决:在
- Z轴问题:很多2D DXF的实体Z坐标都为0,但有些可能带有Z值。
three-dxf会保留这些Z值。如果你希望强制所有图形在同一个平面上,可以在添加到场景前,遍历dxfGroup的几何体,将所有顶点的Z坐标设为0。
4.4 内存泄漏与资源释放
在单页应用(SPA)中,组件切换时,如果不释放WebGL资源,会导致内存持续增长。
- 我们在
onUnmounted和加载新文件前已经做了一些清理工作:调用dispose()方法释放几何体和材质,并尝试释放WebGL上下文。 - 更彻底的清理:Three.js的渲染器、纹理等都需要手动管理。确保所有由
three-dxf创建的材质和几何体都被遍历并dispose()。我们的代码中通过getObjectByName('dxfContent')找到组并遍历清理,是一个好习惯。
4.5 与Vue3响应式系统的集成
Three.js的对象(Scene, Mesh等)是普通的JavaScript对象,不是Vue的响应式对象。直接将其放入reactive或ref的.value中可能会导致性能问题或意外行为。
- 最佳实践:像我们代码中那样,使用
ref来持有它们的引用(const scene = ref(null)),但只将其作为“盒子”来存储和访问。所有对Three.js对象的操作(修改位置、颜色、添加删除子对象)都直接操作scene.value这个原始对象,不要试图用Vue的响应式去驱动Three.js的渲染。渲染循环(requestAnimationFrame)是独立的驱动力量。
5. 功能增强与交互实现
一个基础的查看器还不够。我们通常需要更丰富的交互。
5.1 图层控制面板
让用户能够开关不同的图层,这是CAD查看器的基本功能。
<template> <!-- 在 controls div 内添加 --> <div class="layer-control" v-if="layerVisibility.length > 0"> <h4>图层控制</h4> <div v-for="layer in layerVisibility" :key="layer.name"> <label> <input type="checkbox" v-model="layer.visible" @change="toggleLayer(layer.name)" /> {{ layer.name }} ({{ layer.color }}) </label> </div> </div> </template> <script setup lang="ts"> // 新增响应式状态 interface LayerInfo { name: string; visible: boolean; color: string; // 可以存储颜色代码 } const layerVisibility = ref<LayerInfo[]>([]); // 在 parseAndRenderDxf 函数中,解析后提取图层信息 const extractLayerInfo = (parsedData: any) => { const layers: LayerInfo[] = []; if (parsedData.tables && parsedData.tables.layer && parsedData.tables.layer.layers) { const layerTable = parsedData.tables.layer.layers; for (const layerName in layerTable) { const layer = layerTable[layerName]; layers.push({ name: layerName, visible: true, // 默认可见 color: `#${layer.color?.toString(16).padStart(6, '0') || '000000'}`, }); } } layerVisibility.value = layers; }; // 在 parseAndRenderDxf 中调用 // const parsedData = parser.parseSync(...); extractLayerInfo(parsedData); // 切换图层显示/隐藏的函数 const toggleLayer = (layerName: string) => { const dxfGroup = scene.value?.getObjectByName('dxfContent'); if (!dxfGroup) return; dxfGroup.traverse((obj) => { // 这里依赖于 three-dxf 如何将图层信息附加到对象上。 // 通常,它会将图层名放在 obj.userData.layer 或 obj.name 中。 // 你需要检查 three-dxf 生成的对象结构来确定。 // 假设它放在 obj.userData.layer if (obj.userData && obj.userData.layer === layerName) { obj.visible = layerVisibility.value.find(l => l.name === layerName)?.visible || false; } }); }; </script>注意:
three-dxf是如何将图层信息附加到每个图形对象上的,需要你实际打印出dxfGroup的子对象结构来确认。可能是userData.layer,也可能是其他属性。这是集成时需要仔细探查的地方。
5.2 实体选择与高亮
允许用户点击选择图纸中的某个图形,并高亮显示。
// 新增响应式状态 const selectedObject = ref<THREE.Object3D | null>(null); const raycaster = new THREE.Raycaster(); const mouse = new THREE.Vector2(); // 在模板中给 canvas container 添加点击事件 // <div ref="canvasContainer" class="canvas-container" @click="onCanvasClick"></div> const onCanvasClick = (event: MouseEvent) => { if (!camera.value || !scene.value || !renderer.value || !canvasContainer.value) return; // 1. 将鼠标点击位置归一化为设备坐标 (-1 到 +1) const rect = canvasContainer.value.getBoundingClientRect(); mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1; mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1; // 2. 更新射线投射器 raycaster.setFromCamera(mouse, camera.value); // 3. 计算相交对象 const dxfGroup = scene.value.getObjectByName('dxfContent'); const intersectObjects = dxfGroup ? [dxfGroup] : []; // 只检测DXF内容 const intersects = raycaster.intersectObjects(intersectObjects, true); // true 表示递归检测所有后代 // 4. 处理选中逻辑 if (intersects.length > 0) { const selected = intersects[0].object; // 取消之前的高亮 if (selectedObject.value) { // 恢复原貌,这里需要你记录对象原始材质,简单演示用颜色变化 if (selectedObject.value instanceof THREE.Mesh || selectedObject.value instanceof THREE.Line) { (selectedObject.value.material as THREE.Material).color?.setHex(0x000000); // 假设恢复黑色 } } // 设置新的高亮 selectedObject.value = selected; if (selected instanceof THREE.Mesh || selected instanceof THREE.Line) { (selected.material as THREE.Material).color?.setHex(0xff0000); // 高亮为红色 } console.log('选中对象:', selected, '图层:', selected.userData?.layer); } else { // 点击空白处,取消选择 if (selectedObject.value) { // ... 恢复原始外观 selectedObject.value = null; } } };这个高亮示例非常基础,实际应用中你可能需要更复杂的高亮逻辑(比如外发光、描边效果),并且需要妥善管理对象的原始材质状态。
5.3 测量与标注
这是一个更高级的功能。思路是监听鼠标事件,在屏幕上绘制临时图形,并计算世界坐标系中的距离。
- 进入“测量模式”。
- 第一次点击记录起点,并在场景中添加一个点标记。
- 鼠标移动时,实时绘制一条从起点到当前鼠标位置的线段。
- 第二次点击记录终点,完成测量,计算两点间的距离(
startPoint.distanceTo(endPoint)),并在终点添加文字标注显示距离。 这需要你管理一个额外的“测量层”场景,并处理大量的坐标转换和图形更新。
6. 项目构建与部署注意事项
6.1 处理Three.js的Tree Shaking
Three.js很大。在构建时,Vite/Rollup的Tree Shaking可能无法完全剔除未使用的代码。为了获得最小的打包体积,建议使用子路径导入:
// 替换 import * as THREE from 'three'; import { Scene, PerspectiveCamera, WebGLRenderer, Color, Box3, Vector3, AmbientLight, DirectionalLight } from 'three'; import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';6.2 字体文件的加载与打包
如果使用了字体文件,需要确保构建工具能正确处理它。在Vite中,使用?url查询字符串可以将其作为URL引入。你可能需要将字体文件放在public目录或配置资源复制规则。
6.3 兼容性与Polyfill
确保你的目标浏览器支持WebGL。可以在初始化渲染器时检查:
if (!WebGLRenderingContext) { // 提示用户浏览器不支持 alert('您的浏览器不支持WebGL,无法显示图纸。'); return; } const renderer = new THREE.WebGLRenderer({ ... });对于较老的浏览器,可能需要引入resize-observer-polyfill等。
6.4 性能监控
在开发过程中,多使用浏览器的性能分析工具(Performance tab)和Three.js自带的统计信息(THREE.Stats)来监控帧率、内存使用和绘制调用,及时发现性能瓶颈。
将dxf-parser和three-dxf集成到Vue3项目中,构建一个DXF查看器,是一个典型的“库整合”任务。关键在于理解数据流:文件 -> 解析为数据 -> 转换为Three.js对象 -> 由Vue组件管理渲染循环和交互。过程中最大的挑战往往不是代码本身,而是对DXF格式复杂性的处理、性能优化以及两个库(dxf-parser和three-dxf)在细节行为上的探查。希望这篇详细的实践记录,能为你扫清这些障碍,让你在实现类似功能时,有一个坚实可靠的起点。记住,遇到奇怪的显示问题,第一件事永远是打开控制台,打印出parsedData和dxfGroup的结构,数据不会说谎。
