├── .github └── workflows │ └── test.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE.md ├── LICENSE-MIT.md ├── README.md ├── doc-assets ├── difference-and-inflate.png ├── difference.png ├── inflate.png ├── intersect.png ├── simplify.png ├── union.png └── xor.png ├── examples ├── difference-and-inflate.rs ├── difference.rs ├── helpers.rs ├── inflate.rs ├── intersect.rs ├── simplify.rs ├── union.rs └── xor.rs └── src ├── bounds.rs ├── clipper.rs ├── lib.rs ├── operations ├── difference.rs ├── inflate.rs ├── intersect.rs ├── mod.rs ├── pointinpolygon.rs ├── simplify.rs ├── union.rs └── xor.rs ├── options.rs ├── path.rs ├── paths.rs └── point.rs /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: "Test Suite" 2 | on: 3 | push: 4 | pull_request: 5 | 6 | jobs: 7 | # Check formatting with rustfmt 8 | formatting: 9 | name: cargo fmt 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | # Ensure rustfmt is installed and setup problem matcher 14 | - uses: actions-rust-lang/setup-rust-toolchain@v1 15 | with: 16 | components: rustfmt 17 | - name: Rustfmt Check 18 | uses: actions-rust-lang/rustfmt@v1 19 | 20 | test_all_ubuntu: 21 | name: cargo test all features (ubuntu) 22 | needs: formatting 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v4 26 | - uses: actions-rust-lang/setup-rust-toolchain@v1 27 | - run: cargo test --all-features 28 | 29 | test_ubuntu: 30 | name: cargo test (ubuntu) 31 | needs: formatting 32 | runs-on: ubuntu-latest 33 | steps: 34 | - uses: actions/checkout@v4 35 | - uses: actions-rust-lang/setup-rust-toolchain@v1 36 | - run: cargo test 37 | 38 | test_all_macos14_arm64: 39 | name: cargo test all features (macos 14 arm64) 40 | needs: 41 | - test_all_ubuntu 42 | - test_ubuntu 43 | runs-on: macos-latest 44 | steps: 45 | - uses: actions/checkout@v4 46 | - uses: actions-rust-lang/setup-rust-toolchain@v1 47 | - run: cargo test --all-features 48 | 49 | test_all_windows: 50 | name: cargo test all features (windows) 51 | needs: 52 | - test_all_ubuntu 53 | - test_ubuntu 54 | runs-on: windows-latest 55 | steps: 56 | - uses: actions/checkout@v4 57 | - uses: actions-rust-lang/setup-rust-toolchain@v1 58 | - run: cargo test --all-features 59 | 60 | test_macos14_arm64: 61 | name: cargo test (macos 14 arm64) 62 | needs: 63 | - test_all_ubuntu 64 | - test_ubuntu 65 | runs-on: macos-latest 66 | steps: 67 | - uses: actions/checkout@v4 68 | - uses: actions-rust-lang/setup-rust-toolchain@v1 69 | - run: cargo test 70 | 71 | test_windows: 72 | name: cargo test (windows) 73 | needs: 74 | - test_all_ubuntu 75 | - test_ubuntu 76 | runs-on: windows-latest 77 | steps: 78 | - uses: actions/checkout@v4 79 | - uses: actions-rust-lang/setup-rust-toolchain@v1 80 | - run: cargo test 81 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | /target 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ### [0.5.2](https://github.com/tirithen/clipper2/compare/v0.5.1...v0.5.2) (2025-03-19) 6 | 7 | 8 | ### Features 9 | 10 | * **path:** add path.append(other_path) ([d17cf53](https://github.com/tirithen/clipper2/commit/d17cf5392e394cde76d2acd3596070467b430921)) 11 | * **path:** add path.closest_point(point) method ([cc06ddf](https://github.com/tirithen/clipper2/commit/cc06ddf6fc01187fd80808e96e17ccbf6a6f0dfa)) 12 | * **path:** add path.shift_start_to(point) method ([04e29bd](https://github.com/tirithen/clipper2/commit/04e29bd05508f5e40ef529da5d70752a404b9ddb)) 13 | * **path:** add path.surrounds_path(path) method ([736f50c](https://github.com/tirithen/clipper2/commit/736f50ca31ece155ba8018184ddfe8447a6e2462)) 14 | * **paths:** add paths.append(other_paths) ([5b8b842](https://github.com/tirithen/clipper2/commit/5b8b84288d4cfae8958ae7e96934bc898b784d96)) 15 | * **point:** add point.distance_to(other_point) ([0ba5baf](https://github.com/tirithen/clipper2/commit/0ba5bafc2e00cafaff46a71d9872068cf5ccfd09)) 16 | 17 | 18 | ### Bug Fixes 19 | 20 | * **point:** fix distance_to measurement ([0332d01](https://github.com/tirithen/clipper2/commit/0332d0123d2c8d9fe12636d027c0b4ec0a275ada)) 21 | 22 | ### [0.5.1](https://github.com/tirithen/clipper2/compare/v0.5.0...v0.5.1) (2025-01-29) 23 | 24 | 25 | ### Features 26 | 27 | * impl Default for points and paths ([cc77f7b](https://github.com/tirithen/clipper2/commit/cc77f7b4f9560a048cdf27764b079622c130927a)) 28 | 29 | ## [0.5.0](https://github.com/tirithen/clipper2/compare/v0.4.1...v0.5.0) (2025-01-21) 30 | 31 | 32 | ### ⚠ BREAKING CHANGES 33 | 34 | * **Path:** Path

::inflate now returns Paths

instead of 35 | Path

. The method will no longer panic when 0 points remain after 36 | inflating with a negative number, instead a Paths

struct with length 37 | 0 will be returned in those cases. 38 | 39 | ### Features 40 | 41 | * **Path:** Path

::inflate return Paths

