├── .github └── workflows │ ├── TagBot.yml │ └── CI.yml ├── .gitignore ├── Project.toml ├── test ├── runtests.jl ├── clipper_offset_test.jl └── clipper_test.jl ├── README.md ├── LICENSE.md └── src └── Clipper.jl /.github/workflows/TagBot.yml: -------------------------------------------------------------------------------- 1 | name: TagBot 2 | on: 3 | schedule: 4 | - cron: 0 * * * * 5 | jobs: 6 | TagBot: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: JuliaRegistries/TagBot@v1 10 | with: 11 | token: ${{ secrets.GITHUB_TOKEN }} 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.ipynb_checkpoints 3 | .ropeproject 4 | test/config 5 | *.jl.cov 6 | *.jl.*.cov 7 | *.jl.mem 8 | *.jl.*.mem 9 | *.gcode.new 10 | *.obj 11 | *.dll 12 | *.lib 13 | *.exp 14 | *.o 15 | *.so 16 | deps/usr 17 | deps/deps.jl 18 | deps/build.log 19 | Manifest.toml -------------------------------------------------------------------------------- /Project.toml: -------------------------------------------------------------------------------- 1 | name = "Clipper" 2 | uuid = "c8f6d549-b3ab-5508-a0d1-48fe138e8cc1" 3 | version = "0.6.2" 4 | 5 | [deps] 6 | Clipper_jll = "1721f0f4-5627-55cb-8b31-c466f04189fe" 7 | 8 | [compat] 9 | Clipper_jll = "6.4.2" 10 | julia = "1.5" 11 | 12 | [extras] 13 | Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" 14 | 15 | [targets] 16 | test = ["Test"] 17 | -------------------------------------------------------------------------------- /test/runtests.jl: -------------------------------------------------------------------------------- 1 | using Clipper 2 | using Test 3 | 4 | function test(run::Function, name::AbstractString; verbose=true) 5 | test_name = "Test $(name)" 6 | if verbose 7 | println(stderr, test_name) 8 | end 9 | try 10 | run() 11 | catch e 12 | println("Error running: ", test_name) 13 | rethrow(e) 14 | end 15 | end 16 | 17 | include("clipper_test.jl") 18 | include("clipper_offset_test.jl") 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/JuliaGeometry/Clipper.jl.svg?branch=master)](https://travis-ci.org/JuliaGeometry/Clipper.jl) 2 | 3 | [![Build status](https://ci.appveyor.com/api/projects/status/khjapo84tbrm8lfd?svg=true)](https://ci.appveyor.com/project/JuliaGeometry/clipper-jl) 4 | 5 | # Clipper 6 | 7 | Clipper.jl is a Julia wrapper for [Angus Johnson's Clipper library (ver. 6.4.2).](http://www.angusj.com/delphi/clipper.php). 8 | 9 | > Clipper - an open source freeware library for clipping and offsetting lines and polygons. 10 | 11 | > The Clipper library performs line & polygon clipping - intersection, union, difference & exclusive-or, and line & polygon offsetting. The library is based on Vatti's clipping algorithm. 12 | 13 | ## License 14 | Available under the [Boost Software License - Version 1.0](http://www.boost.org/LICENSE_1_0.txt). 15 | See: [LICENSE.md](./LICENSE.md). 16 | -------------------------------------------------------------------------------- /test/clipper_offset_test.jl: -------------------------------------------------------------------------------- 1 | test("Add path to offset") do 2 | path = Vector{IntPoint}() 3 | 4 | push!(path, IntPoint(0, 0)) 5 | push!(path, IntPoint(0, 1)) 6 | 7 | c = ClipperOffset() 8 | 9 | return add_path!(c, path, JoinTypeRound, EndTypeClosedPolygon) 10 | end 11 | 12 | test("Add paths to offset") do 13 | path1 = Vector{IntPoint}() 14 | 15 | push!(path1, IntPoint(0, 0)) 16 | push!(path1, IntPoint(0, 1)) 17 | 18 | path2 = Vector{IntPoint}() 19 | 20 | push!(path2, IntPoint(5, 0)) 21 | push!(path2, IntPoint(2, 1)) 22 | 23 | paths = Vector{IntPoint}[path1, path2] 24 | 25 | c = ClipperOffset() 26 | 27 | return add_paths!(c, paths, JoinTypeRound, EndTypeClosedPolygon) 28 | end 29 | 30 | test("Clear") do 31 | path = Vector{IntPoint}() 32 | 33 | push!(path, IntPoint(0, 0)) 34 | push!(path, IntPoint(0, 1)) 35 | 36 | c = ClipperOffset() 37 | 38 | add_path!(c, path, JoinTypeRound, EndTypeClosedPolygon) 39 | 40 | return Clipper.clear!(c) 41 | end 42 | 43 | test("Offset") do 44 | path = Vector{IntPoint}() 45 | 46 | push!(path, IntPoint(0, 0)) 47 | push!(path, IntPoint(0, 1)) 48 | 49 | c = ClipperOffset() 50 | 51 | add_path!(c, path, JoinTypeRound, EndTypeOpenRound) 52 | 53 | poly = execute(c, 1.0) 54 | 55 | @test poly == [[IntPoint(1, 2), IntPoint(-1, 2), IntPoint(-1, -1), IntPoint(1, -1)]] 56 | end 57 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The Clipper.jl (including C++ and Julia source code, other accompanying 2 | code, examples and documentation), hereafter called "the Software", has been 3 | released under the following license, terms and conditions: 4 | 5 | Boost Software License - Version 1.0 - August 17th, 2003 6 | http://www.boost.org/LICENSE_1_0.txt 7 | 8 | Permission is hereby granted, free of charge, to any person or organization 9 | obtaining a copy of the Software covered by this license to use, reproduce, 10 | display, distribute, execute, and transmit the Software, and to prepare 11 | derivative works of the Software, and to permit third-parties to whom the 12 | Software is furnished to do so, all subject to the following: 13 | 14 | The copyright notices in the Software and this entire statement, including the 15 | above license grant, this restriction and the following disclaimer, must be 16 | included in all copies of the Software, in whole or in part, and all derivative 17 | works of the Software, unless such copies or derivative works are solely in the 18 | form of machine-executable object code generated by a source language processor. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT SHALL 23 | THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE FOR ANY 24 | DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, ARISING 25 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 26 | THE SOFTWARE. 27 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | - push 4 | - pull_request 5 | jobs: 6 | test: 7 | name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} - ${{ github.event_name }} 8 | runs-on: ${{ matrix.os }} 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | version: 13 | - '1.5' 14 | - '1.6' 15 | #- 'nightly' 16 | os: 17 | - ubuntu-latest 18 | - macOS-latest 19 | arch: 20 | - x64 21 | steps: 22 | - uses: actions/checkout@v2 23 | - uses: julia-actions/setup-julia@v1 24 | with: 25 | version: ${{ matrix.version }} 26 | arch: ${{ matrix.arch }} 27 | - uses: actions/cache@v1 28 | env: 29 | cache-name: cache-artifacts 30 | with: 31 | path: ~/.julia/artifacts 32 | key: ${{ runner.os }}-test-${{ env.cache-name }}-${{ hashFiles('**/Project.toml') }} 33 | restore-keys: | 34 | ${{ runner.os }}-test-${{ env.cache-name }}- 35 | ${{ runner.os }}-test- 36 | ${{ runner.os }}- 37 | - uses: julia-actions/julia-buildpkg@v1 38 | - uses: julia-actions/julia-runtest@v1 39 | - uses: julia-actions/julia-processcoverage@v1 40 | - uses: coverallsapp/github-action@master 41 | with: 42 | path-to-lcov: lcov.info 43 | github-token: ${{ secrets.GITHUB_TOKEN }} 44 | flag-name: run${{ matrix.os }}-${{ matrix.version }}-${{ matrix.arch }} 45 | parallel: true 46 | # docs: 47 | # name: Documentation 48 | # runs-on: ubuntu-latest 49 | # steps: 50 | # - uses: actions/checkout@v2 51 | # - uses: julia-actions/setup-julia@v1 52 | # with: 53 | # version: '1' 54 | # - run: | 55 | # julia --project=docs -e ' 56 | # using Pkg 57 | # Pkg.develop(PackageSpec(path=pwd())) 58 | # Pkg.instantiate()' 59 | # - run: | 60 | # julia --project=docs -e ' 61 | # using Documenter: DocMeta, doctest 62 | # using Clipper 63 | # DocMeta.setdocmeta!(Clipper, :DocTestSetup, :(using Clipper); recursive=true) 64 | # doctest(Clipper)' 65 | # - run: julia --project=docs docs/make.jl 66 | # env: 67 | # JULIA_PKG_SERVER: "" 68 | # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 69 | # DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }} 70 | # finish: 71 | # needs: test 72 | # if: always() 73 | # runs-on: ubuntu-latest 74 | # steps: 75 | # - name: Coveralls Finished 76 | # uses: coverallsapp/github-action@master 77 | # with: 78 | # github-token: ${{ secrets.GITHUB_TOKEN }} 79 | # parallel-finished: true -------------------------------------------------------------------------------- /src/Clipper.jl: -------------------------------------------------------------------------------- 1 | module Clipper 2 | 3 | using Clipper_jll 4 | 5 | export PolyType, PolyTypeSubject, PolyTypeClip, ClipType, ClipTypeIntersection, 6 | ClipTypeUnion, ClipTypeDifference, ClipTypeXor, PolyFillType, PolyFillTypeEvenOdd, 7 | PolyFillTypeNonZero, PolyFillTypePositive, PolyFillTypeNegative, JoinType, 8 | JoinTypeSquare, JoinTypeRound, JoinTypeMiter, EndType, EndTypeClosedPolygon, 9 | EndTypeClosedLine, EndTypeOpenSquare, EndTypeOpenRound, EndTypeOpenButt, Clip, 10 | add_path!, add_paths!, execute, clear!, get_bounds, IntPoint, IntRect, orientation, 11 | area, pointinpolygon, ClipperOffset, PolyNode, execute_pt, contour, ishole, contour, 12 | children, tofloat, minkowski_sum, minkowski_difference 13 | 14 | @enum PolyType PolyTypeSubject = 0 PolyTypeClip = 1 15 | 16 | @enum ClipType ClipTypeIntersection = 0 ClipTypeUnion = 1 ClipTypeDifference = 2 ClipTypeXor = 3 17 | 18 | @enum PolyFillType PolyFillTypeEvenOdd = 0 PolyFillTypeNonZero = 1 PolyFillTypePositive = 2 PolyFillTypeNegative = 3 19 | 20 | @enum JoinType JoinTypeSquare = 0 JoinTypeRound = 1 JoinTypeMiter = 2 21 | 22 | @enum EndType EndTypeClosedPolygon = 0 EndTypeClosedLine = 1 EndTypeOpenSquare = 2 EndTypeOpenRound = 3 EndTypeOpenButt = 4 23 | 24 | struct IntPoint 25 | X::Int64 26 | Y::Int64 27 | end 28 | 29 | """ 30 | IntPoint(x, y) 31 | 32 | Create an IntPoint from integer values. 33 | 34 | IntPoint(x, y, magnitude, precision) 35 | 36 | Create an IntPoint from floating point values with the given number of digits of precision. 37 | magnitude = number of digits above zero (90 => 2, 9 => 1, 0.9 => 0, 0.09 => -1) 38 | sigdigits = number of digits to preserve (94.3856 with 4 => 94.39) 39 | 40 | ```julia 41 | a = IntPoint(5.483, 55.8739, 2, 4) # [548, 5587] 42 | b,c = tofloat(a, 2, 4) # 5.48, 55.87 43 | ``` 44 | """ 45 | function IntPoint(x::Union{Float16,Float32,Float64}, y::Union{Float16,Float32,Float64}, 46 | magnitude::Int64, sigdigits::Int64) 47 | factor = exp10(sigdigits - magnitude) 48 | xInt = Int(round(x * factor)) 49 | yInt = Int(round(y * factor)) 50 | return IntPoint(xInt, yInt) 51 | end 52 | 53 | """ 54 | tofloat(intpoint, magnitude, precision) 55 | 56 | Restore an IntPoint to floating point values using the specified magnitude and precision. 57 | magnitude = number of digits to be above zero (90 => 2, 9 => 1, 0.9 => 0, 0.09 => -1) 58 | sigdigits = number of digits that were preserved (94.3856 with 4 => 94.39) 59 | """ 60 | function tofloat(intpoint::IntPoint, magnitude::Int64, sigdigits::Int64) 61 | factor = exp10(sigdigits - magnitude) 62 | xFloat = intpoint.X / factor 63 | yFloat = intpoint.Y / factor 64 | return xFloat, yFloat 65 | end 66 | 67 | mutable struct PolyNode{T} 68 | contour::Vector{T} 69 | hole::Bool 70 | open::Bool 71 | children::Vector{PolyNode{T}} 72 | parent::PolyNode{T} 73 | PolyNode{T}(a, b, c) where {T} = new{T}(a, b, c) 74 | function PolyNode{T}(a, b, c, d) where {T} 75 | p = new{T}(a, b, c, d) 76 | p.parent = p 77 | return p 78 | end 79 | PolyNode{T}(a, b, c, d, e) where {T} = new{T}(a, b, c, d, e) 80 | end 81 | 82 | Base.convert(::Type{PolyNode{T}}, x::PolyNode{T}) where {T} = x 83 | function Base.convert(::Type{PolyNode{S}}, x::PolyNode{T}) where {S,T} 84 | parent(x) !== x && error("must convert a top-level PolyNode (i.e. a PolyTree).") 85 | 86 | pn = PolyNode{S}(convert(Vector{S}, contour(x)), ishole(x), isopen(x)) 87 | pn.children = [PolyNode(y, pn) for y in children(x)] 88 | return pn.parent = pn 89 | end 90 | function PolyNode(x::PolyNode, parent::PolyNode{S}) where {S} 91 | pn = PolyNode{S}(contour(x), ishole(x), isopen(x)) 92 | pn.children = [PolyNode(y, pn) for y in children(x)] 93 | pn.parent = parent 94 | return pn 95 | end 96 | 97 | @inline ishole(x::PolyNode) = x.hole 98 | @inline Base.isopen(x::PolyNode) = x.open 99 | @inline contour(x::PolyNode) = x.contour 100 | @inline children(x::PolyNode) = x.children 101 | @inline Base.parent(x::PolyNode) = x.parent 102 | 103 | function Base.show(io::IO, node::PolyNode) 104 | if parent(node) === node 105 | print(io, "Top-level PolyNode with $(length(children(node))) immediate children.") 106 | else 107 | if isopen(node) 108 | print(io, "Open ") 109 | else 110 | print(io, "Closed ") 111 | end 112 | print(io, "PolyNode ") 113 | ishole(node) && print(io, "(hole) ") 114 | println(io, "with contour:") 115 | show(io, contour(node)) 116 | println(io, "") 117 | print(io, "...and $(length(children(node))) immediate children.") 118 | end 119 | end 120 | 121 | function Base.show(io::IO, point::IntPoint) 122 | return print(io, "[$(point.X),$(point.Y)]") 123 | end 124 | 125 | function append_poly!(outputArray::Ptr{Cvoid}, polyIndex::Csize_t, point::IntPoint) 126 | ourArray = unsafe_pointer_to_objref(outputArray)::Vector{Vector{IntPoint}} 127 | 128 | while (polyIndex + 1) > length(ourArray) 129 | push!(ourArray, Vector{IntPoint}()) 130 | end 131 | 132 | return push!(ourArray[polyIndex + 1], point) 133 | end 134 | 135 | # private 136 | function appendpn!(jl_node::Ptr{Cvoid}, point::IntPoint) 137 | node = unsafe_pointer_to_objref(jl_node)::PolyNode{IntPoint} 138 | return push!(contour(node), point) 139 | end 140 | 141 | # private 142 | function newnode(outputTree::Ptr{Cvoid}, ishole::Bool, isopen::Bool) 143 | tree = unsafe_pointer_to_objref(outputTree)::PolyNode{IntPoint} 144 | node = PolyNode{IntPoint}(IntPoint[], ishole, isopen, PolyNode{IntPoint}[], tree) 145 | push!(children(tree), node) 146 | return pointer_from_objref(node) 147 | end 148 | 149 | #==============================================================# 150 | # Static functions 151 | #==============================================================# 152 | function orientation(path::Vector{IntPoint}) 153 | return ccall((:orientation, libcclipper), Cuchar, (Ptr{IntPoint}, Csize_t), path, 154 | length(path)) == 1 155 | end 156 | 157 | function area(path::Vector{IntPoint}) 158 | return ccall((:area, libcclipper), Float64, (Ptr{IntPoint}, Csize_t), path, 159 | length(path)) 160 | end 161 | 162 | function pointinpolygon(pt::IntPoint, path::Vector{IntPoint}) 163 | return ccall((:pointinpolygon, libcclipper), Cint, (IntPoint, Ptr{IntPoint}, Csize_t), 164 | pt, path, length(path)) 165 | end 166 | 167 | #==============================================================# 168 | # Clipper object 169 | #==============================================================# 170 | mutable struct Clip 171 | clipper_ptr::Ptr{Cvoid} 172 | 173 | function Clip() 174 | clipper = new(ccall((:get_clipper, libcclipper), Ptr{Cvoid}, ())) 175 | finalizer(c -> ccall((:delete_clipper, libcclipper), Cvoid, (Ptr{Cvoid},), 176 | c.clipper_ptr), clipper) 177 | return clipper 178 | end 179 | end 180 | 181 | function add_path!(c::Clip, path::Vector{IntPoint}, polyType::PolyType, closed::Bool) 182 | return ccall((:add_path, libcclipper), Cuchar, 183 | (Ptr{Cvoid}, Ptr{IntPoint}, Csize_t, Cint, Cuchar), c.clipper_ptr, path, 184 | length(path), Int(polyType), closed) == 1 185 | end 186 | 187 | function add_paths!(c::Clip, paths::Vector{Vector{IntPoint}}, polyType::PolyType, 188 | closed::Bool) 189 | lengths = Vector{UInt64}() 190 | for path in paths 191 | push!(lengths, length(path)) 192 | end 193 | 194 | return ccall((:add_paths, libcclipper), Cuchar, 195 | (Ptr{Cvoid}, Ptr{Ptr{IntPoint}}, Ptr{Csize_t}, Csize_t, Cint, Cuchar), 196 | c.clipper_ptr, paths, lengths, length(paths), Int(polyType), closed) == 1 197 | end 198 | 199 | function execute(c::Clip, clipType::ClipType, subjFillType::PolyFillType, 200 | clipFillType::PolyFillType) 201 | polys = Vector{Vector{IntPoint}}() 202 | 203 | result = ccall((:execute, libcclipper), Cuchar, 204 | (Ptr{Cvoid}, Cint, Cint, Cint, Any, Ptr{Cvoid}), c.clipper_ptr, 205 | Int(clipType), Int(subjFillType), Int(clipFillType), polys, 206 | @cfunction(append_poly!, Any, (Ptr{Cvoid}, Csize_t, IntPoint))) 207 | 208 | return result == 1, polys 209 | end 210 | 211 | function execute_pt(c::Clip, clipType::ClipType, subjFillType::PolyFillType, 212 | clipFillType::PolyFillType) 213 | pt = PolyNode{IntPoint}(IntPoint[], false, false, PolyNode{IntPoint}[]) 214 | 215 | result = ccall((:execute_pt, libcclipper), Cuchar, 216 | (Ptr{Cvoid}, Cint, Cint, Cint, Any, Ptr{Cvoid}, Ptr{Cvoid}), 217 | c.clipper_ptr, Int(clipType), Int(subjFillType), Int(clipFillType), pt, 218 | @cfunction(newnode, Ptr{Cvoid}, (Ptr{Cvoid}, Bool, Bool)), 219 | @cfunction(appendpn!, Any, (Ptr{Cvoid}, IntPoint))) 220 | 221 | return result == 1, pt 222 | end 223 | 224 | function clear!(c::Clip) 225 | return ccall((:clear, libcclipper), Cvoid, (Ptr{Cvoid},), c.clipper_ptr) 226 | end 227 | 228 | mutable struct IntRect 229 | left::Int64 230 | top::Int64 231 | right::Int64 232 | bottom::Int64 233 | end 234 | 235 | function get_bounds(c::Clip) 236 | return ccall((:get_bounds, libcclipper), IntRect, (Ptr{Cvoid},), c.clipper_ptr) 237 | end 238 | 239 | #==============================================================# 240 | # ClipperOffset object 241 | #==============================================================# 242 | mutable struct ClipperOffset 243 | clipper_ptr::Ptr{Cvoid} 244 | 245 | function ClipperOffset(miterLimit::Float64=2.0, roundPrecision::Float64=0.25) 246 | clipper = new(ccall((:get_clipper_offset, libcclipper), Ptr{Cvoid}, 247 | (Cdouble, Cdouble), miterLimit, roundPrecision)) 248 | finalizer(c -> ccall((:delete_clipper_offset, libcclipper), Cvoid, (Ptr{Cvoid},), 249 | c.clipper_ptr), clipper) 250 | 251 | return clipper 252 | end 253 | end 254 | 255 | function add_path!(c::ClipperOffset, path::Vector{IntPoint}, joinType::JoinType, 256 | endType::EndType) 257 | return ccall((:add_offset_path, libcclipper), Cvoid, 258 | (Ptr{Cvoid}, Ptr{IntPoint}, Csize_t, Cint, Cint), c.clipper_ptr, path, 259 | length(path), Int(joinType), Int(endType)) 260 | end 261 | 262 | function add_paths!(c::ClipperOffset, paths::Vector{Vector{IntPoint}}, joinType::JoinType, 263 | endType::EndType) 264 | lengths = Vector{UInt64}() 265 | for path in paths 266 | push!(lengths, length(path)) 267 | end 268 | 269 | return ccall((:add_offset_paths, libcclipper), Cvoid, 270 | (Ptr{Cvoid}, Ptr{Ptr{IntPoint}}, Ptr{Csize_t}, Csize_t, Cint, Cint), 271 | c.clipper_ptr, paths, lengths, length(paths), Int(joinType), Int(endType)) 272 | end 273 | 274 | function clear!(c::ClipperOffset) 275 | return ccall((:clear_offset, libcclipper), Cvoid, (Ptr{Cvoid},), c.clipper_ptr) 276 | end 277 | 278 | function execute(c::ClipperOffset, delta::Float64) 279 | polys = Vector{Vector{IntPoint}}() 280 | result = ccall((:execute_offset, libcclipper), Cvoid, 281 | (Ptr{Cvoid}, Cdouble, Any, Ptr{Cvoid}), c.clipper_ptr, delta, polys, 282 | @cfunction(append_poly!, Any, (Ptr{Cvoid}, Csize_t, IntPoint))) 283 | 284 | return polys 285 | end 286 | 287 | function simplify_polygons(polys::Vector{Vector{IntPoint}}, 288 | filltype::PolyFillType=PolyFillTypeEvenOdd) 289 | simplified = Vector{Vector{IntPoint}}() 290 | counts = Csize_t.(length.(polys)) 291 | count = Csize_t(length(counts)) 292 | result = ccall((:simplify_polygons, libcclipper), Cvoid, 293 | (Ptr{Ptr{IntPoint}}, Ptr{Csize_t}, Csize_t, Cint, Any, Ptr{Cvoid}), 294 | polys, counts, count, filltype, simplified, 295 | @cfunction(append_poly!, Any, (Ptr{Cvoid}, Csize_t, IntPoint))) 296 | return simplified 297 | end 298 | 299 | function minkowski_sum(poly1::Vector{IntPoint}, poly2::Vector{IntPoint}, 300 | is_closed::Bool = true) 301 | polys = Vector{Vector{IntPoint}}() 302 | @ccall libcclipper.minkowski_sum( 303 | poly1::Ptr{IntPoint}, length(poly1)::Csize_t, 304 | poly2::Ptr{IntPoint}, length(poly2)::Csize_t, 305 | polys::Any, 306 | @cfunction(append_poly!, Any, (Ptr{Cvoid}, Csize_t, IntPoint))::Ptr{Cvoid}, 307 | is_closed::Cuchar)::Cvoid 308 | return polys 309 | end 310 | function minkowski_difference(poly1::Vector{IntPoint}, poly2::Vector{IntPoint}) 311 | polys = Vector{Vector{IntPoint}}() 312 | @ccall libcclipper.minkowski_difference( 313 | poly1::Ptr{IntPoint}, length(poly1)::Csize_t, 314 | poly2::Ptr{IntPoint}, length(poly2)::Csize_t, 315 | polys::Any, 316 | @cfunction(append_poly!, Any, (Ptr{Cvoid}, Csize_t, IntPoint))::Ptr{Cvoid})::Cvoid 317 | return polys 318 | end 319 | 320 | 321 | end 322 | -------------------------------------------------------------------------------- /test/clipper_test.jl: -------------------------------------------------------------------------------- 1 | test("Add path to clipper") do 2 | path = Vector{IntPoint}() 3 | 4 | push!(path, IntPoint(0, 0)) 5 | push!(path, IntPoint(0, 1)) 6 | 7 | c = Clip() 8 | 9 | @test add_path!(c, path, PolyTypeSubject, true) == false 10 | 11 | push!(path, IntPoint(1, 1)) 12 | 13 | @test add_path!(c, path, PolyTypeSubject, true) == true 14 | end 15 | 16 | test("Union") do 17 | path1 = Vector{IntPoint}() 18 | push!(path1, IntPoint(0, 0)) 19 | push!(path1, IntPoint(0, 1)) 20 | push!(path1, IntPoint(1, 1)) 21 | push!(path1, IntPoint(1, 0)) 22 | 23 | path2 = Vector{IntPoint}() 24 | push!(path2, IntPoint(1, 0)) 25 | push!(path2, IntPoint(1, 1)) 26 | push!(path2, IntPoint(2, 1)) 27 | push!(path2, IntPoint(2, 0)) 28 | 29 | c = Clip() 30 | add_path!(c, path1, PolyTypeSubject, true) 31 | add_path!(c, path2, PolyTypeSubject, true) 32 | 33 | result, polys = execute(c, ClipTypeUnion, PolyFillTypeEvenOdd, PolyFillTypeEvenOdd) 34 | 35 | @test result == true 36 | @test polys[1][1] == Clipper.IntPoint(0, 0) 37 | @test polys[1][2] == Clipper.IntPoint(2, 0) 38 | @test polys[1][3] == Clipper.IntPoint(2, 1) 39 | @test polys[1][4] == Clipper.IntPoint(0, 1) 40 | 41 | result, pt = execute_pt(c, ClipTypeUnion, PolyFillTypeEvenOdd, PolyFillTypeEvenOdd) 42 | @test result == true 43 | @test string(pt) == "Top-level PolyNode with 1 immediate children." 44 | @test length(children(pt)) === 1 45 | 46 | pn = children(pt)[1] 47 | 48 | @test length(children(pn)) == 0 49 | @test contour(pn)[1] == Clipper.IntPoint(0, 0) 50 | @test contour(pn)[2] == Clipper.IntPoint(2, 0) 51 | @test contour(pn)[3] == Clipper.IntPoint(2, 1) 52 | @test contour(pn)[4] == Clipper.IntPoint(0, 1) 53 | end 54 | 55 | test("Difference") do 56 | path1 = Vector{IntPoint}() 57 | push!(path1, IntPoint(0, 0)) 58 | push!(path1, IntPoint(0, 10)) 59 | push!(path1, IntPoint(10, 10)) 60 | push!(path1, IntPoint(10, 0)) 61 | 62 | path2 = Vector{IntPoint}() 63 | push!(path2, IntPoint(4, 0)) 64 | push!(path2, IntPoint(4, 10)) 65 | push!(path2, IntPoint(6, 10)) 66 | push!(path2, IntPoint(6, 0)) 67 | 68 | c = Clip() 69 | add_path!(c, path1, PolyTypeSubject, true) 70 | add_path!(c, path2, PolyTypeClip, true) 71 | 72 | result, polys = execute(c, ClipTypeDifference, PolyFillTypeEvenOdd, PolyFillTypeEvenOdd) 73 | 74 | @test result == true 75 | @test polys[1][1] == Clipper.IntPoint(10, 10) 76 | @test polys[1][2] == Clipper.IntPoint(6, 10) 77 | @test polys[1][3] == Clipper.IntPoint(6, 0) 78 | @test polys[1][4] == Clipper.IntPoint(10, 0) 79 | 80 | @test polys[2][1] == Clipper.IntPoint(0, 10) 81 | @test polys[2][2] == Clipper.IntPoint(0, 0) 82 | @test polys[2][3] == Clipper.IntPoint(4, 0) 83 | @test polys[2][4] == Clipper.IntPoint(4, 10) 84 | 85 | result, pt = execute_pt(c, ClipTypeDifference, PolyFillTypeEvenOdd, PolyFillTypeEvenOdd) 86 | @test result == true 87 | @test string(pt) == "Top-level PolyNode with 2 immediate children." 88 | @test length(children(pt)) === 2 89 | 90 | pn1, pn2 = (children(pt)...,) 91 | @test length(children(pn1)) == 0 92 | @test length(children(pn2)) == 0 93 | 94 | @test contour(pn1)[1] == Clipper.IntPoint(10, 10) 95 | @test contour(pn1)[2] == Clipper.IntPoint(6, 10) 96 | @test contour(pn1)[3] == Clipper.IntPoint(6, 0) 97 | @test contour(pn1)[4] == Clipper.IntPoint(10, 0) 98 | 99 | @test contour(pn2)[1] == Clipper.IntPoint(0, 10) 100 | @test contour(pn2)[2] == Clipper.IntPoint(0, 0) 101 | @test contour(pn2)[3] == Clipper.IntPoint(4, 0) 102 | @test contour(pn2)[4] == Clipper.IntPoint(4, 10) 103 | end 104 | 105 | test("GetBounds") do 106 | path1 = Vector{IntPoint}() 107 | push!(path1, IntPoint(0, 0)) 108 | push!(path1, IntPoint(0, 1)) 109 | push!(path1, IntPoint(1, 1)) 110 | push!(path1, IntPoint(1, 0)) 111 | 112 | path2 = Vector{IntPoint}() 113 | push!(path2, IntPoint(1, 0)) 114 | push!(path2, IntPoint(1, 1)) 115 | push!(path2, IntPoint(2, 1)) 116 | push!(path2, IntPoint(2, 0)) 117 | 118 | c = Clip() 119 | add_path!(c, path1, PolyTypeSubject, true) 120 | add_path!(c, path2, PolyTypeSubject, true) 121 | 122 | rect = get_bounds(c) 123 | 124 | @test rect.left == 0 125 | @test rect.top == 0 126 | @test rect.right == 2 127 | @test rect.bottom == 1 128 | end 129 | 130 | test("Clear") do 131 | path1 = Vector{IntPoint}() 132 | push!(path1, IntPoint(0, 0)) 133 | push!(path1, IntPoint(0, 10)) 134 | push!(path1, IntPoint(10, 10)) 135 | push!(path1, IntPoint(10, 0)) 136 | 137 | path2 = Vector{IntPoint}() 138 | push!(path2, IntPoint(4, 0)) 139 | push!(path2, IntPoint(4, 10)) 140 | push!(path2, IntPoint(6, 10)) 141 | push!(path2, IntPoint(6, 0)) 142 | 143 | c = Clip() 144 | add_path!(c, path1, PolyTypeSubject, true) 145 | add_path!(c, path2, PolyTypeSubject, true) 146 | 147 | Clipper.clear!(c) 148 | 149 | rect = get_bounds(c) 150 | 151 | @test rect.left == 0 152 | @test rect.top == 0 153 | @test rect.right == 0 154 | @test rect.bottom == 0 155 | end 156 | 157 | test("AddPaths") do 158 | path1 = Vector{IntPoint}() 159 | push!(path1, IntPoint(0, 0)) 160 | push!(path1, IntPoint(0, 1)) 161 | push!(path1, IntPoint(1, 1)) 162 | push!(path1, IntPoint(1, 0)) 163 | 164 | path2 = Vector{IntPoint}() 165 | push!(path2, IntPoint(1, 0)) 166 | push!(path2, IntPoint(1, 1)) 167 | push!(path2, IntPoint(2, 1)) 168 | push!(path2, IntPoint(2, 0)) 169 | 170 | paths = Vector{IntPoint}[path1, path2] 171 | 172 | c = Clip() 173 | add_paths!(c, paths, PolyTypeSubject, true) 174 | 175 | result, polys = execute(c, ClipTypeUnion, PolyFillTypeEvenOdd, PolyFillTypeEvenOdd) 176 | 177 | @test result == true 178 | @test polys[1][1] == Clipper.IntPoint(0, 0) 179 | @test polys[1][2] == Clipper.IntPoint(2, 0) 180 | @test polys[1][3] == Clipper.IntPoint(2, 1) 181 | @test polys[1][4] == Clipper.IntPoint(0, 1) 182 | end 183 | 184 | test("Orientation") do 185 | path1 = Vector{IntPoint}() 186 | push!(path1, IntPoint(0, 0)) 187 | push!(path1, IntPoint(0, 1)) 188 | push!(path1, IntPoint(1, 1)) 189 | push!(path1, IntPoint(1, 0)) 190 | 191 | @test orientation(path1) == false 192 | 193 | path1 = Vector{IntPoint}() 194 | push!(path1, IntPoint(1, 0)) 195 | push!(path1, IntPoint(1, 1)) 196 | push!(path1, IntPoint(0, 1)) 197 | push!(path1, IntPoint(0, 0)) 198 | 199 | @test orientation(path1) == true 200 | end 201 | 202 | test("Area") do 203 | path1 = Vector{IntPoint}() 204 | push!(path1, IntPoint(0, 0)) 205 | push!(path1, IntPoint(0, 1)) 206 | push!(path1, IntPoint(1, 1)) 207 | 208 | @test area(path1) == -0.5 209 | end 210 | 211 | test("Point in polygon") do 212 | path1 = Vector{IntPoint}() 213 | push!(path1, IntPoint(0, 0)) 214 | push!(path1, IntPoint(4, 0)) 215 | push!(path1, IntPoint(0, 4)) 216 | 217 | # Returns -1 if on boundary (check all the boundary points) 218 | @test pointinpolygon(IntPoint(0, 0), path1) == -1 219 | @test pointinpolygon(IntPoint(1, 0), path1) == -1 220 | @test pointinpolygon(IntPoint(2, 0), path1) == -1 221 | @test pointinpolygon(IntPoint(3, 0), path1) == -1 222 | @test pointinpolygon(IntPoint(4, 0), path1) == -1 223 | @test pointinpolygon(IntPoint(3, 1), path1) == -1 224 | @test pointinpolygon(IntPoint(2, 2), path1) == -1 225 | @test pointinpolygon(IntPoint(1, 3), path1) == -1 226 | @test pointinpolygon(IntPoint(0, 4), path1) == -1 227 | @test pointinpolygon(IntPoint(0, 3), path1) == -1 228 | @test pointinpolygon(IntPoint(0, 2), path1) == -1 229 | @test pointinpolygon(IntPoint(0, 1), path1) == -1 230 | 231 | # Returns 1 if inside (check all the interior points) 232 | @test pointinpolygon(IntPoint(1, 1), path1) == 1 233 | @test pointinpolygon(IntPoint(2, 1), path1) == 1 234 | @test pointinpolygon(IntPoint(1, 2), path1) == 1 235 | 236 | # Returns 0 if outside (check a few places outside) 237 | @test pointinpolygon(IntPoint(10, 10), path1) == 0 238 | @test pointinpolygon(IntPoint(-1, -1), path1) == 0 239 | end 240 | 241 | struct IntPoint2 242 | X::Int64 243 | Y::Int64 244 | end 245 | Base.convert(::Type{IntPoint2}, x::IntPoint) = IntPoint2(x.X, x.Y) 246 | Base.convert(::Type{IntPoint}, x::IntPoint2) = IntPoint(x.X, x.Y) 247 | 248 | test("PolyTrees / PolyNodes") do 249 | path1 = Vector{IntPoint}() 250 | push!(path1, IntPoint(8, 8)) 251 | push!(path1, IntPoint(0, 8)) 252 | push!(path1, IntPoint(0, 0)) 253 | push!(path1, IntPoint(8, 0)) 254 | 255 | path2 = Vector{IntPoint}() 256 | push!(path2, IntPoint(1, 1)) 257 | push!(path2, IntPoint(1, 7)) 258 | push!(path2, IntPoint(7, 7)) 259 | push!(path2, IntPoint(7, 1)) 260 | 261 | path3 = Vector{IntPoint}() 262 | push!(path3, IntPoint(6, 6)) 263 | push!(path3, IntPoint(2, 6)) 264 | push!(path3, IntPoint(2, 2)) 265 | push!(path3, IntPoint(6, 2)) 266 | 267 | paths = Vector{IntPoint}[path1, path2, path3] 268 | 269 | c = Clip() 270 | add_paths!(c, paths, PolyTypeSubject, true) 271 | 272 | result, pt = execute_pt(c, ClipTypeUnion, PolyFillTypeEvenOdd, PolyFillTypeEvenOdd) 273 | 274 | # test expected PolyTree structure 275 | @test result == true 276 | @test isa(pt, PolyNode{IntPoint}) 277 | @test parent(pt) === pt # in the wrapper we set the parent of top level to itself 278 | @test length(children(pt)) === 1 279 | @test contour(pt) == IntPoint[] # top level has no contour 280 | @test length(children(pt)) == 1 281 | 282 | pn1 = children(pt)[1] 283 | @test !ishole(pn1) 284 | @test !isopen(pn1) 285 | @test contour(pn1) == path1 286 | @test length(children(pn1)) === 1 287 | @test parent(pn1) === pt 288 | @test length(children(pn1)) == 1 289 | @test contour(pn1)[1] == Clipper.IntPoint(8, 8) 290 | @test contour(pn1)[2] == Clipper.IntPoint(0, 8) 291 | @test contour(pn1)[3] == Clipper.IntPoint(0, 0) 292 | @test contour(pn1)[4] == Clipper.IntPoint(8, 0) 293 | 294 | pn2 = children(pn1)[1] 295 | @test ishole(pn2) 296 | @test !isopen(pn2) 297 | @test contour(pn2) == path2 298 | @test length(children(pn2)) === 1 299 | @test parent(pn2) === pn1 300 | @test length(children(pn2)) == 1 301 | @test contour(pn2)[1] == Clipper.IntPoint(1, 1) 302 | @test contour(pn2)[2] == Clipper.IntPoint(1, 7) 303 | @test contour(pn2)[3] == Clipper.IntPoint(7, 7) 304 | @test contour(pn2)[4] == Clipper.IntPoint(7, 1) 305 | 306 | pn3 = children(pn2)[1] 307 | @test !ishole(pn3) 308 | @test !isopen(pn3) 309 | @test contour(pn3) == path3 310 | @test isempty(children(pn3)) 311 | @test parent(pn3) === pn2 312 | @test length(children(pn3)) == 0 313 | @test contour(pn3)[1] == Clipper.IntPoint(6, 6) 314 | @test contour(pn3)[2] == Clipper.IntPoint(2, 6) 315 | @test contour(pn3)[3] == Clipper.IntPoint(2, 2) 316 | @test contour(pn3)[4] == Clipper.IntPoint(6, 2) 317 | 318 | # Test that we can preserve the tree structure when converting between types. 319 | pt2 = convert(PolyNode{IntPoint2}, pt) 320 | pt3 = convert(PolyNode{IntPoint}, pt2) 321 | 322 | @test result == true 323 | @test isa(pt3, PolyNode{IntPoint}) 324 | @test parent(pt3) === pt3 # in the wrapper we set the parent of top level to itself 325 | @test length(children(pt3)) === 1 326 | @test contour(pt3) == IntPoint[] # top level has no contour 327 | 328 | pn1 = children(pt3)[1] 329 | @test !ishole(pn1) 330 | @test !isopen(pn1) 331 | @test contour(pn1) == path1 332 | @test length(children(pn1)) === 1 333 | @test parent(pn1) === pt3 334 | 335 | pn2 = children(pn1)[1] 336 | @test ishole(pn2) 337 | @test !isopen(pn2) 338 | @test contour(pn2) == path2 339 | @test length(children(pn2)) === 1 340 | @test parent(pn2) === pn1 341 | 342 | pn3 = children(pn2)[1] 343 | @test !ishole(pn3) 344 | @test !isopen(pn3) 345 | @test contour(pn3) == path3 346 | @test isempty(children(pn3)) 347 | @test parent(pn3) === pn2 348 | 349 | # Only convert top-level PolyNodes (i.e. PolyTrees) 350 | @test_throws ErrorException convert(PolyNode{IntPoint2}, pn3) 351 | end 352 | 353 | test("IntPoint from floating point") do 354 | value1 = 58.4309 355 | value2 = 9.39547 356 | value3 = 10.393 357 | value4 = 132.45 358 | value5 = 0.0045 359 | 360 | test1 = IntPoint(value1, value2, 2, 4) 361 | @test IntPoint(5843, 940) == test1 362 | 363 | test2 = IntPoint(value3, value4, 3, 5) 364 | @test IntPoint(1039, 13245) == test2 365 | 366 | test3 = IntPoint(value1, value4, 6, 2) 367 | @test IntPoint(0, 0) == test3 368 | 369 | test4 = IntPoint(value2, value3, 1, 4) 370 | @test IntPoint(9395, 10393) == test4 371 | 372 | test5 = IntPoint(value4, value5, -2, 5) 373 | @test IntPoint(1324500000, 45000) == test5 374 | 375 | test6 = IntPoint(value4, value5, 3, 5) 376 | @test IntPoint(13245, 0) == test6 377 | end 378 | 379 | test("IntPoint to floating point") do 380 | point = IntPoint(13245, 340) 381 | 382 | @test tofloat(point, 3, 5) == (132.45, 3.4) 383 | @test tofloat(point, -4, 5) == (0.000013245, 0.00000034) 384 | @test tofloat(point, 3, 4) == (1324.5, 34) 385 | @test tofloat(point, -4, 4) == (0.00013245, 0.0000034) 386 | end 387 | 388 | test("Minkowski sum") do 389 | tri = [IntPoint(0,0), IntPoint(10,0), IntPoint(0,20)] 390 | @test minkowski_sum(tri,tri) == 391 | [[IntPoint(0,0), IntPoint(20,0), IntPoint(0,40)]] 392 | end 393 | --------------------------------------------------------------------------------