
做上位機運動控制的工程師應(yīng)該都遇到過這種場面客戶拿來一張CAD圖說設(shè)備要照著圖上的輪廓跑一遍。圖紙本身只是一堆線條和圓弧控制設(shè)備真正需要的是刀路數(shù)據(jù)——一段一段帶坐標的小線段。從圖形到線段中間隔著導圖、解析、離散化、坐標折算這一整條鏈路。今天這篇就聊聊這條鏈路里的第一環(huán)CAD導圖。這套“C#運動控制開源”系列主要整理我在某激光加工設(shè)備項目里沉淀的上位機代碼整個項目源碼是開源的模塊劃分比較清晰。CAD導圖模塊放在最前面因為它是后面小線段速度前瞻、插補輸出和軌跡運動的基礎(chǔ)。沒有一份干凈、有序、單位統(tǒng)一的線段數(shù)據(jù)速度前瞻算得再漂亮也白搭。這篇文章面向打算自己寫運動控制上位機的工程師或者做CAD圖形解析但又不想被第三方庫綁死的人我會把導圖模塊的設(shè)計思路、解析重點、離散化細節(jié)和踩過的坑一起說清楚。1. 為什么要在運動控制里專門做一個CAD導圖1.1 人工描點這種傳統(tǒng)做法有多痛苦早年的運動控制設(shè)備調(diào)試員拿到圖紙以后先量關(guān)鍵點坐標再一個個手動敲進參數(shù)表。圖形簡單還好碰上包含幾百個圓弧、樣條線、復雜多段線的零件一個通宵都未必敲得完而且坐標敲錯幾位數(shù)加工出來就是廢件。我接過一個項目對方拿來的圖紙上是一條不規(guī)則的異形輪廓里面是連續(xù)幾段圓弧和直線人工描點描出來的軌跡在機床上一跑接刀處能明顯看到棱。當時的處理辦法就是手寫一個小工具把CAD文件里的實體導出來從此我再也不想回到純手工時代。1.2 CAD導圖在整個運動控制鏈路中的位置運動控制上位機的加工流程大致是這樣圖紙文件導入、實體解析、坐標提取、圖層過濾、離散化生成小線段、路徑排序、速度前瞻插補、驅(qū)動設(shè)備執(zhí)行。CAD導圖承擔的是最前面這部分——把圖紙里的幾何信息完整地讀出來轉(zhuǎn)成運動控制能認識的線段數(shù)據(jù)。后面速度前瞻的輸入源就是這里輸出的小線段列表。如果只把CAD導圖理解成“把圖顯示到界面上”那會有個大坑顯示用的圖形數(shù)據(jù)精度要求不高但加工用的路徑數(shù)據(jù)必須精確到小數(shù)點后三到四位。我見過有些同事直接拿PDF轉(zhuǎn)的圖片當?shù)讏D再描點生成加工路徑這方法碰上質(zhì)檢嚴格一點的零件就廢了。真正要做的是解析圖紙里的矢量實體讓后續(xù)每一個坐標點都來自原始幾何定義。2. CAD文件格式選型與DXF解析方案2.1 該選哪種CAD格式接入上位機CAD生態(tài)里常見格式有DWG、DXF、PLT等幾種。DWG是原生格式壓縮過、結(jié)構(gòu)復雜想解析它得依賴商業(yè)組件或逆向工程做上位機項目時沒必要在這上面死磕。PLT是HP繪圖儀格式精度有限適合簡單輪廓。DXF是公開的ASCII/二進制交換格式最早就是為了在不同CAD軟件之間交換數(shù)據(jù)設(shè)計的文本格式便于定位問題也方便自己寫解析器。做運動控制上位機我建議把DXF作為首選導入格式跟設(shè)計端約定“導出為DXF”能省掉大量格式兼容的麻煩。實際項目里我遇到的設(shè)計軟件不下四五種只要它們能導出DXF文件內(nèi)容基本都能往同一套解析邏輯里塞。個別軟件導出的DXF不標準主要體現(xiàn)在實體類型不全、圖層組織混亂、單位信息缺失上面這些問題后面會在解析層做兼容處理。選DXF還有個好處文件結(jié)構(gòu)透明出現(xiàn)解析異常時直接打開文本文件看對應(yīng)位置比對著二進制流猜字段省事得多。2.2 DXF文件里到底存了什么DXF文件的組織方式是“段”主要有HEADER段、TABLES段、BLOCKS段和ENTITIES段。我們加工路徑關(guān)心的核心內(nèi)容都在ENTITIES段LINE、ARC、CIRCLE、LWPOLYLINE、SPLINE、POINT這幾種實體基本覆蓋了絕大多數(shù)切割、雕刻、點膠路徑。DXF的每一條信息都由兩行組成第一行是組碼Group Code第二行是組碼對應(yīng)的值。組碼是有約定的比如10、20、30分別是X、Y、Z坐標0表示開頭一個對象8是圖層名40是半徑50是角度。整個文件就是由這樣成對行的順序流拼出來的。解析DXF不需要高深算法核心就一個循環(huán)逐行讀入按組碼分類處理。我用C#實現(xiàn)的讀取框架大致是這樣的public ListCadEntity ParseDxf(string filePath) { var entities new ListCadEntity(); using var reader new StreamReader(filePath, Encoding.ASCII); string line; string lastName ; var pairs new List(int Code, string Value)(); while ((line reader.ReadLine()) ! null) { int code int.Parse(line.Trim()); string value reader.ReadLine()?.Trim() ?? ; pairs.Add((code, value)); if (code 0) { // 上一個對象結(jié)束把累積的組碼交給具體實體解析器處理 if (pairs.Count 1) { var (type, entity) EntityFactory.Create(pairs); if (entity ! null) entities.Add(entity); } pairs.Clear(); lastName value; } } // 處理文件尾部可能殘存的最后一個對象 if (pairs.Count 1 !string.IsNullOrEmpty(lastName)) { var (type, entity) EntityFactory.Create(pairs); if (entity ! null) entities.Add(entity); } return entities; }這個框架不依賴任何第三方庫StreamReader逐行讀取對大文件很友好。真正的難點在EntityFactory里——不同實體要把各自的組碼轉(zhuǎn)成有意義的幾何對象。2.3 自己寫解析器還是用第三方庫很多同事一提到DXF就想引入現(xiàn)成庫我在初期也試過幾個常見的解析類庫但發(fā)現(xiàn)兩個問題一是大多數(shù)庫面向CAD查看器場景重顯示、輕提取拿到的實體類型要么不全要么缺失圖塊展開二是輸出數(shù)據(jù)結(jié)構(gòu)不一定貼合運動控制需求比如把多段線直接展開成點集但沒保存圖層和閉合信息后面做軌跡分組就很被動。自己寫解析器聽上去工作量大其實直線、圓弧、圓、多段線這四類實體就能覆蓋九成加工場景。實體不多的時候解析代碼比想象中簡單而且數(shù)據(jù)結(jié)構(gòu)完全由你控制。我采用的方案是定義統(tǒng)一基類CadEntity子類有CadLine、CadArc、CadCircle、CadPolyline、CadSpline全部歸入“可離散化實體”列表。這樣后面寫速度前瞻時拿到的是一個統(tǒng)一的“線段源”概念不用關(guān)心來源是直線還是一條被拆散的圓弧。3. CAD導圖核心實現(xiàn)從實體到小線段3.1 直線、圓弧、多段線的關(guān)鍵組碼解析直線是最基礎(chǔ)的實體組碼結(jié)構(gòu)很直白10、20是起點11、21是終點。圓弧稍微拐了個彎10、20是圓心40是半徑50是起始角度51是終止角度單位是度。這里有個特別容易踩的坑CAD的圓弧默認逆時針方向從起始角轉(zhuǎn)到終止角。如果圖紙里的圓弧是順時針CAD會把它存成起始角大于終止角的逆時針圓弧比如300度到60度。解析時得先判斷角度大小關(guān)系必要時補上360度偏移。圓就更簡單了圓心加半徑?jīng)]有角度一說。多段線分兩種存儲方式POLYLINE是老式的頂點分散存儲LWPOLYLINE是新式的輕量緊湊存儲這兩類實體在DXF里同時存在解析時必須都支持。LWPOLYLINE的關(guān)鍵組碼是90頂點數(shù)量、10和20頂點坐標、42凸度。凸度這個概念我第一次接觸時繞了不少彎它是相鄰兩個頂點之間圓弧的半角正切值。凸度為0表示兩個頂點之間是直線大于0表示逆時針凸起小于0表示順時針凹陷。想從頂點坐標和凸度算出圓弧的圓心與半徑涉及弦長和矢高的幾何換算我提煉了一個現(xiàn)成的計算方法后面代碼里直接套用即可。樣條線在切割圖紙里出現(xiàn)頻率不高但偶爾會遇到。處理方式不多想直接采樣離散成一系列點再擬合成小線段組精度控制用采樣步長加最大弓高誤差雙重限制。3.2 圓弧離散化小線段生成的誤差控制運動控制插補不認圓弧實體只認小線段。圓弧離散化是CAD導圖模塊的精度命門。很多初版實現(xiàn)直接按固定角度間隔切圓弧比如每1度切一段結(jié)果大半徑圓弧切出來特別密、數(shù)據(jù)量巨大小半徑圓弧又切得太粗、輪廓失真。合理的做法是按最大弓高誤差來決定切分步長。上面那段代碼里的maxError就是允許的最大弓高誤差。激光切割一般取0.005毫米粗加工可以放到0.02毫米。我項目里默認值是0.01毫米實際跑下來既保證精度又不會產(chǎn)生過多線段。選這個值是有講究的它直接決定后續(xù)速度前瞻的計算代價——小線段數(shù)量越多前瞻計算量越大所以離散化不能盲目追求極致精度要跟加工需求匹配。值得提醒的是結(jié)束角度不一定要生成的最后一個點落在理論終點上但為了路徑閉合準確還是要把終點強制加入。我代碼里的for循環(huán)從0到steps而不是到steps-1就是為了保證起點和終點都落在離散后的點集里。3.3 圖層過濾和刀路篩選的工程化處理圖紙里除了加工輪廓經(jīng)常還帶著尺寸標注、中心線、輔助線、注釋文字。如果一股腦全導入成加工路徑設(shè)備會沿著標注線跑出奇怪軌跡。圖層過濾是CAD導圖里絕對不能省的一步。我習慣在界面上開放一個圖層列表解析完DXF后把所有用到的圖層都列出來用戶勾選哪個圖層參與加工哪個圖層只做底圖參考。內(nèi)部實現(xiàn)就是在處理每個實體時讀取組碼8的圖層名然后跟一個白名單集合比對。白名單未命中且不需要顯示的實體直接跳過連離散化都不用做。這個做法親測能省掉至少一半的數(shù)據(jù)量。有些設(shè)計工程師習慣把無數(shù)標注放在同一個默認圖層里過濾做不好后面整條路徑都會亂套。建議導圖界面上一開始就跑一次、顯示一次確認圖層是否無誤后再正式生成加工路徑。這塊其實還有一個隱藏收益圖層過濾之后的實體順序基本還是文件原序為了滿足后續(xù)速度前瞻的路徑連續(xù)性要求還得做路徑排序這部分下一篇文章會展開。4. 坐標轉(zhuǎn)換、顯示交互與數(shù)據(jù)組織4.1 CAD坐標系與設(shè)備坐標系的換算CAD圖里用的一版是設(shè)計坐標系設(shè)備機械零點是另一個坐標系這兩者之間通常隔著平移、縮放、鏡像甚至旋轉(zhuǎn)。如果直接把解析出來的坐標發(fā)給運動控制卡設(shè)備跑出來的位置肯定是天南地北。我封裝了一個Transform類內(nèi)部維護一個仿射變換矩陣。加載DXF后從HEADER段嘗試讀取單位信息再從界面輸入框拿到工件原點在設(shè)計坐標系里的坐標然后自動把縱軸方向按設(shè)備習慣翻轉(zhuǎn)一下這樣一個統(tǒng)一變換矩陣就能映射到設(shè)備坐標。這套邏輯在好幾個現(xiàn)場項目里都驗證過只要不會把單位的毫米/英寸搞混定位誤差都能控制在重復定位精度范圍內(nèi)。位移換算也可以用四組參數(shù)完成原點X、原點Y、縮放比例、鏡像標記。建立一個函數(shù)ConvertPoint(x, y)把原始坐標一步步套進去即可。不過請注意CAD圖的Y軸方向和部分運動控制系統(tǒng)的Y軸方向相反漏掉這一步輪廓會左右/上下整體顛倒這是現(xiàn)場最容易犯的失誤之一。我的經(jīng)驗是必須有“預(yù)覽對比”功能讓用戶在界面看到設(shè)計原圖跟轉(zhuǎn)換后的路徑疊加效果一眼就能發(fā)現(xiàn)方向問題。4.2 導入后的圖形顯示實現(xiàn)要點上位機界面要顯示CAD圖形多數(shù)人會想到用GDI或WPF繪圖我這里用的是自繪控件。解析完成后的實體對象轉(zhuǎn)成屏幕坐標進行繪制。關(guān)鍵點在于渲染性能幾千個實體還好幾萬甚至十幾萬個小線段時直接逐段調(diào)用繪圖API會卡成PPT。我用的優(yōu)化手段是“可見區(qū)域裁剪”加“簡化繪制”。先根據(jù)屏幕縮放比例判斷哪些線段落在當前視口內(nèi)只繪制可見部分的實體。放大到一定程度時把小線段直接合并成一個折線批次用一條Path一次畫完。這樣畫面幀率能穩(wěn)定維持在可接受的范圍用戶拖動縮放時也不會卡頓。另外繪圖層級上區(qū)分“底圖灰顯”和“加工路徑高亮”避免用戶混淆參考圖形和實際刀路。關(guān)于顯示性能還有一個不起眼但是很好用的小技巧離散化小線段時把首尾坐標用結(jié)構(gòu)體存到一個連續(xù)數(shù)組里繪制時整個數(shù)組的坐標點一次性提交給繪圖上下文真正卡住性能的往往是逐點構(gòu)造對象帶來的開銷而不是繪圖本身。4.3 線段數(shù)據(jù)的統(tǒng)一組織與存儲CAD導圖模塊的最終產(chǎn)物是我定義的一個路徑數(shù)據(jù)模型。它不是零散的坐標點而是包含多個“子路徑”的有序列表。每個子路徑對應(yīng)一條連續(xù)的加工輪廓子路徑里是排好序的線段節(jié)點每個線段節(jié)點包含起點、終點、線型類型、原始實體引用、圖層名和長度。這個模型是為了和后面的速度前瞻無縫銜接設(shè)計的。速度前瞻算法是典型的局部窗口優(yōu)化需要快速知道當前線段的下一條線段在哪里以及兩條線段之間的夾角和長度關(guān)系。用結(jié)構(gòu)體數(shù)組存儲線段內(nèi)存連續(xù)Cache命中高幾萬條線段的前瞻計算在毫秒級就能完成。我實際測量過一個包含2.3萬條小線段的輪廓文件CAD導圖模塊從讀入到離散化完畢、輸出路徑模型總耗時在0.8秒左右。這個成績對大部分運動控制場景都夠用了如果現(xiàn)場要求更高下一步還可以用并行解析。但可能落后的地方是圖層白名單要求的UI優(yōu)先級最高先保證功能正確再考慮性能。5. 為小線段速度前瞻做的數(shù)據(jù)預(yù)處理5.1 線段排序與路徑連續(xù)性處理DXF文件里的實體順序通常是畫圖的人按設(shè)計順序畫的但加工必須按空間連續(xù)順序走。離得近的兩條線段可能在文件里隔了十萬八千里如果不做排序設(shè)備就會在加工區(qū)域內(nèi)來回跳空跑效率極低。排序邏輯我用的是最近鄰貪心算法選一個尚未訪問的線段作為當前起點在所有未訪問線段里找距離當前終點最近的那條接上去反復迭代直到閉合回起點。接近O(n)到O(n2)的復雜度問題用空間網(wǎng)格索引處理實際跑起來可以接受。極端情況下文件里有上千條獨立路徑排序算法也不應(yīng)超過兩秒鐘。需要注意的是如果一條輪廓是閉合的排序結(jié)果需要形成一個閉環(huán)如果不閉合就要保證起終點順序跟工藝要求一致比如切割順邊還是逆邊。這些規(guī)則在導圖界面里做成選項用戶可以根據(jù)工藝自行選擇。這步排序做完后面速度前瞻拿到的數(shù)據(jù)才是真正的“連續(xù)路徑流”。5.2 輪廓分組與刀補預(yù)留排序后的線列還要分組每個閉合四邊形或者多段點子集是一個獨立的輪廓組。加工時每個輪廓組通常對應(yīng)一次下刀、一次抬刀或者一組補償參數(shù)。這個信息對CAM后處理分外重要我在路徑模型里為每個輪廓組附加了輪廓編號、包圍盒、層級關(guān)系。刀補預(yù)留這個問題屬于“不做不知道一做嚇一跳”。當使用刀具半徑補償時CAD圖里通常畫的是設(shè)計輪廓實際切割路徑是偏置后的輪廓。是做外偏置還是內(nèi)偏置偏置量是刀具半徑加補償值這些參數(shù)都要在導圖模塊里提前配置生成加工路徑時統(tǒng)一處理。如果把這些邏輯堆到速度前瞻階段再處理算法復雜度會爆炸式上升。5.3 頂點合并與數(shù)據(jù)去噪圖紙本身不總是干凈的。兩個看似相連的線段終點可能差0.02毫米多段線里可能混著一截長度幾乎為零的重疊線段。這些“毛刺”會讓速度前瞻在處理夾角時產(chǎn)生抖動也容易在運動控制里引起往復震動。我增加了一個預(yù)處理步驟先按容差比如0.005毫米合并重合頂點再刪除長度小于某個值的劣化線段。這步看似小但直接把前瞻算法的異常分支減少了很多?,F(xiàn)場加工時也能明顯感覺到路徑過濾得干凈的設(shè)備動作更平滑震動更小。容差的取值要控制在運動控制系統(tǒng)分辨率以內(nèi)。設(shè)得太大會把本該存在的細小輪廓細節(jié)吞掉設(shè)得太小過濾不掉噪聲數(shù)據(jù)。我的經(jīng)驗是取系統(tǒng)最小進給步長的1/2到1倍。這個值跟設(shè)備有關(guān)別照抄別人的參數(shù)。6. 常見問題與排查技巧實錄6.1 常見問題速查表整理一份我在現(xiàn)場和調(diào)試中遇到的高頻問題按現(xiàn)象、排查方向、解決方案列成下表方便直接對照。問題現(xiàn)象排查方向解決方案導入后圖形整體過大或過小文件和設(shè)備的單位不一致讀取HEADER段單位信息換算成設(shè)備毫米并校驗比例圖形整體鏡像翻轉(zhuǎn)Y軸方向定義、鏡像標記未處理在坐標變換處統(tǒng)一處理Y軸翻轉(zhuǎn)用預(yù)覽對比確認圓弧方向反了起始角與終止角判斷不對按CAD逆時針規(guī)則處理角度差為負時補360度加工時路徑亂跳圖層未過濾、實體順序未排序啟用圖層白名單執(zhí)行最近鄰路徑排序?qū)牒笕绷艘徊糠謱嶓w未處理INSERT塊引用或隱藏圖層展開BLOCKS段塊實體隱藏圖層實體默認跳過但提供開關(guān)大文件導入慢顯示卡頓逐點構(gòu)建對象、未裁剪視口用結(jié)構(gòu)體數(shù)組存儲線段坐標可見區(qū)域裁剪批量繪制LWPOLYLINE中的圓弧方向不對凸度符號判斷問題凸度大于0逆時針、小于0順時針轉(zhuǎn)換確認方向6.2 被圖層白名單坑過的真實教訓之前有個樣板項目對方出的圖紙所有尺寸標注都放在主輪廓圖層里我導圖后發(fā)現(xiàn)刀路里多了一堆沿著尺寸數(shù)字跑的短線。前線排查時盯著實體類型怎么都沒想到是圖層污染。后來在導圖界面上加了一列“圖層實體統(tǒng)計”每個圖層下面有多少個實體一覽無余三分鐘就定位到污染源。所以我現(xiàn)在做任何圖形導入功能一定會保留圖層級別的統(tǒng)計信息這是排查問題最快的一條路徑。6.3 解析不標準DXF的兼容技巧有些CAD軟件導出DXF時不按標準寫比如組碼順序不是嚴格按固定順序或者在非標準位置插入額外組碼。我的解析器寫得不死板在每個對象內(nèi)循環(huán)收集所有組碼注冊一個字典按Key取需要的組碼值而不是死板順序判斷。自由度一下就上去了。還有一個兼容點是字符串編碼。部分老版本CAD導出為ANSI編碼UTF-8讀取后圖層名可能變成亂碼圖層過濾全部失效。我的辦法是嘗試用StreamReader的編碼檢測先按UTF-8讀發(fā)現(xiàn)非法字符就回退到GBK或ANSI。這個細節(jié)在中文圖紙環(huán)境下幾乎避不開調(diào)試時注意一下能少掉一大半頭發(fā)。結(jié)語CAD導圖這個模塊從功能上看只是把圖變成線段但真正做起來涉及格式解析、幾何計算、坐標轉(zhuǎn)換、性能優(yōu)化和一堆現(xiàn)場才知道的兼容性問題。我花了蠻多時間打磨邊界條件現(xiàn)在這套代碼在幾個不同設(shè)備上跑下來給我省下的調(diào)試時間遠超當初的開發(fā)投入。如果正在做運動控制上位機建議先聚焦核心實體類型把精度和排序做好再逐步補齊兼容層別一開始就想吃掉所有格式。個人體會最深的一點是CAD導圖不僅僅是“讀文件”它決定了后續(xù)速度前瞻的上限。數(shù)據(jù)組織不到位整個加工質(zhì)量都會受影響。好在這些坑我已經(jīng)幫大家蹚過一遍照著上面的思路做路徑數(shù)據(jù)這塊會扎實很多。CAD導圖聊到這里下一篇我會接著寫小線段速度前瞻的優(yōu)化——就是拿這份整理好的線段數(shù)據(jù)怎么最合理地規(guī)劃速度既保證效率又不讓設(shè)備抖動。那部分的算法細節(jié)比導圖還要有意思等下次更新一起聊。