([cbb999b](https://github.com/tirithen/clipper2/commit/cbb999bcb964afed4d36f455711def0fe3346f55)) 42 | 43 | ### [0.4.1](https://github.com/tirithen/clipper2/compare/v0.4.0...v0.4.1) (2024-07-30) 44 | 45 | 46 | ### Features 47 | 48 | * add .push for Path and Paths ([3764676](https://github.com/tirithen/clipper2/commit/37646761f20c803e667f98f8ede7a68f49af6df3)) 49 | 50 | 51 | ### Bug Fixes 52 | 53 | * add bug fixes from Clipper2 C++ library ([58d2653](https://github.com/tirithen/clipper2/commit/58d2653ab6eee4a6841ce544b17cf2a73f1bad11)) 54 | 55 | ## [0.4.0](https://github.com/tirithen/clipper2/compare/v0.3.0...v0.4.0) (2024-06-04) 56 | 57 | 58 | ### ⚠ BREAKING CHANGES 59 | 60 | * Use .translate instead of .offset for Paths and Path 61 | structs. 62 | * Removes the custom iterators PathIterator and 63 | PathsIterator, instead rely on the standard iterator types from vec and 64 | slice. 65 | 66 | ### Features 67 | 68 | * improve the iterator impls for Paths and Path ([0c93f5d](https://github.com/tirithen/clipper2/commit/0c93f5da4c7c5c2a19acb5b0de2fa11217727c82)) 69 | * remove depr. .offset method from Paths/Path ([4ee9fd4](https://github.com/tirithen/clipper2/commit/4ee9fd4d8196c2bdf841353b5675719bcf58a9d6)) 70 | 71 | ## [0.3.0](https://github.com/tirithen/clipper2/compare/v0.2.3...v0.3.0) (2024-06-01) 72 | 73 | 74 | ### ⚠ BREAKING CHANGES 75 | 76 | * scale now takes two arguments, allowing separate x and 77 | y scaling factors 78 | 79 | ### Features 80 | 81 | * implement IntoIterator for Path/Paths ([846602c](https://github.com/tirithen/clipper2/commit/846602c96a8a103d7097fc97d445f2e9f01c3c85)) 82 | * **Path:** add .rectangle method ([a1dbb5c](https://github.com/tirithen/clipper2/commit/a1dbb5cbaf16e5415585cba11f60bb4efe28b144)) 83 | * **serde:** ser./deser. Path and Paths ([b9800b7](https://github.com/tirithen/clipper2/commit/b9800b71deb805c2142153ea96b238f2f0a75c7c)) 84 | * support calculating signed path areas ([b1c6386](https://github.com/tirithen/clipper2/commit/b1c63862ef4effcbbec87861af8564f7b1d8bad1)) 85 | * support scaling around a point ([ba6dec3](https://github.com/tirithen/clipper2/commit/ba6dec3d14c92754c7294e9f0a4d9cab6d261925)) 86 | 87 | ### [0.2.3](https://github.com/tirithen/clipper2/compare/v0.2.2...v0.2.3) (2024-05-13) 88 | 89 | 90 | ### Features 91 | 92 | * expose clipper builder, add path methods ([0702f67](https://github.com/tirithen/clipper2/commit/0702f679425e20c8f833cb5ac52e9210432aebb5)) 93 | 94 | ### [0.2.2](https://github.com/tirithen/clipper2/compare/v0.2.1...v0.2.2) (2024-05-07) 95 | 96 | 97 | ### Features 98 | 99 | * **path:** add .flip_x and .flip_y to path structs ([6323292](https://github.com/tirithen/clipper2/commit/6323292bd0514cb1eeb544c799fb472cf9b2cf90)) 100 | * **path:** add .rotate(rad) method to Path/Paths ([150715a](https://github.com/tirithen/clipper2/commit/150715aeea21b2246efbbb99bff4b2f808fb120f)) 101 | * **path:** add .scale(scale) method to Path/Paths ([447ed8d](https://github.com/tirithen/clipper2/commit/447ed8dbfd6e5da23e1789c9a16c7522d6a8ba83)) 102 | * **path:** rename .offset(x,y) to .translate(x,y) ([06bcfb3](https://github.com/tirithen/clipper2/commit/06bcfb3d769e25e807d268e64110d09538c4662a)) 103 | 104 | 105 | ### Bug Fixes 106 | 107 | * **path:** keep path bounds centered during flip ([d87993e](https://github.com/tirithen/clipper2/commit/d87993e19d578872bc3f6df520f90fcaa736a47f)) 108 | 109 | ### [0.2.1](https://github.com/tirithen/clipper2/compare/v0.2.0...v0.2.1) (2024-05-03) 110 | 111 | 112 | ### Features 113 | 114 | * add .offset(x, y) method to Path and Paths ([be676f5](https://github.com/tirithen/clipper2/commit/be676f5beebbe0b18e1422a3852bea30a856eb96)) 115 | * add bounds struct to path/paths ([3d541f8](https://github.com/tirithen/clipper2/commit/3d541f8219d474d800e2578fde2675a950fcfdf9)) 116 | * add bounds struct to path/paths ([b17ccfd](https://github.com/tirithen/clipper2/commit/b17ccfd524c1bd5f16ae3d911cd1c71c04ce2802)) 117 | * **Paths:** add from Vec> for Paths ([39ea7a1](https://github.com/tirithen/clipper2/commit/39ea7a1658ac0982f7043da2d428b12ac16e6333)) 118 | * **simplify:** add simplify function ([418b98f](https://github.com/tirithen/clipper2/commit/418b98f54333db977460a2c931486f08f554fea2)) 119 | 120 | ## [0.2.0](https://github.com/tirithen/clipper2/compare/v0.1.2...v0.2.0) (2024-04-29) 121 | 122 | 123 | ### ⚠ BREAKING CHANGES 124 | 125 | * The API has been replaced, see 126 | https://docs.rs/clipper2/latest/clipper2/ or `examples/` directory for 127 | details. 128 | 129 | ### Features 130 | 131 | * swap out ffi mappings, custom scaling, ref. ([5d1e7d2](https://github.com/tirithen/clipper2/commit/5d1e7d2189d236ecaf8f01d3fd3a815589f293fd)) 132 | * windows support and example ([468e9aa](https://github.com/tirithen/clipper2/commit/468e9aaae6e3aedcaa3d5a1d582c4a2be1062af7)) 133 | 134 | ### [0.1.2](https://github.com/tirithen/clipper2/compare/v0.1.1...v0.1.2) (2024-03-17) 135 | 136 | 137 | ### Features 138 | 139 | * add intersect, union, difference, and xor ops ([83e6408](https://github.com/tirithen/clipper2/commit/83e64084b069b452fe753f4262ce48677b121754)) 140 | 141 | ### [0.1.1](https://github.com/tirithen/clipper2/compare/v0.1.0...v0.1.1) (2024-03-03) 142 | 143 | ## 0.1.0 (2024-03-03) 144 | 145 | 146 | ### Features 147 | 148 | * **inflate:** expose inflate offsetting function ([1e842e2](https://github.com/tirithen/clipper2/commit/1e842e2756634752fdfcc38500509a901e01fd99)) 149 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Code contributions that opens up more of the Clipper2 API for safe Rust 4 | is more than welcome. 5 | 6 | ## Feature requests 7 | 8 | If you find a feature missing, please open a github issue at 9 | [](https://github.com/tirithen/clipper2/issues). 10 | 11 | ## Code contributions 12 | 13 | The commit messages merged into this project should follow the 14 | Conventional Commits specification 15 | (for details see [](https://www.conventionalcommits.org/en/v1.0.0/)). 16 | 17 | Each merge request/branch should contain one feature or fix and be squashed 18 | into one single commit following the conventional commits annotation. The 19 | reason behind this is that this enables automatic changelog generation that 20 | makes it clear which features, bug fixes and breaking changes each release 21 | provides. 22 | 23 | ## Bindings 24 | 25 | If you update clipper or bindgen, run `cargo build --features update-bindings`. This requires 26 | libclang. 27 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "adler2" 7 | version = "2.0.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 10 | 11 | [[package]] 12 | name = "ahash" 13 | version = "0.8.11" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" 16 | dependencies = [ 17 | "cfg-if", 18 | "once_cell", 19 | "version_check", 20 | "zerocopy", 21 | ] 22 | 23 | [[package]] 24 | name = "autocfg" 25 | version = "1.4.0" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 28 | 29 | [[package]] 30 | name = "base64" 31 | version = "0.13.1" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" 34 | 35 | [[package]] 36 | name = "bitflags" 37 | version = "1.3.2" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 40 | 41 | [[package]] 42 | name = "bytemuck" 43 | version = "1.21.0" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" 46 | 47 | [[package]] 48 | name = "byteorder" 49 | version = "1.5.0" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 52 | 53 | [[package]] 54 | name = "cc" 55 | version = "1.2.10" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "13208fcbb66eaeffe09b99fffbe1af420f00a7b35aa99ad683dfc1aa76145229" 58 | dependencies = [ 59 | "shlex", 60 | ] 61 | 62 | [[package]] 63 | name = "cfg-if" 64 | version = "1.0.0" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 67 | 68 | [[package]] 69 | name = "clipper2" 70 | version = "0.5.2" 71 | dependencies = [ 72 | "clipper2c-sys", 73 | "embed-doc-image", 74 | "libc", 75 | "macroquad", 76 | "serde", 77 | "serde_json", 78 | "thiserror", 79 | ] 80 | 81 | [[package]] 82 | name = "clipper2c-sys" 83 | version = "0.1.4" 84 | source = "registry+https://github.com/rust-lang/crates.io-index" 85 | checksum = "e2995248c63ec82bf22c921cfd26fe407a4402b7039e045d286f7470cd83fe9e" 86 | dependencies = [ 87 | "cc", 88 | "libc", 89 | "serde", 90 | ] 91 | 92 | [[package]] 93 | name = "color_quant" 94 | version = "1.1.0" 95 | source = "registry+https://github.com/rust-lang/crates.io-index" 96 | checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" 97 | 98 | [[package]] 99 | name = "crc32fast" 100 | version = "1.4.2" 101 | source = "registry+https://github.com/rust-lang/crates.io-index" 102 | checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" 103 | dependencies = [ 104 | "cfg-if", 105 | ] 106 | 107 | [[package]] 108 | name = "embed-doc-image" 109 | version = "0.1.4" 110 | source = "registry+https://github.com/rust-lang/crates.io-index" 111 | checksum = "af36f591236d9d822425cb6896595658fa558fcebf5ee8accac1d4b92c47166e" 112 | dependencies = [ 113 | "base64", 114 | "proc-macro2", 115 | "quote", 116 | "syn 1.0.109", 117 | ] 118 | 119 | [[package]] 120 | name = "fdeflate" 121 | version = "0.3.7" 122 | source = "registry+https://github.com/rust-lang/crates.io-index" 123 | checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" 124 | dependencies = [ 125 | "simd-adler32", 126 | ] 127 | 128 | [[package]] 129 | name = "flate2" 130 | version = "1.0.35" 131 | source = "registry+https://github.com/rust-lang/crates.io-index" 132 | checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" 133 | dependencies = [ 134 | "crc32fast", 135 | "miniz_oxide", 136 | ] 137 | 138 | [[package]] 139 | name = "fontdue" 140 | version = "0.7.3" 141 | source = "registry+https://github.com/rust-lang/crates.io-index" 142 | checksum = "0793f5137567643cf65ea42043a538804ff0fbf288649e2141442b602d81f9bc" 143 | dependencies = [ 144 | "hashbrown", 145 | "ttf-parser", 146 | ] 147 | 148 | [[package]] 149 | name = "glam" 150 | version = "0.27.0" 151 | source = "registry+https://github.com/rust-lang/crates.io-index" 152 | checksum = "9e05e7e6723e3455f4818c7b26e855439f7546cf617ef669d1adedb8669e5cb9" 153 | 154 | [[package]] 155 | name = "hashbrown" 156 | version = "0.13.2" 157 | source = "registry+https://github.com/rust-lang/crates.io-index" 158 | checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" 159 | dependencies = [ 160 | "ahash", 161 | ] 162 | 163 | [[package]] 164 | name = "image" 165 | version = "0.24.9" 166 | source = "registry+https://github.com/rust-lang/crates.io-index" 167 | checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d" 168 | dependencies = [ 169 | "bytemuck", 170 | "byteorder", 171 | "color_quant", 172 | "num-traits", 173 | "png", 174 | ] 175 | 176 | [[package]] 177 | name = "itoa" 178 | version = "1.0.14" 179 | source = "registry+https://github.com/rust-lang/crates.io-index" 180 | checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" 181 | 182 | [[package]] 183 | name = "libc" 184 | version = "0.2.169" 185 | source = "registry+https://github.com/rust-lang/crates.io-index" 186 | checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" 187 | 188 | [[package]] 189 | name = "macroquad" 190 | version = "0.4.13" 191 | source = "registry+https://github.com/rust-lang/crates.io-index" 192 | checksum = "81fef16b2d4de22ac372b5d7d76273c0ead0a31f9de9bc649af8f816816db8a8" 193 | dependencies = [ 194 | "fontdue", 195 | "glam", 196 | "image", 197 | "macroquad_macro", 198 | "miniquad", 199 | "quad-rand", 200 | "slotmap", 201 | ] 202 | 203 | [[package]] 204 | name = "macroquad_macro" 205 | version = "0.1.8" 206 | source = "registry+https://github.com/rust-lang/crates.io-index" 207 | checksum = "64b1d96218903768c1ce078b657c0d5965465c95a60d2682fd97443c9d2483dd" 208 | 209 | [[package]] 210 | name = "malloc_buf" 211 | version = "0.0.6" 212 | source = "registry+https://github.com/rust-lang/crates.io-index" 213 | checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" 214 | dependencies = [ 215 | "libc", 216 | ] 217 | 218 | [[package]] 219 | name = "memchr" 220 | version = "2.7.4" 221 | source = "registry+https://github.com/rust-lang/crates.io-index" 222 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 223 | 224 | [[package]] 225 | name = "miniquad" 226 | version = "0.4.6" 227 | source = "registry+https://github.com/rust-lang/crates.io-index" 228 | checksum = "2124e5e6bab86d96f263d78dc763c29e0da4c4f7ff0e1bb33f1d3905d1121262" 229 | dependencies = [ 230 | "libc", 231 | "ndk-sys", 232 | "objc", 233 | "winapi", 234 | ] 235 | 236 | [[package]] 237 | name = "miniz_oxide" 238 | version = "0.8.3" 239 | source = "registry+https://github.com/rust-lang/crates.io-index" 240 | checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924" 241 | dependencies = [ 242 | "adler2", 243 | "simd-adler32", 244 | ] 245 | 246 | [[package]] 247 | name = "ndk-sys" 248 | version = "0.2.2" 249 | source = "registry+https://github.com/rust-lang/crates.io-index" 250 | checksum = "e1bcdd74c20ad5d95aacd60ef9ba40fdf77f767051040541df557b7a9b2a2121" 251 | 252 | [[package]] 253 | name = "num-traits" 254 | version = "0.2.19" 255 | source = "registry+https://github.com/rust-lang/crates.io-index" 256 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 257 | dependencies = [ 258 | "autocfg", 259 | ] 260 | 261 | [[package]] 262 | name = "objc" 263 | version = "0.2.7" 264 | source = "registry+https://github.com/rust-lang/crates.io-index" 265 | checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" 266 | dependencies = [ 267 | "malloc_buf", 268 | ] 269 | 270 | [[package]] 271 | name = "once_cell" 272 | version = "1.20.2" 273 | source = "registry+https://github.com/rust-lang/crates.io-index" 274 | checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" 275 | 276 | [[package]] 277 | name = "png" 278 | version = "0.17.16" 279 | source = "registry+https://github.com/rust-lang/crates.io-index" 280 | checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" 281 | dependencies = [ 282 | "bitflags", 283 | "crc32fast", 284 | "fdeflate", 285 | "flate2", 286 | "miniz_oxide", 287 | ] 288 | 289 | [[package]] 290 | name = "proc-macro2" 291 | version = "1.0.93" 292 | source = "registry+https://github.com/rust-lang/crates.io-index" 293 | checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" 294 | dependencies = [ 295 | "unicode-ident", 296 | ] 297 | 298 | [[package]] 299 | name = "quad-rand" 300 | version = "0.2.3" 301 | source = "registry+https://github.com/rust-lang/crates.io-index" 302 | checksum = "5a651516ddc9168ebd67b24afd085a718be02f8858fe406591b013d101ce2f40" 303 | 304 | [[package]] 305 | name = "quote" 306 | version = "1.0.38" 307 | source = "registry+https://github.com/rust-lang/crates.io-index" 308 | checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" 309 | dependencies = [ 310 | "proc-macro2", 311 | ] 312 | 313 | [[package]] 314 | name = "ryu" 315 | version = "1.0.18" 316 | source = "registry+https://github.com/rust-lang/crates.io-index" 317 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 318 | 319 | [[package]] 320 | name = "serde" 321 | version = "1.0.217" 322 | source = "registry+https://github.com/rust-lang/crates.io-index" 323 | checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" 324 | dependencies = [ 325 | "serde_derive", 326 | ] 327 | 328 | [[package]] 329 | name = "serde_derive" 330 | version = "1.0.217" 331 | source = "registry+https://github.com/rust-lang/crates.io-index" 332 | checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" 333 | dependencies = [ 334 | "proc-macro2", 335 | "quote", 336 | "syn 2.0.96", 337 | ] 338 | 339 | [[package]] 340 | name = "serde_json" 341 | version = "1.0.137" 342 | source = "registry+https://github.com/rust-lang/crates.io-index" 343 | checksum = "930cfb6e6abf99298aaad7d29abbef7a9999a9a8806a40088f55f0dcec03146b" 344 | dependencies = [ 345 | "itoa", 346 | "memchr", 347 | "ryu", 348 | "serde", 349 | ] 350 | 351 | [[package]] 352 | name = "shlex" 353 | version = "1.3.0" 354 | source = "registry+https://github.com/rust-lang/crates.io-index" 355 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 356 | 357 | [[package]] 358 | name = "simd-adler32" 359 | version = "0.3.7" 360 | source = "registry+https://github.com/rust-lang/crates.io-index" 361 | checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" 362 | 363 | [[package]] 364 | name = "slotmap" 365 | version = "1.0.7" 366 | source = "registry+https://github.com/rust-lang/crates.io-index" 367 | checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" 368 | dependencies = [ 369 | "version_check", 370 | ] 371 | 372 | [[package]] 373 | name = "syn" 374 | version = "1.0.109" 375 | source = "registry+https://github.com/rust-lang/crates.io-index" 376 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 377 | dependencies = [ 378 | "proc-macro2", 379 | "quote", 380 | "unicode-ident", 381 | ] 382 | 383 | [[package]] 384 | name = "syn" 385 | version = "2.0.96" 386 | source = "registry+https://github.com/rust-lang/crates.io-index" 387 | checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" 388 | dependencies = [ 389 | "proc-macro2", 390 | "quote", 391 | "unicode-ident", 392 | ] 393 | 394 | [[package]] 395 | name = "thiserror" 396 | version = "2.0.11" 397 | source = "registry+https://github.com/rust-lang/crates.io-index" 398 | checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" 399 | dependencies = [ 400 | "thiserror-impl", 401 | ] 402 | 403 | [[package]] 404 | name = "thiserror-impl" 405 | version = "2.0.11" 406 | source = "registry+https://github.com/rust-lang/crates.io-index" 407 | checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" 408 | dependencies = [ 409 | "proc-macro2", 410 | "quote", 411 | "syn 2.0.96", 412 | ] 413 | 414 | [[package]] 415 | name = "ttf-parser" 416 | version = "0.15.2" 417 | source = "registry+https://github.com/rust-lang/crates.io-index" 418 | checksum = "7b3e06c9b9d80ed6b745c7159c40b311ad2916abb34a49e9be2653b90db0d8dd" 419 | 420 | [[package]] 421 | name = "unicode-ident" 422 | version = "1.0.14" 423 | source = "registry+https://github.com/rust-lang/crates.io-index" 424 | checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" 425 | 426 | [[package]] 427 | name = "version_check" 428 | version = "0.9.5" 429 | source = "registry+https://github.com/rust-lang/crates.io-index" 430 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 431 | 432 | [[package]] 433 | name = "winapi" 434 | version = "0.3.9" 435 | source = "registry+https://github.com/rust-lang/crates.io-index" 436 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 437 | dependencies = [ 438 | "winapi-i686-pc-windows-gnu", 439 | "winapi-x86_64-pc-windows-gnu", 440 | ] 441 | 442 | [[package]] 443 | name = "winapi-i686-pc-windows-gnu" 444 | version = "0.4.0" 445 | source = "registry+https://github.com/rust-lang/crates.io-index" 446 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 447 | 448 | [[package]] 449 | name = "winapi-x86_64-pc-windows-gnu" 450 | version = "0.4.0" 451 | source = "registry+https://github.com/rust-lang/crates.io-index" 452 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 453 | 454 | [[package]] 455 | name = "zerocopy" 456 | version = "0.7.35" 457 | source = "registry+https://github.com/rust-lang/crates.io-index" 458 | checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" 459 | dependencies = [ 460 | "zerocopy-derive", 461 | ] 462 | 463 | [[package]] 464 | name = "zerocopy-derive" 465 | version = "0.7.35" 466 | source = "registry+https://github.com/rust-lang/crates.io-index" 467 | checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" 468 | dependencies = [ 469 | "proc-macro2", 470 | "quote", 471 | "syn 2.0.96", 472 | ] 473 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "clipper2" 3 | version = "0.5.2" 4 | authors = ["Fredrik Söderström "] 5 | edition = "2021" 6 | license = "MIT OR Apache-2.0" 7 | readme = "README.md" 8 | repository = "https://github.com/tirithen/clipper2" 9 | documentation = "https://docs.rs/clipper2/" 10 | description = "A polygon Clipping and Offsetting library for Rust." 11 | keywords = ["polygon", "boolean", "clip", "clipper", "clipper2"] 12 | categories = ["algorithms"] 13 | 14 | [features] 15 | default = ["doc-images"] 16 | doc-images = [] 17 | serde = ["dep:serde", "clipper2c-sys/serde"] 18 | 19 | [dependencies] 20 | libc = "0.2" 21 | clipper2c-sys = "0.1.4" 22 | thiserror = "2" 23 | serde = { version = "1", features = ["derive"], optional = true } 24 | 25 | [dev-dependencies] 26 | macroquad = "0.4.13" 27 | embed-doc-image = "0.1" 28 | serde_json = "1" 29 | 30 | [package.metadata.docs.rs] 31 | # docs.rs uses a nightly compiler, so by instructing it to use our `doc-images` feature we 32 | # ensure that it will render any images that we may have in inner attribute documentation. 33 | features = ["doc-images"] 34 | -------------------------------------------------------------------------------- /LICENSE-APACHE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2024 Fredrik Söderström 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /LICENSE-MIT.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2022 Fredrik Söderström 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # clipper2 2 | 3 | [![crate.io](https://img.shields.io/crates/v/clipper2.svg)](https://crates.io/crates/clipper2) 4 | [![docs.rs](https://docs.rs/clipper2/badge.svg)](https://docs.rs/clipper2) 5 | 6 | A path/polygon clipping and offsetting library for Rust. 7 | 8 | The focus of this crate is to provide an easy to use API, staying as close to 9 | the core `Vec` and `fn` types as possible. 10 | 11 | The create uses the [clipper2c-sys](https://crates.io/crates/clipper2c-sys) 12 | crate that in turn is a Rust wrapper around the C++ version of 13 | [Clipper2](https://github.com/AngusJohnson/Clipper2). 14 | 15 | ## Example 16 | 17 | The below example uses macroquad to visualize the result of the operations and 18 | some helpers from the `examples/` directory. See the examples directory for more 19 | examples. 20 | 21 | ```rust 22 | use clipper2::*; 23 | use helpers::{circle_path, draw_paths}; 24 | use macroquad::prelude::*; 25 | 26 | mod helpers; 27 | 28 | #[macroquad::main("Difference and inflate")] 29 | async fn main() -> Result<(), ClipperError> { 30 | let circle = circle_path((5.0, 5.0), 3.0, 32); 31 | let circle2 = circle_path((7.0, 7.0), 1.0, 32); 32 | let rectangle = vec![(0.0, 0.0), (5.0, 0.0), (5.0, 6.0), (0.0, 6.0)]; 33 | 34 | let result = circle 35 | .to_clipper_subject() 36 | .add_clip(circle2) 37 | .add_clip(rectangle) 38 | .difference(FillRule::default())?; 39 | 40 | let result2 = result 41 | .inflate(1.0, JoinType::Round, EndType::Polygon, 0.0) 42 | .simplify(0.01, false); 43 | 44 | loop { 45 | clear_background(BLACK); 46 | draw_paths(&result, SKYBLUE); 47 | draw_paths(&result2, GREEN); 48 | next_frame().await 49 | } 50 | } 51 | ``` 52 | 53 | This is how the resulting shapes looks: 54 | 55 | ![Image displaying the result of the difference and inflate example](https://raw.githubusercontent.com/tirithen/clipper2/main/doc-assets/difference-and-inflate.png) 56 | 57 | ## API uses f64, but i64 under the hood 58 | 59 | The Clipper2 library is [using `i64` values](https://www.angusj.com/clipper2/Docs/Robustness.htm) 60 | to guarantee the robustness of all calculations. The C++ library exposes both 61 | `int64_t`/`i64` and `double`/`f64` versions of several types. This crate 62 | therefore internally uses the `int64_t`/`i64` types only, but for now only 63 | exposes an `f64` API. 64 | 65 | The types `Point`, `Path`, and `Paths` therefore offers a `PointScaler` trait 66 | and generic parameter that allows the user to choose the scaling is used when 67 | it interally converts from `f64` to `i64`. By default it uses the `Centi` struct 68 | that will scale the values by `100`. 69 | 70 | ## Early days 71 | 72 | This project is in a super early stage and has for now only opened up a small 73 | part of what the C++ Clipper2 library has to offer. Expect breaking changes now 74 | and then for some more time to come as we find and explore more eregonomic ways 75 | of exposing the API in a Rust ideomatic way. 76 | 77 | Please also feel free to come with suggestions on how the API can be simplified 78 | or send code contributions directly. See 79 | [CONTRIBUTING.md](https://github.com/tirithen/clipper2/blob/main/CONTRIBUTING.md) 80 | for more details. 81 | 82 | ## License 83 | 84 | Licensed under either of [Apache License, Version 2.0](https://github.com/tirithen/clipper2/blob/main/LICENSE-APACHE.md) 85 | or [MIT license](https://github.com/tirithen/clipper2/blob/main/LICENSE-MIT.md) 86 | at your option. 87 | 88 | Unless you explicitly state otherwise, any contribution intentionally submitted 89 | for inclusion in Serde by you, as defined in the Apache-2.0 license, shall be 90 | dual licensed as above, without any additional terms or conditions. 91 | -------------------------------------------------------------------------------- /doc-assets/difference-and-inflate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tirithen/clipper2/b046cadef664345cf02a287551a42930e5e5f252/doc-assets/difference-and-inflate.png -------------------------------------------------------------------------------- /doc-assets/difference.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tirithen/clipper2/b046cadef664345cf02a287551a42930e5e5f252/doc-assets/difference.png -------------------------------------------------------------------------------- /doc-assets/inflate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tirithen/clipper2/b046cadef664345cf02a287551a42930e5e5f252/doc-assets/inflate.png -------------------------------------------------------------------------------- /doc-assets/intersect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tirithen/clipper2/b046cadef664345cf02a287551a42930e5e5f252/doc-assets/intersect.png -------------------------------------------------------------------------------- /doc-assets/simplify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tirithen/clipper2/b046cadef664345cf02a287551a42930e5e5f252/doc-assets/simplify.png -------------------------------------------------------------------------------- /doc-assets/union.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tirithen/clipper2/b046cadef664345cf02a287551a42930e5e5f252/doc-assets/union.png -------------------------------------------------------------------------------- /doc-assets/xor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tirithen/clipper2/b046cadef664345cf02a287551a42930e5e5f252/doc-assets/xor.png -------------------------------------------------------------------------------- /examples/difference-and-inflate.rs: -------------------------------------------------------------------------------- 1 | use clipper2::*; 2 | use helpers::{circle_path, draw_paths}; 3 | use macroquad::prelude::*; 4 | 5 | mod helpers; 6 | 7 | #[macroquad::main("Difference and inflate")] 8 | async fn main() -> Result<(), ClipperError> { 9 | let circle = circle_path((5.0, 5.0), 3.0, 32); 10 | let circle2 = circle_path((7.0, 7.0), 1.0, 32); 11 | let rectangle = vec![(0.0, 0.0), (5.0, 0.0), (5.0, 6.0), (0.0, 6.0)]; 12 | 13 | // Functional API 14 | let _result = difference(circle.clone(), circle2.clone(), FillRule::default())?; 15 | let _result = difference(_result, rectangle.clone(), FillRule::default())?; 16 | 17 | let _result2 = inflate(_result, 1.0, JoinType::Round, EndType::Polygon, 0.0); 18 | let _result2 = simplify(_result2, 0.01, false); 19 | 20 | // Alternative Clipper builder API 21 | let result = circle 22 | .to_clipper_subject() 23 | .add_clip(circle2) 24 | .add_clip(rectangle) 25 | .difference(FillRule::default())?; 26 | 27 | let result2 = result 28 | .inflate(1.0, JoinType::Round, EndType::Polygon, 0.0) 29 | .simplify(0.01, false); 30 | 31 | loop { 32 | clear_background(BLACK); 33 | draw_paths(&result, SKYBLUE); 34 | draw_paths(&result2, GREEN); 35 | next_frame().await 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /examples/difference.rs: -------------------------------------------------------------------------------- 1 | use clipper2::*; 2 | use helpers::draw_paths; 3 | use macroquad::prelude::*; 4 | 5 | mod helpers; 6 | 7 | #[macroquad::main("Difference")] 8 | async fn main() -> Result<(), ClipperError> { 9 | let path_a: Paths = vec![(0.2, 0.2), (6.0, 0.2), (6.0, 6.0), (0.2, 6.0)].into(); 10 | let path_b: Paths = vec![(5.0, 5.0), (8.0, 5.0), (8.0, 8.0), (5.0, 8.0)].into(); 11 | 12 | // Functional API 13 | let _result = difference(path_a.clone(), path_b.clone(), FillRule::default())?; 14 | 15 | // Alternative Clipper builder API 16 | let result = path_a 17 | .to_clipper_subject() 18 | .add_clip(path_b.clone()) 19 | .difference(FillRule::default())?; 20 | 21 | loop { 22 | clear_background(BLACK); 23 | draw_paths(&path_a, GRAY); 24 | draw_paths(&path_b, GRAY); 25 | draw_paths(&result, SKYBLUE); 26 | next_frame().await 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /examples/helpers.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use std::f64::consts::PI; 4 | 5 | use clipper2::*; 6 | use macroquad::prelude::*; 7 | 8 | const SCALE: f32 = 50.0; 9 | 10 | pub fn draw_paths(paths: &Paths, color: Color) { 11 | for path in paths.iter() { 12 | draw_path(path, color); 13 | } 14 | } 15 | 16 | pub fn draw_path(path: &Path, color: Color) { 17 | let mut last_point = path.iter().last().unwrap_or(&Point::ZERO); 18 | 19 | for point in path.iter() { 20 | draw_line( 21 | last_point.x() as f32 * SCALE, 22 | last_point.y() as f32 * SCALE, 23 | point.x() as f32 * SCALE, 24 | point.y() as f32 * SCALE, 25 | 3.0, 26 | color, 27 | ); 28 | last_point = point; 29 | } 30 | } 31 | 32 | pub fn draw_paths_points(paths: &Paths, color: Color) { 33 | for path in paths.iter() { 34 | draw_path_points(path, color); 35 | } 36 | } 37 | 38 | pub fn draw_path_points(path: &Path, color: Color) { 39 | for point in path.iter() { 40 | draw_circle( 41 | point.x() as f32 * SCALE, 42 | point.y() as f32 * SCALE, 43 | 6.0, 44 | color, 45 | ); 46 | } 47 | } 48 | 49 | pub fn circle_path(offset: (f64, f64), radius: f64, segments: usize) -> Paths { 50 | let mut points = vec![]; 51 | 52 | for i in 0..segments { 53 | let angle = (i as f64 / segments as f64) * 2.0 * PI; 54 | points.push(( 55 | angle.sin() * radius + offset.0, 56 | angle.cos() * radius + offset.1, 57 | )); 58 | } 59 | 60 | points.into() 61 | } 62 | 63 | // Dummy main to pass tests as module is for exporting helpers 64 | 65 | fn main() {} 66 | -------------------------------------------------------------------------------- /examples/inflate.rs: -------------------------------------------------------------------------------- 1 | use clipper2::*; 2 | use helpers::draw_paths; 3 | use macroquad::prelude::*; 4 | 5 | mod helpers; 6 | 7 | #[macroquad::main("Inflate")] 8 | async fn main() { 9 | let path: Paths = vec![(2.0, 2.0), (6.0, 2.0), (6.0, 10.0), (2.0, 6.0)].into(); 10 | 11 | // Functional API 12 | let _result = inflate(path.clone(), 1.0, JoinType::Round, EndType::Polygon, 0.0); 13 | let _result = simplify(_result, 0.01, false); 14 | 15 | // Alternative paths API 16 | let result = path 17 | .inflate(1.0, JoinType::Round, EndType::Polygon, 0.0) 18 | .simplify(0.01, false); 19 | 20 | // NOTE: It is recommented to run simplify after each inflate call as extra 21 | // closely positioned points are likely to be added on each inflate 22 | // call that needs cleanup to reduce the amount of points and 23 | // distortion. 24 | 25 | loop { 26 | clear_background(BLACK); 27 | draw_paths(&path, GRAY); 28 | draw_paths(&result, SKYBLUE); 29 | next_frame().await 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /examples/intersect.rs: -------------------------------------------------------------------------------- 1 | use clipper2::*; 2 | use helpers::draw_paths; 3 | use macroquad::prelude::*; 4 | 5 | mod helpers; 6 | 7 | #[macroquad::main("Intersect")] 8 | async fn main() -> Result<(), ClipperError> { 9 | let path_a: Paths = vec![(0.2, 0.2), (6.0, 0.2), (6.0, 6.0), (0.2, 6.0)].into(); 10 | let path_b: Paths = vec![(5.0, 5.0), (8.0, 5.0), (8.0, 8.0), (5.0, 8.0)].into(); 11 | 12 | // Functional API 13 | let _result = intersect::(path_a.clone(), path_b.clone(), FillRule::default())?; 14 | 15 | // Alternative Clipper builder API 16 | let result = path_a 17 | .to_clipper_subject() 18 | .add_clip(path_b.clone()) 19 | .intersect(FillRule::default())?; 20 | 21 | loop { 22 | clear_background(BLACK); 23 | draw_paths(&path_a, GRAY); 24 | draw_paths(&path_b, GRAY); 25 | draw_paths(&result, SKYBLUE); 26 | next_frame().await 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /examples/simplify.rs: -------------------------------------------------------------------------------- 1 | use clipper2::*; 2 | use helpers::{draw_path, draw_path_points}; 3 | use macroquad::prelude::*; 4 | 5 | mod helpers; 6 | 7 | #[macroquad::main("Simplify")] 8 | async fn main() { 9 | let path: Path = vec![(1.0, 2.0), (1.0, 2.5), (1.2, 4.0), (1.8, 6.0)].into(); 10 | 11 | // Functional API 12 | let _path = path.translate(3.0, 3.0); 13 | let _simplified = simplify(path.clone(), 0.5, false); 14 | 15 | // Alternative paths API 16 | let simplified = path.translate(3.0, 0.0).simplify(0.5, false); 17 | 18 | loop { 19 | clear_background(BLACK); 20 | draw_path(&path, SKYBLUE); 21 | draw_path(&simplified, SKYBLUE); 22 | draw_path_points(&path, GREEN); 23 | draw_path_points(&simplified, GREEN); 24 | next_frame().await 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/union.rs: -------------------------------------------------------------------------------- 1 | use clipper2::*; 2 | use helpers::draw_paths; 3 | use macroquad::prelude::*; 4 | 5 | mod helpers; 6 | 7 | #[macroquad::main("Union")] 8 | async fn main() -> Result<(), ClipperError> { 9 | let path_a: Paths = vec![(0.2, 0.2), (6.0, 0.2), (6.0, 6.0), (0.2, 6.0)].into(); 10 | let path_b: Paths = vec![(5.0, 5.0), (8.0, 5.0), (8.0, 8.0), (5.0, 8.0)].into(); 11 | 12 | // Functional API 13 | let _result = union(path_a.clone(), path_b.clone(), FillRule::default())?; 14 | 15 | // Alternative Clipper builder API 16 | let result = path_a 17 | .to_clipper_subject() 18 | .add_clip(path_b.clone()) 19 | .union(FillRule::default())?; 20 | 21 | loop { 22 | clear_background(BLACK); 23 | draw_paths(&path_a, GRAY); 24 | draw_paths(&path_b, GRAY); 25 | draw_paths(&result, SKYBLUE); 26 | next_frame().await 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /examples/xor.rs: -------------------------------------------------------------------------------- 1 | use clipper2::*; 2 | use helpers::draw_paths; 3 | use macroquad::prelude::*; 4 | 5 | mod helpers; 6 | 7 | #[macroquad::main("XOR")] 8 | async fn main() -> Result<(), ClipperError> { 9 | let path_a: Paths = vec![(0.2, 0.2), (6.0, 0.2), (6.0, 6.0), (0.2, 6.0)].into(); 10 | let path_b: Paths = vec![(5.0, 5.0), (8.0, 5.0), (8.0, 8.0), (5.0, 8.0)].into(); 11 | 12 | // Functional API 13 | let _result = xor(path_a.clone(), path_b.clone(), FillRule::default())?; 14 | 15 | // Alternative Clipper builder API 16 | let result = path_a 17 | .to_clipper_subject() 18 | .add_clip(path_b.clone()) 19 | .xor(FillRule::default())?; 20 | 21 | loop { 22 | clear_background(BLACK); 23 | draw_paths(&path_a, GRAY); 24 | draw_paths(&path_b, GRAY); 25 | draw_paths(&result, SKYBLUE); 26 | next_frame().await 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/bounds.rs: -------------------------------------------------------------------------------- 1 | use crate::{Centi, Point, PointScaler}; 2 | 3 | /// Represents an area from one min and one max [Point](struct.Point.html). 4 | #[derive(Default, Debug, Copy, Clone, PartialEq)] 5 | pub struct Bounds { 6 | /// Minimum point of the boundary. 7 | pub min: Point

, 8 | /// Maximum point of the boundary. 9 | pub max: Point

, 10 | } 11 | 12 | impl Bounds

{ 13 | /// Create a `Bounds` struct starting at xy 0.0 and ending at the given xy 14 | /// coordinates. 15 | #[must_use] 16 | pub fn new(x: f64, y: f64) -> Self { 17 | Self { 18 | min: Point::new(0.0, 0.0), 19 | max: Point::new(x, y), 20 | } 21 | } 22 | 23 | /// Create a `Bounds` struct using xy maximum value as min and minimum value 24 | /// for max. 25 | #[must_use] 26 | pub fn minmax() -> Self { 27 | Self { 28 | min: Point::MAX, 29 | max: Point::MIN, 30 | } 31 | } 32 | 33 | /// Return the size of the bounds area as a [Point](struct.Point.html). 34 | #[must_use] 35 | pub fn size(&self) -> Point

{ 36 | Point::new(self.max.x() - self.min.x(), self.max.y() - self.min.y()) 37 | } 38 | 39 | /// Return the center of the bounds area as a [Point](struct.Point.html). 40 | #[must_use] 41 | pub fn center(&self) -> Point

{ 42 | let size = self.size(); 43 | Point::new(self.min.x() + size.x() / 2.0, self.min.y() + size.y() / 2.0) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/clipper.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | use clipper2c_sys::{ 4 | clipper_clipper64, clipper_clipper64_add_clip, clipper_clipper64_add_open_subject, 5 | clipper_clipper64_add_subject, clipper_clipper64_execute, clipper_clipper64_size, 6 | clipper_delete_clipper64, clipper_delete_paths64, ClipperClipper64, 7 | }; 8 | 9 | use crate::{malloc, Centi, ClipType, FillRule, Paths, PointScaler}; 10 | 11 | /// The state of the Clipper struct. 12 | pub trait ClipperState {} 13 | 14 | /// A state indicating no subjects and no clips. 15 | #[derive(Debug)] 16 | pub struct NoSubjects {} 17 | impl ClipperState for NoSubjects {} 18 | 19 | /// A state indicating one or more subjects and no clips. 20 | #[derive(Debug)] 21 | pub struct WithSubjects {} 22 | impl ClipperState for WithSubjects {} 23 | 24 | /// A state indicating one or more subjects and one or more clips. 25 | #[derive(Debug)] 26 | pub struct WithClips {} 27 | impl ClipperState for WithClips {} 28 | 29 | /// The Clipper struct used as a builder for applying boolean operations to paths. 30 | #[derive(Debug)] 31 | pub struct Clipper { 32 | ptr: *mut ClipperClipper64, 33 | keep_ptr_on_drop: bool, 34 | _marker: PhantomData

, 35 | _state: S, 36 | } 37 | 38 | impl Clipper { 39 | /// Creates a new empty Clipper instance. 40 | pub fn new() -> Clipper { 41 | let ptr = unsafe { 42 | let mem = malloc(clipper_clipper64_size()); 43 | clipper_clipper64(mem) 44 | }; 45 | 46 | Clipper:: { 47 | ptr, 48 | keep_ptr_on_drop: false, 49 | _marker: PhantomData, 50 | _state: NoSubjects {}, 51 | } 52 | } 53 | } 54 | 55 | impl Clipper { 56 | /// Adds a subject path to the Clipper instance. 57 | /// 58 | /// # Examples 59 | /// 60 | /// ```rust 61 | /// use clipper2::*; 62 | /// 63 | /// let path: Paths = vec![(0.2, 0.2), (6.0, 0.2), (6.0, 6.0), (0.2, 6.0)].into(); 64 | /// 65 | /// let clipper = Clipper::new().add_subject(path); 66 | /// ``` 67 | pub fn add_subject(mut self, subject: impl Into>) -> Clipper { 68 | self.keep_ptr_on_drop = true; 69 | 70 | let clipper = Clipper:: { 71 | ptr: self.ptr, 72 | keep_ptr_on_drop: false, 73 | _marker: PhantomData, 74 | _state: WithSubjects {}, 75 | }; 76 | 77 | drop(self); 78 | 79 | clipper.add_subject(subject) 80 | } 81 | 82 | /// Adds an open subject path to the Clipper instance. 83 | /// 84 | /// # Examples 85 | /// 86 | /// ```rust 87 | /// use clipper2::*; 88 | /// 89 | /// let path: Paths = vec![(0.2, 0.2), (6.0, 0.2), (6.0, 6.0), (0.2, 6.0)].into(); 90 | /// 91 | /// let clipper = Clipper::new().add_open_subject(path); 92 | /// ``` 93 | pub fn add_open_subject(mut self, subject: impl Into>) -> Clipper { 94 | self.keep_ptr_on_drop = true; 95 | 96 | let clipper = Clipper:: { 97 | ptr: self.ptr, 98 | keep_ptr_on_drop: false, 99 | _marker: PhantomData, 100 | _state: WithSubjects {}, 101 | }; 102 | 103 | drop(self); 104 | 105 | clipper.add_open_subject(subject) 106 | } 107 | } 108 | 109 | impl Clipper { 110 | /// Adds another subject path to the Clipper instance. 111 | /// 112 | /// # Examples 113 | /// 114 | /// ```rust 115 | /// use clipper2::*; 116 | /// 117 | /// let path: Paths = vec![(0.2, 0.2), (6.0, 0.2), (6.0, 6.0), (0.2, 6.0)].into(); 118 | /// let path2: Paths = vec![(1.2, 1.2), (4.0, 1.2), (1.2, 4.0)].into(); 119 | /// 120 | /// let clipper = Clipper::new().add_subject(path).add_subject(path2); 121 | /// ``` 122 | pub fn add_subject(self, subject: impl Into>) -> Self { 123 | unsafe { 124 | let subject_ptr = subject.into().to_clipperpaths64(); 125 | clipper_clipper64_add_subject(self.ptr, subject_ptr); 126 | clipper_delete_paths64(subject_ptr); 127 | } 128 | 129 | self 130 | } 131 | 132 | /// Adds another open subject path to the Clipper instance. 133 | /// 134 | /// # Examples 135 | /// 136 | /// ```rust 137 | /// use clipper2::*; 138 | /// 139 | /// let path: Paths = vec![(0.2, 0.2), (6.0, 0.2), (6.0, 6.0), (0.2, 6.0)].into(); 140 | /// let path2: Paths = vec![(1.2, 1.2), (4.0, 1.2), (1.2, 4.0)].into(); 141 | /// 142 | /// let clipper = Clipper::new().add_subject(path).add_open_subject(path2); 143 | /// ``` 144 | pub fn add_open_subject(self, subject: impl Into>) -> Self { 145 | unsafe { 146 | let subject_ptr = subject.into().to_clipperpaths64(); 147 | clipper_clipper64_add_open_subject(self.ptr, subject_ptr); 148 | clipper_delete_paths64(subject_ptr); 149 | } 150 | 151 | self 152 | } 153 | 154 | /// Adds a clip path to the Clipper instance. 155 | /// 156 | /// # Examples 157 | /// 158 | /// ```rust 159 | /// use clipper2::*; 160 | /// 161 | /// let path: Paths = vec![(0.2, 0.2), (6.0, 0.2), (6.0, 6.0), (0.2, 6.0)].into(); 162 | /// let path2: Paths = vec![(1.2, 1.2), (4.0, 1.2), (1.2, 4.0)].into(); 163 | /// 164 | /// let clipper = Clipper::new().add_subject(path).add_clip(path2); 165 | /// ``` 166 | pub fn add_clip(mut self, clip: impl Into>) -> Clipper { 167 | self.keep_ptr_on_drop = true; 168 | 169 | let clipper = Clipper:: { 170 | ptr: self.ptr, 171 | keep_ptr_on_drop: false, 172 | _marker: PhantomData, 173 | _state: WithClips {}, 174 | }; 175 | 176 | drop(self); 177 | 178 | clipper.add_clip(clip) 179 | } 180 | } 181 | 182 | impl Clipper { 183 | /// Adds another clip path to the Clipper instance. 184 | /// 185 | /// # Examples 186 | /// 187 | /// ```rust 188 | /// use clipper2::*; 189 | /// 190 | /// let path: Paths = vec![(0.2, 0.2), (6.0, 0.2), (6.0, 6.0), (0.2, 6.0)].into(); 191 | /// let path2: Paths = vec![(1.2, 1.2), (4.0, 1.2), (1.2, 4.0)].into(); 192 | /// let path3: Paths = vec![(2.2, 2.2), (5.0, 2.2), (2.2, 5.0)].into(); 193 | /// 194 | /// let clipper = Clipper::new().add_subject(path).add_clip(path2).add_clip(path3); 195 | /// ``` 196 | pub fn add_clip(self, clip: impl Into>) -> Self { 197 | unsafe { 198 | let clip_ptr = clip.into().to_clipperpaths64(); 199 | clipper_clipper64_add_clip(self.ptr, clip_ptr); 200 | clipper_delete_paths64(clip_ptr); 201 | } 202 | 203 | self 204 | } 205 | 206 | /// Applies a union boolean operation to the Clipper instance. 207 | /// 208 | /// # Examples 209 | /// 210 | /// ```rust 211 | /// use clipper2::*; 212 | /// 213 | /// let path: Paths = vec![(0.2, 0.2), (6.0, 0.2), (6.0, 6.0), (0.2, 6.0)].into(); 214 | /// let path2: Paths = vec![(1.2, 1.2), (4.0, 1.2), (1.2, 4.0)].into(); 215 | /// 216 | /// let result = Clipper::new().add_subject(path).add_clip(path2).union(FillRule::NonZero); 217 | /// ``` 218 | /// 219 | /// For more details see the original [union](https://www.angusj.com/clipper2/Docs/Units/Clipper/Functions/Union.htm) docs. 220 | pub fn union(self, fill_rule: FillRule) -> Result, ClipperError> { 221 | self.boolean_operation(ClipType::Union, fill_rule) 222 | } 223 | 224 | /// Applies a difference boolean operation to the Clipper instance. 225 | /// 226 | /// # Examples 227 | /// 228 | /// ```rust 229 | /// use clipper2::*; 230 | /// 231 | /// let path: Paths = vec![(0.2, 0.2), (6.0, 0.2), (6.0, 6.0), (0.2, 6.0)].into(); 232 | /// let path2: Paths = vec![(1.2, 1.2), (4.0, 1.2), (1.2, 4.0)].into(); 233 | /// 234 | /// let result = Clipper::new().add_subject(path).add_clip(path2).difference(FillRule::NonZero); 235 | /// ``` 236 | /// 237 | /// For more details see the original [difference](https://www.angusj.com/clipper2/Docs/Units/Clipper/Functions/Difference.htm) docs. 238 | pub fn difference(self, fill_rule: FillRule) -> Result, ClipperError> { 239 | self.boolean_operation(ClipType::Difference, fill_rule) 240 | } 241 | 242 | /// Applies an intersection boolean operation to the Clipper instance. 243 | /// 244 | /// # Examples 245 | /// 246 | /// ```rust 247 | /// use clipper2::*; 248 | /// 249 | /// let path: Paths = vec![(0.2, 0.2), (6.0, 0.2), (6.0, 6.0), (0.2, 6.0)].into(); 250 | /// let path2: Paths = vec![(1.2, 1.2), (4.0, 1.2), (1.2, 4.0)].into(); 251 | /// 252 | /// let result = Clipper::new().add_subject(path).add_clip(path2).intersect(FillRule::NonZero); 253 | /// ``` 254 | /// 255 | /// For more details see the original [intersect](https://www.angusj.com/clipper2/Docs/Units/Clipper/Functions/Intersect.htm) docs. 256 | pub fn intersect(self, fill_rule: FillRule) -> Result, ClipperError> { 257 | self.boolean_operation(ClipType::Intersection, fill_rule) 258 | } 259 | 260 | /// Applies an xor boolean operation to the Clipper instance. 261 | /// 262 | /// # Examples 263 | /// 264 | /// ```rust 265 | /// use clipper2::*; 266 | /// 267 | /// let path: Paths = vec![(0.2, 0.2), (6.0, 0.2), (6.0, 6.0), (0.2, 6.0)].into(); 268 | /// let path2: Paths = vec![(1.2, 1.2), (4.0, 1.2), (1.2, 4.0)].into(); 269 | /// 270 | /// let result = Clipper::new().add_subject(path).add_clip(path2).xor(FillRule::NonZero); 271 | /// ``` 272 | /// 273 | /// For more details see the original [xor](https://www.angusj.com/clipper2/Docs/Units/Clipper/Functions/XOR.htm) docs. 274 | pub fn xor(self, fill_rule: FillRule) -> Result, ClipperError> { 275 | self.boolean_operation(ClipType::Xor, fill_rule) 276 | } 277 | 278 | fn boolean_operation( 279 | self, 280 | clip_type: ClipType, 281 | fill_rule: FillRule, 282 | ) -> Result, ClipperError> { 283 | let closed_path = unsafe { Paths::

::new(Vec::new()).to_clipperpaths64() }; 284 | let open_path = unsafe { Paths::

::new(Vec::new()).to_clipperpaths64() }; 285 | 286 | let result = unsafe { 287 | let success = clipper_clipper64_execute( 288 | self.ptr, 289 | clip_type.into(), 290 | fill_rule.into(), 291 | closed_path, 292 | open_path, 293 | ); 294 | 295 | if success != 1 { 296 | clipper_delete_paths64(closed_path); 297 | clipper_delete_paths64(open_path); 298 | return Err(ClipperError::FailedBooleanOperation); 299 | } 300 | 301 | let path = Paths::from_clipperpaths64(closed_path); 302 | clipper_delete_paths64(closed_path); 303 | clipper_delete_paths64(open_path); 304 | 305 | Ok(path) 306 | }; 307 | 308 | drop(self); 309 | 310 | result 311 | } 312 | } 313 | 314 | impl Default for Clipper { 315 | fn default() -> Self { 316 | Self::new() 317 | } 318 | } 319 | 320 | impl Drop for Clipper { 321 | fn drop(&mut self) { 322 | if !self.keep_ptr_on_drop { 323 | unsafe { clipper_delete_clipper64(self.ptr) } 324 | } 325 | } 326 | } 327 | 328 | /// Errors that can occur during clipper operations. 329 | #[derive(Debug, thiserror::Error)] 330 | pub enum ClipperError { 331 | /// Failed execute boolean operation. 332 | #[error("Failed boolean operation")] 333 | FailedBooleanOperation, 334 | } 335 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![warn(missing_docs)] 2 | 3 | //! clipper2 is a path/polygon clipping and offsetting library that supports 4 | //! operations like difference, inflate, intersect, point-in-polygon, union, 5 | //! xor, simplify. 6 | //! 7 | //! The create uses the [clipper2c-sys](https://crates.io/crates/clipper2c-sys) 8 | //! crate that in turn is a Rust wrapper around the C++ version of 9 | //! [Clipper2](https://github.com/AngusJohnson/Clipper2). 10 | //! 11 | //! The crate exposes the Clipper API in two alternative ways until the best 12 | //! version has been figured out. 13 | //! 14 | //! 1. Through the [`Path`]/[`Paths`] struct methods: 15 | //! * [`Path::inflate`] 16 | //! * [`Path::simplify`] 17 | //! * [`Path::is_point_inside`] 18 | //! * [`Paths::inflate`] 19 | //! * [`Paths::simplify`] 20 | //! * [`Paths::to_clipper_subject`] returns a [`Clipper`] builder struct 21 | //! with the current set of paths as the first subject, and allowing to 22 | //! make boolean operations on several sets of paths in one go. 23 | //! * [`Paths::to_clipper_open_subject`] similar but adds the current set 24 | //! of paths as an open "line" rather than a closed path/polygon. 25 | //! 2. Via the plain functions: 26 | //! * [`difference`] 27 | //! * [`inflate`] 28 | //! * [`intersect`] 29 | //! * [`point_in_polygon`] 30 | //! * [`simplify`] 31 | //! * [`union`] 32 | //! * [`xor`] 33 | //! 34 | //! The [`Path`]/[`Paths`] structs also thas some transformation methods such 35 | //! as: 36 | //! 37 | //! * [`Path::translate`] / [`Paths::translate`] for moving a path in by a x/y 38 | //! offset 39 | //! * [`Path::rotate`] / [`Paths::rotate`] for rotating a path in by x radians 40 | //! * [`Path::scale`] / [`Paths::scale`] for scaling a path by multiplier 41 | //! 42 | //! # Examples 43 | //! 44 | //! ```rust 45 | //! use clipper2::*; 46 | //! 47 | //! let path_a: Paths = vec![(0.2, 0.2), (6.0, 0.2), (6.0, 6.0), (0.2, 6.0)].into(); 48 | //! let path_b: Paths = vec![(5.0, 5.0), (8.0, 5.0), (8.0, 8.0), (5.0, 8.0)].into(); 49 | //! 50 | //! let output: Vec> = path_a 51 | //! .to_clipper_subject() 52 | //! .add_clip(path_b) 53 | //! .difference(FillRule::default()) 54 | //! .expect("Failed difference operation") 55 | //! .into(); 56 | //! 57 | //! dbg!(output); 58 | //! ``` 59 | //! 60 | //! ![Image displaying the result of the difference example](https://raw.githubusercontent.com/tirithen/clipper2/main/doc-assets/difference.png) 61 | //! 62 | //! More examples can be found in the 63 | //! [examples](https://github.com/tirithen/clipper2/tree/main/examples) 64 | //! directory. 65 | 66 | mod bounds; 67 | mod clipper; 68 | mod operations; 69 | mod options; 70 | mod path; 71 | mod paths; 72 | mod point; 73 | 74 | pub use crate::bounds::*; 75 | pub use crate::clipper::*; 76 | pub use crate::operations::*; 77 | pub use crate::options::*; 78 | pub use crate::path::*; 79 | pub use crate::paths::*; 80 | pub use crate::point::*; 81 | 82 | pub(crate) unsafe fn malloc(size: usize) -> *mut std::os::raw::c_void { 83 | libc::malloc(size) 84 | } 85 | -------------------------------------------------------------------------------- /src/operations/difference.rs: -------------------------------------------------------------------------------- 1 | use crate::{Clipper, ClipperError, FillRule, Paths, PointScaler}; 2 | 3 | /// This function differences closed subject paths from clip paths. 4 | /// 5 | /// # Examples 6 | /// 7 | /// ```rust 8 | /// use clipper2::*; 9 | /// 10 | /// let path_a: Paths = vec![(0.2, 0.2), (6.0, 0.2), (6.0, 6.0), (0.2, 6.0)].into(); 11 | /// let path_b: Paths = vec![(5.0, 5.0), (8.0, 5.0), (8.0, 8.0), (5.0, 8.0)].into(); 12 | /// 13 | /// let output: Vec> = difference(path_a, path_b, FillRule::default()) 14 | /// .expect("Failed to run boolean operation").into(); 15 | /// 16 | /// dbg!(output); 17 | /// ``` 18 | /// 19 | /// ![Image displaying the result of the difference example](https://raw.githubusercontent.com/tirithen/clipper2/main/doc-assets/difference.png) 20 | /// 21 | /// For more details see the original [difference](https://www.angusj.com/clipper2/Docs/Units/Clipper/Functions/Difference.htm) docs. 22 | pub fn difference( 23 | subject: impl Into>, 24 | clip: impl Into>, 25 | fill_rule: FillRule, 26 | ) -> Result, ClipperError> { 27 | Clipper::new() 28 | .add_subject(subject) 29 | .add_clip(clip) 30 | .difference(fill_rule) 31 | } 32 | 33 | #[cfg(test)] 34 | mod test { 35 | use crate::Centi; 36 | 37 | use super::*; 38 | 39 | #[test] 40 | fn test_difference() { 41 | let path1 = vec![(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)]; 42 | let path2 = vec![(0.5, 0.5), (1.5, 0.5), (1.5, 1.5), (0.5, 1.5)]; 43 | let expected_output = vec![vec![ 44 | (1.0, 0.5), 45 | (0.5, 0.5), 46 | (0.5, 1.0), 47 | (0.0, 1.0), 48 | (0.0, 0.0), 49 | (1.0, 0.0), 50 | ]]; 51 | 52 | let output: Vec> = difference::(path1, path2, FillRule::default()) 53 | .unwrap() 54 | .into(); 55 | assert_eq!(output, expected_output); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/operations/inflate.rs: -------------------------------------------------------------------------------- 1 | use clipper2c_sys::{clipper_delete_paths64, clipper_paths64_inflate, clipper_paths64_size}; 2 | 3 | use crate::{malloc, EndType, JoinType, Paths, PointScaler}; 4 | 5 | /// This function performs both closed path and open path offsetting. 6 | /// 7 | /// For closed paths passing a positive delta number will inflate the path 8 | /// where passing a negative number will shrink the path. 9 | /// 10 | /// **NOTE:** Inflate calls will frequently generate a large amount of very 11 | /// close extra points and it is therefore recommented to almost always call 12 | /// [`simplify`](./fn.simplify.html) on the path after inflating/shrinking it. 13 | /// 14 | /// # Example 15 | /// 16 | /// ```rust 17 | /// use clipper2::*; 18 | /// 19 | /// let paths: Paths = vec![(2.0, 2.0), (6.0, 2.0), (6.0, 10.0), (2.0, 6.0)].into(); 20 | /// 21 | /// let output = inflate(paths, 1.0, JoinType::Round, EndType::Polygon, 0.0); 22 | /// let output = simplify(output, 0.01, false); 23 | /// 24 | /// dbg!(output); 25 | /// ``` 26 | /// 27 | /// ![Image displaying the result of the inflate example](https://raw.githubusercontent.com/tirithen/clipper2/main/doc-assets/inflate.png) 28 | /// 29 | /// For more details see the original [inflate paths](https://www.angusj.com/clipper2/Docs/Units/Clipper/Functions/InflatePaths.htm) docs. 30 | pub fn inflate( 31 | paths: impl Into>, 32 | delta: f64, 33 | join_type: JoinType, 34 | end_type: EndType, 35 | miter_limit: f64, 36 | ) -> Paths

