圖:從DXF解析到小線段速度前瞻的工程實踐)
做運動控制的這幾年我最大的一個體會是不管算法多漂亮只要圖紙數(shù)據(jù)進(jìn)不來后面全都白搭。很多做上位機的人一開始沒把CAD導(dǎo)圖當(dāng)回事覺得不就是讀個文件嘛結(jié)果等到接激光切割、水刀、雕刻機的時候才發(fā)現(xiàn)從CAD到實際加工路徑之間藏著無數(shù)細(xì)節(jié)。所以這個開源項目我第一個分享的主題就是CAD導(dǎo)圖順便把和它緊密相關(guān)的小線段速度前瞻也帶幾句——畢竟這兩者在運動控制里是前后腳的關(guān)系。這篇文章適合正在做上位機軟件開發(fā)、想了解運動控制系統(tǒng)中圖形數(shù)據(jù)如何落地的朋友也適合已經(jīng)在做插補運動、但被DXF解析和路徑銜接折磨過的人。我不會只貼代碼而是把設(shè)計思路、踩過的坑、為什么這么選型都講清楚。1. 為什么運動控制要先做好CAD導(dǎo)圖這一環(huán)1.1 從“畫圖”到“走軌跡”之間的鴻溝很多人以為CAD導(dǎo)圖就是把DXF里的線條讀出來再畫一遍到屏幕上這就大錯特錯了。運動控制里要的不是“顯示”而是把圖形數(shù)據(jù)變成機床能執(zhí)行的指令序列。這個轉(zhuǎn)換過程包含了實體識別、坐標(biāo)變換、曲線離散化、連接點處理等一系列環(huán)節(jié)。任何一個環(huán)節(jié)出問題加工出來的東西就是廢品。舉個例子。CAD圖紙上畫了一個帶圓弧的輪廓如果直接把DXF里的圓弧實體當(dāng)成一個“圓弧命令”發(fā)給運動控制器很多控制器是不認(rèn)的或者認(rèn)了但圓弧的起點、終點、方向定義和你的插補器不一致加工軌跡就會偏。所以普遍的做法是把圓弧等曲線離散成小線段然后交給速度前瞻模塊去做拐角速度規(guī)劃。這個過程本身不復(fù)雜復(fù)雜的是離散得夠不夠好、銜接處處理得夠不夠聰明。1.2 開源項目里CAD模塊的價值邊界做開源運動控制項目最容易犯的毛病是貪大求全。我在設(shè)計這個項目的時候給CAD模塊劃定了一條清晰的價值邊界只管“把圖紙變成軌跡數(shù)據(jù)”不做CAD編輯不做復(fù)雜渲染不做工藝規(guī)劃。這不是偷懶而是運動控制系統(tǒng)的本職工作就是執(zhí)行圖形處理只是入口。把入口做扎實比什么都重要。具體來說這個模塊要解決三件事第一解析常見CAD格式首選DXF因為它是文本格式、公開文檔、幾乎所有CAD軟件都支持導(dǎo)出第二把圖元統(tǒng)一轉(zhuǎn)成內(nèi)部軌跡模型包括點的坐標(biāo)、線段屬性、連接關(guān)系第三輸出可供速度前瞻模塊直接使用的數(shù)據(jù)流。這三件事每一件都值得單獨展開。2. DXF文件解析別小看這個老掉牙的格式2.1 實體類型的取舍最初我試圖支持DXF里所有實體類型很快發(fā)現(xiàn)這是給自己挖坑。DXF的實體類型有幾十種像三維面、網(wǎng)格、代理對象這些在2D加工場景里根本用不上。硬要去解析它們只會增加出錯概率。我最終只保留了四類核心實體LINE直線、ARC圓弧、CIRCLE整圓、LWPOLYLINE輕量多段線再加上SPLINE樣條曲線的處理但樣條曲線我后面會細(xì)講。對于塊引用INSERT我做了遞歸展開也就是把塊里的實體按變換矩陣展開成獨立實體。這樣做的原因是很多圖紙里重復(fù)的圖形都做成塊如果你不去展開加工路徑會缺一堆東西。有個細(xì)節(jié)值得注意DXF里同一個圖形可能有多重表示。比如一條直線可能在LINE實體里也可能在LWPOLYLINE里作為一段子線段。解析時如果不做去重后面生成的軌跡會重復(fù)加工輕則浪費時間重則在切割場景里直接切壞工件。我的處理方式是在生成內(nèi)部軌跡模型前做一個歸一化把所有實體統(tǒng)一成“點序列”的形式然后在點序列層面做距離判定去重。2.2 組碼機制與解析器設(shè)計DXF格式的核心就是組碼Group Code每個數(shù)據(jù)項前面有一個整數(shù)標(biāo)記它的意義。比如0表示實體類型、8表示圖層、10和20表示點的X和Y坐標(biāo)、40表示半徑、50和51表示起始角和終止角等。解析器本質(zhì)上就是一個循環(huán)讀組碼、根據(jù)組碼歸類數(shù)據(jù)的有限狀態(tài)機。這里我強烈建議不要用正則表達(dá)式去匹配整個文件而是逐行讀取、用狀態(tài)機方式解析。原因很簡單DXF文件的結(jié)構(gòu)是順序的狀態(tài)機天然匹配這種流式處理正則在大文件上慢而且容易在特殊字符上出錯。我用C#寫了一個基于StreamReader逐行讀的解析器配合int.TryParse和double.TryParse做轉(zhuǎn)換每組數(shù)據(jù)本地化存儲。實測下來一個包含十萬個圖元的DXF文件幾秒鐘就能解析完成。解析器的狀態(tài)機設(shè)計大概是這樣的先讀一個組碼如果為0讀取實體類型字符串根據(jù)類型創(chuàng)建對應(yīng)的實體對象然后繼續(xù)讀組碼和值直到碰到下一個0表示當(dāng)前實體結(jié)束。這里有個隱藏的坑組碼和值交替出現(xiàn)但值的行不能隨便丟因為有些值可能跨行。保險的做法是只要讀到組碼就接著讀下一行作為值無論這一組數(shù)據(jù)看起來多“奇怪”。2.3 單位、坐標(biāo)系與縮放這是我認(rèn)為整個CAD導(dǎo)圖里最陰險的坑——單位問題。DXF文件本身有單位設(shè)置通過變量$INSUNITS標(biāo)記但這個值很多畫圖的人根本不注意導(dǎo)致DXF里寫的單位未必是毫米。如果你拿到一張單位是英寸的圖紙直接按毫米處理整個圖形的尺寸就全錯了。我的處理方式是做一個單位歸一化層解析完文件后讀取$INSUNITS如果是英寸就乘25.4轉(zhuǎn)成毫米如果是英尺就乘304.8其他單位類似。這個轉(zhuǎn)換必須發(fā)生在整個數(shù)據(jù)鏈路的最前端一旦坐標(biāo)進(jìn)入內(nèi)部模型后面所有環(huán)節(jié)都基于毫米計算不要再回頭去改。坐標(biāo)系方面DXF使用世界坐標(biāo)系WCS而加工時通常以工件的某個角為原點。所以解析后要做一次坐標(biāo)平移把圖紙的原點映射到工件原點。這里要小心的是不同CAM軟件導(dǎo)出的DXF原點位置可能天差地別有的在圖形中心有的在左下角有的在絕對零點。所以我在UI上提供了“手動拾取基準(zhǔn)點”的功能讓用戶點圖紙上的某個點作為加工原點這個原點信息存下來后續(xù)都基于它做變換。3. 圖元到加工軌跡的變換鏈路3.1 坐標(biāo)系對齊與工件原點設(shè)置解析完成之后CAD模塊的第二個核心任務(wù)就是把圖元坐標(biāo)變換成加工坐標(biāo)。這一步聽起來簡單實際上涉及旋轉(zhuǎn)、平移、鏡像三個基本操作。旋轉(zhuǎn)是因為有些圖紙的X軸并不對齊機床的X軸鏡像是因為CAD的坐標(biāo)系Y軸向上而有些運動控制器的坐標(biāo)系Y軸可能向下不做鏡像的話整個圖形是反的。我在項目里維護(hù)了一個變換矩陣類采用齊次坐標(biāo)的方式統(tǒng)一處理平移、旋轉(zhuǎn)、縮放。這樣做的好處是不管做多少種變換最后都是矩陣連乘不容易出錯也方便在調(diào)試時直接看變換矩陣的數(shù)值來檢查問題。工件原點的設(shè)置我沒有做成全局唯一的而是做成每個零件圖元組獨立可設(shè)置的。原因是我遇到過一種情況一張大圖紙上有十幾個零件排樣圖加工時是需要一個一個零件來切割的。如果你只有一個全局原點就沒法快速定位到每個零件。所以我在圖元歸一化階段就把實體分組每個組有獨立的包圍盒加工時可以根據(jù)包圍盒或者用戶點擊來設(shè)置該組的原點。3.2 曲線離散化精度與效率的平衡圓弧和樣條曲線最終都要離散成小線段。離散的密度直接決定了兩個矛盾的東西——加工精度和運行速度。離散得越密輪廓越光滑但小線段數(shù)量暴增速度前瞻模塊的壓力就會劇增運動控制器的插補周期可能吃不消離散得太疏輪廓上就會出現(xiàn)明顯的折角加工表面質(zhì)量差。這里要用到一個基礎(chǔ)但極其重要的參數(shù)弓高誤差弦高差。圓弧離散的時候相鄰兩個離散點之間的連線是弦圓弧和這根弦之間的最大距離就是弓高誤差。公式不復(fù)雜給定圓弧半徑R、離散夾角θ弓高誤差h ≈ R(1 - cos(θ/2))。反過來給定允許的弓高誤差h可以算出圓心角步長θ 2 * acos(1 - h/R)。我項目里默認(rèn)把弓高誤差設(shè)為0.01mm這個精度對大多數(shù)切割場景足夠了。實測下來一個直徑100mm的圓按0.01mm弓高誤差離散大約會產(chǎn)生1400多個小線段。如果在CAD圖里有一千個圓那就是一百多萬個線段這個數(shù)量級對速度前瞻模塊就有壓力了所以后面我加了一個二重離散策略先粗離散用于顯示和快速預(yù)覽再精離散用于實際加工。這樣可以兼顧界面操作流暢度和加工精度。樣條曲線的離散比圓弧麻煩得多。我的做法是用De Boor算法做均勻節(jié)點采樣然后再用同樣的弓高誤差準(zhǔn)則做細(xì)分。具體來說先均勻取一些參數(shù)點計算曲線上的位置然后檢查相鄰點連線和曲線之間的偏差如果超過弓高誤差就在中間插入一個新點遞歸細(xì)分直到滿足精度。這個算法實現(xiàn)起來不算太難但要注意不要對整個樣條一次性加密因為樣條可能出現(xiàn)局部曲率特別大的地方而均勻加密會讓曲率小的區(qū)域浪費大量線段。3.3 圓弧方向、法向與補償?shù)念A(yù)處理另一個容易忽視的點是圓弧方向的一致性。DXF里圓弧的起始角和終止角是逆時針為正的但運動控制器的坐標(biāo)系定義可能不同。如果方向搞反了加工的圓弧就會向外側(cè)偏尺寸直接不對。我在解析時就把所有圓弧歸一化成統(tǒng)一的逆時針方向并記錄中心、半徑、起點角度、終點角度和一個布爾標(biāo)記表示是否跨越了0度角。這里的跨0度角是個小陷阱——比如起始角350度、終止角20度的圓弧如果你只是簡單地用“角度遞增”來計算它只會畫30度而不是應(yīng)該畫的30度其實是跨過0度從350到360再繼續(xù)到20相當(dāng)于走370度。這個計算必須對終止角做修正或調(diào)整為順逆方向加一圈的邏輯。法向補償?shù)膯栴}在激光切割和銑削里特別常見。刀具或激光焦點沿輪廓行走時需要偏置半個刀具直徑或光束半徑這個偏置方向必須垂直于輪廓切線方向。圓弧的偏置方向和直線的偏置方向在連接點上會產(chǎn)生過度切割或欠切的風(fēng)險。我在CAD導(dǎo)圖模塊里雖然不直接做完整的刀具補償?shù)仨氼A(yù)先給每條線段標(biāo)定好左側(cè)/右側(cè)法向信息這樣后續(xù)的補償算法才能避免每個拐角重新計算法向。這個預(yù)處理就是純粹的“臟活”但它能讓后續(xù)算法簡單不少。4. 速度前瞻與CAD導(dǎo)圖的銜接設(shè)計4.1 小線段前瞻的基本思路小線段速度前瞻是整個運動控制系統(tǒng)的靈魂。它的任務(wù)是給定一連串密集的小線段在滿足最大加速度和最大速度限制的前提下規(guī)劃出一條盡可能快的速度曲線。核心困難在于——相鄰線段之間存在方向變化方向變化越劇烈能通過的最大速度就越低。如果不做前瞻干脆讓機床每段都減速到零再加速那速度就上不去加工效率慘不忍睹。前瞻算法的基本思想是“看一窗口”從當(dāng)前線段出發(fā)向前預(yù)讀N條線段提前判斷哪里必須減速、哪里可以巡航從而形成一個速度曲線。窗口大小很關(guān)鍵。窗口太小前瞻距離不夠遇到連續(xù)拐彎會頻繁減速窗口太大計算量劇增實時性保不住。我在項目中允許用戶設(shè)置前瞻窗口為50到200條線段實測下來對大多數(shù)2D輪廓100條的窗口在效率和計算量之間比較均衡。4.2 為什么CAD導(dǎo)圖質(zhì)量直接決定前瞻上限這里我想強調(diào)一個很多教程不提的事實前瞻算法的效果天花板其實在CAD導(dǎo)圖階段就已經(jīng)被決定了。舉個例子如果圓弧離散時弓高誤差太嚴(yán)線段數(shù)暴增但每條線段都很短。短線段的好處是逼近精度高壞處是前瞻窗口能覆蓋的路徑長度很短——100條線段可能只覆蓋了2mm的距離遇到拐彎時根本來不及提前減速。反過來如果離散太粗線段很長方向變化集中在線段交點處拐角就很尖銳。前瞻算法面對尖銳拐角只能強制降速到很低速度曲線就會像鋸齒一樣起起落落。所以CAD導(dǎo)圖的離散策略必須和前瞻算法的約束條件聯(lián)動設(shè)計。我在代碼里把弓高誤差和前視距離做了一個聯(lián)動允許用戶設(shè)置最大前視物理距離比如5mm然后根據(jù)當(dāng)前線段密度自動調(diào)整前瞻窗口條數(shù)而不是鎖死一個固定值。4.3 連接點處理G0/G1切換的邊界情況CAD導(dǎo)圖輸出的軌跡里不僅有連續(xù)的加工段G1還有空移段G0——就是從一個輪廓跳到另一個輪廓的快速定位移動。G0和G1的切換處是最容易出邊界bug的地方。舉個例子一個排樣圖要切割五個零件。切完第一個零件后機床需要抬刀或關(guān)激光快速移到第二個零件起點。這時候速度規(guī)劃必須區(qū)分在G0段里可以用最大空移速度但進(jìn)入G1段的第一條線段前必須把速度降到滿足輪廓精度要求的值。如果不做特殊處理速度前瞻會把G0的高速和G1的低速混在一起結(jié)果第一個加工線段起點速度過高過切量極大。我在CAD導(dǎo)圖階段就為軌跡打上“段類型”標(biāo)記并且在段與段的連接處設(shè)置一個速度硬約束G0到G1切換處的目標(biāo)速度強制設(shè)為0除非用戶明確啟用“預(yù)加速功能”。這么設(shè)計雖然犧牲了一點效率但換來了穩(wěn)定性和安全性工業(yè)場景里安全永遠(yuǎn)是第一位的。5. 實測性能與踩坑記錄5.1 十萬級圖元加載實測項目里我特意準(zhǔn)備了一個壓力測試文件里面包含了大約十萬個圖元有大量的小圓孔和復(fù)雜樣條輪廓。實測結(jié)果如下解析DXF文件耗時約2.8秒C#Release模式i5處理器實體歸一化與去重約1.2秒樣條離散與圓弧離散約5.6秒總耗時約10秒其中離散化占了超過一半。輸出軌跡小線段數(shù)量約480萬條。這里的問題很明顯——480萬條線段直接丟給速度前瞻模塊內(nèi)存和CPU都扛不住。所以我加了一個“加工段合并”的優(yōu)化共線的相鄰直線段合并成一條長直線段共圓的相鄰圓弧段合并成一條大圓弧。合并之后線段數(shù)量降到了約80萬條雖然還是不小但已經(jīng)可以接受了。合并的判據(jù)很簡單就是三點共線中間點到兩端點連線的距離小于0.001mm和兩點共圓三點共圓的法向偏差和半徑偏差都在容許范圍內(nèi)。這個優(yōu)化做完之后速度前瞻的計算時間幾乎沒變但插補執(zhí)行時的內(nèi)存占用減少了很多。5.2 幾個影響穩(wěn)定性的細(xì)節(jié)問題這一部分我記錄幾個在實際使用中被反復(fù)折騰的問題希望看的人能繞開。第一個是浮點數(shù)精度導(dǎo)致的鋸齒線。DXF里經(jīng)常出現(xiàn)類似“直線端點差0.0000001mm”的情況視覺上沒問題但在幾何判定時會被當(dāng)成兩條不連續(xù)線段。速度前瞻在斷點處強制降速導(dǎo)致實際加工時表面出現(xiàn)一個個小停頓看起來就像鋸齒。解決辦法是引入一個容差機制所有坐標(biāo)比較都在一個絕對容差內(nèi)進(jìn)行默認(rèn)設(shè)成0.001mm后續(xù)如果需要更高精度再調(diào)小但不要直接設(shè)為0。第二個是塊引用的循環(huán)遞歸問題。有些圖紙里A塊引用了B塊B塊又引用了A塊。解析器如果不對塊引用深度做限制就會無限遞歸直到棧溢出。我在展開塊引用時設(shè)了一個最大深度默認(rèn)10層超過就報警提示圖紙異常。第三個是縮放系數(shù)為0的問題。某些CAD軟件導(dǎo)出的DXF會帶一個縮放系數(shù)在塊引用屬性里如果這個系數(shù)是0坐標(biāo)變換矩陣就會退化所有點都會跑到原點。解析時要顯式檢查變換矩陣的行列式是否為0是的話直接跳過該實體并記錄警告。第四個是圖層篩選。加工圖紙里通常有關(guān)標(biāo)注層、尺寸層、輔助線層這些不能拿去加工。我在解析器里加了圖層過濾規(guī)則默認(rèn)跳過名稱包含“DIM”“TEXT”“CENTER”“HIDDEN”這類前綴的圖層。這個規(guī)則可配置因為不同公司畫圖命名習(xí)慣差別很大。5.3 工具鏈與調(diào)試技巧說到調(diào)試我強烈建議在CAD導(dǎo)圖模塊里加一個可視化調(diào)試器。純靠打斷點和日志排查幾何問題太痛苦了。我的做法是在WinForms界面上把解析出的實體用GDI繪制出來并且支持把離散后的點列和原始圖元疊加顯示。這樣一眼就能看出離散效果好不好、有沒有斷點。還有一個技巧是導(dǎo)出調(diào)試數(shù)據(jù)把中間數(shù)據(jù)比如每個點的坐標(biāo)、線段長度、夾角導(dǎo)出成CSV文件用表格或Excel做離線分析。這個比看界面直接多了。性能瓶頸的定位我用的工具很簡單——C#自帶的Stopwatch和Diagnostics類在每個處理階段都打一個計時點。跑完一個測試文件后直接在日志里輸出各階段耗時這樣每次改動都能立刻看到是快是慢。對離散化這種計算密集部分我用Parallel.For做了簡單并行化因為每條線段的離散互不依賴并行化收益非常直接。實測在四核機器上離散那段從5.6秒降到了1.9秒提升效果明顯。最后再說一個和我個人習(xí)慣有關(guān)的經(jīng)驗開源項目一定要從一開始就把數(shù)據(jù)格式定義穩(wěn)定下來哪怕功能簡單一點。我最早圖省事直接用了一個臨時結(jié)構(gòu)體存軌跡點后面前瞻模塊和插補模塊都基于它寫等到想改格式的時候牽一發(fā)動全身幾乎重寫了一遍?,F(xiàn)在項目里所有模塊依賴的是一份統(tǒng)一的TrajectoryPoint定義包含坐標(biāo)、線型標(biāo)記G0/G1、弓高誤差、速度約束等字段。改動一次以后再沒有因為數(shù)據(jù)格式不一致引發(fā)過問題。如果這篇分享能幫你在CAD導(dǎo)圖這塊少走點彎路那我的目的就達(dá)到了。