├── .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 | [](https://crates.io/crates/clipper2)
4 | [](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 | 
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 | //! 
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 | /// 
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 | /// 
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 | /// 
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 | /// 
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 | /// 
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 | /// 
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