{ 37 | let delta = P::scale(delta); 38 | let miter_limit = P::scale(miter_limit); 39 | let paths: Paths

= paths.into(); 40 | 41 | unsafe { 42 | let mem = malloc(clipper_paths64_size()); 43 | let paths_ptr = paths.to_clipperpaths64(); 44 | let result_ptr = clipper_paths64_inflate( 45 | mem, 46 | paths_ptr, 47 | delta, 48 | join_type.into(), 49 | end_type.into(), 50 | miter_limit, 51 | ); 52 | clipper_delete_paths64(paths_ptr); 53 | let result = Paths::from_clipperpaths64(result_ptr); 54 | clipper_delete_paths64(result_ptr); 55 | result 56 | } 57 | } 58 | 59 | #[cfg(test)] 60 | mod test { 61 | use crate::Centi; 62 | 63 | use super::*; 64 | 65 | #[test] 66 | fn test_inflate() { 67 | let paths = vec![(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)]; 68 | let expected_output = vec![vec![ 69 | (2.0, -0.41), 70 | (2.0, 1.41), 71 | (1.41, 2.0), 72 | (-0.41, 2.0), 73 | (-1.0, 1.41), 74 | (-1.0, -0.41), 75 | (-0.41, -1.0), 76 | (1.41, -1.0), 77 | ]]; 78 | 79 | let output: Vec> = 80 | inflate::(paths, 1.0, JoinType::Square, EndType::Polygon, 0.0).into(); 81 | assert_eq!(output, expected_output); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/operations/intersect.rs: -------------------------------------------------------------------------------- 1 | use crate::{Clipper, ClipperError, FillRule, Paths, PointScaler}; 2 | 3 | /// This function intersects closed subject paths with clip paths. 4 | /// 5 | /// # Examples 6 | /// 7 | /// ```rust 8 | /// use clipper2::*; 9 | /// 10 | /// let path_a: Paths = vec![(0.2, 0.2), (6.0, 0.2), (6.0, 6.0), (0.2, 6.0)].into(); 11 | /// let path_b: Paths = vec![(5.0, 5.0), (8.0, 5.0), (8.0, 8.0), (5.0, 8.0)].into(); 12 | /// 13 | /// let output: Vec> = intersect(path_a, path_b, FillRule::default()) 14 | /// .expect("Failed to run boolean operation").into(); 15 | /// 16 | /// dbg!(output); 17 | /// ``` 18 | /// ![Image displaying the result of the intersect example](https://raw.githubusercontent.com/tirithen/clipper2/main/doc-assets/intersect.png) 19 | /// 20 | /// For more details see the original [intersect](https://www.angusj.com/clipper2/Docs/Units/Clipper/Functions/Intersect.htm) docs. 21 | pub fn intersect( 22 | subject: impl Into>, 23 | clip: impl Into>, 24 | fill_rule: FillRule, 25 | ) -> Result, ClipperError> { 26 | Clipper::new() 27 | .add_subject(subject) 28 | .add_clip(clip) 29 | .intersect(fill_rule) 30 | } 31 | 32 | #[cfg(test)] 33 | mod test { 34 | use crate::Centi; 35 | 36 | use super::*; 37 | 38 | #[test] 39 | fn test_intersect() { 40 | let path1 = vec![(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)]; 41 | let path2 = vec![(0.5, 0.5), (1.5, 0.5), (1.5, 1.5), (0.5, 1.5)]; 42 | let expected_output = vec![vec![(1.0, 1.0), (0.5, 1.0), (0.5, 0.5), (1.0, 0.5)]]; 43 | 44 | let output: Vec> = intersect::(path1, path2, FillRule::default()) 45 | .unwrap() 46 | .into(); 47 | assert_eq!(output, expected_output); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/operations/mod.rs: -------------------------------------------------------------------------------- 1 | mod difference; 2 | mod inflate; 3 | mod intersect; 4 | mod pointinpolygon; 5 | mod simplify; 6 | mod union; 7 | mod xor; 8 | 9 | pub use difference::*; 10 | pub use inflate::*; 11 | pub use intersect::*; 12 | pub use pointinpolygon::*; 13 | pub use simplify::*; 14 | pub use union::*; 15 | pub use xor::*; 16 | -------------------------------------------------------------------------------- /src/operations/pointinpolygon.rs: -------------------------------------------------------------------------------- 1 | use clipper2c_sys::{clipper_delete_path64, clipper_point_in_path64}; 2 | 3 | use crate::{Path, Point, PointInPolygonResult, PointScaler}; 4 | 5 | /// The function result indicates whether the point is inside, or outside, or on 6 | /// one of the specified polygon's edges. 7 | /// 8 | /// # Examples 9 | /// 10 | /// ```rust 11 | /// use clipper2::*; 12 | /// 13 | /// let path = vec![(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)]; 14 | /// 15 | /// let output = point_in_polygon::(Point::new(0.5, 0.5), &path.into()); 16 | /// 17 | /// dbg!(output); 18 | /// ``` 19 | /// 20 | /// For more details see the original [point-in-polygon](https://www.angusj.com/clipper2/Docs/Units/Clipper/Functions/PointInPolygon.htm) docs. 21 | pub fn point_in_polygon(point: Point

