A C++20 library for polygon boolean operations (union, intersection, XOR, etc.) based on snap rounding and winding number face traversal.
Global snap rounding is a technique to convert arbitrary-precision geometric arrangements into integer-grid representations while preserving topological correctness. Global means it will take every segments into consideration when snaping everyone point. And the snap will not cross any segment, or call it topo inconsistency. The theoretical foundation comes from:
Guibas, L. J., & Marimont, D. H. (1998). Rounding Arrangements Dynamically. International Journal of Computational Geometry & Applications, Vol. 8, No. 2, pp. 157-176.
The key idea: all segment endpoints and intersection points are treated as hot pixels (integer grid points). Every segment is then "snapped" to passed hot pixels.
Unlike the original paper which handles dynamic (online) insertion, this library processes all segments in batch (offline). Instead of a dynamic rounding structure, a spatial index tree (uniform grid now) is used to accelerate segment-intersection and point-on-segment queries. All geometric computations use strict integer arithmetic - no floating point inaccuracy.
Pipeline for snap rounding:
- Find all segment-segment intersection points, snapped to integer as hot pixels
- All endpoint of segment is hot pixels, too.
- sort and unique hot pixels.
- For each original segment, find all hot pixels lying on it
- Split each segment into sub-segments between consecutive hot pixels
- merge sub-segments and build chains.
Definition. For a point P in the plane, its winding number w(P) is defined by using Residue theorem. According to Residue theorem, even point in same face have same winding number. PS: A face is a maximal connected region.
An edge split two face, so we can view an edge as two half edge, Forward half edge and Reverse half edge. Each half edge belong to a face. Normally, people call it half-edge data structure. What's more, we call Forward half edge and Reverse half edge as dual half edge. Then we can define edge power as: winding number of the face that Forward half edge belong to sub winding number of the face that Reverse half edge belong to = power of edge According to the definition, we can simplify merge edge power by add.
Adjacent edges with the same power and no branching (each intermediate vertex has exactly one incoming and one outgoing edge with the same power) are collapsed into a chain. This significantly reduces the graph size for subsequent steps.
Same as edge, each chain is split into two half chains, Forward half chain and Reverse half chain. :
In above definition, we only said: Forward half edge and Reverse half edge. Each half edge belong to a face. However, we don't clearly define which face Forward half edge or Reverse half edge belong to. The definition will following a rule: For a valid multipolygon, even interior point's winding number is 1. Therefore, Forward half edge belong to interior face, and Reverse half edge belonging to outer face.
| Operation | Winding Number Condition |
|---|---|
| OR (union) | w > 0 |
| AND (intersection) | w > 1 |
| XOR | w == 1 |
| A - B | w = w_A + (-1) * w_B > 0 |
At current, we use Boost.Geometry multipolygon set. For boost, outer ring match CW, so every half-edge belongs to the face on its right. In the future, we will support CCW, too. For multipolygon that has CCW outer ring, every half-edge belongs to the face on its left. (we said left and right for half edge that is from down to up.) A face is contained by many half chains. And we can find the next half chains by sorting half chains that has same start point. However, same face have many rings, we can find them by ray cast. Ray cast can also check whether a half chain is belong to the exterior face whose winding number is always zero. By Dual half chain, next half chain, Ray cast pair half chain and Ray cast exterior half chain, we can use DFS to calculte the winding number of every half chain and then filter them.
- Filter half chains: keep those whose
windingsatisfies the operation (e.g.winding > 0for union) - Dual cancellation: if both forward and reverse half chains of a chain survive, kill both and merge faces.
- Rebuild
next_half_chainrespecting surviving half chains and face boundaries - Trace surviving half chains along
next_half_chaininto closed rings. - Assemble rings into polygons. next half chain, Ray cast pair half chain and merged dual half belong to same face, a face is a polygon.
- left most point belong to outer ring, rest = holes
Self-intersecting input polygons are handled naturally: the snap rounding step splits all intersecting edges at their intersection points, creating a proper planar subdivision.
O(n log n) for snap rounding (dominated by spatial index queries), O(n) for face traversal.
- Boost.Geometry: use boost geometry for representation and unit test (WKT I/O)
- clipper2: for testing.
Snap rounding is a global algorithm, we have to query every hot_pixels with segments. What's more, to simplify implementation, we don't remove useless hot_pixels before snap rounding. It will be time consuming. But it's acceptable because we have do query for all segments, too. The runtime is average. Cache friendly algorithm and cluster will corver the time cost. uniform grid only works for bigger data, when data is small, it's slow.
cd best_cliiper && mkdir build && cmake -B build- Guibas, L. J., & Marimont, D. H. (1998). Rounding Arrangements Dynamically. IJCGA, 8(2), 157-176.
- Greene, D. H., & Yao, F. F. (1986). Finite-resolution computational geometry. FOCS 1986.
- Hobby, J. D. (1999). Practical segment intersection with finite precision output. CGTA, 13(4), 199-214.
- support two stage cluster, for stage 1, use bbox. And totally sperate different cluster. For stage 2, use bbox and segs, different cluster has different snap rounding and chains build logic, but share ray casting logic.
- add more practice testing, may get from GIS data set.
- num * dx, num is multi_coord, dx is coord, high overflow risk, need find a better algo.
- use power_2 to handle robust input.
- sizing support
- clipping linestring support (need study to define new power)
- define geometry types and remove boost geometry dependency. New geometry type should depend on chains and support one ring (same face) contains many chains.
- CUDA speed up for both graph and BVH glgo.
- More quickly snap rounding geometry functions.
- Multi thread support for both graph and BVH algo.
Claude code auto generated code, need check and add more practice cases.
Claude code auto generated code, need check and add more.