簡介
Clipper Library(以下簡稱為Clipper庫或ClipperLib或Clipper)提供了對線段和多邊形的裁剪(Clipping)以及偏置(offseting)的功能
和其他的裁剪庫相比,Clipper具有以下特征:
1. 它能夠接受各類多邊形輸入,包含自交的多邊形
2. 它支持多種填充規則(奇偶填充、非零填充、正填充、負填充)
3. 它相較於其他庫效率極高
4. 它數值穩定(魯棒性強),魯棒性:https://baike.baidu.com/item/%E9%B2%81%E6%A3%92%E6%80%A7/832302?fr=aladdin
5. 它支持多邊形和線段的偏置
6. 它對於商用以及免費軟件來說都是免費的
術語:
裁剪(Clipping):通常是指在二維平面把一個圖形在指定的矩形框以外的部分去除掉。在更廣義的角度,指定的裁剪范圍不一定是一個矩形,可以是各種各樣的多邊形,甚至是多個多邊形;同樣的,我們一般裁剪指的是形態學上的“求交”,在Clipper庫中裁剪可以實現4中布爾運算(求交、求和、求異、求異或);
路徑(Path):是指一系列的有序的點的集合,用來定義一個輪廓(Contour),這個輪廓既可以是代指一條線/開放路徑(line/open path),也可以代指一個多邊形/閉合輪廓(polygon/ closed path)
線(Line):與Polyline同義,代指一個具有兩個點或以上點的開放路徑
輪廓(Contour):與路徑同義
孔洞/內輪廓(Hole):代指一個多邊形內部表示該部分不屬於該多邊形的閉合區域:一個“內輪廓(Hole Polygon)”就是一個代指孔的外圍點集的封閉路徑
多邊形填充規則(Polygon Filling Rule):即在一系列的閉合路徑中,來定義哪些屬於“內部”,哪些屬於“外部”
預定義宏(Defines)
編譯器預定義以下類型的宏:
use_int32:當啟用32位精度,效率提升,但是使用范圍縮小
use_xyz:添加一個Z位數據,對性能影響很小
use_lines:啟用開放路徑的裁剪;如果該功能關閉(默認開啟),大約有一個小於5%的性能的提升
use_deprecated:確保代碼與6.0版本以前的內容相兼容;該功能未來可能會刪除
取整(Rounding)
通過使用一個整形數據,Clipper庫已經能夠避免因為數值穩定性造成的重大錯誤,關於數值取整問題和可能的解決方法如下:
首先要強調的是,取整導致點會對他們本來的理論坐標形成一定程序的偏移,但是結果的不精確性是可以通過正確的縮放來進行避免的;
Clipper庫自身支持可以縮放到一個相對較高精度,它所支持的整形坐標值范圍在±0x3FFFFFFFFFFFFFFF (± 4.6e+18)之間
另一個使用離散數據(相較於使用浮動類型數據)的隱患在於可能在極少數的情況下會造成小的自交情況在沒有縮放的左側圖片中(這里單位為1像素),兩個多邊形的相交部分被用亮綠色標出;
一個30倍放大的交點部分。下圖顯示該圖其實有很小程度的自交情況,三個黑點表明了實際的交點情況(通過展示它們小數點部分);紅色顯示在取整之后交點的情況,會很容易觀察到取整讓方向變反並且引起了一定程度的小的自交;
盡管這些小的自交是不常見的,如果這些被認為是有必要被考慮的,最好使用**CleanPolygon**的屬性來對這種情況進行清除;(將Clipper對象的StrictlySimple屬性設定為true同樣會對這類自交產生影響但是小的多余的人為引起的錯誤方向的多邊形仍然是不可避免的)
數據類型
cInt
cInt是Clipper庫用來表示點坐標數據使用的,目前Clipper庫使用整形數據代替浮點數據來保證數據的魯棒性。
默認的cInt代表了一個有符號64位整形數據並且整個多邊形的坐標范圍可以達到±9.2e+18的范圍內。這容納了整個浮點數坐標可以使其精度得到最完整的保留。但是,如果坐標范圍可以被控制在 ± 3.0e+9之內,那么通過避免了大數的運算,可以得到大約百分之十的效率提升。如果預編譯器定義了use_int32,則效率可以更高。
IntPoint
整形點數據結構用來代表Clipper庫中相關的所有點;Clipper庫刻意選擇了整形存儲類型類保證數值穩定。
一系列的IntPoint被保存在Path結構中,構成了一個輪廓。
在版本6.0以上,IntPoint現在可以擁有第三個成員“Z”,這可以通過預編譯器中定義“use_xyz”來實現,當Z軸數據也被加入時,它的值也會被考慮進裁剪的行為中去,但是,在那些沒有對應的Z值交點處,該值會被設定為0,除非用戶提供了回調函數。
用戶如果希望使用浮點數進行Clip,那么必須手動的進行正確的對浮點數進行縮放為IntPoint,在使用Clipper進行處理。
Path
這個結構包含一系列的整形數據點來定義一個單獨的輪廓;路徑由兩個及以上的點數據組成,並且可以是開放的,當它閉合的時候也可以代表多邊形(Polygon);路徑的開放與否是由其內容決定的,封閉的路徑可能是“外輪廓”或“內輪廓(內孔)”,這主要由其方向(順時針或者逆時針)決定的
多個路徑可以添加進Paths結構體組成一組路徑。
Paths
由多個Path組成的基本類型;
可以包含開輪廓或者閉合輪廓;
InitOptions
IntRect
用於接受Clipper庫的GetBounds()函數的結果(包圍盒);
ClipType
總共有四種裁剪運算類型:AND、OR、NOT和XOR
他們的類型主要取決於他們的點集信息和填充規則,規則如下:
AND(Intersection求交):獲取兩者相交的部分;
OR(Union求並):獲取兩者並集部分;
NOT(difference求異):獲取Clip區域以外的區域;
XOR(exclusive求異或):獲取兩個區域互不重復的區域;
所有的多邊形裁剪都是通過一個Clipper對象通過傳遞一個具體的布爾運算類型給它的Execute方法來實現的
考慮到非封閉線段(多線段),裁剪的原則基本上和封閉圖形是相同的。但是,如果一個裁剪案例中出現了以下幾種基本情況,會采取以下裁剪原則:
並集:多線段將會被任何重疊的多邊形裁剪,導致沒有被覆蓋的部分會聯通重疊多邊形一起返回;
交集、非運算和異或運算:多線段只會被Clip的多邊形切割,並且在多線段原多邊形之間不會有任何交點;
PolyType
多邊形的布爾運算類型主要來設置兩組多邊形的屬性,來分別代表被裁剪(subject)和裁剪(clip)的多邊形。無論何時路徑組(Paths)被添加到Clipper對象中,它們都應該被指定類型到底是subject還是clip。
求和運算(UNION)可以在單個路徑組或兩個路徑組當中,但是所有其他的布爾運算需要設定兩個路徑組的類型獲取有意義的結果。
PolyFillType
FILLing(填充)代指存在一個封閉路徑內的區域,Clipper主要支持4種情況:Even-Odd、Non-Zero、Positive以及Negative;
最簡單的填充策略屬於Even-Odd(有時候稱為可選擇性填充Alternate filling)。給定了一組封閉路徑,從該組路徑的外側的一個點出發,通過一根假想的線,當第一個被交到的路徑將會被填充,當下一個路徑相交時,則不被填充。同樣的,每次一個路徑是被環繞的,都會交疊式的被填充;
和Even-Odd填充方式不同,其他填充准則都是基於“邊緣方向”和“環繞次數”。輪廓的方向是由點集的順序決定的,同時輪廓的方向也是用來決定環繞次數的依據。
環繞次數的獲取方法:
1. 從0開始計數;
2. 從整個輪廓組外一點p1,直到需要確認填充與否的區域內一點p2;
3. 當遍歷p1到p2的每一個經過的點時,對於每一個穿越了這條假想線段如果是由右向左穿越,則穿越數+1,如果是從左向右穿越,則-1;
4. 得到最終的穿越數,即環繞次數;
Even-Odd:計數次環繞的部分被填充,偶數次的部分不填充;
Non-Zero:所有的非零環繞次數部分都被填充;
Positive:所有環繞次數大於零的部分被填充;
Negative:所有環繞次數小於零的被填充;
多邊形是由一系列的自交或者不自交的封閉線段構成:一個單獨多邊形區域(單連通域)可以定義為一個無自交的路徑,常常由一個外輪廓和多個內輪廓(可以沒有內輪廓)構成。
目前最廣泛使用的填充原則是:Even-Odd和Non-Zero
有一條定律是輪廓的方向不會影響最終計算出環繞次數奇偶性(這也是為什么在Even-Odd中方向性不需要確定的原因)
Y軸的方向的確會影響多邊形的順逆時針性。但是,改變Y軸的方向只會改變環繞次數的正負號,包括量級和各類填充方法的結果,是不會改變的。
JoinType
當在ClipperOffset對象中通過AddPaths函數添加路徑時,相交類型參數可從jtMiter,jtSquare和jtRound中選一個;
jtMiter:對於斜接式交點來說,有必要設定一個閥值,因為偏置的輪廓在極其窄的相交點處可能會造成“尖角”。為了將這些潛在的尖角包含在內,ClippperOffset對象的MiterLimit屬性用來制定一個偏置點所能容忍的最大值;對於所有給定的邊緣交點處,一旦斜接式交點超過了該閥值,那么方形交角將會被應用;
jtRound:當扁平的路徑始終無法完美的獲取角度信息,他們等價於一系列的圓弧曲線(可以查閱ClipperObject和ArcTolerance屬性)
jtSquare:相當於斜接式當中的delta值始終為1的情況;
EndType
關於EndType(結束類型)總共有五種值:
etClosedPolygon:末點是相交的,並且使用了JoinType來使路徑視作一個多邊形填充;
etClosedLine:末點是相交的,並且使用了JoinType來使路徑視作一條線進行填充;
etOpenSqure:末點使用方形尾和擴展的delta角度;
etOpenRound:末點使用圓形尾和擴展的delta角度;
etOpenButt:末點使用了方向尾,沒有任何擴展;
注意:在etClosedPolygon和etClosedLine類型中,無論首末點是否重合,路徑結束的情況將會相同。(主要針對部分圖形格式,規定首末點相同,這里Clipper統一采用了非首末點相同的情況)
ZFillFunction
類
ClipperBase
ClipperBase是Clipper的一個抽象基類,不應當直接單獨實例話使用一個ClipperBase對象
ClipperBase.AddPath
任何數量的實體路徑和剪切路徑都可以被添加到一個AddPath()方法中去,或者通過AddPaths()來實現添加到一個組中去,或者兩者混用;
實體路徑可以是開線段集或者是閉合線段,但是裁剪線段必須是閉合的;
在閉合的路徑中,*方向問題*應當結合filling rule來進行考慮,並且是通過Clipper庫當中的Execute()方法來傳遞的;
路徑的坐標范圍:類似前中文的IntPoint范圍;
返回值內容:如果輸入的路徑不正確,該函數會返回false;在以下情況路徑會被視為不正確:
該路徑少於兩個點;
有兩個點但是不是一個開放路徑;
點集全部是共線的但是不是一個開放路徑;
ClipperBase.AddPaths
同AddPath
ClipperBase.Clear
Clear()方法去除了Clipper對象中所有存在的被裁剪對象和裁剪對象,使得該Clipper對象可以重用
ClipperBase.GetBounds
該方法會返回Clipper對象中包含的所有多邊形的最小包圍矩形(該矩形長和高和坐標軸平行)。
Clipper
繼承自*ClipperBase*
Clipper類將布爾運算內容(包含交並否異或)進行封裝,稱之為多邊形裁剪;
輸入的多邊形,不管是subject還是clip,都通過AddPath或者AddPaths方法傳遞給Clipper對象,最后使用Execute函數進行裁剪,多個布爾運算可以通過輸入相同的多邊形,然后反復執行Execute函數來實現;
Clipper.Constructor
構建一個Clipper對象,可以使用InitOptions來進行初始化;
Clipper.Execute
一旦裁剪路徑組和被裁剪路徑組已經被設定(通過AddPath后者AddPaths方法),*Execute*方法可以執行布爾運算,具體運算類型由clipType來指定;
最終的solution參數可以是一個路徑組(Path)或者一個多邊形樹(PolyTree)類型。因為路徑結構比多邊形樹結構簡單,所以效率相對較高(10%);但是,PolyTree的信息結構可能提供給了用戶更多的有用信息。首先,PolyTree結構保留了網狀的多邊形父子關系(例如外輪廓包含孔洞,孔洞內包含內輪廓等)。相同的,只有PolyTree類型可以區別開輪廓和閉合輪廓,因為每一個PolyNode結構有IsOpen屬性(Path類型沒有任何成員來告知它是否為開放的還是閉合的),正因為如此,當一個開放輪廓組被傳遞給一個Clipper對象,使用者必須用一個PolyTree類型來接收solution的結果,否則會引起錯誤;
當在使用開放路徑進行裁剪時,Clipper庫提供了兩個有效的函數來快速的分離結果當中的開放路徑部分和閉合輪廓組部分OpenPathsFromPolyTree和ClosedPathsFromPolyTree。同時Clipper也提供了PolyTreeToPaths來快速將PolyTree類型轉換為Paths。
關於solution返回的路徑組,有以下幾點需要注意:
並沒有固定的順序;
不會出現疊加和自交的情況;
內孔的方向永遠和外孔相反;
solution的填充類型可以認為是Even-Odd或者Non-Zero中任意一種,即兼容這兩種填充方式;
被填充圖形填充類型(subjFillTYpe)和裁剪圖形填充類型(clipFillType)決定了多邊形的填充規則和相對規則;
Execute可以執行多次,不需要重置,即可以對一個多邊形進行多次Execute的處理。
Clipper.PreserveCollinear
默認的,當輸入被裁剪和裁剪多邊形的內容中三個或者更多的點是共線的,Clipper對象會在進行布爾運算之前對多余的共線點進行刪除;當設定PreserveColinear屬性防止了這種行為,來允許共線的點的情況出現在結果當中;
Clipper.StrictlySimple
術語:
一個簡單多邊形是指沒有自交現象的多邊形;
一個弱簡單的多邊形是指一個簡單多邊形包含重合點以及重合的線段;
一個強簡單多邊形是指一個簡單多邊形不包含重合點以及重合的線段;
對於點“重合”的定義是指如果多邊形內的兩個以上的點具有相同的坐標(並在序列上來說不是相鄰點,即相鄰點除外)。一條邊接觸到另一條邊的定義是指這條邊的一個端點與接觸到另一條邊(相鄰邊除外)相接觸,或者他們是共線或覆蓋關系(包括相鄰邊)。
由Clipper庫的裁剪操作返回的多邊形都是簡單多邊形,當StrictlySimple屬性被啟用,返回的多邊形將會是是強簡單多邊形,否則會返回弱簡單多邊形。算法因為計算強簡單多邊形太大,導致該選項是默認關閉的。
注意:目前無法保證輸出多邊形是嚴格簡單的,因為“簡單化”工作還在進行當中;
上面兩張圖顯示出兩個弱簡單多邊形被打斷為強簡單多邊形(外圍輪廓的方向是為了避免視覺上點集順序的錯誤)
Clipper.ReverseSolution
當這個屬性被設定為true時,由Execute方法返回的函數將會與其正常方向相反;
ClipperOffset
ClipperOffset.AddPath
向ClipperOffset對象添加一個路徑用來准備偏置;
其中開放式路徑只能被偏移一個正值;
CLipperOffset.AddPaths
同AddPath,添加路徑
ClipperOffset.Constructor
構造函數包含了兩個可選參數,MiterLimit和ArcTolerance,這兩個參數和其同名的屬性的含義是相同的。MiterLimit只是在JoinType是jtMiter的時候才啟用,ArcTolerance只有在JoinType是jtRound或者當EndType是開放式輪廓的時候才有效;
ClipperOffset.Execute
該方法有兩個參數,第一個是接受結果的結構(可以是PolyTree或Paths中的一種),第二個參數是指偏移量的大小--*負值會縮水,正值會膨脹*
該方法可以被調用多次,來實現對相同路徑實現不同程度的偏置
ClipperOffset.Clear
清空所有多邊形對象
ClipperOffset.Arctolerance
ClipperOffset.MiterLimit
該屬性包含了一個比例系數(=最大容忍距離/偏置距離),當大於最大容忍距離時,則會使用1*delta距離來進行;
*默認的MiterLimit值大小是2*,這也是允許的最小MiterLimit大小,如果沒有規定合理的MiterLimit,在部分尖銳的拐角處就會形成長的突起,如下圖:
PolyTree
*繼承自PolyNode*
PolyTree被設計成一個*只讀*數據結構,只能用來接收裁剪或者偏置的結果。一般在結果中經常會選擇是否使用Paths或者PolyTree來獲取最終的結果;PolyTree數據結構優於Paths,能夠正確反應返回類型的*父子關系*,能夠對開放路徑和閉合輪廓進行區別。也正因為PolyTree有着比Paths更復雜的結構,導致算法效率下降大約5%以上,PolyTree目前只能用於尋找父子關系或開放路徑需要考慮的場合;
在Clipper.Execute和ClipperOffset.Execute兩個函數中,可以使用一個空的PolyTree對象來作為接收solution參數接收結果,一旦裁剪工作和偏置工作完成后,函數就會將結果已PolyTree的形式返回出來;
一個PolyTree對象可以包含任何數目的PolyNode子對象,其中每一個PolyNode代表了多邊形的一個輪廓(內輪廓或者外輪廓)。PolyTree本身就是一個特殊的PolyNode對象,它的子對象即代表結果中了最高級別的外輪廓,而它自身的Contour數據始終為空,被包含的最高級別的PolyNode還可能包含它自己的子對象(內孔),甚至自己的內孔還可以包括被嵌套的外輪廓等。但是外輪廓的子對象永遠是孔,孔的子對象永遠是外輪廓。
PolyTree可以包含開放路徑。開放路徑永遠會在最高級別的子輪廓中存儲,提供了兩個函數可以快速的分離提取PolyTree開放路徑和封閉輪廓---OpenPathsFromPolyTree和ClosedPathsFromPolyTree。
PolyTree.GetFirst
該方法返回第一個有效的polygon的PolyNode的指針,否則返回空;
該方法等價於調用Child[0]除非一個PolyTree的對象為空(無任何子對象),這種情況下使用Child[0]會產生越界。
PolyTree.Total
返回該PolyTree所包含的所有PolyNodes的數量,該值不應和ChildCount混淆,后者是返回當前PolyNode所包含的下一層子輪廓數量。
PolyTree.Clear
該方法將刪除PolyTree對象中所有的子對象。
Clear一般不需要顯式調用,每次執行Clipper.Execute方法每次接受一個PolyTree參數,並在生成新的結果之前會自動清楚其中的內容。同樣的,PolyTree的析構函數會自動清理其內部包含的PolyNode。
PolyNode
PolyNodes是被封裝在PolyTree的容器中,同時提供了一個數據結構來代表由Excute()方法返回的多邊形輪廓中的父子關系。
一個PolyNode對象代表一個多邊形;它的“IsHole”屬性表明它是一個“外輪廓”還是一個“內孔”,PolyNodes可能包含任意數量的PolyNode子對象,一般為外輪廓的子對象是內輪廓,而內輪廓的子對象是(嵌套的)外輪廓;
PolyNode.ChildCount
返回一個PolyNode所有子對象數量
PolyNode.Contour
返回該路徑的一系列(List)點集坐標;
PolyNode.GetNext
該方法返回的PolyNode將會是第一個子對象,然后依次是下一個對象,否則是下一個對象;
一個PolyTree可以很方便的被遍歷,通過使用GetFIrst(),然后接下來使用GetNext()知道最終返回一個空指針;
PolyNode.IsHole
當該PolyNode為孔洞時,返回True;
外輪廓的子對象永遠是孔洞,同時孔洞的子對象永遠是(被嵌套)外輪廓;
對於PolyNode來說沒有定義IsHole屬性,但是其最高層的子對象永遠都是外輪廓;
PolyNode.IsOpen
當該輪廓為開放路徑時該屬性為True,只有最高級的PolyNode才可以包含開放輪廓;
PolyNode.Parent
返回父節點;
對於PolyTree對象(同樣是一個PolyNode)來說沒有父對象,將會返回一個空指針;
函數
Area
該函數返回多邊形的面積(默認輸入的多邊形為封閉輪廓)。根據方向的不同,該值大小可能為正或負。如果方向為正,則結果為正;如果方向為負,則結果為負;
CleanPolygon
主要用來移除以下類型的點集:
連接點共線的邊,或者近似共線的邊(那么在指定距離內不會存在共線點);
距離過近的相鄰點(在指定范圍內);
在次相鄰的線段以及無關線段之間距離過小(在指定范圍之內);
次相鄰是指兩個點之間還有一個點(無關點);
函數中的distance默認值為√2,所有輪廓上每個點如果在X或者Y方向上與相鄰點或者次相鄰點之間的距離小於1個單位的點將會被去除。(如果相鄰邊無關點次相鄰也會被移除)
C++限定:該函數被重載,在第一種定義中,in_poly和out_poly可以指同一個路徑,來防止進入到第二種重載就自動清楚Path中的內容(第二種指Paths)
CleanPolygons
和CleanPolgon相同
ClosedPathsFromPolyTree
從PolyTree中提取閉合輪廓部分
OpenPathsFromPolyTree
從PolyTree中提取開放路徑部分
MinkowskiDiff
閔可夫斯基差,用來檢測多邊形碰撞使用
MinkowskiSum
閔可夫斯基和
Orientation
方向只有在封閉的曲線里面是重要的,方向的定義就是在給定的封閉曲線中給出點集是按照順時針還是逆時針進行排列;
方向同樣和軸的方向有關聯:
在Y軸向上顯示的模式中,方向變量為true對應的是逆時針輪廓;
在Y軸向下顯示模式中,方向變量為true對應的是順時針輪廓;
*注意*
自交多邊形有着不確定的方向性,此時函數不會返回一個有意義的值;
大部分2D圖形顯示庫(例如GDI、GDI+)甚至SVG文件格式有其坐標原點(在左上角,Y軸向下)。但是,一部分顯示庫(OpenGL等),可以自定義坐標原點,或者自定義Y方向朝上;
對於Non-Zero填充的多邊形,內環(內孔) 的方向必須與外環相反;
對於由Clipper庫的Execute方法,他們的外孔結果永遠為true,內孔結果永遠為false;(除非ReverseSolution屬性已經被啟用)
PointInPolygon
判斷點是否在多邊形內,如果返回0則是不在,-1則在多邊形邊緣上,1在多邊形內;
PolyTreeToPaths
將PolyTree類型數據轉換為Paths數據
ReversePath
將點集組進行倒序排列
ReversePaths
將點集組進行倒序排列
SimplifyPolygon
將自交部分從自身的部分剔除(通過指定一個FillType來實現一個布爾求和操作)
多個沒有相鄰重復點(或者說沒有"接觸")的多邊形將會被分割為兩個多邊形組;
SimplifyPolygons
同SimplifyPolygon