, path: &Path

) -> PointInPolygonResult { 22 | let point_ptr = point.as_clipperpoint64(); 23 | let path_ptr = unsafe { path.to_clipperpath64() }; 24 | let result = unsafe { clipper_point_in_path64(path_ptr, *point_ptr) }; 25 | unsafe { clipper_delete_path64(path_ptr) }; 26 | result.into() 27 | } 28 | 29 | #[cfg(test)] 30 | mod test { 31 | use crate::Centi; 32 | 33 | use super::*; 34 | 35 | #[test] 36 | fn test_point_in_polygon() { 37 | let path = vec![ 38 | (0.0, 0.0), 39 | (1.0, 0.0), 40 | (1.2, 0.2), 41 | (1.0, 1.0), 42 | (0.5, 1.0), 43 | (0.0, 1.0), 44 | ] 45 | .into(); 46 | 47 | let output = point_in_polygon::(Point::new(-10.0, 0.0), &path); 48 | assert_eq!(output, PointInPolygonResult::IsOutside); 49 | 50 | let output = point_in_polygon::(Point::new(0.5, 0.5), &path); 51 | assert_eq!(output, PointInPolygonResult::IsInside); 52 | 53 | let output = point_in_polygon::(Point::new(0.0, 0.0), &path); 54 | assert_eq!(output, PointInPolygonResult::IsOn); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/operations/simplify.rs: -------------------------------------------------------------------------------- 1 | use clipper2c_sys::{clipper_delete_paths64, clipper_paths64_simplify, clipper_paths64_size}; 2 | 3 | use crate::{malloc, Paths, PointScaler}; 4 | 5 | /// This function removes points that are less than the specified epsilon 6 | /// distance from an imaginary line that passes through its 2 adjacent points. 7 | /// Logically, smaller epsilon values will be less aggressive in removing 8 | /// points than larger epsilon values. 9 | /// 10 | /// This function is strongly recommended following offsetting 11 | /// (ie inflating/shrinking), especially when offsetting paths multiple times. 12 | /// Offsetting often creates tiny segments that don't improve path quality. 13 | /// Further these tiny segments can be at angles that have been affected by 14 | /// integer rounding. While these tiny segments are too small to be noticeable 15 | /// following a single offset procedure, they can degrade both the shape quality 16 | /// and the performance of subsequent offsets. 17 | /// 18 | /// # Examples 19 | /// 20 | /// ```rust 21 | /// use clipper2::*; 22 | /// 23 | /// let path: Path = vec![(1.0, 2.0), (1.0, 2.5), (1.2, 4.0), (1.8, 6.0)].into(); 24 | /// let path_simplified = simplify(path.translate(3.0, 0.0), 0.5, false); 25 | /// 26 | /// dbg!(path, path_simplified); 27 | /// ``` 28 | /// ![Image displaying the result of the simplify example](https://raw.githubusercontent.com/tirithen/clipper2/main/doc-assets/simplify.png) 29 | /// 30 | /// For more details see the original [simplify](https://www.angusj.com/clipper2/Docs/Units/Clipper/Functions/SimplifyPaths.htm) docs. 31 | pub fn simplify( 32 | paths: impl Into>, 33 | epsilon: f64, 34 | is_open: bool, 35 | ) -> Paths

{ 36 | let epsilon = P::scale(epsilon); 37 | 38 | unsafe { 39 | let mem = malloc(clipper_paths64_size()); 40 | let paths_ptr = paths.into().to_clipperpaths64(); 41 | let result_ptr = clipper_paths64_simplify(mem, paths_ptr, epsilon, is_open.into()); 42 | clipper_delete_paths64(paths_ptr); 43 | let result = Paths::from_clipperpaths64(result_ptr); 44 | clipper_delete_paths64(result_ptr); 45 | result 46 | } 47 | } 48 | 49 | #[cfg(test)] 50 | mod test { 51 | use crate::Centi; 52 | 53 | use super::*; 54 | 55 | #[test] 56 | fn test_simplify() { 57 | let path = vec![(0.0, 1.0), (0.1, 0.3), (1.0, 0.0), (1.3, 0.0), (2.0, 0.0)]; 58 | let expected_output = vec![vec![(0.0, 1.0), (0.1, 0.3), (2.0, 0.0)]]; 59 | 60 | let output: Vec> = simplify::(path, 0.2, true).into(); 61 | assert_eq!(output, expected_output); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/operations/union.rs: -------------------------------------------------------------------------------- 1 | use crate::{Clipper, ClipperError, FillRule, Paths, PointScaler}; 2 | 3 | /// This function joins a set of closed subject paths, with and without clip 4 | /// paths. 5 | /// 6 | /// # Examples 7 | /// 8 | /// ```rust 9 | /// use clipper2::*; 10 | /// 11 | /// let path_a = vec![(0.2, 0.2), (6.0, 0.2), (6.0, 6.0), (0.2, 6.0)]; 12 | /// let path_b = vec![(5.0, 5.0), (8.0, 5.0), (8.0, 8.0), (5.0, 8.0)]; 13 | /// 14 | /// let output: Vec> = union::(path_a, path_b, FillRule::default()) 15 | /// .expect("Failed to run boolean operation").into(); 16 | /// 17 | /// dbg!(output); 18 | /// ``` 19 | /// ![Image displaying the result of the union example](https://raw.githubusercontent.com/tirithen/clipper2/main/doc-assets/union.png) 20 | /// 21 | /// For more details see the original [union](https://www.angusj.com/clipper2/Docs/Units/Clipper/Functions/Union.htm) docs. 22 | pub fn union( 23 | subject: impl Into>, 24 | clip: impl Into>, 25 | fill_rule: FillRule, 26 | ) -> Result, ClipperError> { 27 | Clipper::new() 28 | .add_subject(subject) 29 | .add_clip(clip) 30 | .union(fill_rule) 31 | } 32 | 33 | #[cfg(test)] 34 | mod test { 35 | use crate::Centi; 36 | 37 | use super::*; 38 | 39 | #[test] 40 | fn test_union() { 41 | let path1 = vec![(0.2, 0.2), (6.0, 0.2), (6.0, 6.0), (0.2, 6.0)]; 42 | let path2 = vec![(5.0, 5.0), (8.0, 5.0), (8.0, 8.0), (5.0, 8.0)]; 43 | let expected_output = vec![vec![ 44 | (6.0, 5.0), 45 | (8.0, 5.0), 46 | (8.0, 8.0), 47 | (5.0, 8.0), 48 | (5.0, 6.0), 49 | (0.2, 6.0), 50 | (0.2, 0.2), 51 | (6.0, 0.2), 52 | ]]; 53 | 54 | let output: Vec> = union::(path1, path2, FillRule::default()) 55 | .unwrap() 56 | .into(); 57 | assert_eq!(output, expected_output); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/operations/xor.rs: -------------------------------------------------------------------------------- 1 | use crate::{Clipper, ClipperError, FillRule, Paths, PointScaler}; 2 | 3 | /// This function 'XORs' closed subject paths and clip paths. 4 | /// 5 | /// # Examples 6 | /// 7 | /// ```rust 8 | /// use clipper2::*; 9 | /// 10 | /// let path_a: Paths = vec![(0.2, 0.2), (6.0, 0.2), (6.0, 6.0), (0.2, 6.0)].into(); 11 | /// let path_b: Paths = vec![(5.0, 5.0), (8.0, 5.0), (8.0, 8.0), (5.0, 8.0)].into(); 12 | /// 13 | /// let output: Vec> = xor(path_a, path_b, FillRule::default()) 14 | /// .expect("Failed to run boolean operation").into(); 15 | /// 16 | /// dbg!(output); 17 | /// ``` 18 | /// ![Image displaying the result of the xor example](https://raw.githubusercontent.com/tirithen/clipper2/main/doc-assets/xor.png) 19 | /// 20 | /// For more details see the original [xor](https://www.angusj.com/clipper2/Docs/Units/Clipper/Functions/XOR.htm) docs. 21 | pub fn xor( 22 | subject: impl Into>, 23 | clip: impl Into>, 24 | fill_rule: FillRule, 25 | ) -> Result, ClipperError> { 26 | Clipper::new() 27 | .add_subject(subject) 28 | .add_clip(clip) 29 | .xor(fill_rule) 30 | } 31 | 32 | #[cfg(test)] 33 | mod test { 34 | use crate::Centi; 35 | 36 | use super::*; 37 | 38 | #[test] 39 | fn test_xor() { 40 | let path1 = vec![(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)]; 41 | let path2 = vec![(0.5, 0.5), (1.5, 0.5), (1.5, 1.5), (0.5, 1.5)]; 42 | let expected_output = vec![ 43 | vec![ 44 | (1.5, 1.5), 45 | (0.5, 1.5), 46 | (0.5, 1.0), 47 | (1.0, 1.0), 48 | (1.0, 0.5), 49 | (1.5, 0.5), 50 | ], 51 | vec![ 52 | (1.0, 0.5), 53 | (0.5, 0.5), 54 | (0.5, 1.0), 55 | (0.0, 1.0), 56 | (0.0, 0.0), 57 | (1.0, 0.0), 58 | ], 59 | ]; 60 | 61 | let output: Vec> = xor::(path1, path2, FillRule::default()) 62 | .unwrap() 63 | .into(); 64 | assert_eq!(output, expected_output); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/options.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_upper_case_globals)] 2 | 3 | use clipper2c_sys::{ 4 | ClipperClipType, ClipperClipType_DIFFERENCE, ClipperClipType_INTERSECTION, 5 | ClipperClipType_NONE, ClipperClipType_UNION, ClipperClipType_XOR, ClipperEndType, 6 | ClipperEndType_BUTT_END, ClipperEndType_JOINED_END, ClipperEndType_POLYGON_END, 7 | ClipperEndType_ROUND_END, ClipperEndType_SQUARE_END, ClipperFillRule, ClipperFillRule_EVEN_ODD, 8 | ClipperFillRule_NEGATIVE, ClipperFillRule_NON_ZERO, ClipperFillRule_POSITIVE, ClipperJoinType, 9 | ClipperJoinType_BEVEL_JOIN, ClipperJoinType_MITER_JOIN, ClipperJoinType_ROUND_JOIN, 10 | ClipperJoinType_SQUARE_JOIN, ClipperPointInPolygonResult, 11 | ClipperPointInPolygonResult_IS_INSIDE, ClipperPointInPolygonResult_IS_ON, 12 | ClipperPointInPolygonResult_IS_OUTSIDE, 13 | }; 14 | 15 | /// The Clipper Library supports 4 filling rules: Even-Odd, Non-Zero, Positive 16 | /// and Negative. These rules are base on the winding numbers (see below) of 17 | /// each polygon sub-region, which in turn are based on the orientation of each 18 | /// path. Orientation is determined by the order in which vertices are declared 19 | /// during path construction, and whether these vertices progress roughly 20 | /// clockwise or counter-clockwise. 21 | /// 22 | /// Winding numbers for polygon sub-regions can be derived using the following 23 | /// algorithm: 24 | /// 25 | /// * From a point that's outside a given polygon, draw an imaginary line 26 | /// through the polygon. 27 | /// * Starting with a winding number of zero, and beginning at the start of this 28 | /// imaginary line, follow the line as it crosses the polygon. 29 | /// * For each polygon contour that you cross, increment the winding number if 30 | /// the line is heading right to left (relative to your imaginary line), 31 | /// otherwise decrement the winding number. 32 | /// * And as you enter each polygon sub-region, allocate to it the current 33 | /// winding number. 34 | /// 35 | /// * Even-Odd: Only odd numbered sub-regions are filled 36 | /// * Non-Zero: Only non-zero sub-regions are filled 37 | /// * Positive: Only sub-regions with winding counts > 0 are filled 38 | /// * Negative: Only sub-regions with winding counts < 0 are filled 39 | /// 40 | /// For more details see [FillRule](https://www.angusj.com/clipper2/Docs/Units/Clipper/Types/FillRule.htm). 41 | #[derive(Debug, Default, Clone, Copy, PartialEq)] 42 | pub enum FillRule { 43 | /// Even-Odd filling rule 44 | #[default] 45 | EvenOdd, 46 | /// Non-Zero filling rule 47 | NonZero, 48 | /// Positive filling rule 49 | Positive, 50 | /// Negative filling rule 51 | Negative, 52 | } 53 | 54 | /// There are four boolean operations: 55 | /// 56 | /// * AND (intersection) - regions covered by both subject and clip polygons 57 | /// * OR (union) - regions covered by subject or clip polygons, or both polygons 58 | /// * NOT (difference) - regions covered by subject, but not clip polygons 59 | /// * XOR (exclusive or) - regions covered by subject or clip polygons, but not 60 | /// both 61 | /// 62 | /// Of these four operations, only difference is non-commutative. This means 63 | /// that subject and clip paths are interchangeable except when performing 64 | /// difference operations (and as long as subject paths are closed). 65 | /// 66 | /// For more details see [ClipType](https://www.angusj.com/clipper2/Docs/Units/Clipper/Types/ClipType.htm). 67 | #[derive(Debug, Clone, Copy, PartialEq)] 68 | pub(crate) enum ClipType { 69 | #[allow(dead_code)] 70 | None, 71 | Intersection, 72 | Union, 73 | Difference, 74 | Xor, 75 | } 76 | 77 | /// The JoinType enumeration is only needed when offsetting 78 | /// (inflating/shrinking). It isn't needed for polygon clipping. It specifies 79 | /// how to manage offsetting at convex angled joins. Concave joins will always 80 | /// be offset with a mitered join. 81 | /// 82 | /// When adding paths to a ClipperOffset object via the AddPaths method, the 83 | /// JoinType parameter must specify one of the following types - Miter, Square, 84 | /// Bevel or Round. 85 | /// 86 | /// For more details see [JoinType](https://www.angusj.com/clipper2/Docs/Units/Clipper/Types/JoinType.htm). 87 | #[derive(Debug, Clone, Copy, PartialEq)] 88 | pub enum JoinType { 89 | /// Square join 90 | Square, 91 | /// Bevel join 92 | Bevel, 93 | /// Round join 94 | Round, 95 | /// Miter join 96 | Miter, 97 | } 98 | 99 | /// The EndType enumerator is only needed when offsetting (inflating/shrinking). 100 | /// It isn't needed for polygon clipping. 101 | /// 102 | /// EndType has 5 values: 103 | /// 104 | /// * Polygon: the path is treated as a polygon 105 | /// * Joined: ends are joined and the path treated as a polyline 106 | /// * Butt: ends are squared off without any extension 107 | /// * Square: ends extend the offset amount while being squared off 108 | /// * Round: ends extend the offset amount while being rounded off 109 | /// 110 | /// With both EndType.Polygon and EndType.Joined, path closure will occur 111 | /// regardless of whether or not the first and last vertices in the path match. 112 | /// 113 | /// For more details see [EndType](https://www.angusj.com/clipper2/Docs/Units/Clipper/Types/EndType.htm). 114 | #[derive(Debug, Clone, Copy, PartialEq)] 115 | pub enum EndType { 116 | /// Polygon: the path is treated as a polygon 117 | Polygon, 118 | /// Joined: ends are joined and the path treated as a polyline 119 | Joined, 120 | /// Butt: ends are squared off without any extension 121 | Butt, 122 | /// Square: ends extend the offset amount while being squared off 123 | Square, 124 | /// Round: ends extend the offset amount while being rounded off 125 | Round, 126 | } 127 | 128 | /// The result indicates whether the point is inside, or outside, or on one of 129 | /// the specified polygon's edges. 130 | /// 131 | /// * IsOn: the point is on the path 132 | /// * IsInside: the point is inside the path 133 | /// * IsOutside: the point is outside the path 134 | /// 135 | /// For more details see [PointInPolygonResult](https://www.angusj.com/clipper2/Docs/Units/Clipper/Types/PointInPolygonResult.htm). 136 | #[derive(Debug, Clone, Copy, PartialEq)] 137 | pub enum PointInPolygonResult { 138 | /// The point is on the path 139 | IsOn, 140 | /// The point is inside the path 141 | IsInside, 142 | /// The point is outside the path 143 | IsOutside, 144 | } 145 | 146 | impl From for ClipperClipType { 147 | fn from(value: ClipType) -> Self { 148 | match value { 149 | ClipType::None => ClipperClipType_NONE, 150 | ClipType::Intersection => ClipperClipType_INTERSECTION, 151 | ClipType::Union => ClipperClipType_UNION, 152 | ClipType::Difference => ClipperClipType_DIFFERENCE, 153 | ClipType::Xor => ClipperClipType_XOR, 154 | } 155 | } 156 | } 157 | 158 | impl From for ClipperFillRule { 159 | fn from(value: FillRule) -> Self { 160 | match value { 161 | FillRule::EvenOdd => ClipperFillRule_EVEN_ODD, 162 | FillRule::NonZero => ClipperFillRule_NON_ZERO, 163 | FillRule::Positive => ClipperFillRule_POSITIVE, 164 | FillRule::Negative => ClipperFillRule_NEGATIVE, 165 | } 166 | } 167 | } 168 | 169 | impl From for ClipperJoinType { 170 | fn from(value: JoinType) -> Self { 171 | match value { 172 | JoinType::Square => ClipperJoinType_SQUARE_JOIN, 173 | JoinType::Bevel => ClipperJoinType_BEVEL_JOIN, 174 | JoinType::Round => ClipperJoinType_ROUND_JOIN, 175 | JoinType::Miter => ClipperJoinType_MITER_JOIN, 176 | } 177 | } 178 | } 179 | 180 | impl From for ClipperEndType { 181 | fn from(value: EndType) -> Self { 182 | match value { 183 | EndType::Polygon => ClipperEndType_POLYGON_END, 184 | EndType::Joined => ClipperEndType_JOINED_END, 185 | EndType::Butt => ClipperEndType_BUTT_END, 186 | EndType::Square => ClipperEndType_SQUARE_END, 187 | EndType::Round => ClipperEndType_ROUND_END, 188 | } 189 | } 190 | } 191 | 192 | impl From for ClipperPointInPolygonResult { 193 | fn from(value: PointInPolygonResult) -> Self { 194 | match value { 195 | PointInPolygonResult::IsOn => ClipperPointInPolygonResult_IS_ON, 196 | PointInPolygonResult::IsInside => ClipperPointInPolygonResult_IS_INSIDE, 197 | PointInPolygonResult::IsOutside => ClipperPointInPolygonResult_IS_OUTSIDE, 198 | } 199 | } 200 | } 201 | 202 | impl From for PointInPolygonResult { 203 | fn from(value: ClipperPointInPolygonResult) -> Self { 204 | match value { 205 | ClipperPointInPolygonResult_IS_ON => PointInPolygonResult::IsOn, 206 | ClipperPointInPolygonResult_IS_INSIDE => PointInPolygonResult::IsInside, 207 | ClipperPointInPolygonResult_IS_OUTSIDE => PointInPolygonResult::IsOutside, 208 | _ => panic!( 209 | "Invalid ClipperPointInPolygonResult value {}", 210 | value as usize 211 | ), 212 | } 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /src/path.rs: -------------------------------------------------------------------------------- 1 | use clipper2c_sys::{ 2 | clipper_delete_path64, clipper_path64_area, clipper_path64_get_point, clipper_path64_length, 3 | clipper_path64_of_points, clipper_path64_simplify, clipper_path64_size, ClipperPath64, 4 | ClipperPoint64, 5 | }; 6 | 7 | use crate::{ 8 | inflate, malloc, point_in_polygon, Bounds, Centi, EndType, JoinType, Paths, Point, 9 | PointInPolygonResult, PointScaler, 10 | }; 11 | 12 | /// A collection of points. 13 | /// 14 | /// # Examples 15 | /// 16 | /// ```rust 17 | /// use clipper2::*; 18 | /// 19 | /// let path_from_tuples: Path = vec![(0.0, 0.0), (5.0, 0.0), (5.0, 6.0), (0.0, 6.0)].into(); 20 | /// let path_from_slices: Path = vec![[0.0, 0.0], [5.0, 0.0], [5.0, 6.0], [0.0, 6.0]].into(); 21 | /// ``` 22 | #[derive(Debug, Clone, Default, PartialEq, Hash)] 23 | #[cfg_attr( 24 | feature = "serde", 25 | derive(serde::Serialize, serde::Deserialize), 26 | serde(bound = "P: PointScaler") 27 | )] 28 | pub struct Path(Vec>); 29 | 30 | impl Eq for Path

{} 31 | 32 | impl Path

{ 33 | /// Create a new path from a vector of points. 34 | pub fn new(points: Vec>) -> Self { 35 | Path(points) 36 | } 37 | 38 | /// In place push point onto this path. 39 | pub fn push(&mut self, point: impl Into>) { 40 | self.0.push(point.into()); 41 | } 42 | 43 | /// Append another path onto this one, cloning the other path. 44 | pub fn append(&mut self, path: impl Into>>) { 45 | let mut points = path.into(); 46 | self.0.append(&mut points); 47 | } 48 | 49 | /// Returns the number of points in the path. 50 | pub fn len(&self) -> usize { 51 | self.0.len() 52 | } 53 | 54 | /// Returns `true` if the path is empty. 55 | pub fn is_empty(&self) -> bool { 56 | self.0.is_empty() 57 | } 58 | 59 | /// Returns `true` if the path contains at least one point 60 | pub fn contains_points(&self) -> bool { 61 | self.is_empty() 62 | } 63 | 64 | /// Creates a path in a rectangle shape 65 | pub fn rectangle(x: f64, y: f64, size_x: f64, size_y: f64) -> Self { 66 | vec![ 67 | (x, y), 68 | (x + size_x, y), 69 | (x + size_x, y + size_y), 70 | (x, y + size_y), 71 | ] 72 | .into() 73 | } 74 | 75 | /// Returns an iterator over the points in the path. 76 | pub fn iter(&self) -> std::slice::Iter<'_, Point

> { 77 | self.0.iter() 78 | } 79 | 80 | /// Construct a clone with each point offset by a x/y distance 81 | pub fn translate(&self, x: f64, y: f64) -> Self { 82 | Self::new( 83 | self.0 84 | .iter() 85 | .map(|p| Point::

::new(p.x() + x, p.y() + y)) 86 | .collect(), 87 | ) 88 | } 89 | 90 | /// Construct a scaled clone of the path with the origin at the path center 91 | /// 92 | /// # Examples 93 | /// 94 | /// ```rust 95 | /// use clipper2::Path; 96 | /// let path: Path = vec![(-1.0, -1.0), (1.0, 1.0)].into(); 97 | /// let scaled = path.scale(2.0, 2.0); 98 | /// assert_eq!(scaled.iter().map(|p| (p.x(), p.y())).collect::>(), vec![(-2.0, -2.0), (2.0, 2.0)]); 99 | /// ``` 100 | pub fn scale(&self, scale_x: f64, scale_y: f64) -> Self { 101 | let center = self.bounds().center(); 102 | self.scale_around_point(scale_x, scale_y, center) 103 | } 104 | 105 | /// Construct a scaled clone of the path with the origin at a given point 106 | /// 107 | /// # Examples 108 | /// 109 | /// ```rust 110 | /// use clipper2::Path; 111 | /// let path: Path = vec![(0.0, 0.0), (1.0, 1.0)].into(); 112 | /// let scaled = path.scale_around_point(2.0, 2.0, (0.0, 0.0).into()); 113 | /// assert_eq!(scaled.iter().map(|p| (p.x(), p.y())).collect::>(), vec![(0.0, 0.0), (2.0, 2.0)]); 114 | /// ``` 115 | pub fn scale_around_point(&self, scale_x: f64, scale_y: f64, point: Point

) -> Self { 116 | Self::new( 117 | self.0 118 | .iter() 119 | .map(|p| { 120 | Point::

::new( 121 | (p.x() - point.x()) * scale_x + point.x(), 122 | (p.y() - point.y()) * scale_y + point.y(), 123 | ) 124 | }) 125 | .collect(), 126 | ) 127 | } 128 | 129 | /// Construct a rotated clone of the path with the origin at the path center 130 | pub fn rotate(&self, radians: f64) -> Self { 131 | let bounds = self.bounds(); 132 | let center = bounds.center(); 133 | let cos = radians.cos(); 134 | let sin = radians.sin(); 135 | 136 | Self::new( 137 | self.0 138 | .iter() 139 | .map(|p| { 140 | Point::

::new( 141 | (center.x() - p.x()) * cos - (center.y() - p.y()) * sin + center.x(), 142 | (center.x() - p.x()) * sin + (center.y() - p.y()) * cos + center.y(), 143 | ) 144 | }) 145 | .collect(), 146 | ) 147 | } 148 | 149 | /// Construct a clone with each point x value flipped 150 | pub fn flip_x(&self) -> Self { 151 | let bounds = self.bounds(); 152 | let center = bounds.center(); 153 | 154 | Self::new( 155 | self.0 156 | .iter() 157 | .map(|p| Point::

::new(center.x() + (center.x() - p.x()), p.y())) 158 | .collect(), 159 | ) 160 | } 161 | 162 | /// Construct a clone with each point y value flipped 163 | pub fn flip_y(&self) -> Self { 164 | let bounds = self.bounds(); 165 | let center = bounds.center(); 166 | 167 | Self::new( 168 | self.0 169 | .iter() 170 | .map(|p| Point::

::new(p.x(), center.y() + (center.y() - p.y()))) 171 | .collect(), 172 | ) 173 | } 174 | 175 | /// Returns the bounds for this path 176 | pub fn bounds(&self) -> Bounds

{ 177 | let mut bounds = Bounds::minmax(); 178 | 179 | for p in &self.0 { 180 | let x = p.x(); 181 | let y = p.y(); 182 | 183 | if x < bounds.min.x() { 184 | bounds.min = Point::new(x, bounds.min.y()); 185 | } 186 | 187 | if y < bounds.min.y() { 188 | bounds.min = Point::new(bounds.min.x(), y); 189 | } 190 | 191 | if x > bounds.max.x() { 192 | bounds.max = Point::new(x, bounds.max.y()); 193 | } 194 | 195 | if y > bounds.max.y() { 196 | bounds.max = Point::new(bounds.max.x(), y); 197 | } 198 | } 199 | 200 | bounds 201 | } 202 | 203 | /// Construct a paths offset from this one by a delta distance. 204 | /// 205 | /// For closed paths passing a positive delta number will inflate the path 206 | /// where passing a negative number will shrink the path. 207 | /// 208 | /// **NOTE 1:** This method returns [`Paths

`](struct.Paths.html) instead 209 | /// of `Path

` as inflating a path might cause the path to split into 210 | /// several paths, or implode the complete path leaving no points in it. 211 | /// 212 | /// **NOTE 2:** Inflate calls will frequently generate a large amount of very 213 | /// close extra points and it is therefore recommented to almost always call 214 | /// [`Path::simplify`] on the path after inflating/deflating it. 215 | /// 216 | /// # Examples 217 | /// 218 | /// ```rust 219 | /// use clipper2::*; 220 | /// 221 | /// let path: Path = vec![(0.0, 0.0), (5.0, 0.0), (5.0, 6.0), (0.0, 6.0)].into(); 222 | /// let inflated_paths = path 223 | /// .inflate(1.0, JoinType::Square, EndType::Polygon, 2.0) 224 | /// .simplify(0.01, false); 225 | /// ``` 226 | /// 227 | /// For more details see the original [inflate paths](https://www.angusj.com/clipper2/Docs/Units/Clipper/Functions/InflatePaths.htm) docs. 228 | pub fn inflate( 229 | &self, 230 | delta: f64, 231 | join_type: JoinType, 232 | end_type: EndType, 233 | miter_limit: f64, 234 | ) -> Paths

{ 235 | inflate(self.clone(), delta, join_type, end_type, miter_limit) 236 | } 237 | 238 | /// Construct a new path from this one but with a reduced set of points. 239 | /// 240 | /// # Examples 241 | /// 242 | /// ```rust 243 | /// use clipper2::*; 244 | /// 245 | /// let path: Path = vec![(0.0, 0.0), (5.0, 0.002), (5.0, 0.01), (5.1, 0.0), (5.0, 6.0), (0.0, 6.0)].into(); 246 | /// let simplified = path.simplify(1.0, true); 247 | /// ``` 248 | /// 249 | /// For more details see the original [simplify](https://www.angusj.com/clipper2/Docs/Units/Clipper/Functions/SimplifyPaths.htm) docs. 250 | pub fn simplify(&self, epsilon: f64, is_open: bool) -> Self { 251 | let epsilon = P::scale(epsilon); 252 | 253 | unsafe { 254 | let mem = malloc(clipper_path64_size()); 255 | let paths_ptr = self.to_clipperpath64(); 256 | let result_ptr = clipper_path64_simplify(mem, paths_ptr, epsilon, is_open.into()); 257 | clipper_delete_path64(paths_ptr); 258 | let result = Path::from_clipperpath64(result_ptr); 259 | clipper_delete_path64(result_ptr); 260 | result 261 | } 262 | } 263 | 264 | /// The function result indicates whether the point is inside, or outside, 265 | /// or on one of the edges edges of this path. 266 | /// 267 | /// # Examples 268 | /// 269 | /// ```rust 270 | /// use clipper2::*; 271 | /// 272 | /// let path: Path = vec![(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)].into(); 273 | /// 274 | /// let output = path.is_point_inside(Point::new(0.5, 0.5)); 275 | /// 276 | /// dbg!(output); 277 | /// ``` 278 | /// 279 | /// For more details see the original [point-in-polygon](https://www.angusj.com/clipper2/Docs/Units/Clipper/Functions/PointInPolygon.htm) docs. 280 | pub fn is_point_inside(&self, point: Point

) -> PointInPolygonResult { 281 | point_in_polygon(point, self) 282 | } 283 | 284 | /// The function returns true if all points in a given path is inside this 285 | /// path. 286 | /// 287 | /// # Examples 288 | /// 289 | /// ```rust 290 | /// use clipper2::*; 291 | /// 292 | /// let path_outer: Path = vec![(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)].into(); 293 | /// let path_inner: Path = vec![(0.2, 0.2), (0.8, 0.2), (0.8, 0.8), (0.2, 0.8)].into(); 294 | /// let path_external: Path = vec![(12.2, 0.2), (0.8, 0.2), (0.8, 0.8), (0.2, 0.8)].into(); 295 | /// 296 | /// let output = path_outer.surrounds_path(&path_inner); 297 | /// assert_eq!(output, true); 298 | /// 299 | /// let output = path_outer.surrounds_path(&path_external); 300 | /// assert_eq!(output, false); 301 | /// ``` 302 | pub fn surrounds_path(&self, path: &Path

) -> bool { 303 | for p in path { 304 | if self.is_point_inside(*p) != PointInPolygonResult::IsInside { 305 | return false; 306 | } 307 | } 308 | 309 | true 310 | } 311 | 312 | /// This function returns the area of the supplied polygon. It's assumed 313 | /// that the path is closed and does not self-intersect. 314 | /// 315 | /// Depending on the path's winding orientation, this value may be positive 316 | /// or negative. Assuming paths are displayed in a Cartesian plane (with X 317 | /// values increasing heading right and Y values increasing heading up) then 318 | /// clockwise winding will have negative areas and counter-clockwise winding 319 | /// have positive areas. 320 | /// 321 | /// Conversely, when paths are displayed where Y values increase heading 322 | /// down, then clockwise paths will have positive areas, and 323 | /// counter-clockwise paths will have negative areas. 324 | /// 325 | /// # Examples 326 | /// 327 | /// ```rust 328 | /// use clipper2::*; 329 | /// 330 | /// let path: Path = vec![(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)].into(); 331 | /// 332 | /// assert_eq!(path.signed_area(), 1.0); 333 | /// ``` 334 | /// 335 | pub fn signed_area(&self) -> f64 { 336 | unsafe { clipper_path64_area(self.to_clipperpath64()) / (P::MULTIPLIER * P::MULTIPLIER) } 337 | } 338 | 339 | /// Returns the closest point on the path to a given point 340 | /// 341 | /// # Examples 342 | /// 343 | /// ```rust 344 | /// use clipper2::*; 345 | /// 346 | /// let path: Path = vec![(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)].into(); 347 | /// let closest_point = path.closest_point(Point::new(0.3, 0.3)); 348 | /// assert_eq!(closest_point, (Point::new(0.0, 0.0), 0.4242640687119285)); 349 | /// ``` 350 | pub fn closest_point(&self, point: impl Into>) -> (Point

, f64) { 351 | let point = point.into(); 352 | let mut closest_point = Point::MAX; 353 | let mut closest_distance = f64::MAX; 354 | 355 | for p in self.iter() { 356 | let distance = p.distance_to(&point); 357 | if distance < closest_distance { 358 | closest_point = *p; 359 | closest_distance = distance; 360 | } 361 | } 362 | 363 | (closest_point, closest_distance) 364 | } 365 | 366 | /// Shifts a given point to become the first point in the array 367 | /// 368 | /// ```rust 369 | /// use clipper2::*; 370 | /// 371 | /// let mut path = Path::::rectangle(-20.0, 25.0, -40.0, 30.0); 372 | /// path.shift_start_to(Point::new(-60.0, 25.0)).expect("Path not shifted"); 373 | /// ``` 374 | pub fn shift_start_to(&mut self, point: Point

) -> Result<(), PathError> { 375 | if let Some(index) = self.iter().position(|p| *p == point) { 376 | self.0.rotate_left(index); 377 | return Ok(()); 378 | } 379 | 380 | Err(PathError::PointNotInPath { 381 | x: point.x(), 382 | y: point.y(), 383 | }) 384 | } 385 | 386 | pub(crate) fn from_clipperpath64(ptr: *mut ClipperPath64) -> Self { 387 | let paths = unsafe { 388 | let len: i32 = clipper_path64_length(ptr).try_into().unwrap(); 389 | (0..len) 390 | .map(|i| clipper_path64_get_point(ptr, i).into()) 391 | .collect() 392 | }; 393 | Self::new(paths) 394 | } 395 | 396 | pub(crate) unsafe fn to_clipperpath64(&self) -> *mut ClipperPath64 { 397 | let mem = malloc(clipper_path64_size()); 398 | clipper_path64_of_points( 399 | mem, 400 | self.0 401 | .iter() 402 | .cloned() 403 | .map(|point: Point

| ClipperPoint64 { 404 | x: point.x_scaled(), 405 | y: point.y_scaled(), 406 | }) 407 | .collect::>() 408 | .as_mut_ptr(), 409 | self.len(), 410 | ) 411 | } 412 | } 413 | 414 | impl IntoIterator for Path

{ 415 | type Item = Point

; 416 | type IntoIter = std::vec::IntoIter; 417 | 418 | fn into_iter(self) -> Self::IntoIter { 419 | self.0.into_iter() 420 | } 421 | } 422 | 423 | impl FromIterator> for Path

{ 424 | fn from_iter>>(iter: T) -> Self { 425 | Path(iter.into_iter().collect()) 426 | } 427 | } 428 | 429 | impl From> for Vec> { 430 | fn from(path: Path

) -> Self { 431 | path.0.clone() 432 | } 433 | } 434 | 435 | impl From> for Vec<(f64, f64)> { 436 | fn from(path: Path

) -> Self { 437 | path.iter().map(|point| (point.x(), point.y())).collect() 438 | } 439 | } 440 | 441 | impl From> for Vec<[f64; 2]> { 442 | fn from(path: Path

) -> Self { 443 | path.iter().map(|point| [point.x(), point.y()]).collect() 444 | } 445 | } 446 | 447 | impl From>> for Path

{ 448 | fn from(points: Vec>) -> Self { 449 | Path::new(points) 450 | } 451 | } 452 | 453 | impl From> for Path

{ 454 | fn from(points: Vec<(f64, f64)>) -> Self { 455 | Path::

::new(points.iter().map(Point::

::from).collect()) 456 | } 457 | } 458 | 459 | impl From> for Path

{ 460 | fn from(points: Vec<[f64; 2]>) -> Self { 461 | Path::

::new(points.iter().map(Point::

::from).collect()) 462 | } 463 | } 464 | 465 | /// Path related errors 466 | #[derive(Debug, thiserror::Error, PartialEq)] 467 | pub enum PathError { 468 | /// The given point is not within (one of the points of) the given path 469 | #[error("Point ({x}, {y}) is not in path")] 470 | PointNotInPath { 471 | /// x coordinate of the point not within the path 472 | x: f64, 473 | /// y coordinate of the point not within the path 474 | y: f64, 475 | }, 476 | } 477 | 478 | #[cfg(test)] 479 | mod test { 480 | use crate::Deci; 481 | 482 | use super::*; 483 | 484 | #[test] 485 | fn test_default() { 486 | let path: Path = Path::default(); 487 | assert_eq!(path.len(), 0); 488 | } 489 | 490 | #[test] 491 | fn test_default_deci_precision() { 492 | let path = Path::::default(); 493 | assert_eq!(path.len(), 0); 494 | } 495 | 496 | #[test] 497 | fn test_default_as_struct_field() { 498 | #[derive(Default)] 499 | struct Foo { 500 | path: Path, 501 | } 502 | 503 | let path = Foo::default(); 504 | assert_eq!(path.path.len(), 0); 505 | } 506 | 507 | #[test] 508 | fn test_negative_inflate_removing_imploded_paths() { 509 | let path: Path = vec![(0.0, 0.0), (5.0, 0.0), (5.0, 6.0), (0.0, 6.0)].into(); 510 | let delta = -3.0; 511 | let result = path 512 | .inflate(delta, JoinType::Round, EndType::Polygon, 0.0) 513 | .simplify(0.01, false); 514 | 515 | assert_eq!(result.len(), 0); 516 | } 517 | 518 | #[test] 519 | fn test_from() { 520 | let path = Path::::from(vec![(0.0, 0.0), (1.0, 1.0)]); 521 | let output: Vec<(f64, f64)> = path.into(); 522 | assert_eq!(output, vec![(0.0, 0.0), (1.0, 1.0)]); 523 | } 524 | 525 | #[test] 526 | fn test_from_custom_scaler() { 527 | #[derive(Debug, Default, Clone, Copy, PartialEq, Hash)] 528 | struct CustomScaler; 529 | 530 | impl PointScaler for CustomScaler { 531 | const MULTIPLIER: f64 = 1000.0; 532 | } 533 | 534 | let path = Path::::from(vec![(0.0, 0.0), (1.0, 1.0)]); 535 | let output: Vec<(f64, f64)> = path.clone().into(); 536 | assert_eq!(output, vec![(0.0, 0.0), (1.0, 1.0)]); 537 | assert_eq!(path.0[0].x_scaled(), 0); 538 | assert_eq!(path.0[0].y_scaled(), 0); 539 | assert_eq!(path.0[1].x_scaled(), 1000); 540 | assert_eq!(path.0[1].y_scaled(), 1000); 541 | } 542 | 543 | #[test] 544 | fn test_into_iterator() { 545 | let path = Path::::from(vec![(0.0, 0.0), (1.0, 1.0)]); 546 | 547 | let mut count = 0; 548 | 549 | for point in path { 550 | assert_eq!(point.x(), point.y()); 551 | count += 1; 552 | } 553 | 554 | assert_eq!(count, 2); 555 | } 556 | 557 | #[test] 558 | fn test_iter() { 559 | let path = Path::::from(vec![(0.0, 0.0), (1.0, 1.0)]); 560 | 561 | let mut count = 0; 562 | 563 | for point in path.iter() { 564 | assert_eq!(point.x(), point.y()); 565 | count += 1; 566 | } 567 | 568 | assert_eq!(count, 2); 569 | 570 | let x_values: Vec<_> = path.iter().map(|point| point.x()).collect(); 571 | assert_eq!(x_values, vec![0.0, 1.0]); 572 | } 573 | 574 | #[test] 575 | fn test_into_iter() { 576 | let path = Path::::from(vec![(0.0, 0.0), (1.0, 1.0)]); 577 | 578 | let mut count = 0; 579 | 580 | for point in path.clone().into_iter() { 581 | assert_eq!(point.x(), point.y()); 582 | count += 1; 583 | } 584 | 585 | assert_eq!(count, 2); 586 | 587 | let x_values: Vec<_> = path.into_iter().map(|point| point.x()).collect(); 588 | assert_eq!(x_values, vec![0.0, 1.0]); 589 | } 590 | 591 | #[test] 592 | fn test_signed_area() { 593 | let path = Path::::rectangle(10.0, 20.0, 30.0, 15.0); 594 | let area = path.signed_area(); 595 | assert_eq!(area, 450.0); 596 | } 597 | 598 | #[test] 599 | fn test_signed_area_negative() { 600 | let path = Path::::rectangle(-20.0, 25.0, -40.0, 30.0); 601 | let area = path.signed_area(); 602 | assert_eq!(area, -1200.0); 603 | } 604 | 605 | #[cfg(feature = "serde")] 606 | #[test] 607 | fn test_serde() { 608 | let path = Path::::from(vec![(0.0, 0.0), (1.0, 1.0)]); 609 | let serialized = serde_json::to_string(&path).unwrap(); 610 | assert_eq!(serialized, r#"[{"x":0,"y":0},{"x":100,"y":100}]"#); 611 | 612 | let deserialized: Path = serde_json::from_str(&serialized).unwrap(); 613 | assert_eq!(deserialized, path); 614 | } 615 | 616 | #[test] 617 | fn test_shift_start_to() { 618 | let mut path = Path::::rectangle(-20.0, 25.0, -40.0, 30.0); 619 | let mut iter = path.iter(); 620 | assert_eq!(iter.next(), Some(&Point::new(-20.0, 25.0))); 621 | assert_eq!(iter.next(), Some(&Point::new(-60.0, 25.0))); 622 | assert_eq!(iter.next(), Some(&Point::new(-60.0, 55.0))); 623 | assert_eq!(iter.next(), Some(&Point::new(-20.0, 55.0))); 624 | assert_eq!(iter.next(), None); 625 | 626 | let path_shift_result = path.shift_start_to(Point::new(-660.0, 155.0)); 627 | assert_eq!( 628 | path_shift_result.err(), 629 | Some(PathError::PointNotInPath { 630 | x: -660.0, 631 | y: 155.0 632 | }) 633 | ); 634 | 635 | path.shift_start_to(Point::new(-60.0, 55.0)).unwrap(); 636 | let mut iter = path.iter(); 637 | assert_eq!(iter.next(), Some(&Point::new(-60.0, 55.0))); 638 | assert_eq!(iter.next(), Some(&Point::new(-20.0, 55.0))); 639 | assert_eq!(iter.next(), Some(&Point::new(-20.0, 25.0))); 640 | assert_eq!(iter.next(), Some(&Point::new(-60.0, 25.0))); 641 | assert_eq!(iter.next(), None); 642 | } 643 | 644 | #[test] 645 | fn test_closest_point() { 646 | let path = Path::::rectangle(10.0, 5.0, 30.0, 30.0); 647 | let closest_point = path.closest_point(Point::new(15.0, 7.0)); 648 | assert_eq!(closest_point, (Point::new(10.0, 5.0), 5.385164807134504)); 649 | } 650 | } 651 | -------------------------------------------------------------------------------- /src/paths.rs: -------------------------------------------------------------------------------- 1 | use clipper2c_sys::{ 2 | clipper_delete_path64, clipper_paths64_area, clipper_paths64_get_point, clipper_paths64_length, 3 | clipper_paths64_of_paths, clipper_paths64_path_length, clipper_paths64_size, ClipperPath64, 4 | ClipperPaths64, 5 | }; 6 | 7 | use crate::{ 8 | inflate, malloc, simplify, Bounds, Centi, Clipper, EndType, JoinType, Path, Point, PointScaler, 9 | WithSubjects, 10 | }; 11 | 12 | /// A collection of paths. 13 | /// 14 | /// # Examples 15 | /// 16 | /// ```rust 17 | /// use clipper2::*; 18 | /// 19 | /// let paths_from_single_vec: Paths = vec![(0.0, 0.0), (5.0, 0.0), (5.0, 6.0), (0.0, 6.0)].into(); 20 | /// let paths_from_vec_of_vecs: Paths = vec![vec![(0.0, 0.0), (5.0, 0.0), (5.0, 6.0), (0.0, 6.0)]].into(); 21 | /// ``` 22 | #[derive(Debug, Default, Clone, PartialEq, Eq, Hash)] 23 | #[cfg_attr( 24 | feature = "serde", 25 | derive(serde::Serialize, serde::Deserialize), 26 | serde(bound = "P: PointScaler") 27 | )] 28 | pub struct Paths(Vec>); 29 | 30 | impl Paths

{ 31 | /// Create a new paths from a vector of paths. 32 | pub fn new(paths: Vec>) -> Self { 33 | Paths(paths) 34 | } 35 | 36 | /// In place push paths onto this set of paths. 37 | pub fn push(&mut self, paths: impl Into>) { 38 | for path in paths.into() { 39 | self.0.push(path); 40 | } 41 | } 42 | 43 | /// Append another set of paths onto this one, cloning the other set. 44 | pub fn append(&mut self, paths: impl Into>>) { 45 | let mut paths = paths.into(); 46 | self.0.append(&mut paths); 47 | } 48 | 49 | /// Returns the number of paths. 50 | pub fn len(&self) -> usize { 51 | self.0.len() 52 | } 53 | 54 | /// Returns `true` if there are no paths added. 55 | pub fn is_empty(&self) -> bool { 56 | self.0.is_empty() 57 | } 58 | 59 | /// Returns `true` if at least one of the paths contains a point. 60 | pub fn contains_points(&self) -> bool { 61 | for path in &self.0 { 62 | if !path.is_empty() { 63 | return true; 64 | } 65 | } 66 | 67 | false 68 | } 69 | 70 | /// Returns a reference to the first path in the set of paths wrapped in an 71 | /// option. 72 | pub fn first(&self) -> Option<&Path

> { 73 | self.iter().next() 74 | } 75 | 76 | /// Returns a reference to the path at the given index in the set of paths 77 | /// wrapped in an option. 78 | pub fn get(&self, index: usize) -> Option<&Path

> { 79 | self.0.get(index) 80 | } 81 | 82 | /// Returns an iterator over the paths in the paths. 83 | pub fn iter(&self) -> std::slice::Iter<'_, Path

> { 84 | self.0.iter() 85 | } 86 | 87 | /// Construct a clone with each point offset by a x/y distance. 88 | pub fn translate(&self, x: f64, y: f64) -> Self { 89 | Self::new(self.0.iter().map(|p| p.translate(x, y)).collect()) 90 | } 91 | 92 | /// Construct a scaled clone of the path with the origin at the path center. 93 | pub fn scale(&self, scale_x: f64, scale_y: f64) -> Self { 94 | let center = self.bounds().center(); 95 | self.scale_around_point(scale_x, scale_y, center) 96 | } 97 | 98 | /// Construct a scaled clone of the path with the origin at a given point. 99 | pub fn scale_around_point(&self, scale_x: f64, scale_y: f64, point: Point

) -> Self { 100 | Self::new( 101 | self.0 102 | .iter() 103 | .map(|p| p.scale_around_point(scale_x, scale_y, point)) 104 | .collect(), 105 | ) 106 | } 107 | 108 | /// Construct a rotated clone of the path with the origin at the path 109 | /// center. 110 | pub fn rotate(&self, radians: f64) -> Self { 111 | Self::new(self.0.iter().map(|p| p.rotate(radians)).collect()) 112 | } 113 | 114 | /// Construct a clone with each point x value flipped. 115 | pub fn flip_x(&self) -> Self { 116 | Self::new(self.0.iter().map(|p| p.flip_x()).collect()) 117 | } 118 | 119 | /// Construct a clone with each point y value flipped. 120 | pub fn flip_y(&self) -> Self { 121 | Self::new(self.0.iter().map(|p| p.flip_y()).collect()) 122 | } 123 | 124 | /// Returns the bounds for this path. 125 | pub fn bounds(&self) -> Bounds

{ 126 | let mut bounds = Bounds::minmax(); 127 | 128 | for p in &self.0 { 129 | let b = p.bounds(); 130 | let min_x = b.min.x(); 131 | let min_y = b.min.y(); 132 | let max_x = b.max.x(); 133 | let max_y = b.max.y(); 134 | 135 | if min_x < bounds.min.x() { 136 | bounds.min = Point::new(min_x, bounds.min.y()); 137 | } 138 | 139 | if min_y < bounds.min.y() { 140 | bounds.min = Point::new(bounds.min.x(), min_y); 141 | } 142 | 143 | if max_x > bounds.max.x() { 144 | bounds.max = Point::new(max_x, bounds.max.y()); 145 | } 146 | 147 | if max_y > bounds.max.y() { 148 | bounds.max = Point::new(bounds.max.x(), max_y); 149 | } 150 | } 151 | 152 | bounds 153 | } 154 | 155 | /// Construct a new set of paths offset from this one by a delta distance. 156 | /// 157 | /// For closed paths passing a positive delta number will inflate the path 158 | /// where passing a negative number will shrink the path. 159 | /// 160 | /// **NOTE:** Inflate calls will frequently generate a large amount of very 161 | /// close extra points and it is therefore recommented to almost always call 162 | /// [`Paths::simplify`] on the path after inflating/shrinking it. 163 | /// 164 | /// # Examples 165 | /// 166 | /// ```rust 167 | /// use clipper2::*; 168 | /// 169 | /// let paths: Paths = vec![vec![(0.0, 0.0), (5.0, 0.0), (5.0, 6.0), (0.0, 6.0)]].into(); 170 | /// let inflated = paths 171 | /// .inflate(1.0, JoinType::Square, EndType::Polygon, 2.0) 172 | /// .simplify(0.01, false); 173 | /// ``` 174 | /// 175 | /// For more details see the original [inflate paths](https://www.angusj.com/clipper2/Docs/Units/Clipper/Functions/InflatePaths.htm) docs. 176 | pub fn inflate( 177 | &self, 178 | delta: f64, 179 | join_type: JoinType, 180 | end_type: EndType, 181 | miter_limit: f64, 182 | ) -> Self { 183 | inflate(self.clone(), delta, join_type, end_type, miter_limit) 184 | } 185 | 186 | /// Construct a new set of paths from these ones but with a reduced set of 187 | /// points. 188 | /// 189 | /// # Examples 190 | /// 191 | /// ```rust 192 | /// use clipper2::*; 193 | /// 194 | /// let paths: Paths = vec![vec![(0.0, 0.0), (5.0, 0.002), (5.0, 0.01), (5.1, 0.0), (5.0, 6.0), (0.0, 6.0)]].into(); 195 | /// let simplified = paths.simplify(1.0, true); 196 | /// ``` 197 | /// 198 | /// For more details see the original [simplify](https://www.angusj.com/clipper2/Docs/Units/Clipper/Functions/SimplifyPaths.htm) docs. 199 | pub fn simplify(&self, epsilon: f64, is_open: bool) -> Self { 200 | simplify(self.clone(), epsilon, is_open) 201 | } 202 | 203 | /// Create a [`Clipper`] builder with this set of paths as the subject that 204 | /// will allow for making boolean operations on this set of paths. 205 | /// 206 | /// # Examples 207 | /// 208 | /// ```rust 209 | /// use clipper2::*; 210 | /// 211 | /// let path: Paths = vec![vec![(0.0, 0.0), (5.0, 6.0), (0.0, 6.0)]].into(); 212 | /// let path2: Paths = vec![vec![(1.0, 1.0), (4.0, 1.0), (1.0, 4.0)]].into(); 213 | /// let result = path.to_clipper_subject().add_clip(path2).union(FillRule::default()); 214 | /// ``` 215 | pub fn to_clipper_subject(&self) -> Clipper { 216 | let clipper = Clipper::new(); 217 | clipper.add_subject(self.clone()) 218 | } 219 | 220 | /// Create a [`Clipper`] builder with this set of paths as the open subject 221 | /// that will allow for making boolean operations on this set of paths. 222 | /// 223 | /// # Examples 224 | /// 225 | /// ```rust 226 | /// use clipper2::*; 227 | /// 228 | /// let path: Paths = vec![vec![(0.0, 0.0), (5.0, 6.0), (0.0, 6.0)]].into(); 229 | /// let path2: Paths = vec![vec![(1.0, 1.0), (4.0, 1.0), (1.0, 4.0)]].into(); 230 | /// let result = path.to_clipper_open_subject().add_clip(path2).difference(FillRule::default()); 231 | /// ``` 232 | pub fn to_clipper_open_subject(&self) -> Clipper { 233 | let clipper = Clipper::new(); 234 | clipper.add_open_subject(self.clone()) 235 | } 236 | 237 | /// This function returns the area of the supplied paths. It's assumed 238 | /// that the paths are closed and do not self-intersect. 239 | /// 240 | /// Depending on the paths' winding orientations, this value may be positive 241 | /// or negative. Assuming paths are displayed in a Cartesian plane (with X 242 | /// values increasing heading right and Y values increasing heading up) then 243 | /// clockwise winding will have negative areas and counter-clockwise winding 244 | /// have positive areas. 245 | /// 246 | /// Conversely, when paths are displayed where Y values increase heading 247 | /// down, then clockwise paths will have positive areas, and 248 | /// counter-clockwise paths will have negative areas. 249 | /// 250 | /// # Examples 251 | /// 252 | /// ```rust 253 | /// use clipper2::*; 254 | /// 255 | /// let paths: Paths = vec![vec![(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)]].into(); 256 | /// 257 | /// assert_eq!(paths.signed_area(), 1.0); 258 | /// ``` 259 | /// 260 | pub fn signed_area(&self) -> f64 { 261 | unsafe { clipper_paths64_area(self.to_clipperpaths64()) / (P::MULTIPLIER * P::MULTIPLIER) } 262 | } 263 | 264 | pub(crate) fn from_clipperpaths64(ptr: *mut ClipperPaths64) -> Self { 265 | let paths = unsafe { 266 | let len: i32 = clipper_paths64_length(ptr).try_into().unwrap(); 267 | (0..len) 268 | .map(|i| { 269 | let point_len: i32 = clipper_paths64_path_length(ptr, i).try_into().unwrap(); 270 | let points = (0..point_len) 271 | .map(|j| clipper_paths64_get_point(ptr, i, j).into()) 272 | .collect(); 273 | Path::new(points) 274 | }) 275 | .collect() 276 | }; 277 | Self::new(paths) 278 | } 279 | 280 | pub(crate) unsafe fn to_clipperpaths64(&self) -> *mut ClipperPaths64 { 281 | let mem = malloc(clipper_paths64_size()); 282 | let mut paths = self 283 | .iter() 284 | .map(|p| p.to_clipperpath64()) 285 | .collect::>(); 286 | 287 | let result = clipper_paths64_of_paths(mem, paths.as_mut_ptr(), self.len()); 288 | 289 | for path in paths { 290 | clipper_delete_path64(path); 291 | } 292 | 293 | result 294 | } 295 | } 296 | 297 | impl<'a, P: PointScaler> IntoIterator for &'a Path

{ 298 | type Item = &'a Point

; 299 | type IntoIter = std::slice::Iter<'a, Point

>; 300 | 301 | fn into_iter(self) -> Self::IntoIter { 302 | self.iter() 303 | } 304 | } 305 | 306 | impl IntoIterator for Paths

{ 307 | type Item = Path

; 308 | type IntoIter = std::vec::IntoIter; 309 | 310 | fn into_iter(self) -> Self::IntoIter { 311 | self.0.into_iter() 312 | } 313 | } 314 | 315 | impl FromIterator> for Paths

{ 316 | fn from_iter>>(iter: T) -> Self { 317 | Paths(iter.into_iter().collect()) 318 | } 319 | } 320 | 321 | impl From> for Paths

{ 322 | fn from(path: Path

) -> Self { 323 | vec![path].into() 324 | } 325 | } 326 | 327 | impl From> for Vec> { 328 | fn from(paths: Paths

) -> Self { 329 | paths.0.clone() 330 | } 331 | } 332 | 333 | impl From> for Vec> { 334 | fn from(paths: Paths

) -> Self { 335 | paths 336 | .iter() 337 | .map(|path| path.iter().map(|point| (point.x(), point.y())).collect()) 338 | .collect() 339 | } 340 | } 341 | 342 | impl From> for Vec> { 343 | fn from(paths: Paths

) -> Self { 344 | paths 345 | .iter() 346 | .map(|path| path.iter().map(|point| [point.x(), point.y()]).collect()) 347 | .collect() 348 | } 349 | } 350 | 351 | impl From>>> for Paths

{ 352 | fn from(points: Vec>>) -> Self { 353 | Paths::

::new(points.into_iter().map(|path| path.into()).collect()) 354 | } 355 | } 356 | 357 | impl From>> for Paths

{ 358 | fn from(points: Vec>) -> Self { 359 | Paths::

::new(points.into_iter().map(|path| path.into()).collect()) 360 | } 361 | } 362 | 363 | impl From>> for Paths

{ 364 | fn from(points: Vec>) -> Self { 365 | Paths::

::new(points.into_iter().map(|path| path.into()).collect()) 366 | } 367 | } 368 | 369 | impl From>> for Paths

{ 370 | fn from(points: Vec>) -> Self { 371 | Paths::

::new(vec![points.into()]) 372 | } 373 | } 374 | 375 | impl From> for Paths

{ 376 | fn from(points: Vec<(f64, f64)>) -> Self { 377 | Paths::

::new(vec![points.into()]) 378 | } 379 | } 380 | 381 | impl From> for Paths

{ 382 | fn from(points: Vec<[f64; 2]>) -> Self { 383 | Paths::

::new(vec![points.into()]) 384 | } 385 | } 386 | 387 | impl From>> for Paths

{ 388 | fn from(points: Vec>) -> Self { 389 | Paths::

::new(points) 390 | } 391 | } 392 | 393 | #[cfg(test)] 394 | mod test { 395 | use crate::Deci; 396 | 397 | use super::*; 398 | 399 | #[test] 400 | fn test_default() { 401 | let paths: Paths = Paths::default(); 402 | assert_eq!(paths.len(), 0); 403 | } 404 | 405 | #[test] 406 | fn test_default_deci_precision() { 407 | let paths = Paths::::default(); 408 | assert_eq!(paths.len(), 0); 409 | } 410 | 411 | #[test] 412 | fn test_default_as_struct_field() { 413 | #[derive(Default)] 414 | struct Foo { 415 | paths: Paths, 416 | } 417 | 418 | let paths = Foo::default(); 419 | assert_eq!(paths.paths.len(), 0); 420 | } 421 | 422 | #[test] 423 | fn test_from() { 424 | let paths = Paths::::from(vec![(0.4, 0.0), (5.0, 1.0)]); 425 | let output: Vec> = paths.clone().into(); 426 | assert_eq!(output, vec![vec![(0.4, 0.0), (5.0, 1.0)]]); 427 | 428 | let mut path_iter = paths.iter().next().unwrap().iter(); 429 | let point1 = path_iter.next().unwrap(); 430 | let point2 = path_iter.next().unwrap(); 431 | assert_eq!(point1.x_scaled(), 40); 432 | assert_eq!(point1.y_scaled(), 0); 433 | assert_eq!(point2.x_scaled(), 500); 434 | assert_eq!(point2.y_scaled(), 100); 435 | } 436 | 437 | #[test] 438 | fn test_from_custom_scaler() { 439 | #[derive(Debug, Default, Clone, Copy, PartialEq, Hash)] 440 | struct CustomScaler; 441 | 442 | impl PointScaler for CustomScaler { 443 | const MULTIPLIER: f64 = 1000.0; 444 | } 445 | 446 | let paths = Paths::::from(vec![(0.0, 0.6), (1.0, 2.0)]); 447 | let output: Vec> = paths.clone().into(); 448 | assert_eq!(output, vec![vec![(0.0, 0.6), (1.0, 2.0)]]); 449 | 450 | let mut path_iter = paths.iter().next().unwrap().iter(); 451 | let point1 = path_iter.next().unwrap(); 452 | let point2 = path_iter.next().unwrap(); 453 | assert_eq!(point1.x_scaled(), 0); 454 | assert_eq!(point1.y_scaled(), 600); 455 | assert_eq!(point2.x_scaled(), 1000); 456 | assert_eq!(point2.y_scaled(), 2000); 457 | } 458 | 459 | #[test] 460 | fn test_into_iterator() { 461 | let paths = Paths::::from(vec![vec![(0.0, 0.0), (1.0, 1.0)]; 2]); 462 | for path in paths { 463 | assert_eq!(path.len(), 2); 464 | } 465 | } 466 | 467 | #[test] 468 | fn test_iter() { 469 | let paths = Paths::::from(vec![vec![(0.0, 0.0), (1.0, 1.0)]; 2]); 470 | 471 | let mut paths_iterator = paths.iter(); 472 | assert_eq!( 473 | paths_iterator.next(), 474 | Some(&Path::from(vec![(0.0, 0.0), (1.0, 1.0)])) 475 | ); 476 | assert_eq!( 477 | paths_iterator.next(), 478 | Some(&Path::from(vec![(0.0, 0.0), (1.0, 1.0)])) 479 | ); 480 | assert_eq!(paths_iterator.next(), None); 481 | 482 | let x_values: Vec<_> = paths.iter().flatten().map(|point| point.x()).collect(); 483 | assert_eq!(x_values, vec![0.0, 1.0, 0.0, 1.0]); 484 | } 485 | 486 | #[test] 487 | fn test_into_iter() { 488 | let paths = Paths::::from(vec![vec![(0.0, 0.0), (1.0, 1.0)]; 2]); 489 | 490 | let mut paths_iterator = paths.iter(); 491 | assert_eq!( 492 | paths_iterator.next(), 493 | Some(&Path::from(vec![(0.0, 0.0), (1.0, 1.0)])) 494 | ); 495 | assert_eq!( 496 | paths_iterator.next(), 497 | Some(&Path::from(vec![(0.0, 0.0), (1.0, 1.0)])) 498 | ); 499 | assert_eq!(paths_iterator.next(), None); 500 | 501 | let x_values: Vec<_> = paths.into_iter().flatten().map(|point| point.x()).collect(); 502 | assert_eq!(x_values, vec![0.0, 1.0, 0.0, 1.0]); 503 | } 504 | 505 | #[test] 506 | fn test_signed_area() { 507 | let paths = Paths::new(vec![ 508 | Path::::rectangle(10.0, 20.0, 30.0, 150.0), 509 | Path::::rectangle(40.0, 20.0, 10.0, 15.0), 510 | ]); 511 | let area = paths.signed_area(); 512 | assert_eq!(area, 4650.0); 513 | } 514 | 515 | #[test] 516 | fn test_signed_area_negative() { 517 | let paths = Paths::new(vec![ 518 | Path::::rectangle(-20.0, 25.0, -45.0, 30.0), 519 | Path::::rectangle(-20.0, 55.0, 15.0, 15.0), 520 | ]); 521 | let area = paths.signed_area(); 522 | assert_eq!(area, -1125.0); 523 | } 524 | 525 | #[test] 526 | fn test_signed_area_counts_overlapping_areas_comulatively_for_each_path() { 527 | let paths = Paths::new(vec![ 528 | Path::::rectangle(10.0, 20.0, 30.0, 150.0), 529 | Path::::rectangle(10.0, 20.0, 100.0, 15.0), 530 | ]); 531 | let area = paths.signed_area(); 532 | assert_eq!(area, 6000.0); 533 | } 534 | 535 | #[test] 536 | fn test_scale_two_separate_triangles() { 537 | let paths = Paths::::from(vec![ 538 | vec![(0.0, 0.0), (1.0, 0.0), (0.0, 1.0)], 539 | vec![(10.0, 10.0), (11.0, 10.0), (10.0, 11.0)], 540 | ]); 541 | 542 | let scaled = paths.scale(4.0, 2.0); 543 | 544 | let expected_output = Paths::from(vec![ 545 | vec![(-16.5, -5.5), (-12.5, -5.5), (-16.5, -3.5)], 546 | vec![(23.5, 14.5), (27.5, 14.5), (23.5, 16.5)], 547 | ]); 548 | 549 | assert_eq!(scaled, expected_output); 550 | } 551 | 552 | #[test] 553 | fn test_scale_overlapping_rectangles() { 554 | let paths = Paths::::from(vec![ 555 | Path::rectangle(-10.0, -20.0, 20.0, 40.0), 556 | Path::rectangle(-20.0, -10.0, 40.0, 20.0), 557 | ]); 558 | let scaled = paths.scale(4.0, 2.0); 559 | 560 | let expected_output = Paths::from(vec![ 561 | vec![(-40.0, -40.0), (40.0, -40.0), (40.0, 40.0), (-40.0, 40.0)], 562 | vec![(-80.0, -20.0), (80.0, -20.0), (80.0, 20.0), (-80.0, 20.0)], 563 | ]); 564 | 565 | assert_eq!(scaled, expected_output); 566 | } 567 | 568 | #[test] 569 | fn test_scale_around_point() { 570 | let paths = Paths::::from(vec![ 571 | Path::rectangle(-10.0, -20.0, 20.0, 40.0), 572 | Path::rectangle(-20.0, -10.0, 40.0, 20.0), 573 | ]); 574 | 575 | let scaled = paths.scale_around_point(4.0, 2.0, Point::new(-10.0, -20.0)); 576 | 577 | let expected_output = Paths::from(vec![ 578 | vec![(-10.0, -20.0), (70.0, -20.0), (70.0, 60.0), (-10.0, 60.0)], 579 | vec![(-50.0, 0.0), (110.0, 0.0), (110.0, 40.0), (-50.0, 40.0)], 580 | ]); 581 | 582 | assert_eq!(scaled, expected_output); 583 | } 584 | 585 | #[test] 586 | fn test_from_iterator() { 587 | let paths = vec![ 588 | Path::rectangle(-10.0, -20.0, 20.0, 40.0), 589 | Path::rectangle(-20.0, -10.0, 40.0, 20.0), 590 | ] 591 | .into_iter() 592 | .collect::(); 593 | 594 | assert_eq!(paths.len(), 2); 595 | } 596 | 597 | #[cfg(feature = "serde")] 598 | #[test] 599 | fn test_serde() { 600 | let paths = Paths::::from(vec![(0.4, 0.0), (5.0, 1.0)]); 601 | let serialized = serde_json::to_string(&paths).unwrap(); 602 | assert_eq!(serialized, r#"[[{"x":40,"y":0},{"x":500,"y":100}]]"#); 603 | 604 | let deserialized: Paths = serde_json::from_str(&serialized).unwrap(); 605 | assert_eq!(deserialized, paths); 606 | } 607 | } 608 | -------------------------------------------------------------------------------- /src/point.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | use clipper2c_sys::ClipperPoint64; 4 | 5 | /// The point scaling trait allows to choose a multiplier for the point values 6 | /// at compile time. 7 | /// 8 | /// The default multiplier is `Centi`, and others are provided by the library, 9 | /// but if needed the user can create a custom scaler struct that implements 10 | /// `PointScaler`. 11 | pub trait PointScaler: Default + Clone + Copy + PartialEq + std::hash::Hash { 12 | /// The point multiplier. This is set to a custom value when implementing 13 | /// the `PointScaler` trait. 14 | const MULTIPLIER: f64; 15 | 16 | /// Scale a value by the multiplier. 17 | fn scale(value: f64) -> f64 { 18 | value * Self::MULTIPLIER 19 | } 20 | 21 | /// Descale/unscale a value by the multiplier. 22 | fn descale(value: f64) -> f64 { 23 | value / Self::MULTIPLIER 24 | } 25 | } 26 | 27 | /// No scaling. 28 | #[derive(Debug, Default, Copy, Clone, PartialEq, Hash)] 29 | pub struct One; 30 | 31 | impl PointScaler for One { 32 | const MULTIPLIER: f64 = 1.0; 33 | } 34 | 35 | /// Scale by 10. 36 | #[derive(Debug, Default, Copy, Clone, PartialEq, Hash)] 37 | pub struct Deci; 38 | 39 | impl PointScaler for Deci { 40 | const MULTIPLIER: f64 = 10.0; 41 | } 42 | 43 | /// Scale by 100. This is the default. 44 | #[derive(Debug, Default, Copy, Clone, PartialEq, Hash)] 45 | pub struct Centi; 46 | 47 | impl PointScaler for Centi { 48 | const MULTIPLIER: f64 = 100.0; 49 | } 50 | 51 | /// Scale by 1000. 52 | #[derive(Debug, Default, Copy, Clone, PartialEq, Hash)] 53 | pub struct Milli; 54 | 55 | impl PointScaler for Milli { 56 | const MULTIPLIER: f64 = 1000.0; 57 | } 58 | 59 | /// XY Point with custom scaler. 60 | /// 61 | /// For 62 | /// [rubustness reasons](https://www.angusj.com/clipper2/Docs/Robustness.htm) 63 | /// clipper2 uses 64bit integers to store coordinates. 64 | /// 65 | /// Therefore you can choose a implementation of PointScaler for your 66 | /// use-case. This library offers `One`, `Deci`, `Centi` and `Milli` multipliers 67 | /// where `Centi` is the default (multiplies values by 100 when converting to 68 | /// i64). 69 | /// 70 | /// # Examples 71 | /// 72 | /// ```rust 73 | /// use clipper2::*; 74 | /// 75 | /// let point = Point::::new(1.0, 2.0); 76 | /// assert_eq!(point.x(), 1.0); 77 | /// assert_eq!(point.y(), 2.0); 78 | /// assert_eq!(point.x_scaled(), 100); 79 | /// assert_eq!(point.y_scaled(), 200); 80 | /// 81 | /// let point = Point::::new(1.0, 2.0); 82 | /// assert_eq!(point.x(), 1.0); 83 | /// assert_eq!(point.y(), 2.0); 84 | /// assert_eq!(point.x_scaled(), 1000); 85 | /// assert_eq!(point.y_scaled(), 2000); 86 | /// ``` 87 | #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] 88 | #[cfg_attr( 89 | feature = "serde", 90 | derive(serde::Serialize, serde::Deserialize), 91 | serde(transparent), 92 | serde(bound = "P: PointScaler") 93 | )] 94 | pub struct Point( 95 | ClipperPoint64, 96 | #[cfg_attr(feature = "serde", serde(skip))] PhantomData

, 97 | ); 98 | 99 | impl Point

{ 100 | /// The zero point. 101 | pub const ZERO: Self = Self(ClipperPoint64 { x: 0, y: 0 }, PhantomData); 102 | 103 | /// The minimum value for a point. 104 | pub const MIN: Self = Self( 105 | ClipperPoint64 { 106 | x: i64::MIN, 107 | y: i64::MIN, 108 | }, 109 | PhantomData, 110 | ); 111 | 112 | /// The maximum value for a point. 113 | pub const MAX: Self = Self( 114 | ClipperPoint64 { 115 | x: i64::MAX, 116 | y: i64::MAX, 117 | }, 118 | PhantomData, 119 | ); 120 | 121 | /// Create a new point. 122 | pub fn new(x: f64, y: f64) -> Self { 123 | Self( 124 | ClipperPoint64 { 125 | x: P::scale(x).round() as i64, 126 | y: P::scale(y).round() as i64, 127 | }, 128 | PhantomData, 129 | ) 130 | } 131 | 132 | /// Create a new point from scaled values, this means that point is 133 | /// constructed as is without applying the scaling multiplier. 134 | pub fn from_scaled(x: i64, y: i64) -> Self { 135 | Self(ClipperPoint64 { x, y }, PhantomData) 136 | } 137 | 138 | /// Returns the x coordinate of the point. 139 | pub fn x(&self) -> f64 { 140 | P::descale(self.0.x as f64) 141 | } 142 | 143 | /// Returns the y coordinate of the point. 144 | pub fn y(&self) -> f64 { 145 | P::descale(self.0.y as f64) 146 | } 147 | 148 | /// Returns the scaled x coordinate of the point. 149 | pub fn x_scaled(&self) -> i64 { 150 | self.0.x 151 | } 152 | 153 | /// Returns the scaled y coordinate of the point. 154 | pub fn y_scaled(&self) -> i64 { 155 | self.0.y 156 | } 157 | 158 | /// Calculate the distance to another point. 159 | pub fn distance_to(&self, to: &Self) -> f64 { 160 | ((self.x() - to.x()).powf(2.0) + (self.y() - to.y()).powf(2.0)).sqrt() 161 | } 162 | 163 | pub(crate) fn as_clipperpoint64(&self) -> *const ClipperPoint64 { 164 | &self.0 165 | } 166 | } 167 | 168 | impl Default for Point

{ 169 | fn default() -> Self { 170 | Self::ZERO 171 | } 172 | } 173 | 174 | impl From for Point

{ 175 | fn from(point: ClipperPoint64) -> Self { 176 | Self(point, PhantomData) 177 | } 178 | } 179 | 180 | impl From> for ClipperPoint64 { 181 | fn from(point: Point

) -> Self { 182 | point.0 183 | } 184 | } 185 | 186 | impl From<(f64, f64)> for Point

{ 187 | fn from((x, y): (f64, f64)) -> Self { 188 | Self::new(x, y) 189 | } 190 | } 191 | 192 | impl From<&(f64, f64)> for Point

{ 193 | fn from((x, y): &(f64, f64)) -> Self { 194 | Self::new(*x, *y) 195 | } 196 | } 197 | 198 | impl From<[f64; 2]> for Point

{ 199 | fn from([x, y]: [f64; 2]) -> Self { 200 | Self::new(x, y) 201 | } 202 | } 203 | 204 | impl From<&[f64; 2]> for Point

{ 205 | fn from([x, y]: &[f64; 2]) -> Self { 206 | Self::new(*x, *y) 207 | } 208 | } 209 | 210 | impl From> for (f64, f64) { 211 | fn from(point: Point

) -> Self { 212 | (point.x(), point.y()) 213 | } 214 | } 215 | 216 | impl From> for [f64; 2] { 217 | fn from(point: Point

) -> Self { 218 | [point.x(), point.y()] 219 | } 220 | } 221 | 222 | #[cfg(test)] 223 | mod test { 224 | use super::*; 225 | 226 | #[test] 227 | fn test_point_default_multiplier() { 228 | let point = Point::::new(1.0, 2.0); 229 | assert_eq!(point.x(), 1.0); 230 | assert_eq!(point.y(), 2.0); 231 | assert_eq!(point.x_scaled(), 100); 232 | assert_eq!(point.y_scaled(), 200); 233 | } 234 | 235 | #[test] 236 | fn test_point_multiplier_rounding() { 237 | let point = Point::::new(2.05, 2.125); 238 | assert_eq!(point.x_scaled(), 205); 239 | assert_eq!(point.y_scaled(), 213); 240 | } 241 | 242 | #[test] 243 | fn test_point_custom_scaler() { 244 | #[derive(Debug, Default, Clone, Copy, PartialEq, Hash)] 245 | struct CustomScaler; 246 | 247 | impl PointScaler for CustomScaler { 248 | const MULTIPLIER: f64 = 2000.0; 249 | } 250 | 251 | let point = Point::::new(1.0, 2.0); 252 | assert_eq!(point.x(), 1.0); 253 | assert_eq!(point.y(), 2.0); 254 | assert_eq!(point.x_scaled(), 2000); 255 | assert_eq!(point.y_scaled(), 4000); 256 | } 257 | 258 | #[cfg(feature = "serde")] 259 | #[test] 260 | fn test_serde() { 261 | let point = Point::::new(1.0, 2.0); 262 | let serialized = serde_json::to_string(&point).unwrap(); 263 | assert_eq!(serialized, r#"{"x":100,"y":200}"#); 264 | 265 | let deserialized: Point = serde_json::from_str(&serialized).unwrap(); 266 | assert_eq!(point, deserialized); 267 | } 268 | 269 | #[test] 270 | fn test_distance_to() { 271 | let point1 = Point::::new(1.0, 2.0); 272 | let point2 = Point::::new(3.0, 4.0); 273 | assert_eq!(point1.distance_to(&point2), 2.8284271247461903); 274 | } 275 | } 276 | --------------------------------------------------------------------------------