kurbo-0.7.1/.cargo_vcs_info.json0000644000000001121375200747300122230ustar { "git": { "sha1": "22bf3269afd04ecd40c26f84f2b8207ea050e65b" } } kurbo-0.7.1/.github/workflows/ci.yml010064400007650000024000000062061367545551600155600ustar 00000000000000on: push: branches: - master pull_request: jobs: rustfmt: runs-on: ubuntu-latest name: cargo fmt steps: - uses: actions/checkout@v2 - name: install stable toolchain uses: actions-rs/toolchain@v1 with: toolchain: stable profile: minimal components: rustfmt override: true - name: install rustfmt run: rustup component add rustfmt - name: cargo fmt uses: actions-rs/cargo@v1 with: command: fmt args: --all -- --check test-stable: runs-on: ${{ matrix.os }} strategy: matrix: os: [macOS-latest, windows-2019, ubuntu-latest] name: cargo clippy+test steps: - uses: actions/checkout@v2 - name: install stable toolchain uses: actions-rs/toolchain@v1 with: toolchain: stable components: clippy profile: minimal override: true - name: cargo clippy uses: actions-rs/cargo@v1 with: command: clippy args: --all-features --all-targets -- -D warnings - name: cargo test uses: actions-rs/cargo@v1 with: command: test args: --all-features test-stable-wasm: runs-on: ${{ matrix.os }} strategy: matrix: os: [macOS-latest, windows-2019, ubuntu-latest] name: cargo clippy+test (wasm32) steps: - uses: actions/checkout@v2 - name: install stable toolchain uses: actions-rs/toolchain@v1 with: toolchain: stable target: wasm32-unknown-unknown components: clippy profile: minimal override: true - name: cargo clippy uses: actions-rs/cargo@v1 with: command: clippy args: --all-features --all-targets --target wasm32-unknown-unknown -- -D warnings # TODO: Find a way to make tests work. Until then the tests are merely compiled. - name: cargo test compile uses: actions-rs/cargo@v1 with: command: test args: --all-features --no-run --target wasm32-unknown-unknown test-nightly: runs-on: ${{ matrix.os }} strategy: matrix: os: [macOS-latest, windows-2019, ubuntu-latest] name: cargo test nightly steps: - uses: actions/checkout@v2 - name: install nightly toolchain uses: actions-rs/toolchain@v1 with: toolchain: nightly profile: minimal override: true - name: cargo test uses: actions-rs/cargo@v1 with: command: test args: --all-features check-docs: name: Docs runs-on: ${{ matrix.os }} strategy: matrix: os: [macOS-latest, windows-2019, ubuntu-latest] steps: - uses: actions/checkout@v2 - name: install stable toolchain uses: actions-rs/toolchain@v1 with: toolchain: stable profile: minimal override: true - name: cargo doc uses: actions-rs/cargo@v1 with: command: doc args: --all-features --document-private-items kurbo-0.7.1/.gitignore010064400007650000024000000000361347303236300130130ustar 00000000000000/target **/*.rs.bk Cargo.lock kurbo-0.7.1/AUTHORS010064400007650000024000000004641347303236300121000ustar 00000000000000# This is the list of kurbo authors for copyright purposes. # # This does not necessarily list everyone who has contributed code, since in # some cases, their employer may be the copyright holder. To see the full list # of contributors, see the revision history in source control. Raph Levien Nicolas Silva kurbo-0.7.1/Cargo.lock0000644000000075521375200747300102150ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. [[package]] name = "arrayvec" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" [[package]] name = "cfg-if" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" [[package]] name = "getrandom" version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc587bc0ec293155d5bfa6b9891ec18a1e330c234f896ea47fbada4cadbe47e6" dependencies = [ "cfg-if", "libc", "wasi", ] [[package]] name = "kurbo" version = "0.7.1" dependencies = [ "arrayvec", "mint", "rand", "serde", ] [[package]] name = "libc" version = "0.2.80" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d58d1b70b004888f764dfbf6a26a3b0342a1632d33968e4a179d8011c760614" [[package]] name = "mint" version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "519df8d6856dcd4b40519947737b408f81be051fc032590659cae5d77d664185" [[package]] name = "ppv-lite86" version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" [[package]] name = "proc-macro2" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" dependencies = [ "unicode-xid", ] [[package]] name = "quote" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" dependencies = [ "proc-macro2", ] [[package]] name = "rand" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" dependencies = [ "getrandom", "libc", "rand_chacha", "rand_core", "rand_hc", ] [[package]] name = "rand_chacha" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" dependencies = [ "ppv-lite86", "rand_core", ] [[package]] name = "rand_core" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" dependencies = [ "getrandom", ] [[package]] name = "rand_hc" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" dependencies = [ "rand_core", ] [[package]] name = "serde" version = "1.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b88fa983de7720629c9387e9f517353ed404164b1e482c970a90c1a4aaf7dc1a" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cbd1ae72adb44aab48f325a02444a5fc079349a8d804c1fc922aed3f7454c74e" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "syn" version = "1.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc371affeffc477f42a221a1e4297aedcea33d47d19b61455588bd9d8f6b19ac" dependencies = [ "proc-macro2", "quote", "unicode-xid", ] [[package]] name = "unicode-xid" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" [[package]] name = "wasi" version = "0.9.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" kurbo-0.7.1/Cargo.toml0000644000000021601375200747300102260ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies # # If you believe there's an error in this file please file an # issue against the rust-lang/cargo repository. If you're # editing this file be aware that the upstream Cargo.toml # will likely look very different (and much more reasonable) [package] edition = "2018" name = "kurbo" version = "0.7.1" authors = ["Raph Levien "] description = "A 2D curves library" readme = "README.md" keywords = ["graphics", "curve", "curves", "bezier", "geometry"] categories = ["graphics"] license = "MIT/Apache-2.0" repository = "https://github.com/linebender/kurbo" [package.metadata.docs.rs] features = ["mint", "serde"] [dependencies.arrayvec] version = "0.5.1" [dependencies.mint] version = "0.5.1" optional = true [dependencies.serde] version = "1.0.105" features = ["derive"] optional = true [dev-dependencies.rand] version = "0.7.2" kurbo-0.7.1/Cargo.toml.orig010064400007650000024000000012311375200744500137130ustar 00000000000000[package] name = "kurbo" version = "0.7.1" authors = ["Raph Levien "] license = "MIT/Apache-2.0" edition = "2018" keywords = ["graphics", "curve", "curves", "bezier", "geometry"] repository = "https://github.com/linebender/kurbo" description = "A 2D curves library" readme = "README.md" categories = ["graphics"] [package.metadata.docs.rs] features = ["mint", "serde"] [dependencies] arrayvec = "0.5.1" [dependencies.mint] version = "0.5.1" optional = true [dependencies.serde] version = "1.0.105" optional = true features = ["derive"] # This is used for research but not really needed; maybe refactor. [dev-dependencies] rand = "0.7.2" kurbo-0.7.1/LICENSE-APACHE010064400007650000024000000261361347303236300127600ustar 00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. kurbo-0.7.1/LICENSE-MIT010064400007650000024000000020371347303236300124620ustar 00000000000000Copyright (c) 2018 Raph Levien 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: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 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. kurbo-0.7.1/README.md010064400007650000024000000044151362207247400123120ustar 00000000000000# kurbo, a Rust 2D curves library [![Build Status](https://travis-ci.com/linebender/kurbo.svg)](https://travis-ci.com/linebender/kurbo) [![Docs](https://docs.rs/kurbo/badge.svg)](https://docs.rs/kurbo) [![Crates.io](https://img.shields.io/crates/v/kurbo.svg?maxAge=2592000)](https://crates.io/crates/kurbo) The kurbo library contains data structures and algorithms for curves and vector paths. It is probably most appropriate for creative tools, but is general enough it might be useful for other applications. The name "kurbo" is Esperanto for "curve". There is a focus on accuracy and good performance in high-accuracy conditions. Thus, the library might be useful in engineering and science contexts as well, as opposed to visual arts where rough approximations are often sufficient. Many approximate functions come with an accuracy parameter, and analytical solutions are used where they are practical. An example is area calculation, which is done using Green's theorem. The library is still in fairly early development stages. There are traits intended to be useful for general curves (not just Béziers), but these will probably be reorganized. ## Similar crates Here we mention a few other curves libraries and touch on some of the decisions made differently here. * [lyon_geom] has a lot of very good vector algorithms. It's most focused on rendering. * [flo_curves] has good Bézier primitives, and seems tuned for animation. It's generic on the coordinate type, while we use `f64` for everything. * [vek] has both 2D and 3D Béziers among other things, and is tuned for game engines. Some code has been copied from lyon_geom with adaptation, thus the author of lyon_geom, Nicolas Silva, is credited in the [AUTHORS] file. ## More info To learn more about Bézier curves, [A Primer on Bézier Curves] by Pomax is indispensable. ## Contributing Contributions are welcome. The [Rust Code of Conduct] applies. Please feel free to add your name to the [AUTHORS] file in any substantive pull request. [Rust Code of Conduct]: https://www.rust-lang.org/policies/code-of-conduct [lyon_geom]: https://crates.io/crates/lyon_geom [flo_curves]: https://crates.io/crates/flo_curves [vek]: https://crates.io/crates/vek [A Primer on Bézier Curves]: https://pomax.github.io/bezierinfo/ [AUTHORS]: ./AUTHORS kurbo-0.7.1/benches/cubic_arclen.rs010064400007650000024000000027341367545551600154350ustar 00000000000000//! Benchmarks of cubic arclength approaches. #![cfg(nightly)] #![feature(test)] extern crate test; use test::Bencher; use kurbo::{CubicBez, ParamCurveArclen}; #[bench] fn bench_cubic_arclen_1e_4(b: &mut Bencher) { let c = CubicBez::new((0.0, 0.0), (1.0 / 3.0, 0.0), (2.0 / 3.0, 1.0), (1.0, 1.0)); b.iter(|| test::black_box(c).arclen(1e-4)) } #[bench] fn bench_cubic_arclen_1e_5(b: &mut Bencher) { let c = CubicBez::new((0.0, 0.0), (1.0 / 3.0, 0.0), (2.0 / 3.0, 1.0), (1.0, 1.0)); b.iter(|| test::black_box(c).arclen(1e-5)) } #[bench] fn bench_cubic_arclen_1e_6(b: &mut Bencher) { let c = CubicBez::new((0.0, 0.0), (1.0 / 3.0, 0.0), (2.0 / 3.0, 1.0), (1.0, 1.0)); b.iter(|| test::black_box(c).arclen(1e-6)) } #[bench] fn bench_cubic_arclen_degenerate(b: &mut Bencher) { let c = CubicBez::new((0.0, 0.0), (0.0, 0.0), (0.0, 0.0), (0.0, 0.0)); b.iter(|| test::black_box(c).arclen(1e-6)) } #[bench] fn bench_cubic_arclen_1e_7(b: &mut Bencher) { let c = CubicBez::new((0.0, 0.0), (1.0 / 3.0, 0.0), (2.0 / 3.0, 1.0), (1.0, 1.0)); b.iter(|| test::black_box(c).arclen(1e-7)) } #[bench] fn bench_cubic_arclen_1e_8(b: &mut Bencher) { let c = CubicBez::new((0.0, 0.0), (1.0 / 3.0, 0.0), (2.0 / 3.0, 1.0), (1.0, 1.0)); b.iter(|| test::black_box(c).arclen(1e-8)) } #[bench] fn bench_cubic_arclen_1e_9(b: &mut Bencher) { let c = CubicBez::new((0.0, 0.0), (1.0 / 3.0, 0.0), (2.0 / 3.0, 1.0), (1.0, 1.0)); b.iter(|| test::black_box(c).arclen(1e-9)) } kurbo-0.7.1/benches/quad_arclen.rs010064400007650000024000000144001367545551600152730ustar 00000000000000//! Benchmarks of quadratic arclength approaches. // TODO: organize so there's less cut'n'paste from arclen_accuracy example. #![cfg(nightly)] #![feature(test)] extern crate test; use test::Bencher; use kurbo::{ParamCurve, ParamCurveArclen, ParamCurveDeriv, QuadBez}; // Based on http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/ fn quad_arclen_analytical(q: QuadBez) -> f64 { let d2 = q.p0.to_vec2() - 2.0 * q.p1.to_vec2() + q.p2.to_vec2(); let a = d2.hypot2(); let d1 = q.p1 - q.p0; let b = 2.0 * d2.dot(d1); let c = d1.hypot2(); let sabc = (a + b + c).sqrt(); let a2 = a.powf(-0.5); let a32 = a2.powi(3); let c2 = 2.0 * c.sqrt(); let ba = b * a2; sabc + 0.25 * (a2 * a2 * b * (2.0 * sabc - c2) + a32 * (4.0 * c * a - b * b) * (((2.0 * a + b) * a2 + 2.0 * sabc) / (ba + c2)).ln()) } /// Calculate arclength using Gauss-Legendre quadrature using formula from Behdad /// in https://github.com/Pomax/BezierInfo-2/issues/77 fn gauss_arclen_3(q: QuadBez) -> f64 { let v0 = (-0.492943519233745 * q.p0.to_vec2() + 0.430331482911935 * q.p1.to_vec2() + 0.0626120363218102 * q.p2.to_vec2()) .hypot(); let v1 = ((q.p2 - q.p0) * 0.4444444444444444).hypot(); let v2 = (-0.0626120363218102 * q.p0.to_vec2() - 0.430331482911935 * q.p1.to_vec2() + 0.492943519233745 * q.p2.to_vec2()) .hypot(); v0 + v1 + v2 } fn awesome_quad_arclen3(q: QuadBez, accuracy: f64, depth: usize) -> f64 { let pm = q.p0.midpoint(q.p2); let d1 = q.p1 - pm; let d = q.p2 - q.p0; let dhypot2 = d.hypot2(); let x = 2.0 * d.dot(d1) / dhypot2; let y = 2.0 * d.cross(d1) / dhypot2; let lc = (q.p2 - q.p0).hypot(); let lp = (q.p1 - q.p0).hypot() + (q.p2 - q.p1).hypot(); let est_err = 0.06 * (lp - lc) * (x * x + y * y).powf(2.0); if est_err < accuracy || depth == 16 { gauss_arclen_3(q) } else { let (q0, q1) = q.subdivide(); awesome_quad_arclen3(q0, accuracy * 0.5, depth + 1) + awesome_quad_arclen3(q1, accuracy * 0.5, depth + 1) } } /// Another implementation, using weights from /// https://pomax.github.io/bezierinfo/legendre-gauss.html fn gauss_arclen_n(q: QuadBez, coeffs: &[(f64, f64)]) -> f64 { let d = q.deriv(); coeffs .iter() .map(|(wi, xi)| wi * d.eval(0.5 * (xi + 1.0)).to_vec2().hypot()) .sum::() * 0.5 } fn gauss_arclen_7(q: QuadBez) -> f64 { gauss_arclen_n( q, &[ (0.4179591836734694, 0.0000000000000000), (0.3818300505051189, 0.4058451513773972), (0.3818300505051189, -0.4058451513773972), (0.2797053914892766, -0.7415311855993945), (0.2797053914892766, 0.7415311855993945), (0.1294849661688697, -0.9491079123427585), (0.1294849661688697, 0.9491079123427585), ], ) } fn gauss_arclen_24(q: QuadBez) -> f64 { gauss_arclen_n( q, &[ (0.1279381953467522, -0.0640568928626056), (0.1279381953467522, 0.0640568928626056), (0.1258374563468283, -0.1911188674736163), (0.1258374563468283, 0.1911188674736163), (0.1216704729278034, -0.3150426796961634), (0.1216704729278034, 0.3150426796961634), (0.1155056680537256, -0.4337935076260451), (0.1155056680537256, 0.4337935076260451), (0.1074442701159656, -0.5454214713888396), (0.1074442701159656, 0.5454214713888396), (0.0976186521041139, -0.6480936519369755), (0.0976186521041139, 0.6480936519369755), (0.0861901615319533, -0.7401241915785544), (0.0861901615319533, 0.7401241915785544), (0.0733464814110803, -0.8200019859739029), (0.0733464814110803, 0.8200019859739029), (0.0592985849154368, -0.8864155270044011), (0.0592985849154368, 0.8864155270044011), (0.0442774388174198, -0.9382745520027328), (0.0442774388174198, 0.9382745520027328), (0.0285313886289337, -0.9747285559713095), (0.0285313886289337, 0.9747285559713095), (0.0123412297999872, -0.9951872199970213), (0.0123412297999872, 0.9951872199970213), ], ) } fn awesome_quad_arclen7(q: QuadBez, accuracy: f64, depth: usize) -> f64 { let pm = q.p0.midpoint(q.p2); let d1 = q.p1 - pm; let d = q.p2 - q.p0; let dhypot2 = d.hypot2(); let x = 2.0 * d.dot(d1) / dhypot2; let y = 2.0 * d.cross(d1) / dhypot2; let lc = dhypot2.sqrt(); let lp = (q.p1 - q.p0).hypot() + (q.p2 - q.p1).hypot(); let est_err = 2.5e-2 * (lp - lc) * (x * x + y * y).powf(8.0).tanh(); if est_err < accuracy || depth == 16 { gauss_arclen_7(q) } else { let (q0, q1) = q.subdivide(); awesome_quad_arclen7(q0, accuracy * 0.5, depth + 1) + awesome_quad_arclen7(q1, accuracy * 0.5, depth + 1) } } #[bench] fn bench_quad_arclen_analytical(b: &mut Bencher) { // Analytical solution is not sensitive to exact shape. let q = QuadBez::new((0.0, 0.0), (1.0, 0.0), (1.0, 1.0)); b.iter(|| quad_arclen_analytical(test::black_box(q))) } const ACCURACY: f64 = 1e-6; #[bench] fn bench_quad_arclen(b: &mut Bencher) { // This is a pretty easy case. let q = QuadBez::new((0.0, 0.0), (1.0, 0.0), (1.0, 1.0)); b.iter(|| (test::black_box(q).arclen(ACCURACY))) } #[bench] fn bench_quad_arclen_gauss3(b: &mut Bencher) { let q = QuadBez::new((0.0, 0.0), (1.0, 0.0), (1.0, 1.0)); b.iter(|| awesome_quad_arclen3(test::black_box(q), ACCURACY, 0)) } #[bench] fn bench_quad_arclen_gauss7(b: &mut Bencher) { let q = QuadBez::new((0.0, 0.0), (1.0, 0.0), (1.0, 1.0)); b.iter(|| awesome_quad_arclen7(test::black_box(q), ACCURACY, 0)) } #[bench] fn bench_quad_arclen_gauss3_one(b: &mut Bencher) { let q = QuadBez::new((0.0, 0.0), (1.0, 0.0), (1.0, 1.0)); b.iter(|| gauss_arclen_3(test::black_box(q))) } #[bench] fn bench_quad_arclen_gauss7_one(b: &mut Bencher) { let q = QuadBez::new((0.0, 0.0), (1.0, 0.0), (1.0, 1.0)); b.iter(|| gauss_arclen_7(test::black_box(q))) } #[bench] fn bench_quad_arclen_gauss24_one(b: &mut Bencher) { let q = QuadBez::new((0.0, 0.0), (1.0, 0.0), (1.0, 1.0)); b.iter(|| gauss_arclen_24(test::black_box(q))) } kurbo-0.7.1/benches/rect_expand.rs010064400007650000024000000030751367545551600153170ustar 00000000000000#![cfg(nightly)] #![feature(test)] extern crate test; use test::Bencher; use kurbo::Rect; // In positive space static RECT_POS: Rect = Rect::new(3.3, 3.6, 5.6, 4.1); // In both positive and negative space static RECT_PAN: Rect = Rect::new(-3.3, -3.6, 5.6, 4.1); // In negative space static RECT_NEG: Rect = Rect::new(-5.6, -4.1, -3.3, -3.6); // In positive space reverse static RECT_POR: Rect = Rect::new(5.6, 4.1, 3.3, 3.6); // In both positive and negative space reverse static RECT_PNR: Rect = Rect::new(5.6, 4.1, -3.3, -3.6); // In negative space reverse static RECT_NER: Rect = Rect::new(-3.3, -3.6, -5.6, -4.1); // In positive space mixed static RECT_POM: Rect = Rect::new(3.3, 4.1, 5.6, 3.6); // In both positive and negative space mixed static RECT_PNM: Rect = Rect::new(-3.3, 4.1, 5.6, -3.6); // In negative space mixed static RECT_NEM: Rect = Rect::new(-5.6, -3.6, -3.3, -4.1); #[inline] fn expand(rects: &[Rect; 9]) { test::black_box(rects[0].expand()); test::black_box(rects[1].expand()); test::black_box(rects[2].expand()); test::black_box(rects[3].expand()); test::black_box(rects[4].expand()); test::black_box(rects[5].expand()); test::black_box(rects[6].expand()); test::black_box(rects[7].expand()); test::black_box(rects[8].expand()); } #[bench] fn bench_expand(b: &mut Bencher) { // Creating the array here to prevent the compiler from optimizing all of it to NOP. let rects: [Rect; 9] = [ RECT_POS, RECT_PAN, RECT_NEG, RECT_POR, RECT_PNR, RECT_NER, RECT_POM, RECT_PNM, RECT_NEM, ]; b.iter(|| expand(&rects)); } kurbo-0.7.1/clippy.toml010064400007650000024000000006021367545551600132340ustar 00000000000000# The default clippy value for this is 8 bytes, which is chosen to improve performance on 32-bit. # Given that kurbo is being designed for the future and already even mobile phones have 64-bit CPUs, # it makes sense to optimize for 64-bit and accept the performance hits on 32-bit. # 16 bytes is the number of bytes that fits into two 64-bit CPU registers. trivial-copy-size-limit = 16 kurbo-0.7.1/examples/arclen_accuracy.rs010064400007650000024000000207261367545551600163520ustar 00000000000000//! A test program to plot the error of arclength approximation. // Lots of stuff is commented out or was just something to try. #![allow(unused)] #![allow(clippy::unreadable_literal)] #![allow(clippy::many_single_char_names)] // TODO: make more functionality accessible from command line rather than uncommenting. use std::env; use std::time::{Duration, Instant}; use kurbo::{ParamCurve, ParamCurveArclen, ParamCurveDeriv, QuadBez}; use kurbo::common::{GAUSS_LEGENDRE_COEFFS_24, GAUSS_LEGENDRE_COEFFS_5, GAUSS_LEGENDRE_COEFFS_7}; /// Calculate arclength using Gauss-Legendre quadrature using formula from Behdad /// in https://github.com/Pomax/BezierInfo-2/issues/77 fn gauss_arclen_3(q: QuadBez) -> f64 { let v0 = (-0.492943519233745 * q.p0.to_vec2() + 0.430331482911935 * q.p1.to_vec2() + 0.0626120363218102 * q.p2.to_vec2()) .hypot(); let v1 = ((q.p2 - q.p0) * 0.4444444444444444).hypot(); let v2 = (-0.0626120363218102 * q.p0.to_vec2() - 0.430331482911935 * q.p1.to_vec2() + 0.492943519233745 * q.p2.to_vec2()) .hypot(); v0 + v1 + v2 } fn awesome_quad_arclen(q: QuadBez, accuracy: f64, depth: usize, count: &mut usize) -> f64 { let pm = q.p0.midpoint(q.p2); let d1 = q.p1 - pm; let d = q.p2 - q.p0; let dhypot2 = d.hypot2(); let x = 2.0 * d.dot(d1) / dhypot2; let y = 2.0 * d.cross(d1) / dhypot2; let lc = (q.p2 - q.p0).hypot(); let lp = (q.p1 - q.p0).hypot() + (q.p2 - q.p1).hypot(); let est_err = 0.06 * (lp - lc) * (x * x + y * y).powf(2.0); if est_err < accuracy || depth == 16 { *count += 1; gauss_arclen_3(q) } else { let (q0, q1) = q.subdivide(); awesome_quad_arclen(q0, accuracy * 0.5, depth + 1, count) + awesome_quad_arclen(q1, accuracy * 0.5, depth + 1, count) } } const MAX_DEPTH: usize = 16; fn gravesen_rec(q: &QuadBez, l0: f64, accuracy: f64, depth: usize, count: &mut usize) -> f64 { let (q0, q1) = q.subdivide(); let l0_q0 = calc_l0(q0); let l0_q1 = calc_l0(q1); let l1 = l0_q0 + l0_q1; let error = (l0 - l1) * (1.0 / 15.0); if error.abs() < accuracy || depth == MAX_DEPTH { *count += 1; l1 - error } else { gravesen_rec(&q0, l0_q0, accuracy * 0.5, depth + 1, count) + gravesen_rec(&q1, l0_q1, accuracy * 0.5, depth + 1, count) } } fn gauss_arclen_5(q: QuadBez) -> f64 { q.gauss_arclen(GAUSS_LEGENDRE_COEFFS_5) } fn gauss_arclen_7(q: QuadBez) -> f64 { q.gauss_arclen(GAUSS_LEGENDRE_COEFFS_7) } fn gauss_arclen_24(q: QuadBez) -> f64 { q.gauss_arclen(GAUSS_LEGENDRE_COEFFS_24) } fn awesome_quad_arclen7(q: QuadBez, accuracy: f64, depth: usize, count: &mut usize) -> f64 { let pm = q.p0.midpoint(q.p2); let d1 = q.p1 - pm; let d = q.p2 - q.p0; let dhypot2 = d.hypot2(); let x = 2.0 * d.dot(d1) / dhypot2; let y = 2.0 * d.cross(d1) / dhypot2; let lc = (q.p2 - q.p0).hypot(); let lp = (q.p1 - q.p0).hypot() + (q.p2 - q.p1).hypot(); let est_err = 2.5e-2 * (lp - lc) * (x * x + y * y).powf(8.0).tanh(); // Increase depth so we can be an accurate baseline for comparison. if est_err < accuracy || depth == 22 { *count += 1; gauss_arclen_7(q) } else { let (q0, q1) = q.subdivide(); awesome_quad_arclen7(q0, accuracy * 0.5, depth + 1, count) + awesome_quad_arclen7(q1, accuracy * 0.5, depth + 1, count) } } fn awesome_quad_arclen24(q: QuadBez, accuracy: f64, depth: usize, count: &mut usize) -> f64 { let pm = q.p0.midpoint(q.p2); let d1 = q.p1 - pm; let d = q.p2 - q.p0; let dhypot2 = d.hypot2(); let x = 2.0 * d.dot(d1) / dhypot2; let y = 2.0 * d.cross(d1) / dhypot2; let lc = (q.p2 - q.p0).hypot(); let lp = (q.p1 - q.p0).hypot() + (q.p2 - q.p1).hypot(); // This isn't quite right near (1.1, 0) let est_err = 1.0 * (lp - lc) * (0.5 * x * x + 0.1 * y * y).powf(16.0).tanh(); if est_err < accuracy || depth == 16 { *count += 1; gauss_arclen_24(q) } else { let (q0, q1) = q.subdivide(); awesome_quad_arclen24(q0, accuracy * 0.5, depth + 1, count) + awesome_quad_arclen24(q1, accuracy * 0.5, depth + 1, count) } } // Based on http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/ fn quad_arclen_analytical(q: QuadBez) -> f64 { let d2 = q.p0.to_vec2() - 2.0 * q.p1.to_vec2() + q.p2.to_vec2(); let a = d2.hypot2(); let d1 = q.p1 - q.p0; let c = d1.hypot2(); if a < 5e-4 * c { return gauss_arclen_3(q); } let b = 2.0 * d2.dot(d1); let sabc = (a + b + c).sqrt(); let a2 = a.powf(-0.5); let a32 = a2.powi(3); let c2 = 2.0 * c.sqrt(); let ba_c2 = b * a2 + c2; let v0 = 0.25 * a2 * a2 * b * (2.0 * sabc - c2) + sabc; // TODO: justify and fine-tune this exact constant. if ba_c2 < 1e-13 { // This case happens for Béziers with a sharp kink. v0 } else { v0 + 0.25 * a32 * (4.0 * c * a - b * b) * (((2.0 * a + b) * a2 + 2.0 * sabc) / ba_c2).ln() } } /// Calculate the L0 metric from "Adaptive subdivision and the length and /// energy of Bézier curves" by Jens Gravesen. fn calc_l0(q: QuadBez) -> f64 { let lc = (q.p2 - q.p0).hypot(); let lp = (q.p1 - q.p0).hypot() + (q.p2 - q.p1).hypot(); (2.0 / 3.0) * lc + (1.0 / 3.0) * lp } fn with_subdiv(q: QuadBez, f: &dyn Fn(QuadBez) -> f64, depth: usize) -> f64 { if depth == 0 { f(q) } else { let (q0, q1) = q.subdivide(); with_subdiv(q0, f, depth - 1) + with_subdiv(q1, f, depth - 1) } } fn duration_to_time(d: Duration) -> f64 { 1e-9 * (d.subsec_nanos() as f64) + (d.as_secs() as f64) } fn run_simple() { let q = QuadBez::new((0.0, 0.0), (0.0, 0.5), (1.0, 1.0)); let true_len = q.arclen(1e-13); for i in 0..20 { let n = 1 << i; let start_time = Instant::now(); let mut est = 0.0; let mut last = q.start(); let dt = (n as f64).recip(); for j in 0..n { let t = ((j + 1) as f64) * dt; let p = q.eval(t); est += (p - last).hypot(); last = p; } let elapsed = duration_to_time(start_time.elapsed()); let err = true_len - est; if i > 0 { println!("{} {}", elapsed, err); } } } /// Generate map data suitable for plotting in Gnuplot. fn main() { let mut n_subdiv = 0; let mut func: &dyn Fn(QuadBez) -> f64 = &gauss_arclen_3; for arg in env::args().skip(1) { if arg == "gauss3" { func = &gauss_arclen_3; } else if arg == "gauss5" { func = &gauss_arclen_5; } else if arg == "gauss7" { func = &gauss_arclen_7; } else if arg == "gauss24" { func = &gauss_arclen_24; } else if arg == "l0" { func = &calc_l0; } else if arg == "simple" { run_simple(); return; } else if let Ok(n) = arg.parse() { n_subdiv = n; } else { println!("usage: arclen_accuracy [func] n_subdiv"); std::process::exit(1); } } let n = 400; let accuracy = 1e-6; for i in 0..=n { let x = 2.0 * (i as f64) * (n as f64).recip(); for j in 0..=n { let y = 2.0 * (j as f64) * (n as f64).recip(); let q = QuadBez::new((-1.0, 0.0), (x, y), (1.0, 0.0)); let mut count = 0; let accurate_arclen = awesome_quad_arclen7(q, 1e-15, 0, &mut count); //count = 0; let start_time = Instant::now(); //let est = awesome_quad_arclen7(q, accuracy, 0, &mut count); let est = quad_arclen_analytical(q); let elapsed = start_time.elapsed(); //let est = gravesen_rec(&q, calc_l0(q), accuracy, 0, &mut count); //let est = with_subdiv(q, func, n_subdiv); let error = est - accurate_arclen; println!("{} {} {}", x, y, (error.abs() + 1e-18).log10()); //println!("{} {} {}", x, y, count); //println!("{} {} {}", x, y, elapsed.subsec_nanos()); //let accurate_arclen = with_subdiv(q, &gauss_arclen_5, 8); //let error = est - accurate_arclen; /* let lc = (q.p2 - q.p0).hypot(); let lp = (q.p1 - q.p0).hypot() + (q.p2 - q.p1).hypot(); let est_err = 1.0 * (lp - lc) * (0.5 * x * x + 0.1 * y * y).powf(16.0).tanh(); println!("{} {} {}", x, y, (est_err/error.abs() + 1e-15).log10()); */ } println!(); } } kurbo-0.7.1/examples/circle.rs010064400007650000024000000010631374161070200144460ustar 00000000000000//! Example of circle use kurbo::{Circle, Shape}; fn main() { let circle = Circle::new((400.0, 400.0), 380.0); println!(""); println!(""); println!(""); println!(""); let path = circle.to_path(1e-3).to_svg(); println!(" ", path); let path = circle.to_path(1.0).to_svg(); println!(" ", path); println!(""); println!(""); println!(""); } kurbo-0.7.1/examples/cubic_arclen.rs010064400007650000024000000173641367545551600156510ustar 00000000000000//! Research testbed for arclengths of cubic Bézier segments. // Lots of stuff is commented out or was just something to try. #![allow(unused)] #![allow(clippy::unreadable_literal)] #![allow(clippy::many_single_char_names)] use kurbo::common::*; use kurbo::{ CubicBez, ParamCurve, ParamCurveArclen, ParamCurveCurvature, ParamCurveDeriv, Point, Vec2, }; /// Calculate arclength using Gauss-Legendre quadrature using formula from Behdad /// in https://github.com/Pomax/BezierInfo-2/issues/77 fn gauss_arclen_5(c: CubicBez) -> f64 { let v0 = (c.p1 - c.p0).hypot() * 0.15; let v1 = (-0.558983582205757 * c.p0.to_vec2() + 0.325650248872424 * c.p1.to_vec2() + 0.208983582205757 * c.p2.to_vec2() + 0.024349751127576 * c.p3.to_vec2()) .hypot(); let v2 = (c.p3 - c.p0 + (c.p2 - c.p1)).hypot() * 0.26666666666666666; let v3 = (-0.024349751127576 * c.p0.to_vec2() - 0.208983582205757 * c.p1.to_vec2() - 0.325650248872424 * c.p2.to_vec2() + 0.558983582205757 * c.p3.to_vec2()) .hypot(); let v4 = (c.p3 - c.p2).hypot() * 0.15; v0 + v1 + v2 + v3 + v4 } fn gauss_arclen_7(c: C) -> f64 { c.gauss_arclen(GAUSS_LEGENDRE_COEFFS_7) } fn est_gauss5_error(c: CubicBez) -> f64 { let lc = (c.p3 - c.p0).hypot(); let lp = (c.p1 - c.p0).hypot() + (c.p2 - c.p1).hypot() + (c.p3 - c.p2).hypot(); let d2 = c.deriv().deriv(); let d3 = d2.deriv(); let lmi = 2.0 / (lp + lc); 7e-8 * (d3.eval(0.5).to_vec2().hypot() * lmi + 5.0 * d2.eval(0.5).to_vec2().hypot() * lmi) .powi(5) * lp } fn gauss_errnorm_n(c: C, coeffs: &[(f64, f64)]) -> f64 where C::DerivResult: ParamCurveDeriv, { let d = c.deriv().deriv(); coeffs .iter() .map(|(wi, xi)| wi * d.eval(0.5 * (xi + 1.0)).to_vec2().hypot2()) .sum::() } // Squared L2 norm of the second derivative of the cubic. fn cubic_errnorm(c: CubicBez) -> f64 { let d = c.deriv().deriv(); let dd = d.end() - d.start(); d.start().to_vec2().hypot2() + d.start().to_vec2().dot(dd) + dd.hypot2() * (1.0 / 3.0) } fn est_gauss7_error(c: CubicBez) -> f64 { let lc = (c.p3 - c.p0).hypot(); let lp = (c.p1 - c.p0).hypot() + (c.p2 - c.p1).hypot() + (c.p3 - c.p2).hypot(); 8e-9 * (2.0 * cubic_errnorm(c) / lc.powi(2)).powi(6) * lp } fn gauss_arclen_9(c: C) -> f64 { c.gauss_arclen(GAUSS_LEGENDRE_COEFFS_9) } fn gauss_arclen_11(c: C) -> f64 { c.gauss_arclen(GAUSS_LEGENDRE_COEFFS_11) } fn est_gauss9_error(c: CubicBez) -> f64 { let lc = (c.p3 - c.p0).hypot(); let lp = (c.p1 - c.p0).hypot() + (c.p2 - c.p1).hypot() + (c.p3 - c.p2).hypot(); 1e-10 * (2.0 * cubic_errnorm(c) / lc.powi(2)).powi(8) * lp } fn est_gauss11_error(c: CubicBez) -> f64 { let lc = (c.p3 - c.p0).hypot(); let lp = (c.p1 - c.p0).hypot() + (c.p2 - c.p1).hypot() + (c.p3 - c.p2).hypot(); 1e-12 * (2.0 * cubic_errnorm(c) / lc.powi(2)).powi(11) * lp } // A new approach based on integrating local error. fn est_gauss11_error_2(c: CubicBez) -> f64 { let d = c.deriv(); let d2 = d.deriv(); GAUSS_LEGENDRE_COEFFS_11 .iter() .map(|(wi, xi)| { wi * { let t = 0.5 * (xi + 1.0); let v = d.eval(t).to_vec2().hypot(); let a2 = d2.eval(t).to_vec2().hypot2(); a2.powi(3) / v.powi(5) } }) .sum::() } #[allow(clippy::neg_cmp_op_on_partial_ord)] fn est_max_curvature(c: CubicBez) -> f64 { let n = 100; let mut max = 0.0; for i in 0..=n { let t = (i as f64) * (n as f64).recip(); let k = c.curvature(t).abs(); if !(k < max) { max = k; } } max } fn est_min_deriv_norm2(c: CubicBez) -> f64 { let d = c.deriv(); let n = 100; let mut min = d.eval(1.0).to_vec2().hypot2(); for i in 0..n { let t = (i as f64) * (n as f64).recip(); min = min.min(d.eval(t).to_vec2().hypot2()) } min } fn est_gauss11_error_3(c: CubicBez) -> f64 { let lc = (c.p3 - c.p0).hypot(); let lp = (c.p1 - c.p0).hypot() + (c.p2 - c.p1).hypot() + (c.p3 - c.p2).hypot(); let pc_err = (lp - lc) * 0.02; let ks = est_max_curvature(c) * lp; let est = ks.powi(3) * lp * 8e-9; if est < pc_err { est } else { pc_err } } fn est_gauss9_error_3(c: CubicBez) -> f64 { let lc = (c.p3 - c.p0).hypot(); let lp = (c.p1 - c.p0).hypot() + (c.p2 - c.p1).hypot() + (c.p3 - c.p2).hypot(); let pc_err = (lp - lc) * 0.02; let ks = est_max_curvature(c) * lp; let est = ks.powi(3) * lp * 5e-8; if est < pc_err { est } else { pc_err } } // A new approach based on integrating local error; the cost of evaluating the // error metric is likely to dominate unless the accuracy buys a lot of subdivisions. fn est_gauss9_error_2(c: CubicBez) -> f64 { let d = c.deriv(); let d2 = d.deriv(); let p = 10; GAUSS_LEGENDRE_COEFFS_9 .iter() .map(|(wi, xi)| { wi * { let t = 0.5 * (xi + 1.0); let v = d.eval(t).to_vec2().hypot(); let a = d2.eval(t).to_vec2().hypot(); (1.0e-1 * a / v).tanh().powi(p) * v } }) .sum::() * 3.0 } fn my_arclen(c: CubicBez, accuracy: f64, depth: usize, count: &mut usize) -> f64 { if depth == 16 || est_gauss5_error(c) < accuracy { *count += 1; gauss_arclen_5(c) } else { let (c0, c1) = c.subdivide(); my_arclen(c0, accuracy * 0.5, depth + 1, count) + my_arclen(c1, accuracy * 0.5, depth + 1, count) } } fn my_arclen7(c: CubicBez, accuracy: f64, depth: usize, count: &mut usize) -> f64 { if depth == 16 || est_gauss7_error(c) < accuracy { *count += 1; gauss_arclen_7(c) } else { let (c0, c1) = c.subdivide(); my_arclen7(c0, accuracy * 0.5, depth + 1, count) + my_arclen7(c1, accuracy * 0.5, depth + 1, count) } } // Should make this generic instead of copy+paste, but we need only one when we're done. fn my_arclen9(c: CubicBez, accuracy: f64, depth: usize, count: &mut usize) -> f64 { if depth == 16 || est_gauss9_error(c) < accuracy { *count += 1; gauss_arclen_9(c) } else { let (c0, c1) = c.subdivide(); my_arclen9(c0, accuracy * 0.5, depth + 1, count) + my_arclen9(c1, accuracy * 0.5, depth + 1, count) } } // This doesn't help; we can't really get a more accurate error bound, so all this // does is overkill the accuracy. fn my_arclen11(c: CubicBez, accuracy: f64, depth: usize, count: &mut usize) -> f64 { if depth == 16 || est_gauss9_error(c) < accuracy { *count += 1; gauss_arclen_11(c) } else { let (c0, c1) = c.subdivide(); my_arclen11(c0, accuracy * 0.5, depth + 1, count) + my_arclen11(c1, accuracy * 0.5, depth + 1, count) } } fn randpt() -> Point { Point::new(rand::random(), rand::random()) } fn randbez() -> CubicBez { CubicBez::new(randpt(), randpt(), randpt(), randpt()) } fn main() { let accuracy = 1e-4; for _ in 0..10_000 { let c = randbez(); let t: f64 = rand::random(); let c = c.subsegment(0.0..t); //let accurate_arclen = c.arclen(1e-12); let mut count = 0; let accurate_arclen = my_arclen9(c, 1e-15, 0, &mut count); let est = gauss_arclen_9(c); let est_err = est_gauss9_error_3(c); let err = (accurate_arclen - est).abs(); println!("{} {}", est_err, err); /* let mut count = 0; let est = my_arclen9(c, accuracy, 0, &mut count); let err = (accurate_arclen - est).abs(); println!("{} {}", err, count); */ } } kurbo-0.7.1/examples/ellipse.rs010064400007650000024000000012101374161070200146340ustar 00000000000000//! Example of ellipse use kurbo::{Ellipse, Shape}; use std::f64::consts::PI; fn main() { let ellipse = Ellipse::new((400.0, 400.0), (200.0, 100.0), 0.25 * PI); println!(""); println!(""); println!(""); println!(""); let path = ellipse.to_path(1e-3).to_svg(); println!(" ", path); let path = ellipse.to_path(1.0).to_svg(); println!(" ", path); println!(""); println!(""); println!(""); } kurbo-0.7.1/rustfmt.toml010064400007650000024000000001071367545551600134400ustar 00000000000000max_width = 100 use_field_init_shorthand = true newline_style = "Unix" kurbo-0.7.1/src/affine.rs010064400007650000024000000234561367545551600134400ustar 00000000000000//! Affine transforms. use std::ops::{Mul, MulAssign}; use crate::{Point, Rect, Vec2}; /// A 2D affine transform. #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Affine([f64; 6]); impl Affine { /// A transform that is flipped on the y-axis. Useful for converting between /// y-up and y-down spaces. pub const FLIP_Y: Affine = Affine::new([1.0, 0., 0., -1.0, 0., 0.]); /// A transform that is flipped on the x-axis. pub const FLIP_X: Affine = Affine::new([-1.0, 0., 0., 1.0, 0., 0.]); /// Construct an affine transform from coefficients. /// /// If the coefficients are `(a, b, c, d, e, f)`, then the resulting /// transformation represents this augmented matrix: /// /// ```text /// | a c e | /// | b d f | /// | 0 0 1 | /// ``` /// /// Note that this convention is transposed from PostScript and /// Direct2D, but is consistent with the /// [Wikipedia](https://en.wikipedia.org/wiki/Affine_transformation) /// formulation of affine transformation as augmented matrix. The /// idea is that `(A * B) * v == A * (B * v)`, where `*` is the /// [`Mul`](https://doc.rust-lang.org/std/ops/trait.Mul.html) trait. #[inline] pub const fn new(c: [f64; 6]) -> Affine { Affine(c) } /// An affine transform representing uniform scaling. #[inline] pub const fn scale(s: f64) -> Affine { Affine([s, 0.0, 0.0, s, 0.0, 0.0]) } /// An affine transform representing non-uniform scaling /// with different scale values for x and y #[inline] pub const fn scale_non_uniform(s_x: f64, s_y: f64) -> Affine { Affine([s_x, 0.0, 0.0, s_y, 0.0, 0.0]) } /// An affine transform representing rotation. /// /// The convention for rotation is that a positive angle rotates a /// positive X direction into positive Y. Thus, in a Y-down coordinate /// system (as is common for graphics), it is a clockwise rotation, and /// in Y-up (traditional for math), it is anti-clockwise. /// /// The angle, `th`, is expressed in radians. #[inline] pub fn rotate(th: f64) -> Affine { let s = th.sin(); let c = th.cos(); Affine([c, s, -s, c, 0.0, 0.0]) } /// An affine transform representing translation. #[inline] pub fn translate>(p: V) -> Affine { let p = p.into(); Affine([1.0, 0.0, 0.0, 1.0, p.x, p.y]) } /// Get the coefficients of the transform. #[inline] pub fn as_coeffs(self) -> [f64; 6] { self.0 } /// Compute the determinant of this transform. pub fn determinant(self) -> f64 { self.0[0] * self.0[3] - self.0[1] * self.0[2] } /// Compute the inverse transform. /// /// Produces NaN values when the determinant is zero. pub fn inverse(self) -> Affine { let inv_det = self.determinant().recip(); Affine([ inv_det * self.0[3], -inv_det * self.0[1], -inv_det * self.0[2], inv_det * self.0[0], inv_det * (self.0[2] * self.0[5] - self.0[3] * self.0[4]), inv_det * (self.0[1] * self.0[4] - self.0[0] * self.0[5]), ]) } /// Compute the bounding box of a transformed rectangle. /// /// Returns the minimal `Rect` that encloses the given `Rect` after affine transformation. /// If the transform is axis-aligned, then this bounding box is "tight", in other words the /// returned `Rect` is the transformed rectangle. /// /// The returned rectangle always has non-negative width and height. pub fn transform_rect_bbox(self, rect: Rect) -> Rect { let p00 = self * Point::new(rect.x0, rect.y0); let p01 = self * Point::new(rect.x0, rect.y1); let p10 = self * Point::new(rect.x1, rect.y0); let p11 = self * Point::new(rect.x1, rect.y1); Rect::from_points(p00, p01).union(Rect::from_points(p10, p11)) } /// Compute the singular value decomposition of the linear transformation (ignoring the /// translation). /// /// All non-degenerate linear transformations can be represented as /// /// 1. a rotation about the origin. /// 2. a scaling along the x and y axes /// 3. another rotation about the origin /// /// composed together. Decomposing a 2x2 matrix in this way is called a "singular value /// decomposition" and is written `U Σ V^T`, where U and V^T are orthogonal (rotations) and Σ /// is a diagonal matrix (a scaling). /// /// Since currently this function is used to calculate ellipse radii and rotation from an /// affine map on the unit circle, we don't calculate V^T, since a rotation of the unit (or /// any) circle about its center always results in the same circle. This is the reason that an /// ellipse mapped using an affine map is always an ellipse. /// /// Will return NaNs if the matrix (or equivalently the linear map) is singular. /// /// First part of the return tuple is the scaling, second part is the angle of rotation (in /// radians) #[inline] pub(crate) fn svd(self) -> (Vec2, f64) { let a = self.0[0]; let a2 = a * a; let b = self.0[1]; let b2 = b * b; let c = self.0[2]; let c2 = c * c; let d = self.0[3]; let d2 = d * d; let ab = a * b; let cd = c * d; let angle = 0.5 * (2.0 * (ab + cd)).atan2(a2 - b2 + c2 - d2); let s1 = a2 + b2 + c2 + d2; let s2 = ((a2 - b2 + c2 - d2).powi(2) + 4.0 * (ab + cd).powi(2)).sqrt(); ( Vec2 { x: (0.5 * (s1 + s2)).sqrt(), y: (0.5 * (s1 - s2)).sqrt(), }, angle, ) } /// Returns the translation part of this affine map (`(self.0[4], self.0[5])`). #[inline] pub(crate) fn get_translation(self) -> Vec2 { Vec2 { x: self.0[4], y: self.0[5], } } /// Replaces the translation portion of this affine map #[must_use] pub(crate) fn set_translation(mut self, trans: Vec2) -> Affine { self.0[4] = trans.x; self.0[5] = trans.y; self } } impl Default for Affine { #[inline] fn default() -> Affine { Affine::scale(1.0) } } impl Mul for Affine { type Output = Point; #[inline] fn mul(self, other: Point) -> Point { Point::new( self.0[0] * other.x + self.0[2] * other.y + self.0[4], self.0[1] * other.x + self.0[3] * other.y + self.0[5], ) } } impl Mul for Affine { type Output = Affine; #[inline] fn mul(self, other: Affine) -> Affine { Affine([ self.0[0] * other.0[0] + self.0[2] * other.0[1], self.0[1] * other.0[0] + self.0[3] * other.0[1], self.0[0] * other.0[2] + self.0[2] * other.0[3], self.0[1] * other.0[2] + self.0[3] * other.0[3], self.0[0] * other.0[4] + self.0[2] * other.0[5] + self.0[4], self.0[1] * other.0[4] + self.0[3] * other.0[5] + self.0[5], ]) } } impl MulAssign for Affine { #[inline] fn mul_assign(&mut self, other: Affine) { *self = self.mul(other); } } impl Mul for f64 { type Output = Affine; #[inline] fn mul(self, other: Affine) -> Affine { Affine([ self * other.0[0], self * other.0[1], self * other.0[2], self * other.0[3], self * other.0[4], self * other.0[5], ]) } } // Conversions to and from mint #[cfg(feature = "mint")] impl From for mint::ColumnMatrix2x3 { #[inline] fn from(a: Affine) -> mint::ColumnMatrix2x3 { mint::ColumnMatrix2x3 { x: mint::Vector2 { x: a.0[0], y: a.0[1], }, y: mint::Vector2 { x: a.0[2], y: a.0[3], }, z: mint::Vector2 { x: a.0[4], y: a.0[5], }, } } } #[cfg(feature = "mint")] impl From> for Affine { #[inline] fn from(m: mint::ColumnMatrix2x3) -> Affine { Affine([m.x.x, m.x.y, m.y.x, m.y.y, m.z.x, m.z.y]) } } #[cfg(test)] mod tests { use crate::{Affine, Point}; use std::f64::consts::PI; fn assert_near(p0: Point, p1: Point) { assert!((p1 - p0).hypot() < 1e-9, "{:?} != {:?}", p0, p1); } #[test] fn affine_basic() { let p = Point::new(3.0, 4.0); assert_near(Affine::default() * p, p); assert_near(Affine::scale(2.0) * p, Point::new(6.0, 8.0)); assert_near(Affine::rotate(0.0) * p, p); assert_near(Affine::rotate(PI / 2.0) * p, Point::new(-4.0, 3.0)); assert_near(Affine::translate((5.0, 6.0)) * p, Point::new(8.0, 10.0)); } #[test] fn affine_mul() { let a1 = Affine::new([1.0, 2.0, 3.0, 4.0, 5.0, 6.0]); let a2 = Affine::new([0.1, 1.2, 2.3, 3.4, 4.5, 5.6]); let px = Point::new(1.0, 0.0); let py = Point::new(0.0, 1.0); let pxy = Point::new(1.0, 1.0); assert_near(a1 * (a2 * px), (a1 * a2) * px); assert_near(a1 * (a2 * py), (a1 * a2) * py); assert_near(a1 * (a2 * pxy), (a1 * a2) * pxy); } #[test] fn affine_inv() { let a = Affine::new([0.1, 1.2, 2.3, 3.4, 4.5, 5.6]); let a_inv = a.inverse(); let px = Point::new(1.0, 0.0); let py = Point::new(0.0, 1.0); let pxy = Point::new(1.0, 1.0); assert_near(a * (a_inv * px), px); assert_near(a * (a_inv * py), py); assert_near(a * (a_inv * pxy), pxy); assert_near(a_inv * (a * px), px); assert_near(a_inv * (a * py), py); assert_near(a_inv * (a * pxy), pxy); } } kurbo-0.7.1/src/arc.rs010064400007650000024000000115021374161070200127220ustar 00000000000000//! A circle arc. use crate::{PathEl, Point, Rect, Shape, Vec2}; use std::{ f64::consts::{FRAC_PI_2, PI}, iter, }; /// A single arc segment. #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Arc { /// The arc's centre point. pub center: Point, /// The arc's radii, where the vector's x-component is the radius in the /// positive x direction after applying `x_rotation`. pub radii: Vec2, /// The start angle in radians. pub start_angle: f64, /// The angle between the start and end of the arc, in radians. pub sweep_angle: f64, /// How much the arc is rotated, in radians. pub x_rotation: f64, } impl Arc { /// Create an iterator generating Bezier path elements. /// /// The generated elemets can be append to an existing bezier path. pub fn append_iter(&self, tolerance: f64) -> ArcAppendIter { let sign = self.sweep_angle.signum(); let scaled_err = self.radii.x.max(self.radii.y) / tolerance; // Number of subdivisions per circle based on error tolerance. // Note: this may slightly underestimate the error for quadrants. let n_err = (1.1163 * scaled_err).powf(1.0 / 6.0).max(3.999_999); let n = (n_err * self.sweep_angle.abs() * (1.0 / (2.0 * PI))).ceil(); let angle_step = self.sweep_angle / n; let n = n as usize; let arm_len = (4.0 / 3.0) * (0.25 * angle_step).abs().tan() * sign; let angle0 = self.start_angle; let p0 = sample_ellipse(self.radii, self.x_rotation, angle0); ArcAppendIter { idx: 0, center: self.center, radii: self.radii, x_rotation: self.x_rotation, n, arm_len, angle_step, p0, angle0, } } /// Converts an Arc into a series of cubic bezier segments. /// /// Closure will be invoked for each segment. pub fn to_cubic_beziers

(self, tolerance: f64, mut p: P) where P: FnMut(Point, Point, Point), { let mut path = self.append_iter(tolerance); while let Some(PathEl::CurveTo(p1, p2, p3)) = path.next() { p(p1, p2, p3); } } } #[doc(hidden)] pub struct ArcAppendIter { idx: usize, center: Point, radii: Vec2, x_rotation: f64, n: usize, arm_len: f64, angle_step: f64, p0: Vec2, angle0: f64, } impl Iterator for ArcAppendIter { type Item = PathEl; fn next(&mut self) -> Option { if self.idx >= self.n { return None; } let angle1 = self.angle0 + self.angle_step; let p0 = self.p0; let p1 = p0 + self.arm_len * sample_ellipse(self.radii, self.x_rotation, self.angle0 + FRAC_PI_2); let p3 = sample_ellipse(self.radii, self.x_rotation, angle1); let p2 = p3 - self.arm_len * sample_ellipse(self.radii, self.x_rotation, angle1 + FRAC_PI_2); self.angle0 = angle1; self.p0 = p3; self.idx += 1; Some(PathEl::CurveTo( self.center + p1, self.center + p2, self.center + p3, )) } } /// Take the ellipse radii, how the radii are rotated and the sweep angle, and return a point on /// the ellipse. fn sample_ellipse(radii: Vec2, x_rotation: f64, angle: f64) -> Vec2 { let u = radii.x * angle.cos(); let v = radii.y * angle.sin(); rotate_pt(Vec2::new(u, v), x_rotation) } /// Rotate `pt` about the origin by `angle` radians. fn rotate_pt(pt: Vec2, angle: f64) -> Vec2 { Vec2::new( pt.x * angle.cos() - pt.y * angle.sin(), pt.x * angle.sin() + pt.y * angle.cos(), ) } impl Shape for Arc { type PathElementsIter = iter::Chain, ArcAppendIter>; fn path_elements(&self, tolerance: f64) -> Self::PathElementsIter { let p0 = sample_ellipse(self.radii, self.x_rotation, self.start_angle); iter::once(PathEl::MoveTo(self.center + p0)).chain(self.append_iter(tolerance)) } /// Note: shape isn't closed so area is not well defined. #[inline] fn area(&self) -> f64 { let Vec2 { x, y } = self.radii; PI * x * y } /// Note: Finding the perimiter of an ellipse is fairly involved, so for now just approximate /// by using the bezier curve representation. (See /// https://en.wikipedia.org/wiki/Ellipse#Circumference) #[inline] fn perimeter(&self, accuracy: f64) -> f64 { self.path_segments(0.1).perimeter(accuracy) } /// Note: shape isn't closed so a point's winding number is not well defined. #[inline] fn winding(&self, pt: Point) -> i32 { self.path_segments(0.1).winding(pt) } #[inline] fn bounding_box(&self) -> Rect { self.path_segments(0.1).bounding_box() } } kurbo-0.7.1/src/bezpath.rs010064400007650000024000001301541374554710500136310ustar 00000000000000//! Bézier paths (up to cubic). #![allow(clippy::many_single_char_names)] use std::iter::{Extend, FromIterator}; use std::mem; use std::ops::{Mul, Range}; use arrayvec::ArrayVec; use crate::common::{solve_cubic, solve_quadratic}; use crate::MAX_EXTREMA; use crate::{ Affine, CubicBez, Line, ParamCurve, ParamCurveArclen, ParamCurveArea, ParamCurveExtrema, ParamCurveNearest, Point, QuadBez, Rect, Shape, TranslateScale, }; /// A Bézier path. /// /// These docs assume basic familiarity with Bézier curves; for an introduction, /// see Pomax's wonderful [A Primer on Bézier Curves]. /// /// This path can contain lines, quadratics ([`QuadBez`]) and cubics /// ([`CubicBez`]), and may contain multiple subpaths. /// /// # Elements and Segments /// /// A Bézier path can be represented in terms of either 'elements' ([`PathEl`]) /// or 'segments' ([`PathSeg`]). Elements map closely to how Béziers are /// generally used in PostScript-style drawing APIs; they can be thought of as /// instructions for drawing the path. Segments more directly describe the /// path itself, with each segment being an independent line or curve. /// /// These different representations are useful in different contexts. /// For tasks like drawing, elements are a natural fit, but when doing /// hit-testing or subdividing, we need to have access to the segments. /// /// Internally, a `BezPath` is a list of [`PathEl`]s; as such it implements /// [`FromIterator`] and [`Extend`]: /// /// ``` /// use kurbo::{BezPath, Rect, Shape, Vec2}; /// let accuracy = 0.1; /// let rect = Rect::from_origin_size((0., 0.,), (10., 10.)); /// // these are equivalent /// let path1 = rect.to_path(accuracy); /// let path2: BezPath = rect.path_elements(accuracy).collect(); /// /// // extend a path with another path: /// let mut path = rect.to_path(accuracy); /// let shifted_rect = rect + Vec2::new(5.0, 10.0); /// path.extend(shifted_rect.to_path(accuracy)); /// ``` /// /// You can iterate the elements of a `BezPath` with the [`iter`] method, /// and the segments with the [`segments`] method: /// /// ``` /// use kurbo::{BezPath, Line, PathEl, PathSeg, Point, Rect, Shape}; /// let accuracy = 0.1; /// let rect = Rect::from_origin_size((0., 0.,), (10., 10.)); /// // these are equivalent /// let path = rect.to_path(accuracy); /// let first_el = PathEl::MoveTo(Point::ZERO); /// let first_seg = PathSeg::Line(Line::new((0., 0.), (10., 0.))); /// assert_eq!(path.iter().next(), Some(first_el)); /// assert_eq!(path.segments().next(), Some(first_seg)); /// ``` /// In addition, if you have some other type that implements /// `Iterator`, you can adapt that to an iterator of segments with /// the [`segments` free function]. /// /// # Advanced functionality /// /// In addition to the basic API, there are several useful pieces of advanced /// functionality available on `BezPath`: /// /// - [`flatten`] does Bézier flattening, converting a curve to a series of /// line segments /// - [`intersect_line`] computes intersections of a path with a line, useful /// for things like subdividing /// /// [A Primer on Bézier Curves]: https://pomax.github.io/bezierinfo/ /// [`PathEl`]: enum.PathEl.html /// [`PathSeg`]: enum.PathSeg.html /// [`QuadBez`]: struct.QuadBez.html /// [`CubicBez`]: struct.CubicBez.html /// [`iter`]: #method.iter /// [`segments`]: #method.segments /// [`flatten`]: #method.flatten /// [`intersect_line`]: #method.intersect_line /// [`segments` free function]: function.segments.html /// [`FromIterator`]: std::iter::FromIterator /// [`Extend`]: std::iter::Extend #[derive(Clone, Default, Debug)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct BezPath(Vec); /// The element of a Bézier path. /// /// A valid path has `MoveTo` at the beginning of each subpath. #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum PathEl { /// Move directly to the point without drawing anything, starting a new /// subpath. MoveTo(Point), /// Draw a line from the current location to the point. LineTo(Point), /// Draw a quadratic bezier using the current location and the two points. QuadTo(Point, Point), /// Draw a cubic bezier using the current location and the three points. CurveTo(Point, Point, Point), /// Close off the path. ClosePath, } /// A segment of a Bézier path. #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum PathSeg { /// A line segment. Line(Line), /// A quadratic bezier segment. Quad(QuadBez), /// A cubic bezier segment. Cubic(CubicBez), } /// An intersection of a [`Line`] and a [`PathSeg`]. /// /// This can be generated with the [`PathSeg::intersect_line`] method. /// /// [`Line`]: struct.Line.html /// [`PathSeg`]: enum.PathSeg.html /// [`PathSeg::intersect_line`]: enum.PathSeg.html#method.intersect_line #[derive(Debug, Clone, Copy)] pub struct LineIntersection { /// The 'time' that the intersection occurs, on the line. /// /// This value is in the range 0..1. pub line_t: f64, /// The 'time' that the intersection occurs, on the path segment. /// /// This value is nominally in the range 0..1, although it may slightly exceed /// that range at the boundaries of segments. pub segment_t: f64, } impl BezPath { /// Create a new path. pub fn new() -> BezPath { Default::default() } /// Create a path from a vector of path elements. /// /// `BezPath` also implements `FromIterator`, so it works with `collect`: /// /// ``` /// // a very contrived example: /// use kurbo::{BezPath, PathEl}; /// /// let path = BezPath::new(); /// let as_vec: Vec = path.into_iter().collect(); /// let back_to_path: BezPath = as_vec.into_iter().collect(); /// ``` pub fn from_vec(v: Vec) -> BezPath { BezPath(v) } /// Push a generic path element onto the path. pub fn push(&mut self, el: PathEl) { self.0.push(el) } /// Push a "move to" element onto the path. pub fn move_to>(&mut self, p: P) { self.push(PathEl::MoveTo(p.into())); } /// Push a "line to" element onto the path. pub fn line_to>(&mut self, p: P) { self.push(PathEl::LineTo(p.into())); } /// Push a "quad to" element onto the path. pub fn quad_to>(&mut self, p1: P, p2: P) { self.push(PathEl::QuadTo(p1.into(), p2.into())); } /// Push a "curve to" element onto the path. pub fn curve_to>(&mut self, p1: P, p2: P, p3: P) { self.push(PathEl::CurveTo(p1.into(), p2.into(), p3.into())); } /// Push a "close path" element onto the path. pub fn close_path(&mut self) { self.push(PathEl::ClosePath); } /// Get the path elements. pub fn elements(&self) -> &[PathEl] { &self.0 } /// Returns an iterator over the path's elements. pub fn iter(&self) -> impl Iterator + '_ { self.0.iter().copied() } /// Iterate over the path segments. pub fn segments(&self) -> impl Iterator + '_ { segments(self.iter()) } /// Flatten the path, invoking the callback repeatedly. /// /// Flattening is the action of approximating a curve with a succession of line segments. /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// The tolerance value controls the maximum distance between the curved input /// segments and their polyline approximations. (In technical terms, this is the /// Hausdorff distance). The algorithm attempts to bound this distance between /// by `tolerance` but this is not absolutely guaranteed. The appropriate value /// depends on the use, but for antialiasted rendering, a value of 0.25 has been /// determined to give good results. The number of segments tends to scale as the /// inverse square root of tolerance. /// /// /// /// /// /// /// /// /// /// /// /// /// The callback will be called in order with each element of the generated /// path. Because the result is made of polylines, these will be straight-line /// path elements only, no curves. /// /// This algorithm is based on the blog post [Flattening quadratic Béziers] /// but with some refinements. For one, there is a more careful approximation /// at cusps. For two, the algorithm is extended to work with cubic Béziers /// as well, by first subdividing into quadratics and then computing the /// subdivision of each quadratic. However, as a clever trick, these quadratics /// are subdivided fractionally, and their endpoints are not included. /// /// TODO: write a paper explaining this in more detail. /// /// Note: the [`flatten`](fn.flatten.html) function provides the same /// functionality but works with slices and other [`PathEl`] iterators. /// /// [Flattening quadratic Béziers]: https://raphlinus.github.io/graphics/curves/2019/12/23/flatten-quadbez.html /// [`PathEl`]: enum.PathEl.html pub fn flatten(&self, tolerance: f64, callback: impl FnMut(PathEl)) { flatten(self, tolerance, callback); } /// Get the segment at the given element index. /// /// The element index counts [`PathEl`](enum.PathEl.html) elements, so /// for example includes an initial `Moveto`. pub fn get_seg(&self, ix: usize) -> Option { if ix == 0 || ix >= self.0.len() { return None; } let last = match self.0[ix - 1] { PathEl::MoveTo(p) => p, PathEl::LineTo(p) => p, PathEl::QuadTo(_, p2) => p2, PathEl::CurveTo(_, _, p3) => p3, _ => return None, }; match self.0[ix] { PathEl::LineTo(p) => Some(PathSeg::Line(Line::new(last, p))), PathEl::QuadTo(p1, p2) => Some(PathSeg::Quad(QuadBez::new(last, p1, p2))), PathEl::CurveTo(p1, p2, p3) => Some(PathSeg::Cubic(CubicBez::new(last, p1, p2, p3))), PathEl::ClosePath => self.0[..ix].iter().rev().find_map(|el| match *el { PathEl::MoveTo(start) => Some(PathSeg::Line(Line::new(last, start))), _ => None, }), _ => None, } } /// Returns `true` if the path contains no segments. pub fn is_empty(&self) -> bool { self.0 .iter() .all(|el| matches!(el, PathEl::MoveTo(..) | PathEl::ClosePath)) } /// Apply an affine transform to the path. pub fn apply_affine(&mut self, affine: Affine) { for el in self.0.iter_mut() { *el = affine * (*el); } } } impl FromIterator for BezPath { fn from_iter>(iter: T) -> Self { let el_vec: Vec<_> = iter.into_iter().collect(); BezPath::from_vec(el_vec) } } /// Allow iteration over references to `BezPath`. /// /// Note: the semantics are slightly different than simply iterating over the /// slice, as it returns `PathEl` items, rather than references. impl<'a> IntoIterator for &'a BezPath { type Item = PathEl; type IntoIter = std::iter::Cloned>; fn into_iter(self) -> Self::IntoIter { self.elements().iter().cloned() } } impl IntoIterator for BezPath { type Item = PathEl; type IntoIter = std::vec::IntoIter; fn into_iter(self) -> Self::IntoIter { self.0.into_iter() } } impl Extend for BezPath { fn extend>(&mut self, iter: I) { self.0.extend(iter); } } /// Proportion of tolerance budget that goes to cubic to quadratic conversion. const TO_QUAD_TOL: f64 = 0.1; /// Flatten the path, invoking the callback repeatedly. /// /// See [`BezPath::flatten`](struct.BezPath.html#method.flatten) for more discussion. /// This signature is a bit more general, allowing flattening of `&[PathEl]` slices /// and other iterators yielding `PathEl`. pub fn flatten( path: impl IntoIterator, tolerance: f64, mut callback: impl FnMut(PathEl), ) { let sqrt_tol = tolerance.sqrt(); let mut last_pt = None; let mut quad_buf = Vec::new(); for el in path { match el { PathEl::MoveTo(p) => { last_pt = Some(p); callback(PathEl::MoveTo(p)); } PathEl::LineTo(p) => { last_pt = Some(p); callback(PathEl::LineTo(p)); } PathEl::QuadTo(p1, p2) => { if let Some(p0) = last_pt { let q = QuadBez::new(p0, p1, p2); let params = q.estimate_subdiv(sqrt_tol); let n = ((0.5 * params.val / sqrt_tol).ceil() as usize).max(1); let step = 1.0 / (n as f64); for i in 1..(n - 1) { let u = (i as f64) * step; let t = q.determine_subdiv_t(¶ms, u); let p = q.eval(t); callback(PathEl::LineTo(p)); } callback(PathEl::LineTo(p2)); } last_pt = Some(p2); } PathEl::CurveTo(p1, p2, p3) => { if let Some(p0) = last_pt { let c = CubicBez::new(p0, p1, p2, p3); // Subdivide into quadratics, and estimate the number of // subdivisions required for each, summing to arrive at an // estimate for the number of subdivisions for the cubic. // Also retain these parameters for later. let iter = c.to_quads(tolerance * TO_QUAD_TOL); quad_buf.clear(); quad_buf.reserve(iter.size_hint().0); let sqrt_remain_tol = sqrt_tol * (1.0 - TO_QUAD_TOL).sqrt(); let mut sum = 0.0; for (_, _, q) in iter { let params = q.estimate_subdiv(sqrt_remain_tol); sum += params.val; quad_buf.push((q, params)); } let n = ((0.5 * sum / sqrt_remain_tol).ceil() as usize).max(1); // Iterate through the quadratics, outputting the points of // subdivisions that fall within that quadratic. let step = sum / (n as f64); let mut i = 1; let mut val_sum = 0.0; for (q, params) in &quad_buf { let mut target = (i as f64) * step; let recip_val = params.val.recip(); while target < val_sum + params.val { let u = (target - val_sum) * recip_val; let t = q.determine_subdiv_t(¶ms, u); let p = q.eval(t); callback(PathEl::LineTo(p)); i += 1; if i == n + 1 { break; } target = (i as f64) * step; } val_sum += params.val; } callback(PathEl::LineTo(p3)); } last_pt = Some(p3); } PathEl::ClosePath => { last_pt = None; callback(PathEl::ClosePath); } } } } impl Mul for Affine { type Output = PathEl; fn mul(self, other: PathEl) -> PathEl { match other { PathEl::MoveTo(p) => PathEl::MoveTo(self * p), PathEl::LineTo(p) => PathEl::LineTo(self * p), PathEl::QuadTo(p1, p2) => PathEl::QuadTo(self * p1, self * p2), PathEl::CurveTo(p1, p2, p3) => PathEl::CurveTo(self * p1, self * p2, self * p3), PathEl::ClosePath => PathEl::ClosePath, } } } impl Mul for Affine { type Output = PathSeg; fn mul(self, other: PathSeg) -> PathSeg { match other { PathSeg::Line(line) => PathSeg::Line(self * line), PathSeg::Quad(quad) => PathSeg::Quad(self * quad), PathSeg::Cubic(cubic) => PathSeg::Cubic(self * cubic), } } } impl Mul for Affine { type Output = BezPath; fn mul(self, other: BezPath) -> BezPath { BezPath(other.0.iter().map(|&el| self * el).collect()) } } impl<'a> Mul<&'a BezPath> for Affine { type Output = BezPath; fn mul(self, other: &BezPath) -> BezPath { BezPath(other.0.iter().map(|&el| self * el).collect()) } } impl Mul for TranslateScale { type Output = PathEl; fn mul(self, other: PathEl) -> PathEl { match other { PathEl::MoveTo(p) => PathEl::MoveTo(self * p), PathEl::LineTo(p) => PathEl::LineTo(self * p), PathEl::QuadTo(p1, p2) => PathEl::QuadTo(self * p1, self * p2), PathEl::CurveTo(p1, p2, p3) => PathEl::CurveTo(self * p1, self * p2, self * p3), PathEl::ClosePath => PathEl::ClosePath, } } } impl Mul for TranslateScale { type Output = PathSeg; fn mul(self, other: PathSeg) -> PathSeg { match other { PathSeg::Line(line) => PathSeg::Line(self * line), PathSeg::Quad(quad) => PathSeg::Quad(self * quad), PathSeg::Cubic(cubic) => PathSeg::Cubic(self * cubic), } } } impl Mul for TranslateScale { type Output = BezPath; fn mul(self, other: BezPath) -> BezPath { BezPath(other.0.iter().map(|&el| self * el).collect()) } } impl<'a> Mul<&'a BezPath> for TranslateScale { type Output = BezPath; fn mul(self, other: &BezPath) -> BezPath { BezPath(other.0.iter().map(|&el| self * el).collect()) } } /// Transform an iterator over path elements into one over path /// segments. /// /// See also [`BezPath::segments`](struct.BezPath.html#method.segments). /// This signature is a bit more general, allowing `&[PathEl]` slices /// and other iterators yielding `PathEl`. pub fn segments(elements: I) -> Segments where I: IntoIterator, { Segments { elements: elements.into_iter(), start_last: None, } } /// An iterator that transforms path elements to path segments. /// /// This struct is created by the [`segments`](fn.segments.html) function. pub struct Segments> { elements: I, start_last: Option<(Point, Point)>, } impl> Iterator for Segments { type Item = PathSeg; fn next(&mut self) -> Option { while let Some(el) = self.elements.next() { // We first need to check whether this is the first // path element we see to fill in the start position. let (start, last) = self.start_last.get_or_insert_with(|| { let point = match el { PathEl::MoveTo(p) => p, PathEl::LineTo(p) => p, PathEl::QuadTo(_, p2) => p2, PathEl::CurveTo(_, _, p3) => p3, PathEl::ClosePath => panic!("Can't start a segment on a ClosePath"), }; (point, point) }); return Some(match el { PathEl::MoveTo(p) => { *start = p; *last = p; continue; } PathEl::LineTo(p) => PathSeg::Line(Line::new(mem::replace(last, p), p)), PathEl::QuadTo(p1, p2) => { PathSeg::Quad(QuadBez::new(mem::replace(last, p2), p1, p2)) } PathEl::CurveTo(p1, p2, p3) => { PathSeg::Cubic(CubicBez::new(mem::replace(last, p3), p1, p2, p3)) } PathEl::ClosePath => { if *last != *start { PathSeg::Line(Line::new(mem::replace(last, *start), *start)) } else { continue; } } }); } None } } impl> Segments { /// Here, `accuracy` specifies the accuracy for each Bézier segment. At worst, /// the total error is `accuracy` times the number of Bézier segments. // TODO: pub? Or is this subsumed by method of &[PathEl]? pub(crate) fn perimeter(self, accuracy: f64) -> f64 { self.map(|seg| seg.arclen(accuracy)).sum() } // Same pub(crate) fn area(self) -> f64 { self.map(|seg| seg.signed_area()).sum() } // Same pub(crate) fn winding(self, p: Point) -> i32 { self.map(|seg| seg.winding(p)).sum() } // Same pub(crate) fn bounding_box(self) -> Rect { let mut bbox: Option = None; for seg in self { let seg_bb = ParamCurveExtrema::bounding_box(&seg); if let Some(bb) = bbox { bbox = Some(bb.union(seg_bb)); } else { bbox = Some(seg_bb) } } bbox.unwrap_or_default() } } impl ParamCurve for PathSeg { fn eval(&self, t: f64) -> Point { match *self { PathSeg::Line(line) => line.eval(t), PathSeg::Quad(quad) => quad.eval(t), PathSeg::Cubic(cubic) => cubic.eval(t), } } fn subsegment(&self, range: Range) -> PathSeg { match *self { PathSeg::Line(line) => PathSeg::Line(line.subsegment(range)), PathSeg::Quad(quad) => PathSeg::Quad(quad.subsegment(range)), PathSeg::Cubic(cubic) => PathSeg::Cubic(cubic.subsegment(range)), } } } impl ParamCurveArclen for PathSeg { fn arclen(&self, accuracy: f64) -> f64 { match *self { PathSeg::Line(line) => line.arclen(accuracy), PathSeg::Quad(quad) => quad.arclen(accuracy), PathSeg::Cubic(cubic) => cubic.arclen(accuracy), } } } impl ParamCurveArea for PathSeg { fn signed_area(&self) -> f64 { match *self { PathSeg::Line(line) => line.signed_area(), PathSeg::Quad(quad) => quad.signed_area(), PathSeg::Cubic(cubic) => cubic.signed_area(), } } } impl ParamCurveNearest for PathSeg { fn nearest(&self, p: Point, accuracy: f64) -> (f64, f64) { match *self { PathSeg::Line(line) => line.nearest(p, accuracy), PathSeg::Quad(quad) => quad.nearest(p, accuracy), PathSeg::Cubic(cubic) => cubic.nearest(p, accuracy), } } } impl ParamCurveExtrema for PathSeg { fn extrema(&self) -> ArrayVec<[f64; MAX_EXTREMA]> { match *self { PathSeg::Line(line) => line.extrema(), PathSeg::Quad(quad) => quad.extrema(), PathSeg::Cubic(cubic) => cubic.extrema(), } } } impl PathSeg { /// Returns a new `PathSeg` describing the same path as `self`, but with /// the points reversed. pub fn reverse(&self) -> PathSeg { match self { PathSeg::Line(Line { p0, p1 }) => PathSeg::Line(Line::new(*p1, *p0)), PathSeg::Quad(q) => PathSeg::Quad(QuadBez::new(q.p2, q.p1, q.p0)), PathSeg::Cubic(c) => PathSeg::Cubic(CubicBez::new(c.p3, c.p2, c.p1, c.p0)), } } /// Convert this segment to a cubic bezier. pub fn to_cubic(&self) -> CubicBez { match *self { PathSeg::Line(Line { p0, p1 }) => CubicBez::new(p0, p0, p1, p1), PathSeg::Cubic(c) => c, PathSeg::Quad(q) => q.raise(), } } // Assumes split at extrema. fn winding_inner(&self, p: Point) -> i32 { let start = self.start(); let end = self.end(); let sign = if end.y > start.y { if p.y < start.y || p.y >= end.y { return 0; } -1 } else if end.y < start.y { if p.y < end.y || p.y >= start.y { return 0; } 1 } else { return 0; }; match *self { PathSeg::Line(_line) => { if p.x < start.x.min(end.x) { return 0; } if p.x >= start.x.max(end.x) { return sign; } // line equation ax + by = c let a = end.y - start.y; let b = start.x - end.x; let c = a * start.x + b * start.y; if (a * p.x + b * p.y - c) * (sign as f64) >= 0.0 { sign } else { 0 } } PathSeg::Quad(quad) => { let p1 = quad.p1; if p.x < start.x.min(end.x).min(p1.x) { return 0; } if p.x >= start.x.max(end.x).max(p1.x) { return sign; } let a = end.y - 2.0 * p1.y + start.y; let b = 2.0 * (p1.y - start.y); let c = start.y - p.y; for t in solve_quadratic(c, b, a) { if t >= 0.0 && t <= 1.0 { let x = quad.eval(t).x; if p.x >= x { return sign; } else { return 0; } } } 0 } PathSeg::Cubic(cubic) => { let p1 = cubic.p1; let p2 = cubic.p2; if p.x < start.x.min(end.x).min(p1.x).min(p2.x) { return 0; } if p.x >= start.x.max(end.x).max(p1.x).max(p2.x) { return sign; } let a = end.y - 3.0 * p2.y + 3.0 * p1.y - start.y; let b = 3.0 * (p2.y - 2.0 * p1.y + start.y); let c = 3.0 * (p1.y - start.y); let d = start.y - p.y; for t in solve_cubic(d, c, b, a) { if t >= 0.0 && t <= 1.0 { let x = cubic.eval(t).x; if p.x >= x { return sign; } else { return 0; } } } 0 } } } /// Compute the winding number contribution of a single segment. /// /// Cast a ray to the left and count intersections. fn winding(&self, p: Point) -> i32 { self.extrema_ranges() .into_iter() .map(|range| self.subsegment(range).winding_inner(p)) .sum() } /// Compute intersections against a line. /// /// Returns a vector of the intersections. For each intersection, /// the `t` value of the segment and line are given. /// /// Note: This test is designed to be inclusive of points near the endpoints /// of the segment. This is so that testing a line against multiple /// contiguous segments of a path will be guaranteed to catch at least one /// of them. In such cases, use higher level logic to coalesce the hits /// (the `t` value may be slightly outside the range of 0..1). /// /// # Examples /// /// ``` /// # use kurbo::*; /// let seg = PathSeg::Line(Line::new((0.0, 0.0), (2.0, 0.0))); /// let line = Line::new((1.0, 2.0), (1.0, -2.0)); /// let intersection = seg.intersect_line(line); /// assert_eq!(intersection.len(), 1); /// let intersection = intersection[0]; /// assert_eq!(intersection.segment_t, 0.5); /// assert_eq!(intersection.line_t, 0.5); /// /// let point = seg.eval(intersection.segment_t); /// assert_eq!(point, Point::new(1.0, 0.0)); /// ``` pub fn intersect_line(&self, line: Line) -> ArrayVec<[LineIntersection; 3]> { const EPSILON: f64 = 1e-9; let p0 = line.p0; let p1 = line.p1; let dx = p1.x - p0.x; let dy = p1.y - p0.y; let mut result = ArrayVec::new(); match self { PathSeg::Line(l) => { let det = dx * (l.p1.y - l.p0.y) - dy * (l.p1.x - l.p0.x); if det.abs() < EPSILON { // Lines are coincident (or nearly so). return result; } let t = dx * (p0.y - l.p0.y) - dy * (p0.x - l.p0.x); // t = position on self let t = t / det; if t >= -EPSILON && t <= 1.0 + EPSILON { // u = position on probe line let u = (l.p0.x - p0.x) * (l.p1.y - l.p0.y) - (l.p0.y - p0.y) * (l.p1.x - l.p0.x); let u = u / det; if u >= 0.0 && u <= 1.0 { result.push(LineIntersection::new(u, t)); } } } PathSeg::Quad(q) => { // The basic technique here is to determine x and y as a quadratic polynomial // as a function of t. Then plug those values into the line equation for the // probe line (giving a sort of signed distance from the probe line) and solve // that for t. let (px0, px1, px2) = quadratic_bez_coefs(q.p0.x, q.p1.x, q.p2.x); let (py0, py1, py2) = quadratic_bez_coefs(q.p0.y, q.p1.y, q.p2.y); let c0 = dy * (px0 - p0.x) - dx * (py0 - p0.y); let c1 = dy * px1 - dx * py1; let c2 = dy * px2 - dx * py2; let invlen2 = (dx * dx + dy * dy).recip(); for t in crate::common::solve_quadratic(c0, c1, c2) { if t >= -EPSILON && t <= 1.0 + EPSILON { let x = px0 + t * px1 + t * t * px2; let y = py0 + t * py1 + t * t * py2; let u = ((x - p0.x) * dx + (y - p0.y) * dy) * invlen2; if u >= 0.0 && u <= 1.0 { result.push(LineIntersection::new(u, t)); } } } } PathSeg::Cubic(c) => { // Same technique as above, but cubic polynomial. let (px0, px1, px2, px3) = cubic_bez_coefs(c.p0.x, c.p1.x, c.p2.x, c.p3.x); let (py0, py1, py2, py3) = cubic_bez_coefs(c.p0.y, c.p1.y, c.p2.y, c.p3.y); let c0 = dy * (px0 - p0.x) - dx * (py0 - p0.y); let c1 = dy * px1 - dx * py1; let c2 = dy * px2 - dx * py2; let c3 = dy * px3 - dx * py3; let invlen2 = (dx * dx + dy * dy).recip(); for t in crate::common::solve_cubic(c0, c1, c2, c3) { if t >= -EPSILON && t <= 1.0 + EPSILON { let x = px0 + t * px1 + t * t * px2 + t * t * t * px3; let y = py0 + t * py1 + t * t * py2 + t * t * t * py3; let u = ((x - p0.x) * dx + (y - p0.y) * dy) * invlen2; if u >= 0.0 && u <= 1.0 { result.push(LineIntersection::new(u, t)); } } } } } result } } impl LineIntersection { fn new(line_t: f64, segment_t: f64) -> Self { LineIntersection { line_t, segment_t } } } // Return polynomial coefficients given cubic bezier coordinates. fn quadratic_bez_coefs(x0: f64, x1: f64, x2: f64) -> (f64, f64, f64) { let p0 = x0; let p1 = 2.0 * x1 - 2.0 * x0; let p2 = x2 - 2.0 * x1 + x0; (p0, p1, p2) } // Return polynomial coefficients given cubic bezier coordinates. fn cubic_bez_coefs(x0: f64, x1: f64, x2: f64, x3: f64) -> (f64, f64, f64, f64) { let p0 = x0; let p1 = 3.0 * x1 - 3.0 * x0; let p2 = 3.0 * x2 - 6.0 * x1 + 3.0 * x0; let p3 = x3 - 3.0 * x2 + 3.0 * x1 - x0; (p0, p1, p2, p3) } impl From for PathSeg { fn from(cubic_bez: CubicBez) -> PathSeg { PathSeg::Cubic(cubic_bez) } } impl From for PathSeg { fn from(line: Line) -> PathSeg { PathSeg::Line(line) } } impl From for PathSeg { fn from(quad_bez: QuadBez) -> PathSeg { PathSeg::Quad(quad_bez) } } impl Shape for BezPath { type PathElementsIter = std::vec::IntoIter; fn path_elements(&self, _tolerance: f64) -> Self::PathElementsIter { self.0.clone().into_iter() } fn to_path(&self, _tolerance: f64) -> BezPath { self.clone() } fn into_path(self, _tolerance: f64) -> BezPath { self } /// Signed area. fn area(&self) -> f64 { self.elements().area() } fn perimeter(&self, accuracy: f64) -> f64 { self.elements().perimeter(accuracy) } /// Winding number of point. fn winding(&self, pt: Point) -> i32 { self.elements().winding(pt) } fn bounding_box(&self) -> Rect { self.elements().bounding_box() } fn as_path_slice(&self) -> Option<&[PathEl]> { Some(&self.0) } } /// Implements [`Shape`] for a slice of [`PathEl`], provided that the first element of the slice is /// not a `PathEl::ClosePath`. If it is, several of these functions will panic. /// /// If the slice starts with `LineTo`, `QuadTo`, or `CurveTo`, it will be treated as a `MoveTo`. impl<'a> Shape for &'a [PathEl] { type PathElementsIter = std::iter::Cloned>; #[inline] fn path_elements(&self, _tolerance: f64) -> Self::PathElementsIter { self.iter().cloned() } fn to_path(&self, _tolerance: f64) -> BezPath { BezPath::from_vec(self.to_vec()) } /// Signed area. fn area(&self) -> f64 { segments(self.iter().copied()).area() } fn perimeter(&self, accuracy: f64) -> f64 { segments(self.iter().copied()).perimeter(accuracy) } /// Winding number of point. fn winding(&self, pt: Point) -> i32 { segments(self.iter().copied()).winding(pt) } fn bounding_box(&self) -> Rect { segments(self.iter().copied()).bounding_box() } #[inline] fn as_path_slice(&self) -> Option<&[PathEl]> { Some(self) } } /// An iterator for path segments. pub struct PathSegIter { seg: PathSeg, ix: usize, } impl Shape for PathSeg { type PathElementsIter = PathSegIter; #[inline] fn path_elements(&self, _tolerance: f64) -> PathSegIter { PathSegIter { seg: *self, ix: 0 } } /// The area under the curve. /// /// We could just return 0, but this seems more useful. fn area(&self) -> f64 { self.signed_area() } #[inline] fn perimeter(&self, accuracy: f64) -> f64 { self.arclen(accuracy) } fn winding(&self, _pt: Point) -> i32 { 0 } #[inline] fn bounding_box(&self) -> Rect { ParamCurveExtrema::bounding_box(self) } fn as_line(&self) -> Option { if let PathSeg::Line(line) = self { Some(*line) } else { None } } } impl Iterator for PathSegIter { type Item = PathEl; fn next(&mut self) -> Option { self.ix += 1; match (self.ix, self.seg) { // yes I could do some fancy bindings thing here but... :shrug: (1, PathSeg::Line(seg)) => Some(PathEl::MoveTo(seg.p0)), (1, PathSeg::Quad(seg)) => Some(PathEl::MoveTo(seg.p0)), (1, PathSeg::Cubic(seg)) => Some(PathEl::MoveTo(seg.p0)), (2, PathSeg::Line(seg)) => Some(PathEl::LineTo(seg.p1)), (2, PathSeg::Quad(seg)) => Some(PathEl::QuadTo(seg.p1, seg.p2)), (2, PathSeg::Cubic(seg)) => Some(PathEl::CurveTo(seg.p1, seg.p2, seg.p3)), _ => None, } } } #[cfg(test)] mod tests { use super::*; fn assert_approx_eq(x: f64, y: f64) { assert!((x - y).abs() < 1e-8, "{} != {}", x, y); } #[test] #[should_panic(expected = "Can't start a segment on a ClosePath")] fn test_elements_to_segments_starts_on_closepath() { let mut path = BezPath::new(); path.close_path(); path.segments().next(); } #[test] fn test_elements_to_segments_closepath_refers_to_last_moveto() { let mut path = BezPath::new(); path.move_to((5.0, 5.0)); path.line_to((15.0, 15.0)); path.move_to((10.0, 10.0)); path.line_to((15.0, 15.0)); path.close_path(); assert_eq!( path.segments().collect::>().last(), Some(&Line::new((15.0, 15.0), (10.0, 10.0)).into()), ); } #[test] fn test_elements_to_segments_starts_on_quad() { let mut path = BezPath::new(); path.quad_to((5.0, 5.0), (10.0, 10.0)); path.line_to((15.0, 15.0)); path.close_path(); let mut segments = path.segments(); assert_eq!( segments.next(), Some(QuadBez::new((10.0, 10.0), (5.0, 5.0), (10.0, 10.0)).into()), ); assert_eq!( segments.next(), Some(Line::new((10.0, 10.0), (15.0, 15.0)).into()), ); assert_eq!( segments.next(), Some(Line::new((15.0, 15.0), (10.0, 10.0)).into()), ); assert_eq!(segments.next(), None); } #[test] fn test_intersect_line() { let h_line = Line::new((0.0, 0.0), (100.0, 0.0)); let v_line = Line::new((10.0, -10.0), (10.0, 10.0)); let intersection = PathSeg::Line(h_line).intersect_line(v_line)[0]; assert_approx_eq(intersection.segment_t, 0.1); assert_approx_eq(intersection.line_t, 0.5); let v_line = Line::new((-10.0, -10.0), (-10.0, 10.0)); assert!(PathSeg::Line(h_line).intersect_line(v_line).is_empty()); let v_line = Line::new((10.0, 10.0), (10.0, 20.0)); assert!(PathSeg::Line(h_line).intersect_line(v_line).is_empty()); } #[test] fn test_intersect_qad() { let q = QuadBez::new((0.0, -10.0), (10.0, 20.0), (20.0, -10.0)); let v_line = Line::new((10.0, -10.0), (10.0, 10.0)); assert_eq!(PathSeg::Quad(q).intersect_line(v_line).len(), 1); let intersection = PathSeg::Quad(q).intersect_line(v_line)[0]; assert_approx_eq(intersection.segment_t, 0.5); assert_approx_eq(intersection.line_t, 0.75); let h_line = Line::new((0.0, 0.0), (100.0, 0.0)); assert_eq!(PathSeg::Quad(q).intersect_line(h_line).len(), 2); } #[test] fn test_intersect_cubic() { let c = CubicBez::new((0.0, -10.0), (10.0, 20.0), (20.0, -20.0), (30.0, 10.0)); let v_line = Line::new((10.0, -10.0), (10.0, 10.0)); assert_eq!(PathSeg::Cubic(c).intersect_line(v_line).len(), 1); let intersection = PathSeg::Cubic(c).intersect_line(v_line)[0]; assert_approx_eq(intersection.segment_t, 0.333333333); assert_approx_eq(intersection.line_t, 0.592592592); let h_line = Line::new((0.0, 0.0), (100.0, 0.0)); assert_eq!(PathSeg::Cubic(c).intersect_line(h_line).len(), 3); } } kurbo-0.7.1/src/circle.rs010064400007650000024000000230131374161070200134160ustar 00000000000000//! Implementation of circle shape. use std::{ f64::consts::{FRAC_PI_2, PI}, iter, ops::{Add, Mul, Sub}, }; use crate::{Affine, Arc, ArcAppendIter, Ellipse, PathEl, Point, Rect, Shape, Vec2}; /// A circle. #[derive(Clone, Copy, Default, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Circle { /// The center. pub center: Point, /// The radius. pub radius: f64, } impl Circle { /// A new circle from center and radius. #[inline] pub fn new(center: impl Into, radius: f64) -> Circle { Circle { center: center.into(), radius, } } /// Create a [`CircleSegment`] by cutting out parts of this circle. /// /// [`CircleSegment`]: struct.CircleSegment.html pub fn segment(self, inner_radius: f64, start_angle: f64, sweep_angle: f64) -> CircleSegment { CircleSegment { center: self.center, outer_radius: self.radius, inner_radius, start_angle, sweep_angle, } } } impl Add for Circle { type Output = Circle; #[inline] fn add(self, v: Vec2) -> Circle { Circle { center: self.center + v, radius: self.radius, } } } impl Sub for Circle { type Output = Circle; #[inline] fn sub(self, v: Vec2) -> Circle { Circle { center: self.center - v, radius: self.radius, } } } impl Mul for Affine { type Output = Ellipse; fn mul(self, other: Circle) -> Self::Output { self * Ellipse::from(other) } } #[doc(hidden)] pub struct CirclePathIter { circle: Circle, delta_th: f64, arm_len: f64, ix: usize, n: usize, } impl Shape for Circle { type PathElementsIter = CirclePathIter; fn path_elements(&self, tolerance: f64) -> CirclePathIter { let scaled_err = self.radius.abs() / tolerance; let (n, arm_len) = if scaled_err < 1.0 / 1.9608e-4 { // Solution from http://spencermortensen.com/articles/bezier-circle/ (4, 0.551915024494) } else { // This is empirically determined to fall within error tolerance. let n = (1.1163 * scaled_err).powf(1.0 / 6.0).ceil() as usize; // Note: this isn't minimum error, but it is simple and we can easily // estimate the error. let arm_len = (4.0 / 3.0) * (FRAC_PI_2 / (n as f64)).tan(); (n, arm_len) }; CirclePathIter { circle: *self, delta_th: 2.0 * PI / (n as f64), arm_len, ix: 0, n, } } #[inline] fn area(&self) -> f64 { PI * self.radius.powi(2) } #[inline] fn perimeter(&self, _accuracy: f64) -> f64 { (2.0 * PI * self.radius).abs() } fn winding(&self, pt: Point) -> i32 { if (pt - self.center).hypot2() < self.radius.powi(2) { 1 } else { 0 } } #[inline] fn bounding_box(&self) -> Rect { let r = self.radius.abs(); let (x, y) = self.center.into(); Rect::new(x - r, y - r, x + r, y + r) } fn as_circle(&self) -> Option { Some(*self) } } impl Iterator for CirclePathIter { type Item = PathEl; fn next(&mut self) -> Option { let a = self.arm_len; let r = self.circle.radius; let (x, y) = self.circle.center.into(); let ix = self.ix; self.ix += 1; if ix == 0 { Some(PathEl::MoveTo(Point::new(x + r, y))) } else if ix <= self.n { let th1 = self.delta_th * (ix as f64); let th0 = th1 - self.delta_th; let (c0, s0) = (th0.cos(), th0.sin()); let (c1, s1) = if ix == self.n { (1.0, 0.0) } else { (th1.cos(), th1.sin()) }; Some(PathEl::CurveTo( Point::new(x + r * (c0 - a * s0), y + r * (s0 + a * c0)), Point::new(x + r * (c1 + a * s1), y + r * (s1 - a * c1)), Point::new(x + r * c1, y + r * s1), )) } else if ix == self.n + 1 { Some(PathEl::ClosePath) } else { None } } } /// A segment of a circle. /// /// If `inner_radius > 0`, then the shape will be a doughnut segment. pub struct CircleSegment { /// The center. pub center: Point, /// The outer radius. pub outer_radius: f64, /// The inner radius. pub inner_radius: f64, /// The angle to start drawing the segment (in radians). pub start_angle: f64, /// The arc length of the segment (in radians). pub sweep_angle: f64, } impl CircleSegment { /// Create a `CircleSegment` out of its constituent parts. pub fn new( center: impl Into, outer_radius: f64, inner_radius: f64, start_angle: f64, sweep_angle: f64, ) -> Self { CircleSegment { center: center.into(), outer_radius, inner_radius, start_angle, sweep_angle, } } } impl Add for CircleSegment { type Output = CircleSegment; #[inline] fn add(self, v: Vec2) -> Self { Self { center: self.center + v, ..self } } } impl Sub for CircleSegment { type Output = CircleSegment; #[inline] fn sub(self, v: Vec2) -> Self { Self { center: self.center - v, ..self } } } type CircleSegmentPathIter = std::iter::Chain< iter::Chain< iter::Chain, iter::Once>, ArcAppendIter>, iter::Once, >, ArcAppendIter, >; impl Shape for CircleSegment { type PathElementsIter = CircleSegmentPathIter; fn path_elements(&self, tolerance: f64) -> CircleSegmentPathIter { iter::once(PathEl::MoveTo(point_on_circle( self.center, self.inner_radius, self.start_angle, ))) // First radius .chain(iter::once(PathEl::LineTo(point_on_circle( self.center, self.outer_radius, self.start_angle, )))) // outer arc .chain( Arc { center: self.center, radii: Vec2::new(self.outer_radius, self.outer_radius), start_angle: self.start_angle, sweep_angle: self.sweep_angle, x_rotation: 0.0, } .append_iter(tolerance), ) // second radius .chain(iter::once(PathEl::LineTo(point_on_circle( self.center, self.inner_radius, self.start_angle + self.sweep_angle, )))) // inner arc .chain( Arc { center: self.center, radii: Vec2::new(self.inner_radius, self.inner_radius), start_angle: self.start_angle + self.sweep_angle, sweep_angle: -self.sweep_angle, x_rotation: 0.0, } .append_iter(tolerance), ) } #[inline] fn area(&self) -> f64 { 0.5 * (self.outer_radius.powi(2) - self.inner_radius.powi(2)).abs() * self.sweep_angle } #[inline] fn perimeter(&self, _accuracy: f64) -> f64 { 2.0 * (self.outer_radius - self.inner_radius).abs() + self.sweep_angle * (self.inner_radius + self.outer_radius) } fn winding(&self, pt: Point) -> i32 { let angle = (pt - self.center).atan2(); if angle < self.start_angle || angle > self.start_angle + self.sweep_angle { return 0; } let dist2 = (pt - self.center).hypot2(); if (dist2 < self.outer_radius.powi(2) && dist2 > self.inner_radius.powi(2)) || // case where outer_radius < inner_radius (dist2 < self.inner_radius.powi(2) && dist2 > self.outer_radius.powi(2)) { 1 } else { 0 } } #[inline] fn bounding_box(&self) -> Rect { // todo this is currently not tight let r = self.inner_radius.max(self.outer_radius); let (x, y) = self.center.into(); Rect::new(x - r, y - r, x + r, y + r) } } #[cfg(test)] mod tests { use crate::{Circle, Point, Shape}; use std::f64::consts::PI; fn assert_approx_eq(x: f64, y: f64) { // Note: we might want to be more rigorous in testing the accuracy // of the conversion into Béziers. But this seems good enough. assert!((x - y).abs() < 1e-7, "{} != {}", x, y); } #[test] fn area_sign() { let center = Point::new(5.0, 5.0); let c = Circle::new(center, 5.0); assert_approx_eq(c.area(), 25.0 * PI); assert_eq!(c.winding(center), 1); let p = c.to_path(1e-9); assert_approx_eq(c.area(), p.area()); assert_eq!(c.winding(center), p.winding(center)); let c_neg_radius = Circle::new(center, -5.0); assert_approx_eq(c_neg_radius.area(), 25.0 * PI); assert_eq!(c_neg_radius.winding(center), 1); let p_neg_radius = c_neg_radius.to_path(1e-9); assert_approx_eq(c_neg_radius.area(), p_neg_radius.area()); assert_eq!(c_neg_radius.winding(center), p_neg_radius.winding(center)); } } #[inline] fn point_on_circle(center: Point, radius: f64, angle: f64) -> Point { center + Vec2 { x: angle.cos() * radius, y: angle.sin() * radius, } } kurbo-0.7.1/src/common.rs010064400007650000024000000216401367545551600134710ustar 00000000000000//! Common mathematical operations #![allow(missing_docs)] use arrayvec::ArrayVec; /// Adds convenience methods to `f32` and `f64`. pub trait FloatExt { /// Rounds to the nearest integer away from zero, /// unless the provided value is already an integer. /// /// It is to `ceil` what `trunc` is to `floor`. /// /// # Examples /// /// ``` /// use kurbo::common::FloatExt; /// /// let f = 3.7_f64; /// let g = 3.0_f64; /// let h = -3.7_f64; /// let i = -5.1_f32; /// /// assert_eq!(f.expand(), 4.0); /// assert_eq!(g.expand(), 3.0); /// assert_eq!(h.expand(), -4.0); /// assert_eq!(i.expand(), -6.0); /// ``` fn expand(&self) -> T; } impl FloatExt for f64 { #[inline] fn expand(&self) -> f64 { self.abs().ceil().copysign(*self) } } impl FloatExt for f32 { #[inline] fn expand(&self) -> f32 { self.abs().ceil().copysign(*self) } } /// Find real roots of cubic equation. /// /// The implementation is not (yet) fully robust, but it does handle the case /// where `c3` is zero (in that case, solving the quadratic equation). /// /// See: /// /// Returns values of x for which c0 + c1 x + c2 x² + c3 x³ = 0. pub fn solve_cubic(c0: f64, c1: f64, c2: f64, c3: f64) -> ArrayVec<[f64; 3]> { let mut result = ArrayVec::new(); let c3_recip = c3.recip(); let scaled_c2 = c2 * c3_recip; let scaled_c1 = c1 * c3_recip; let scaled_c0 = c0 * c3_recip; if !(scaled_c0.is_finite() && scaled_c1.is_finite() && scaled_c2.is_finite()) { // cubic coefficient is zero or nearly so. for root in solve_quadratic(c0, c1, c2) { result.push(root); } return result; } let (c0, c1, c2) = (scaled_c0, scaled_c1, scaled_c2); let q = c1 * (1.0 / 3.0) - c2 * c2 * (1.0 / 9.0); // Q let r = (1.0 / 6.0) * c2 * c1 - (1.0 / 27.0) * c2.powi(3) - c0 * 0.5; // R let d = q.powi(3) + r * r; // D let x0 = c2 * (1.0 / 3.0); // TODO: handle the cases where these intermediate results overflow. if d > 0.0 { let sq = d.sqrt(); let t1 = (r + sq).cbrt() + (r - sq).cbrt(); result.push(t1 - x0); } else if d == 0.0 { let t1 = -r.cbrt(); let x1 = t1 - x0; result.push(x1); result.push(-2.0 * t1 - x0); } else { let sq = (-d).sqrt(); let rho = r.hypot(sq); let th = sq.atan2(r) * (1.0 / 3.0); let cbrho = rho.cbrt(); let c = th.cos(); let ss3 = th.sin() * 3.0f64.sqrt(); result.push(2.0 * cbrho * c - x0); result.push(-cbrho * (c + ss3) - x0); result.push(-cbrho * (c - ss3) - x0); } result } /// Find real roots of quadratic equation. /// /// Returns values of x for which c0 + c1 x + c2 x² = 0. /// /// This function tries to be quite numerically robust. If the equation /// is nearly linear, it will return the root ignoring the quadratic term; /// the other root might be out of representable range. In the degenerate /// case where all coefficients are zero, so that all values of x satisfy /// the equation, a single `0.0` is returned. pub fn solve_quadratic(c0: f64, c1: f64, c2: f64) -> ArrayVec<[f64; 2]> { let mut result = ArrayVec::new(); let sc0 = c0 * c2.recip(); let sc1 = c1 * c2.recip(); if !sc0.is_finite() || !sc1.is_finite() { // c2 is zero or very small, treat as linear eqn let root = -c0 / c1; if root.is_finite() { result.push(root); } else if c0 == 0.0 && c1 == 0.0 { // Degenerate case result.push(0.0); } return result; } let arg = sc1 * sc1 - 4. * sc0; let root1 = if !arg.is_finite() { // Likely, calculation of sc1 * sc1 overflowed. Find one root // using sc1 x + x² = 0, other root as sc0 / root1. -sc1 } else { if arg < 0.0 { return result; } else if arg == 0.0 { result.push(-0.5 * sc1); return result; } // See https://math.stackexchange.com/questions/866331 -0.5 * (sc1 + arg.sqrt().copysign(sc1)) }; let root2 = sc0 / root1; if root2.is_finite() { // Sort just to be friendly and make results deterministic. if root2 > root1 { result.push(root1); result.push(root2); } else { result.push(root2); result.push(root1); } } else { result.push(root1); } result } // Tables of Legendre-Gauss quadrature coefficients, adapted from: // pub const GAUSS_LEGENDRE_COEFFS_3: &[(f64, f64)] = &[ (0.8888888888888888, 0.0000000000000000), (0.5555555555555556, -0.7745966692414834), (0.5555555555555556, 0.7745966692414834), ]; pub const GAUSS_LEGENDRE_COEFFS_5: &[(f64, f64)] = &[ (0.5688888888888889, 0.0000000000000000), (0.4786286704993665, -0.5384693101056831), (0.4786286704993665, 0.5384693101056831), (0.2369268850561891, -0.9061798459386640), (0.2369268850561891, 0.9061798459386640), ]; pub const GAUSS_LEGENDRE_COEFFS_7: &[(f64, f64)] = &[ (0.4179591836734694, 0.0000000000000000), (0.3818300505051189, 0.4058451513773972), (0.3818300505051189, -0.4058451513773972), (0.2797053914892766, -0.7415311855993945), (0.2797053914892766, 0.7415311855993945), (0.1294849661688697, -0.9491079123427585), (0.1294849661688697, 0.9491079123427585), ]; pub const GAUSS_LEGENDRE_COEFFS_9: &[(f64, f64)] = &[ (0.3302393550012598, 0.0000000000000000), (0.1806481606948574, -0.8360311073266358), (0.1806481606948574, 0.8360311073266358), (0.0812743883615744, -0.9681602395076261), (0.0812743883615744, 0.9681602395076261), (0.3123470770400029, -0.3242534234038089), (0.3123470770400029, 0.3242534234038089), (0.2606106964029354, -0.6133714327005904), (0.2606106964029354, 0.6133714327005904), ]; pub const GAUSS_LEGENDRE_COEFFS_11: &[(f64, f64)] = &[ (0.2729250867779006, 0.0000000000000000), (0.2628045445102467, -0.2695431559523450), (0.2628045445102467, 0.2695431559523450), (0.2331937645919905, -0.5190961292068118), (0.2331937645919905, 0.5190961292068118), (0.1862902109277343, -0.7301520055740494), (0.1862902109277343, 0.7301520055740494), (0.1255803694649046, -0.8870625997680953), (0.1255803694649046, 0.8870625997680953), (0.0556685671161737, -0.9782286581460570), (0.0556685671161737, 0.9782286581460570), ]; pub const GAUSS_LEGENDRE_COEFFS_24: &[(f64, f64)] = &[ (0.1279381953467522, -0.0640568928626056), (0.1279381953467522, 0.0640568928626056), (0.1258374563468283, -0.1911188674736163), (0.1258374563468283, 0.1911188674736163), (0.1216704729278034, -0.3150426796961634), (0.1216704729278034, 0.3150426796961634), (0.1155056680537256, -0.4337935076260451), (0.1155056680537256, 0.4337935076260451), (0.1074442701159656, -0.5454214713888396), (0.1074442701159656, 0.5454214713888396), (0.0976186521041139, -0.6480936519369755), (0.0976186521041139, 0.6480936519369755), (0.0861901615319533, -0.7401241915785544), (0.0861901615319533, 0.7401241915785544), (0.0733464814110803, -0.8200019859739029), (0.0733464814110803, 0.8200019859739029), (0.0592985849154368, -0.8864155270044011), (0.0592985849154368, 0.8864155270044011), (0.0442774388174198, -0.9382745520027328), (0.0442774388174198, 0.9382745520027328), (0.0285313886289337, -0.9747285559713095), (0.0285313886289337, 0.9747285559713095), (0.0123412297999872, -0.9951872199970213), (0.0123412297999872, 0.9951872199970213), ]; #[cfg(test)] mod tests { use crate::common::*; use arrayvec::{Array, ArrayVec}; fn verify>(mut roots: ArrayVec, expected: &[f64]) { assert!(expected.len() == roots.len()); let epsilon = 1e-6; roots.sort_by(|a, b| a.partial_cmp(b).unwrap()); for i in 0..expected.len() { assert!((roots[i] - expected[i]).abs() < epsilon); } } #[test] fn test_solve_cubic() { verify(solve_cubic(-5.0, 0.0, 0.0, 1.0), &[5.0f64.cbrt()]); verify(solve_cubic(-5.0, -1.0, 0.0, 1.0), &[1.90416085913492]); verify(solve_cubic(0.0, -1.0, 0.0, 1.0), &[-1.0, 0.0, 1.0]); verify(solve_cubic(-2.0, -3.0, 0.0, 1.0), &[-1.0, 2.0]); verify(solve_cubic(2.0, -3.0, 0.0, 1.0), &[-2.0, 1.0]); verify(solve_cubic(2.0 - 1e-12, 5.0, 4.0, 1.0), &[-2.0, -1.0, -1.0]); verify(solve_cubic(2.0 + 1e-12, 5.0, 4.0, 1.0), &[-2.0]); } #[test] fn test_solve_quadratic() { verify( solve_quadratic(-5.0, 0.0, 1.0), &[-(5.0f64.sqrt()), 5.0f64.sqrt()], ); verify(solve_quadratic(5.0, 0.0, 1.0), &[]); verify(solve_quadratic(5.0, 1.0, 0.0), &[-5.0]); verify(solve_quadratic(1.0, 2.0, 1.0), &[-1.0]); } } kurbo-0.7.1/src/cubicbez.rs010064400007650000024000000412041375200744500137530ustar 00000000000000//! Cubic Bézier segments. use std::ops::{Mul, Range}; use crate::MAX_EXTREMA; use arrayvec::ArrayVec; use crate::common::solve_quadratic; use crate::common::GAUSS_LEGENDRE_COEFFS_9; use crate::{ Affine, ParamCurve, ParamCurveArclen, ParamCurveArea, ParamCurveCurvature, ParamCurveDeriv, ParamCurveExtrema, ParamCurveNearest, PathEl, Point, QuadBez, Rect, Shape, }; /// A single cubic Bézier segment. #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[allow(missing_docs)] pub struct CubicBez { pub p0: Point, pub p1: Point, pub p2: Point, pub p3: Point, } /// An iterator which produces quadratic Bézier segments. struct ToQuads { c: CubicBez, i: usize, n: usize, } impl CubicBez { /// Create a new cubic Bézier segment. #[inline] pub fn new>(p0: P, p1: P, p2: P, p3: P) -> CubicBez { CubicBez { p0: p0.into(), p1: p1.into(), p2: p2.into(), p3: p3.into(), } } /// Convert to quadratic Béziers. /// /// The iterator returns the start and end parameter in the cubic of each quadratic /// segment, along with the quadratic. /// /// Note that the resulting quadratic Béziers are not in general G1 continuous; /// they are optimized for minimizing distance error. /// /// This iterator will always produce at least one `QuadBez`. #[inline] pub fn to_quads(&self, accuracy: f64) -> impl Iterator { // The maximum error, as a vector from the cubic to the best approximating // quadratic, is proportional to the third derivative, which is constant // across the segment. Thus, the error scales down as the third power of // the number of subdivisions. Our strategy then is to subdivide `t` evenly. // // This is an overestimate of the error because only the component // perpendicular to the first derivative is important. But the simplicity is // appealing. // This magic number is the square of 36 / sqrt(3). // See: http://caffeineowl.com/graphics/2d/vectorial/cubic2quad01.html let max_hypot2 = 432.0 * accuracy * accuracy; let p1x2 = 3.0 * self.p1.to_vec2() - self.p0.to_vec2(); let p2x2 = 3.0 * self.p2.to_vec2() - self.p3.to_vec2(); let err = (p2x2 - p1x2).hypot2(); let n = ((err / max_hypot2).powf(1. / 6.0).ceil() as usize).max(1); ToQuads { c: *self, n, i: 0 } } } /// An iterator for cubic beziers. pub struct CubicBezIter { cubic: CubicBez, ix: usize, } impl Shape for CubicBez { type PathElementsIter = CubicBezIter; #[inline] fn path_elements(&self, _tolerance: f64) -> CubicBezIter { CubicBezIter { cubic: *self, ix: 0, } } fn area(&self) -> f64 { 0.0 } #[inline] fn perimeter(&self, accuracy: f64) -> f64 { self.arclen(accuracy) } fn winding(&self, _pt: Point) -> i32 { 0 } #[inline] fn bounding_box(&self) -> Rect { ParamCurveExtrema::bounding_box(self) } } impl Iterator for CubicBezIter { type Item = PathEl; fn next(&mut self) -> Option { self.ix += 1; match self.ix { 1 => Some(PathEl::MoveTo(self.cubic.p0)), 2 => Some(PathEl::CurveTo(self.cubic.p1, self.cubic.p2, self.cubic.p3)), _ => None, } } } impl ParamCurve for CubicBez { #[inline] fn eval(&self, t: f64) -> Point { let mt = 1.0 - t; let v = self.p0.to_vec2() * (mt * mt * mt) + (self.p1.to_vec2() * (mt * mt * 3.0) + (self.p2.to_vec2() * (mt * 3.0) + self.p3.to_vec2() * t) * t) * t; v.to_point() } #[inline] fn start(&self) -> Point { self.p0 } #[inline] fn end(&self) -> Point { self.p3 } fn subsegment(&self, range: Range) -> CubicBez { let (t0, t1) = (range.start, range.end); let p0 = self.eval(t0); let p3 = self.eval(t1); let d = self.deriv(); let scale = (t1 - t0) * (1.0 / 3.0); let p1 = p0 + scale * d.eval(t0).to_vec2(); let p2 = p3 - scale * d.eval(t1).to_vec2(); CubicBez { p0, p1, p2, p3 } } /// Subdivide into halves, using de Casteljau. #[inline] fn subdivide(&self) -> (CubicBez, CubicBez) { let pm = self.eval(0.5); ( CubicBez::new( self.p0, self.p0.midpoint(self.p1), ((self.p0.to_vec2() + self.p1.to_vec2() * 2.0 + self.p2.to_vec2()) * 0.25) .to_point(), pm, ), CubicBez::new( pm, ((self.p1.to_vec2() + self.p2.to_vec2() * 2.0 + self.p3.to_vec2()) * 0.25) .to_point(), self.p2.midpoint(self.p3), self.p3, ), ) } } impl ParamCurveDeriv for CubicBez { type DerivResult = QuadBez; #[inline] fn deriv(&self) -> QuadBez { QuadBez::new( (3.0 * (self.p1 - self.p0)).to_point(), (3.0 * (self.p2 - self.p1)).to_point(), (3.0 * (self.p3 - self.p2)).to_point(), ) } } impl ParamCurveArclen for CubicBez { /// Arclength of a cubic Bézier segment. /// /// This is an adaptive subdivision approach using Legendre-Gauss quadrature /// in the base case, and an error estimate to decide when to subdivide. fn arclen(&self, accuracy: f64) -> f64 { // Squared L2 norm of the second derivative of the cubic. fn cubic_errnorm(c: &CubicBez) -> f64 { let d = c.deriv().deriv(); let dd = d.end() - d.start(); d.start().to_vec2().hypot2() + d.start().to_vec2().dot(dd) + dd.hypot2() * (1.0 / 3.0) } fn est_gauss9_error(c: &CubicBez) -> f64 { let lc2 = (c.p3 - c.p0).hypot2(); let lp = (c.p1 - c.p0).hypot() + (c.p2 - c.p1).hypot() + (c.p3 - c.p2).hypot(); 2.56e-8 * (cubic_errnorm(c) / lc2).powi(8) * lp } const MAX_DEPTH: usize = 16; fn rec(c: &CubicBez, accuracy: f64, depth: usize) -> f64 { if depth == MAX_DEPTH || est_gauss9_error(c) < accuracy { c.gauss_arclen(GAUSS_LEGENDRE_COEFFS_9) } else { let (c0, c1) = c.subdivide(); rec(&c0, accuracy * 0.5, depth + 1) + rec(&c1, accuracy * 0.5, depth + 1) } } // Check if the bezier curve is degenerate, or almost degenerate // A degenerate curve where all points are identical will cause infinite recursion in the rec function (well, until MAX_DEPTH at least) in all branches. // This test will in addition be true if the bezier curve is just a simple line (i.e. p0=p1 and p2=p3). // The constant 0.5 has not been mathematically proven to be small enough, but from empirical tests // a value of about 0.87 should be enough. Thus 0.5 is a conservative value. // See https://github.com/linebender/kurbo/pull/100 for more info. if (self.p1 - self.p0).hypot2() + (self.p2 - self.p3).hypot2() <= 0.5 * accuracy * accuracy { (self.p0 - self.p3).hypot() } else { rec(self, accuracy, 0) } } } impl ParamCurveArea for CubicBez { #[inline] fn signed_area(&self) -> f64 { (self.p0.x * (6.0 * self.p1.y + 3.0 * self.p2.y + self.p3.y) + 3.0 * (self.p1.x * (-2.0 * self.p0.y + self.p2.y + self.p3.y) - self.p2.x * (self.p0.y + self.p1.y - 2.0 * self.p3.y)) - self.p3.x * (self.p0.y + 3.0 * self.p1.y + 6.0 * self.p2.y)) * (1.0 / 20.0) } } impl ParamCurveNearest for CubicBez { /// Find nearest point, using subdivision. fn nearest(&self, p: Point, accuracy: f64) -> (f64, f64) { let mut best_r = None; let mut best_t = 0.0; for (t0, t1, q) in self.to_quads(accuracy) { let (t, r) = q.nearest(p, accuracy); if best_r.map(|best_r| r < best_r).unwrap_or(true) { best_t = t0 + t * (t1 - t0); best_r = Some(r); } } (best_t, best_r.unwrap()) } } impl ParamCurveCurvature for CubicBez {} impl ParamCurveExtrema for CubicBez { fn extrema(&self) -> ArrayVec<[f64; MAX_EXTREMA]> { fn one_coord(result: &mut ArrayVec<[f64; MAX_EXTREMA]>, d0: f64, d1: f64, d2: f64) { let a = d0 - 2.0 * d1 + d2; let b = 2.0 * (d1 - d0); let c = d0; let roots = solve_quadratic(c, b, a); for &t in &roots { if t > 0.0 && t < 1.0 { result.push(t); } } } let mut result = ArrayVec::new(); let d0 = self.p1 - self.p0; let d1 = self.p2 - self.p1; let d2 = self.p3 - self.p2; one_coord(&mut result, d0.x, d1.x, d2.x); one_coord(&mut result, d0.y, d1.y, d2.y); result.sort_by(|a, b| a.partial_cmp(b).unwrap()); result } } impl Mul for Affine { type Output = CubicBez; #[inline] fn mul(self, c: CubicBez) -> CubicBez { CubicBez { p0: self * c.p0, p1: self * c.p1, p2: self * c.p2, p3: self * c.p3, } } } impl Iterator for ToQuads { type Item = (f64, f64, QuadBez); fn next(&mut self) -> Option<(f64, f64, QuadBez)> { if self.i == self.n { return None; } let t0 = self.i as f64 / self.n as f64; let t1 = (self.i + 1) as f64 / self.n as f64; let seg = self.c.subsegment(t0..t1); let p1x2 = 3.0 * seg.p1.to_vec2() - seg.p0.to_vec2(); let p2x2 = 3.0 * seg.p2.to_vec2() - seg.p3.to_vec2(); let result = QuadBez::new(seg.p0, ((p1x2 + p2x2) / 4.0).to_point(), seg.p3); self.i += 1; Some((t0, t1, result)) } fn size_hint(&self) -> (usize, Option) { let remaining = self.n - self.i; (remaining, Some(remaining)) } } #[cfg(test)] mod tests { use crate::{ Affine, CubicBez, ParamCurve, ParamCurveArclen, ParamCurveArea, ParamCurveDeriv, ParamCurveExtrema, ParamCurveNearest, Point, }; #[test] fn cubicbez_deriv() { // y = x^2 let c = CubicBez::new( (0.0, 0.0), (1.0 / 3.0, 0.0), (2.0 / 3.0, 1.0 / 3.0), (1.0, 1.0), ); let deriv = c.deriv(); let n = 10; for i in 0..=n { let t = (i as f64) * (n as f64).recip(); let delta = 1e-6; let p = c.eval(t); let p1 = c.eval(t + delta); let d_approx = (p1 - p) * delta.recip(); let d = deriv.eval(t).to_vec2(); assert!((d - d_approx).hypot() < delta * 2.0); } } #[test] fn cubicbez_arclen() { // y = x^2 let c = CubicBez::new( (0.0, 0.0), (1.0 / 3.0, 0.0), (2.0 / 3.0, 1.0 / 3.0), (1.0, 1.0), ); let true_arclen = 0.5 * 5.0f64.sqrt() + 0.25 * (2.0 + 5.0f64.sqrt()).ln(); for i in 0..12 { let accuracy = 0.1f64.powi(i); let error = c.arclen(accuracy) - true_arclen; assert!(error.abs() < accuracy); } } #[test] fn cubicbez_inv_arclen() { // y = x^2 / 100 let c = CubicBez::new( (0.0, 0.0), (100.0 / 3.0, 0.0), (200.0 / 3.0, 100.0 / 3.0), (100.0, 100.0), ); let true_arclen = 100.0 * (0.5 * 5.0f64.sqrt() + 0.25 * (2.0 + 5.0f64.sqrt()).ln()); for i in 0..12 { let accuracy = 0.1f64.powi(i); let n = 10; for j in 0..=n { let arc = (j as f64) * ((n as f64).recip() * true_arclen); let t = c.inv_arclen(arc, accuracy * 0.5); let actual_arc = c.subsegment(0.0..t).arclen(accuracy * 0.5); assert!( (arc - actual_arc).abs() < accuracy, "at accuracy {:e}, wanted {} got {}", accuracy, actual_arc, arc ); } } // corner case: user passes accuracy larger than total arc length let accuracy = true_arclen * 1.1; let arc = true_arclen * 0.5; let t = c.inv_arclen(arc, accuracy); let actual_arc = c.subsegment(0.0..t).arclen(accuracy); assert!( (arc - actual_arc).abs() < 2.0 * accuracy, "at accuracy {:e}, want {} got {}", accuracy, actual_arc, arc ); } #[test] #[allow(clippy::float_cmp)] fn cubicbez_signed_area_linear() { // y = 1 - x let c = CubicBez::new( (1.0, 0.0), (2.0 / 3.0, 1.0 / 3.0), (1.0 / 3.0, 2.0 / 3.0), (0.0, 1.0), ); let epsilon = 1e-12; assert_eq!((Affine::rotate(0.5) * c).signed_area(), 0.5); assert!(((Affine::rotate(0.5) * c).signed_area() - 0.5).abs() < epsilon); assert!(((Affine::translate((0.0, 1.0)) * c).signed_area() - 1.0).abs() < epsilon); assert!(((Affine::translate((1.0, 0.0)) * c).signed_area() - 1.0).abs() < epsilon); } #[test] fn cubicbez_signed_area() { // y = 1 - x^3 let c = CubicBez::new((1.0, 0.0), (2.0 / 3.0, 1.0), (1.0 / 3.0, 1.0), (0.0, 1.0)); let epsilon = 1e-12; assert!((c.signed_area() - 0.75).abs() < epsilon); assert!(((Affine::rotate(0.5) * c).signed_area() - 0.75).abs() < epsilon); assert!(((Affine::translate((0.0, 1.0)) * c).signed_area() - 1.25).abs() < epsilon); assert!(((Affine::translate((1.0, 0.0)) * c).signed_area() - 1.25).abs() < epsilon); } #[test] fn cubicbez_nearest() { fn verify(result: (f64, f64), expected: f64) { assert!( (result.0 - expected).abs() < 1e-6, "got {:?} expected {}", result, expected ); } // y = x^3 let c = CubicBez::new((0.0, 0.0), (1.0 / 3.0, 0.0), (2.0 / 3.0, 0.0), (1.0, 1.0)); verify(c.nearest((0.1, 0.001).into(), 1e-6), 0.1); verify(c.nearest((0.2, 0.008).into(), 1e-6), 0.2); verify(c.nearest((0.3, 0.027).into(), 1e-6), 0.3); verify(c.nearest((0.4, 0.064).into(), 1e-6), 0.4); verify(c.nearest((0.5, 0.125).into(), 1e-6), 0.5); verify(c.nearest((0.6, 0.216).into(), 1e-6), 0.6); verify(c.nearest((0.7, 0.343).into(), 1e-6), 0.7); verify(c.nearest((0.8, 0.512).into(), 1e-6), 0.8); verify(c.nearest((0.9, 0.729).into(), 1e-6), 0.9); verify(c.nearest((1.0, 1.0).into(), 1e-6), 1.0); verify(c.nearest((1.1, 1.1).into(), 1e-6), 1.0); verify(c.nearest((-0.1, 0.0).into(), 1e-6), 0.0); let a = Affine::rotate(0.5); verify((a * c).nearest(a * Point::new(0.1, 0.001), 1e-6), 0.1); } // ensure to_quads returns something given colinear points #[test] fn degenerate_to_quads() { let c = CubicBez::new((0., 9.), (6., 6.), (12., 3.0), (18., 0.0)); let quads = c.to_quads(1e-6).collect::>(); assert_eq!(quads.len(), 1, "{:?}", &quads); } #[test] fn cubicbez_extrema() { // y = x^2 let q = CubicBez::new((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0)); let extrema = q.extrema(); assert_eq!(extrema.len(), 1); assert!((extrema[0] - 0.5).abs() < 1e-6); let q = CubicBez::new((0.4, 0.5), (0.0, 1.0), (1.0, 0.0), (0.5, 0.4)); let extrema = q.extrema(); assert_eq!(extrema.len(), 4); } #[test] fn cubicbez_toquads() { // y = x^3 let c = CubicBez::new((0.0, 0.0), (1.0 / 3.0, 0.0), (2.0 / 3.0, 0.0), (1.0, 1.0)); for i in 0..10 { let accuracy = 0.1f64.powi(i); let mut worst: f64 = 0.0; for (_count, (t0, t1, q)) in c.to_quads(accuracy).enumerate() { let epsilon = 1e-12; assert!((q.start() - c.eval(t0)).hypot() < epsilon); assert!((q.end() - c.eval(t1)).hypot() < epsilon); let n = 4; for j in 0..=n { let t = (j as f64) * (n as f64).recip(); let p = q.eval(t); let err = (p.y - p.x.powi(3)).abs(); worst = worst.max(err); assert!(err < accuracy, "got {} wanted {}", err, accuracy); } } } } } kurbo-0.7.1/src/ellipse.rs010064400007650000024000000225231374554710500136310ustar 00000000000000//! Implementation of ellipse shape. use std::f64::consts::PI; use std::{ iter, ops::{Add, Mul, Sub}, }; use crate::{Affine, Arc, ArcAppendIter, Circle, PathEl, Point, Rect, Shape, Size, Vec2}; /// An ellipse. #[derive(Clone, Copy, Default, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Ellipse { /// All ellipses can be represented as an affine map of the unit circle, /// centred at (0, 0). Therefore we can store the ellipse as an affine map, /// with the implication it be applied to the unit circle to recover the /// actual shape. inner: Affine, } impl Ellipse { /// Create A new ellipse with a given center, radii, and rotation. /// /// The returned ellipse will be the result of taking a circle, stretching /// it by the `radii` along the x and y axes, then rotating it from the /// x asix by `rotation` radians, before finally translating the center /// to `center`. /// /// Rotation is clockwise in a y-down coordinate system. For more on /// rotation, see [`Affine::rotate`]. /// /// [`Affine::rotate`]: Affine::rotate #[inline] pub fn new(center: impl Into, radii: impl Into, x_rotation: f64) -> Ellipse { let Point { x: cx, y: cy } = center.into(); let Vec2 { x: rx, y: ry } = radii.into(); Ellipse::private_new(Vec2 { x: cx, y: cy }, rx, ry, x_rotation) } /// Returns the largest ellipse that can be bounded by this [`Rect`]. /// /// This uses the absolute width and height of the rectangle. /// /// This ellipse is always axis-aligned; to apply rotation you can call /// [`with_rotation`] with the result. /// /// [`Rect`]: struct.Rect.html /// [`with_rotation`]: #method.with_rotation #[inline] pub fn from_rect(rect: Rect) -> Self { let center = rect.center().to_vec2(); let Size { width, height } = rect.size() / 2.0; Ellipse::private_new(center, width, height, 0.0) } /// Create an ellipse from an affine transformation of the unit circle. #[inline] pub fn from_affine(affine: Affine) -> Self { Ellipse { inner: affine } } /// Create a new `Ellipse` centered on the provided point. #[inline] #[must_use] pub fn with_center(self, new_center: Point) -> Ellipse { let Point { x: cx, y: cy } = new_center; Ellipse { inner: self.inner.set_translation(Vec2 { x: cx, y: cy }), } } /// Create a new `Ellipse` with the provided radii. #[must_use] pub fn with_radii(self, new_radii: Vec2) -> Ellipse { let rotation = self.inner.svd().1; let translation = self.inner.get_translation(); Ellipse::private_new(translation, new_radii.x, new_radii.y, rotation) } /// Create a new `Ellipse`, with the rotation replaced by `rotation` /// radians. /// /// The rotation is clockwise, for a y-down coordinate system. For more /// on rotation, See [`Affine::rotate`]. /// /// [`Affine::rotate`]: Affine::rotate #[must_use] pub fn with_rotation(self, rotation: f64) -> Ellipse { let scale = self.inner.svd().0; let translation = self.inner.get_translation(); Ellipse::private_new(translation, scale.x, scale.y, rotation) } #[deprecated(since = "0.7.0", note = "use with_rotation instead")] #[must_use] #[doc(hidden)] pub fn with_x_rotation(self, rotation_radians: f64) -> Ellipse { self.with_rotation(rotation_radians) } /// This gives us an internal method without any type conversions. #[inline] fn private_new(center: Vec2, scale_x: f64, scale_y: f64, x_rotation: f64) -> Ellipse { // Since the circle is symmetric about the x and y axes, using absolute values for the // radii results in the same ellipse. For simplicity we make this change here. Ellipse { inner: Affine::translate(center) * Affine::rotate(x_rotation) * Affine::scale_non_uniform(scale_x.abs(), scale_y.abs()), } } // Getters and setters. /// Returns the center of this ellipse. #[inline] pub fn center(&self) -> Point { let Vec2 { x: cx, y: cy } = self.inner.get_translation(); Point { x: cx, y: cy } } /// Returns the two radii of this ellipse. /// /// The first number is the horizontal radius and the second is the vertical /// radius, before rotation. pub fn radii(&self) -> Vec2 { self.inner.svd().0 } /// The ellipse's rotation, in radians. /// /// This allows all possible ellipses to be drawn by always starting with /// an ellipse with the two radii on the x and y axes. pub fn rotation(&self) -> f64 { self.inner.svd().1 } #[doc(hidden)] #[deprecated(since = "0.7.0", note = "use rotation() instead")] pub fn x_rotation(&self) -> f64 { self.rotation() } } impl Add for Ellipse { type Output = Ellipse; /// In this context adding a `Vec2` applies the corresponding translation to the eliipse. #[inline] #[allow(clippy::suspicious_arithmetic_impl)] fn add(self, v: Vec2) -> Ellipse { Ellipse { inner: Affine::translate(v) * self.inner, } } } impl Sub for Ellipse { type Output = Ellipse; /// In this context subtracting a `Vec2` applies the corresponding translation to the eliipse. #[inline] fn sub(self, v: Vec2) -> Ellipse { Ellipse { inner: Affine::translate(-v) * self.inner, } } } impl Mul for Affine { type Output = Ellipse; fn mul(self, other: Ellipse) -> Self::Output { Ellipse { inner: self * other.inner, } } } impl From for Ellipse { fn from(circle: Circle) -> Self { Ellipse::new(circle.center, Vec2::splat(circle.radius), 0.0) } } impl Shape for Ellipse { type PathElementsIter = iter::Chain, ArcAppendIter>; fn path_elements(&self, tolerance: f64) -> Self::PathElementsIter { let (radii, x_rotation) = self.inner.svd(); Arc { center: self.center(), radii, start_angle: 0.0, sweep_angle: 2.0 * PI, x_rotation, } .path_elements(tolerance) } #[inline] fn area(&self) -> f64 { let Vec2 { x, y } = self.radii(); PI * x * y } #[inline] fn perimeter(&self, accuracy: f64) -> f64 { // TODO rather than delegate to the bezier path, it is possible to use various series // expansions to compute the perimeter to any accuracy. I believe Ramanujan authored the // quickest to converge. See // https://www.mathematica-journal.com/2009/11/23/on-the-perimeter-of-an-ellipse/ // and https://en.wikipedia.org/wiki/Ellipse#Circumference // self.path_segments(0.1).perimeter(accuracy) } fn winding(&self, pt: Point) -> i32 { // Strategy here is to apply the inverse map to the point and see if it is in the unit // circle. let inv = self.inner.inverse(); if (inv * pt).to_vec2().hypot2() < 1.0 { 1 } else { 0 } } // Compute a tight bounding box of the ellipse. // // See https://www.iquilezles.org/www/articles/ellipses/ellipses.htm. We can get the two // radius vectors by applying the affine map to the two impulses (1, 0) and (0, 1) which gives // (a, b) and (c, d) if the affine map is // // a | c | e // ----------- // b | d | f // // We can then use the method in the link with the translation to get the bounding box. #[inline] fn bounding_box(&self) -> Rect { let aff = self.inner.as_coeffs(); let a2 = aff[0] * aff[0]; let b2 = aff[1] * aff[1]; let c2 = aff[2] * aff[2]; let d2 = aff[3] * aff[3]; let cx = aff[4]; let cy = aff[5]; let range_x = (a2 + c2).sqrt(); let range_y = (b2 + d2).sqrt(); Rect { x0: cx - range_x, y0: cy - range_y, x1: cx + range_x, y1: cy + range_y, } } } #[cfg(test)] mod tests { use crate::{Ellipse, Point, Shape}; use std::f64::consts::PI; fn assert_approx_eq(x: f64, y: f64) { // Note: we might want to be more rigorous in testing the accuracy // of the conversion into Béziers. But this seems good enough. assert!((x - y).abs() < 1e-7, "{} != {}", x, y); } #[test] fn area_sign() { let center = Point::new(5.0, 5.0); let e = Ellipse::new(center, (5.0, 5.0), 1.0); assert_approx_eq(e.area(), 25.0 * PI); let e = Ellipse::new(center, (5.0, 10.0), 1.0); assert_approx_eq(e.area(), 50.0 * PI); assert_eq!(e.winding(center), 1); let p = e.to_path(1e-9); assert_approx_eq(e.area(), p.area()); assert_eq!(e.winding(center), p.winding(center)); let e_neg_radius = Ellipse::new(center, (-5.0, 10.0), 1.0); assert_approx_eq(e_neg_radius.area(), 50.0 * PI); assert_eq!(e_neg_radius.winding(center), 1); let p_neg_radius = e_neg_radius.to_path(1e-9); assert_approx_eq(e_neg_radius.area(), p_neg_radius.area()); assert_eq!(e_neg_radius.winding(center), p_neg_radius.winding(center)); } } kurbo-0.7.1/src/insets.rs010064400007650000024000000203701367545551600135050ustar 00000000000000// Copyright 2019 The kurbo Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //! A description of the distances between the edges of two rectangles. use std::ops::{Add, Neg, Sub}; use crate::{Rect, Size}; /// Insets from the edges of a rectangle. /// /// /// The inset value for each edge can be thought of as a delta computed from /// the center of the rect to that edge. For instance, with an inset of `2.0` on /// the x-axis, a rectange with the origin `(0.0, 0.0)` with that inset added /// will have the new origin at `(-2.0, 0.0)`. /// /// Put alternatively, a positive inset represents increased distance from center, /// and a negative inset represents decreased distance from center. /// /// # Examples /// /// Positive insets added to a [`Rect`] produce a larger [`Rect`]: /// ``` /// # use kurbo::{Insets, Rect}; /// let rect = Rect::from_origin_size((0., 0.,), (10., 10.,)); /// let insets = Insets::uniform_xy(3., 0.,); /// /// let inset_rect = rect + insets; /// assert_eq!(inset_rect.width(), 16.0, "10.0 + 3.0 × 2"); /// assert_eq!(inset_rect.x0, -3.0); /// ``` /// /// Negative insets added to a [`Rect`] produce a smaller [`Rect`]: /// /// ``` /// # use kurbo::{Insets, Rect}; /// let rect = Rect::from_origin_size((0., 0.,), (10., 10.,)); /// let insets = Insets::uniform_xy(-3., 0.,); /// /// let inset_rect = rect + insets; /// assert_eq!(inset_rect.width(), 4.0, "10.0 - 3.0 × 2"); /// assert_eq!(inset_rect.x0, 3.0); /// ``` /// /// [`Insets`] operate on the absolute rectangle [`Rect::abs`], and so ignore /// existing negative widths and heights. /// /// ``` /// # use kurbo::{Insets, Rect}; /// let rect = Rect::new(7., 11., 0., 0.,); /// let insets = Insets::uniform_xy(0., 1.,); /// /// assert_eq!(rect.width(), -7.0); /// /// let inset_rect = rect + insets; /// assert_eq!(inset_rect.width(), 7.0); /// assert_eq!(inset_rect.x0, 0.0); /// assert_eq!(inset_rect.height(), 13.0); /// ``` /// /// The width and height of an inset operation can still be negative if the /// [`Insets`]' dimensions are greater than the dimensions of the original [`Rect`]. /// /// ``` /// # use kurbo::{Insets, Rect}; /// let rect = Rect::new(0., 0., 3., 5.); /// let insets = Insets::uniform_xy(0., 7.,); /// /// let inset_rect = rect - insets; /// assert_eq!(inset_rect.height(), -9., "5 - 7 × 2") /// ``` /// /// `Rect - Rect = Insets`: /// /// /// ``` /// # use kurbo::{Insets, Rect}; /// let rect = Rect::new(0., 0., 5., 11.); /// let insets = Insets::uniform_xy(1., 7.,); /// /// let inset_rect = rect + insets; /// let insets2 = inset_rect - rect; /// /// assert_eq!(insets2.x0, insets.x0); /// assert_eq!(insets2.y1, insets.y1); /// assert_eq!(insets2.x_value(), insets.x_value()); /// assert_eq!(insets2.y_value(), insets.y_value()); /// ``` /// /// [`Rect`]: struct.Rect.html /// [`Insets`]: struct.Insets.html /// [`Rect::abs`]: struct.Rect.html#method.abs #[derive(Clone, Copy, Default, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Insets { /// The minimum x coordinate (left edge). pub x0: f64, /// The minimum y coordinate (top edge in y-down spaces). pub y0: f64, /// The maximum x coordinate (right edge). pub x1: f64, /// The maximum y coordinate (bottom edge in y-down spaces). pub y1: f64, } impl Insets { /// Zero'd insets. pub const ZERO: Insets = Insets::uniform(0.); /// New uniform insets. #[inline] pub const fn uniform(d: f64) -> Insets { Insets { x0: d, y0: d, x1: d, y1: d, } } /// New insets with uniform values along each axis. #[inline] pub const fn uniform_xy(x: f64, y: f64) -> Insets { Insets { x0: x, y0: y, x1: x, y1: y, } } /// New insets. The ordering of the arguments is "left, top, right, bottom", /// assuming a y-down coordinate space. #[inline] pub const fn new(x0: f64, y0: f64, x1: f64, y1: f64) -> Insets { Insets { x0, y0, x1, y1 } } /// The total delta on the x-axis represented by these insets. /// /// # Examples /// /// ``` /// use kurbo::Insets; /// /// let insets = Insets::uniform_xy(3., 8.); /// assert_eq!(insets.x_value(), 6.); /// /// let insets = Insets::new(5., 0., -12., 0.,); /// assert_eq!(insets.x_value(), -7.); /// ``` #[inline] pub fn x_value(self) -> f64 { self.x0 + self.x1 } /// The total delta on the y-axis represented by these insets. /// /// # Examples /// /// ``` /// use kurbo::Insets; /// /// let insets = Insets::uniform_xy(3., 7.); /// assert_eq!(insets.y_value(), 14.); /// /// let insets = Insets::new(5., 10., -12., 4.,); /// assert_eq!(insets.y_value(), 14.); /// ``` #[inline] pub fn y_value(self) -> f64 { self.y0 + self.y1 } /// Returns the total delta represented by these insets as a [`Size`]. /// /// This is equivalent to creating a [`Size`] from the values returned by /// [`x_value`] and [`y_value`]. /// /// This function may return a a size with negative values. /// /// # Examples /// /// ``` /// use kurbo::{Insets, Size}; /// /// let insets = Insets::new(11.1, -43.3, 3.333, -0.0); /// assert_eq!(insets.size(), Size::new(insets.x_value(), insets.y_value())); /// ``` /// /// [`Size`]: struct.Size.html /// [`x_value`]: #method.x_value /// [`y_value`]: #method.y_value pub fn size(self) -> Size { Size::new(self.x_value(), self.y_value()) } /// Return `true` iff all values are nonnegative. pub fn are_nonnegative(self) -> bool { let Insets { x0, y0, x1, y1 } = self; x0 >= 0.0 && y0 >= 0.0 && x1 >= 0.0 && y1 >= 0.0 } /// Return new `Insets` with all negative values replaced with `0.0`. /// /// This is provided as a convenience for applications where negative insets /// are not meaningful. /// /// # Examples /// /// ``` /// use kurbo::Insets; /// /// let insets = Insets::new(-10., 3., -0.2, 4.); /// let nonnegative = insets.nonnegative(); /// assert_eq!(nonnegative.x_value(), 0.0); /// assert_eq!(nonnegative.y_value(), 7.0); /// ``` pub fn nonnegative(self) -> Insets { let Insets { x0, y0, x1, y1 } = self; Insets { x0: x0.max(0.0), y0: y0.max(0.0), x1: x1.max(0.0), y1: y1.max(0.0), } } } impl Neg for Insets { type Output = Insets; #[inline] fn neg(self) -> Insets { Insets::new(-self.x0, -self.y0, -self.x1, -self.y1) } } impl Add for Insets { type Output = Rect; #[inline] #[allow(clippy::suspicious_arithmetic_impl)] fn add(self, other: Rect) -> Rect { let other = other.abs(); Rect::new( other.x0 - self.x0, other.y0 - self.y0, other.x1 + self.x1, other.y1 + self.y1, ) } } impl Add for Rect { type Output = Rect; #[inline] fn add(self, other: Insets) -> Rect { other + self } } impl Sub for Insets { type Output = Rect; #[inline] fn sub(self, other: Rect) -> Rect { other + -self } } impl Sub for Rect { type Output = Rect; #[inline] fn sub(self, other: Insets) -> Rect { other - self } } impl From for Insets { fn from(src: f64) -> Insets { Insets::uniform(src) } } impl From<(f64, f64)> for Insets { fn from(src: (f64, f64)) -> Insets { Insets::uniform_xy(src.0, src.1) } } impl From<(f64, f64, f64, f64)> for Insets { fn from(src: (f64, f64, f64, f64)) -> Insets { Insets::new(src.0, src.1, src.2, src.3) } } kurbo-0.7.1/src/lib.rs010064400007650000024000000072131374161123100127250ustar 00000000000000// Copyright 2018 The kurbo Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //! 2D geometry, with a focus on curves. //! //! The kurbo library contains data structures and algorithms for curves and //! vector paths. It was designed to serve the needs of 2D graphics applications, //! but it is intended to be general enough to be useful for other applications. //! //! Kurbo is designed to be used by [`Piet`], a crate for drawing 2D graphics, //! and is in turn used by [`Druid`], a cross-platform GUI toolkit. //! //! # Examples //! //! Basic UI-style geometry: //! ``` //! use kurbo::{Insets, Point, Rect, Size, Vec2}; //! //! let pt = Point::new(10.0, 10.0); //! let vector = Vec2::new(5.0, -5.0); //! let pt2 = pt + vector; //! assert_eq!(pt2, Point::new(15.0, 5.0)); //! //! let rect = Rect::from_points(pt, pt2); //! assert_eq!(rect, Rect::from_origin_size((10.0, 5.0), (5.0, 5.0))); //! //! let insets = Insets::uniform(1.0); //! let inset_rect = rect - insets; //! assert_eq!(inset_rect.size(), Size::new(3.0, 3.0)); //! ``` //! //! Finding the closest position on a [`Shape`]'s perimeter to a [`Point`]: //! //! ``` //! use kurbo::{Circle, ParamCurve, ParamCurveNearest, Point, Shape}; //! //! const DESIRED_ACCURACY: f64 = 0.1; //! //! /// Given a shape and a point, returns the closest position on the shape's //! /// parimeter, or `None` if the shape is malformed. //! fn closest_perimeter_point(shape: impl Shape, pt: Point) -> Option { //! let mut best: Option<(Point, f64)> = None; //! for segment in shape.path_segments(DESIRED_ACCURACY) { //! let (t, distance) = segment.nearest(pt, DESIRED_ACCURACY); //! if best.map(|(_, best_d)| distance < best_d).unwrap_or(true) { //! best = Some((segment.eval(t), distance)) //! } //! } //! best.map(|(point, _)| point) //! } //! //! let circle = Circle::new((5.0, 5.0), 5.0); //! let hit_point = Point::new(5.0, -2.0); //! let expectation = Point::new(5.0, 0.0); //! let hit = closest_perimeter_point(circle, hit_point).unwrap(); //! assert!(hit.distance(expectation) <= DESIRED_ACCURACY); //! ``` //! //! [`Shape`]: trait.Shape.html //! [`Point`]: struct.Point.html //! [`Piet`]: https://docs.rs/piet //! [`Druid`]: https://docs.rs/druid #![forbid(unsafe_code)] #![deny(missing_docs, clippy::trivially_copy_pass_by_ref)] #![warn(broken_intra_doc_links)] #![allow( clippy::unreadable_literal, clippy::many_single_char_names, clippy::excessive_precision )] mod affine; mod arc; mod bezpath; mod circle; pub mod common; mod cubicbez; mod ellipse; mod insets; mod line; mod param_curve; mod point; mod quadbez; mod rect; mod rounded_rect; mod shape; mod size; mod svg; mod translate_scale; mod vec2; pub use crate::affine::*; pub use crate::arc::*; pub use crate::bezpath::*; pub use crate::circle::*; pub use crate::cubicbez::*; pub use crate::ellipse::*; pub use crate::insets::*; pub use crate::line::*; pub use crate::param_curve::*; pub use crate::point::*; pub use crate::quadbez::*; pub use crate::rect::*; pub use crate::rounded_rect::*; pub use crate::shape::*; pub use crate::size::*; pub use crate::svg::*; pub use crate::translate_scale::*; pub use crate::vec2::*; kurbo-0.7.1/src/line.rs010064400007650000024000000124541374161070200131130ustar 00000000000000//! Lines. use std::ops::{Add, Mul, Range, Sub}; use arrayvec::ArrayVec; use crate::{ Affine, ParamCurve, ParamCurveArclen, ParamCurveArea, ParamCurveCurvature, ParamCurveDeriv, ParamCurveExtrema, ParamCurveNearest, PathEl, Point, Rect, Shape, Vec2, DEFAULT_ACCURACY, MAX_EXTREMA, }; /// A single line. #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Line { /// The line's start point. pub p0: Point, /// The line's end point. pub p1: Point, } impl Line { /// Create a new line. #[inline] pub fn new(p0: impl Into, p1: impl Into) -> Line { Line { p0: p0.into(), p1: p1.into(), } } /// The length of the line. #[inline] pub fn length(self) -> f64 { self.arclen(DEFAULT_ACCURACY) } } impl ParamCurve for Line { #[inline] fn eval(&self, t: f64) -> Point { self.p0.lerp(self.p1, t) } #[inline] fn start(&self) -> Point { self.p0 } #[inline] fn end(&self) -> Point { self.p1 } #[inline] fn subsegment(&self, range: Range) -> Line { Line { p0: self.eval(range.start), p1: self.eval(range.end), } } } impl ParamCurveDeriv for Line { type DerivResult = ConstPoint; #[inline] fn deriv(&self) -> ConstPoint { ConstPoint((self.p1 - self.p0).to_point()) } } impl ParamCurveArclen for Line { #[inline] fn arclen(&self, _accuracy: f64) -> f64 { (self.p1 - self.p0).hypot() } } impl ParamCurveArea for Line { #[inline] fn signed_area(&self) -> f64 { self.p0.to_vec2().cross(self.p1.to_vec2()) * 0.5 } } impl ParamCurveNearest for Line { fn nearest(&self, p: Point, _accuracy: f64) -> (f64, f64) { let d = self.p1 - self.p0; let dotp = d.dot(p - self.p0); let d_squared = d.dot(d); if dotp <= 0.0 { (0.0, (p - self.p0).hypot2()) } else if dotp >= d_squared { (1.0, (p - self.p1).hypot2()) } else { let t = dotp / d_squared; let dist = (p - self.eval(t)).hypot2(); (t, dist) } } } impl ParamCurveCurvature for Line { #[inline] fn curvature(&self, _t: f64) -> f64 { 0.0 } } impl ParamCurveExtrema for Line { #[inline] fn extrema(&self) -> ArrayVec<[f64; MAX_EXTREMA]> { ArrayVec::new() } } /// A trivial "curve" that is just a constant. #[derive(Clone, Copy, Debug)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct ConstPoint(Point); impl ParamCurve for ConstPoint { #[inline] fn eval(&self, _t: f64) -> Point { self.0 } #[inline] fn subsegment(&self, _range: Range) -> ConstPoint { *self } } impl ParamCurveDeriv for ConstPoint { type DerivResult = ConstPoint; #[inline] fn deriv(&self) -> ConstPoint { ConstPoint(Point::new(0.0, 0.0)) } } impl ParamCurveArclen for ConstPoint { #[inline] fn arclen(&self, _accuracy: f64) -> f64 { 0.0 } } impl Mul for Affine { type Output = Line; #[inline] fn mul(self, other: Line) -> Line { Line { p0: self * other.p0, p1: self * other.p1, } } } impl Add for Line { type Output = Line; #[inline] fn add(self, v: Vec2) -> Line { Line::new(self.p0 + v, self.p1 + v) } } impl Sub for Line { type Output = Line; #[inline] fn sub(self, v: Vec2) -> Line { Line::new(self.p0 - v, self.p1 - v) } } /// An iterator yielding the path for a single line. #[doc(hidden)] pub struct LinePathIter { line: Line, ix: usize, } impl Shape for Line { type PathElementsIter = LinePathIter; #[inline] fn path_elements(&self, _tolerance: f64) -> LinePathIter { LinePathIter { line: *self, ix: 0 } } /// Returning zero here is consistent with the contract (area is /// only meaningful for closed shapes), but an argument can be made /// that the contract should be tightened to include the Green's /// theorem contribution. fn area(&self) -> f64 { 0.0 } #[inline] fn perimeter(&self, _accuracy: f64) -> f64 { (self.p1 - self.p0).hypot() } /// Same consideration as `area`. fn winding(&self, _pt: Point) -> i32 { 0 } #[inline] fn bounding_box(&self) -> Rect { Rect::from_points(self.p0, self.p1) } #[inline] fn as_line(&self) -> Option { Some(*self) } } impl Iterator for LinePathIter { type Item = PathEl; fn next(&mut self) -> Option { self.ix += 1; match self.ix { 1 => Some(PathEl::MoveTo(self.line.p0)), 2 => Some(PathEl::LineTo(self.line.p1)), _ => None, } } } #[cfg(test)] mod tests { use crate::{Line, ParamCurveArclen}; #[test] fn line_arclen() { let l = Line::new((0.0, 0.0), (1.0, 1.0)); let true_len = 2.0f64.sqrt(); let epsilon = 1e-9; assert!(l.arclen(epsilon) - true_len < epsilon); let t = l.inv_arclen(true_len / 3.0, epsilon); assert!((t - 1.0 / 3.0).abs() < epsilon); } } kurbo-0.7.1/src/param_curve.rs010064400007650000024000000155521375200744500145000ustar 00000000000000//! A trait for curves parametrized by a scalar. use std::ops::Range; use arrayvec::ArrayVec; use crate::{Point, Rect}; /// A default value for methods that take an 'accuracy' argument. /// /// This value is intended to be suitable for general-purpose use, such as /// 2d graphics. pub const DEFAULT_ACCURACY: f64 = 1e-6; /// A curve parametrized by a scalar. /// /// If the result is interpreted as a point, this represents a curve. /// But the result can be interpreted as a vector as well. pub trait ParamCurve: Sized { /// Evaluate the curve at parameter `t`. /// /// Generally `t` is in the range [0..1]. fn eval(&self, t: f64) -> Point; /// Get a subsegment of the curve for the given parameter range. fn subsegment(&self, range: Range) -> Self; /// Subdivide into (roughly) halves. #[inline] fn subdivide(&self) -> (Self, Self) { (self.subsegment(0.0..0.5), self.subsegment(0.5..1.0)) } /// The start point. fn start(&self) -> Point { self.eval(0.0) } /// The end point. fn end(&self) -> Point { self.eval(1.0) } } // TODO: I might not want to have separate traits for all these. /// A differentiable parametrized curve. pub trait ParamCurveDeriv { /// The parametric curve obtained by taking the derivative of this one. type DerivResult: ParamCurve; /// The derivative of the curve. /// /// Note that the type of the return value is somewhat inaccurate, as /// the derivative of a curve (mapping of param to point) is a mapping /// of param to vector. We choose to accept this rather than have a /// more complex type scheme. fn deriv(&self) -> Self::DerivResult; /// Estimate arclength using Gaussian quadrature. /// /// The coefficients are assumed to cover the range (-1..1), which is /// traditional. #[inline] fn gauss_arclen(&self, coeffs: &[(f64, f64)]) -> f64 { let d = self.deriv(); coeffs .iter() .map(|(wi, xi)| wi * d.eval(0.5 * (xi + 1.0)).to_vec2().hypot()) .sum::() * 0.5 } } /// A parametrized curve that can have its arc length measured. pub trait ParamCurveArclen: ParamCurve { /// The arc length of the curve. /// /// The result is accurate to the given accuracy (subject to /// roundoff errors for ridiculously low values). Compute time /// may vary with accuracy, if the curve needs to be subdivided. fn arclen(&self, accuracy: f64) -> f64; /// Solve for the parameter that has the given arclength from the start. /// /// This implementation is bisection, which is very robust but not /// necessarily the fastest. It does measure increasingly short /// segments, though, which should be good for subdivision algorithms. fn inv_arclen(&self, arclen: f64, accuracy: f64) -> f64 { // invariant: the curve's arclen on [0..t_last] + remaining = arclen let mut remaining = arclen; let mut t_last = 0.0; let mut t0 = 0.0; let mut t1 = 1.0; let n = (self.arclen(accuracy) / accuracy).log2().ceil().max(1.0); let inner_accuracy = accuracy / n; let n = n as usize; for i in 0..n { let tm = 0.5 * (t0 + t1); let (range, dir) = if tm > t_last { (t_last..tm, 1.0) } else { (tm..t_last, -1.0) }; let range_size = range.end - range.start; let arc = self.subsegment(range).arclen(inner_accuracy); remaining -= arc * dir; if i == n - 1 || (remaining).abs() < accuracy { // Allocate remaining arc evenly. return tm + range_size * remaining / arc; } if remaining > 0.0 { t0 = tm; } else { t1 = tm; } t_last = tm; } unreachable!(); } } /// A parametrized curve that can have its signed area measured. pub trait ParamCurveArea { /// Compute the signed area under the curve. /// /// For a closed path, the signed area of the path is the sum of signed /// areas of the segments. This is a variant of the "shoelace formula." /// See: /// and /// /// /// This can be computed exactly for Béziers thanks to Green's theorem, /// and also for simple curves such as circular arcs. For more exotic /// curves, it's probably best to subdivide to cubics. We leave that /// to the caller, which is why we don't give an accuracy param here. fn signed_area(&self) -> f64; } /// A parametrized curve that reports the nearest point. pub trait ParamCurveNearest { /// Find the point on the curve nearest the given point. /// /// Returns the parameter and the square of the distance. fn nearest(&self, p: Point, accuracy: f64) -> (f64, f64); } /// A parametrized curve that reports its curvature. pub trait ParamCurveCurvature: ParamCurveDeriv where Self::DerivResult: ParamCurveDeriv, { /// Compute the signed curvature at parameter `t`. #[inline] fn curvature(&self, t: f64) -> f64 { let deriv = self.deriv(); let deriv2 = deriv.deriv(); let d = deriv.eval(t).to_vec2(); let d2 = deriv2.eval(t).to_vec2(); // TODO: What's the convention for sign? I think it should match signed // area - a positive area curve should have positive curvature. d2.cross(d) * d.hypot2().powf(-1.5) } } /// The maximum number of extrema that can be reported in the `ParamCurveExtrema` trait. /// /// This is 4 to support cubic Béziers. If other curves are used, they should be /// subdivided to limit the number of extrema. pub const MAX_EXTREMA: usize = 4; /// A parametrized curve that reports its extrema. pub trait ParamCurveExtrema: ParamCurve { /// Compute the extrema of the curve. /// /// Only extrema within the interior of the curve count. /// At most four extrema can be reported, which is sufficient for /// cubic Béziers. /// /// The extrema should be reported in increasing parameter order. fn extrema(&self) -> ArrayVec<[f64; MAX_EXTREMA]>; /// Return parameter ranges, each of which is monotonic within the range. fn extrema_ranges(&self) -> ArrayVec<[Range; MAX_EXTREMA + 1]> { let mut result = ArrayVec::new(); let mut t0 = 0.0; for t in self.extrema() { result.push(t0..t); t0 = t; } result.push(t0..1.0); result } /// The smallest rectangle that encloses the curve in the range (0..1). fn bounding_box(&self) -> Rect { let mut bbox = Rect::from_points(self.start(), self.end()); for t in self.extrema() { bbox = bbox.union_pt(self.eval(t)) } bbox } } kurbo-0.7.1/src/point.rs010064400007650000024000000164361374161070200133210ustar 00000000000000//! A 2D point. use std::fmt; use std::ops::{Add, AddAssign, Sub, SubAssign}; use crate::common::FloatExt; use crate::Vec2; /// A 2D point. #[derive(Clone, Copy, Default, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Point { /// The x coordinate. pub x: f64, /// The y coordinate. pub y: f64, } impl Point { /// The point (0, 0). pub const ZERO: Point = Point::new(0., 0.); /// The point at the origin; (0, 0). pub const ORIGIN: Point = Point::new(0., 0.); /// Create a new `Point` with the provided `x` and `y` coordinates. #[inline] pub const fn new(x: f64, y: f64) -> Self { Point { x, y } } /// Convert this point into a `Vec2`. #[inline] pub const fn to_vec2(self) -> Vec2 { Vec2::new(self.x, self.y) } /// Linearly interpolate between two points. #[inline] pub fn lerp(self, other: Point, t: f64) -> Point { self.to_vec2().lerp(other.to_vec2(), t).to_point() } /// Determine the midpoint of two points. #[inline] pub fn midpoint(self, other: Point) -> Point { Point::new(0.5 * (self.x + other.x), 0.5 * (self.y + other.y)) } /// Euclidean distance. #[inline] pub fn distance(self, other: Point) -> f64 { (self - other).hypot() } /// Returns a new `Point`, /// with `x` and `y` rounded to the nearest integer. /// /// # Examples /// /// ``` /// use kurbo::Point; /// let a = Point::new(3.3, 3.6).round(); /// let b = Point::new(3.0, -3.1).round(); /// assert_eq!(a.x, 3.0); /// assert_eq!(a.y, 4.0); /// assert_eq!(b.x, 3.0); /// assert_eq!(b.y, -3.0); /// ``` #[inline] pub fn round(self) -> Point { Point::new(self.x.round(), self.y.round()) } /// Returns a new `Point`, /// with `x` and `y` rounded up to the nearest integer, /// unless they are already an integer. /// /// # Examples /// /// ``` /// use kurbo::Point; /// let a = Point::new(3.3, 3.6).ceil(); /// let b = Point::new(3.0, -3.1).ceil(); /// assert_eq!(a.x, 4.0); /// assert_eq!(a.y, 4.0); /// assert_eq!(b.x, 3.0); /// assert_eq!(b.y, -3.0); /// ``` #[inline] pub fn ceil(self) -> Point { Point::new(self.x.ceil(), self.y.ceil()) } /// Returns a new `Point`, /// with `x` and `y` rounded down to the nearest integer, /// unless they are already an integer. /// /// # Examples /// /// ``` /// use kurbo::Point; /// let a = Point::new(3.3, 3.6).floor(); /// let b = Point::new(3.0, -3.1).floor(); /// assert_eq!(a.x, 3.0); /// assert_eq!(a.y, 3.0); /// assert_eq!(b.x, 3.0); /// assert_eq!(b.y, -4.0); /// ``` #[inline] pub fn floor(self) -> Point { Point::new(self.x.floor(), self.y.floor()) } /// Returns a new `Point`, /// with `x` and `y` rounded away from zero to the nearest integer, /// unless they are already an integer. /// /// # Examples /// /// ``` /// use kurbo::Point; /// let a = Point::new(3.3, 3.6).expand(); /// let b = Point::new(3.0, -3.1).expand(); /// assert_eq!(a.x, 4.0); /// assert_eq!(a.y, 4.0); /// assert_eq!(b.x, 3.0); /// assert_eq!(b.y, -4.0); /// ``` #[inline] pub fn expand(self) -> Point { Point::new(self.x.expand(), self.y.expand()) } /// Returns a new `Point`, /// with `x` and `y` rounded towards zero to the nearest integer, /// unless they are already an integer. /// /// # Examples /// /// ``` /// use kurbo::Point; /// let a = Point::new(3.3, 3.6).trunc(); /// let b = Point::new(3.0, -3.1).trunc(); /// assert_eq!(a.x, 3.0); /// assert_eq!(a.y, 3.0); /// assert_eq!(b.x, 3.0); /// assert_eq!(b.y, -3.0); /// ``` #[inline] pub fn trunc(self) -> Point { Point::new(self.x.trunc(), self.y.trunc()) } } impl From<(f64, f64)> for Point { #[inline] fn from(v: (f64, f64)) -> Point { Point { x: v.0, y: v.1 } } } impl From for (f64, f64) { #[inline] fn from(v: Point) -> (f64, f64) { (v.x, v.y) } } impl Add for Point { type Output = Point; #[inline] fn add(self, other: Vec2) -> Self { Point::new(self.x + other.x, self.y + other.y) } } impl AddAssign for Point { #[inline] fn add_assign(&mut self, other: Vec2) { *self = Point::new(self.x + other.x, self.y + other.y) } } impl Sub for Point { type Output = Point; #[inline] fn sub(self, other: Vec2) -> Self { Point::new(self.x - other.x, self.y - other.y) } } impl SubAssign for Point { #[inline] fn sub_assign(&mut self, other: Vec2) { *self = Point::new(self.x - other.x, self.y - other.y) } } impl Add<(f64, f64)> for Point { type Output = Point; #[inline] fn add(self, (x, y): (f64, f64)) -> Self { Point::new(self.x + x, self.y + y) } } impl AddAssign<(f64, f64)> for Point { #[inline] fn add_assign(&mut self, (x, y): (f64, f64)) { *self = Point::new(self.x + x, self.y + y) } } impl Sub<(f64, f64)> for Point { type Output = Point; #[inline] fn sub(self, (x, y): (f64, f64)) -> Self { Point::new(self.x - x, self.y - y) } } impl SubAssign<(f64, f64)> for Point { #[inline] fn sub_assign(&mut self, (x, y): (f64, f64)) { *self = Point::new(self.x - x, self.y - y) } } impl Sub for Point { type Output = Vec2; #[inline] fn sub(self, other: Point) -> Vec2 { Vec2::new(self.x - other.x, self.y - other.y) } } impl fmt::Debug for Point { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "({:?}, {:?})", self.x, self.y) } } impl fmt::Display for Point { fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { write!(formatter, "(")?; fmt::Display::fmt(&self.x, formatter)?; write!(formatter, ", ")?; fmt::Display::fmt(&self.y, formatter)?; write!(formatter, ")") } } #[cfg(test)] mod tests { use super::*; #[test] fn point_arithmetic() { assert_eq!( Point::new(0., 0.) - Vec2::new(10., 0.), Point::new(-10., 0.) ); assert_eq!( Point::new(0., 0.) - Point::new(-5., 101.), Vec2::new(5., -101.) ); } #[test] #[allow(clippy::float_cmp)] fn distance() { let p1 = Point::new(0., 10.); let p2 = Point::new(0., 5.); assert_eq!(p1.distance(p2), 5.); let p1 = Point::new(-11., 1.); let p2 = Point::new(-7., -2.); assert_eq!(p1.distance(p2), 5.); } #[test] fn display() { let p = Point::new(0.12345, 9.87654); assert_eq!(format!("{}", p), "(0.12345, 9.87654)"); let p = Point::new(0.12345, 9.87654); assert_eq!(format!("{:.2}", p), "(0.12, 9.88)"); } } #[cfg(feature = "mint")] impl From for mint::Point2 { #[inline] fn from(p: Point) -> mint::Point2 { mint::Point2 { x: p.x, y: p.y } } } #[cfg(feature = "mint")] impl From> for Point { #[inline] fn from(p: mint::Point2) -> Point { Point { x: p.x, y: p.y } } } kurbo-0.7.1/src/quadbez.rs010064400007650000024000000371271374554710500136350ustar 00000000000000//! Quadratic Bézier segments. use std::ops::{Mul, Range}; use arrayvec::ArrayVec; use crate::common::solve_cubic; use crate::MAX_EXTREMA; use crate::{ Affine, CubicBez, Line, ParamCurve, ParamCurveArclen, ParamCurveArea, ParamCurveCurvature, ParamCurveDeriv, ParamCurveExtrema, ParamCurveNearest, PathEl, Point, Rect, Shape, }; /// A single quadratic Bézier segment. #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[allow(missing_docs)] pub struct QuadBez { pub p0: Point, pub p1: Point, pub p2: Point, } impl QuadBez { /// Create a new quadratic Bézier segment. #[inline] pub fn new>(p0: V, p1: V, p2: V) -> QuadBez { QuadBez { p0: p0.into(), p1: p1.into(), p2: p2.into(), } } /// Raise the order by 1. /// /// Returns a cubic Bézier segment that exactly represents this quadratic. #[inline] pub fn raise(&self) -> CubicBez { CubicBez::new( self.p0, self.p0 + (2.0 / 3.0) * (self.p1 - self.p0), self.p2 + (2.0 / 3.0) * (self.p1 - self.p2), self.p2, ) } /// Estimate the number of subdivisions for flattening. pub(crate) fn estimate_subdiv(&self, sqrt_tol: f64) -> FlattenParams { // Determine transformation to $y = x^2$ parabola. let d01 = self.p1 - self.p0; let d12 = self.p2 - self.p1; let dd = d01 - d12; let cross = (self.p2 - self.p0).cross(dd); let x0 = d01.dot(dd) * cross.recip(); let x2 = d12.dot(dd) * cross.recip(); let scale = (cross / (dd.hypot() * (x2 - x0))).abs(); // Compute number of subdivisions needed. let a0 = approx_parabola_integral(x0); let a2 = approx_parabola_integral(x2); let val = if scale.is_finite() { let da = (a2 - a0).abs(); let sqrt_scale = scale.sqrt(); if x0.signum() == x2.signum() { da * sqrt_scale } else { // Handle cusp case (segment contains curvature maximum) let xmin = sqrt_tol / sqrt_scale; sqrt_tol * da / approx_parabola_integral(xmin) } } else { 0.0 }; let u0 = approx_parabola_inv_integral(a0); let u2 = approx_parabola_inv_integral(a2); let uscale = (u2 - u0).recip(); FlattenParams { a0, a2, u0, uscale, val, } } // Maps a value from 0..1 to 0..1. pub(crate) fn determine_subdiv_t(&self, params: &FlattenParams, x: f64) -> f64 { let a = params.a0 + (params.a2 - params.a0) * x; let u = approx_parabola_inv_integral(a); (u - params.u0) * params.uscale } } /// An iterator for quadratic beziers. pub struct QuadBezIter { quad: QuadBez, ix: usize, } impl Shape for QuadBez { type PathElementsIter = QuadBezIter; #[inline] fn path_elements(&self, _tolerance: f64) -> QuadBezIter { QuadBezIter { quad: *self, ix: 0 } } fn area(&self) -> f64 { 0.0 } #[inline] fn perimeter(&self, accuracy: f64) -> f64 { self.arclen(accuracy) } fn winding(&self, _pt: Point) -> i32 { 0 } #[inline] fn bounding_box(&self) -> Rect { ParamCurveExtrema::bounding_box(self) } } impl Iterator for QuadBezIter { type Item = PathEl; fn next(&mut self) -> Option { self.ix += 1; match self.ix { 1 => Some(PathEl::MoveTo(self.quad.p0)), 2 => Some(PathEl::QuadTo(self.quad.p1, self.quad.p2)), _ => None, } } } pub(crate) struct FlattenParams { a0: f64, a2: f64, u0: f64, uscale: f64, /// The number of subdivisions * 2 * sqrt_tol. pub(crate) val: f64, } /// An approximation to $\int (1 + 4x^2) ^ -0.25 dx$ /// /// This is used for flattening curves. fn approx_parabola_integral(x: f64) -> f64 { const D: f64 = 0.67; x / (1.0 - D + (D.powi(4) + 0.25 * x * x).sqrt().sqrt()) } /// An approximation to the inverse parabola integral. fn approx_parabola_inv_integral(x: f64) -> f64 { const B: f64 = 0.39; x * (1.0 - B + (B * B + 0.25 * x * x).sqrt()) } impl ParamCurve for QuadBez { #[inline] fn eval(&self, t: f64) -> Point { let mt = 1.0 - t; (self.p0.to_vec2() * (mt * mt) + (self.p1.to_vec2() * (mt * 2.0) + self.p2.to_vec2() * t) * t) .to_point() } #[inline] fn start(&self) -> Point { self.p0 } #[inline] fn end(&self) -> Point { self.p2 } /// Subdivide into halves, using de Casteljau. #[inline] fn subdivide(&self) -> (QuadBez, QuadBez) { let pm = self.eval(0.5); ( QuadBez::new(self.p0, self.p0.midpoint(self.p1), pm), QuadBez::new(pm, self.p1.midpoint(self.p2), self.p2), ) } fn subsegment(&self, range: Range) -> QuadBez { let (t0, t1) = (range.start, range.end); let p0 = self.eval(t0); let p2 = self.eval(t1); let p1 = p0 + (self.p1 - self.p0).lerp(self.p2 - self.p1, t0) * (t1 - t0); QuadBez { p0, p1, p2 } } } impl ParamCurveDeriv for QuadBez { type DerivResult = Line; #[inline] fn deriv(&self) -> Line { Line::new( (2.0 * (self.p1.to_vec2() - self.p0.to_vec2())).to_point(), (2.0 * (self.p2.to_vec2() - self.p1.to_vec2())).to_point(), ) } } impl ParamCurveArclen for QuadBez { /// Arclength of a quadratic Bézier segment. /// /// This computation is based on an analytical formula. Since that formula suffers /// from numerical instability when the curve is very close to a straight line, we /// detect that case and fall back to Legendre-Gauss quadrature. /// /// Accuracy should be better than 1e-13 over the entire range. /// /// Adapted from /// with permission. fn arclen(&self, _accuracy: f64) -> f64 { let d2 = self.p0.to_vec2() - 2.0 * self.p1.to_vec2() + self.p2.to_vec2(); let a = d2.hypot2(); let d1 = self.p1 - self.p0; let c = d1.hypot2(); if a < 5e-4 * c { // This case happens for nearly straight Béziers. // // Calculate arclength using Legendre-Gauss quadrature using formula from Behdad // in https://github.com/Pomax/BezierInfo-2/issues/77 let v0 = (-0.492943519233745 * self.p0.to_vec2() + 0.430331482911935 * self.p1.to_vec2() + 0.0626120363218102 * self.p2.to_vec2()) .hypot(); let v1 = ((self.p2 - self.p0) * 0.4444444444444444).hypot(); let v2 = (-0.0626120363218102 * self.p0.to_vec2() - 0.430331482911935 * self.p1.to_vec2() + 0.492943519233745 * self.p2.to_vec2()) .hypot(); return v0 + v1 + v2; } let b = 2.0 * d2.dot(d1); let sabc = (a + b + c).sqrt(); let a2 = a.powf(-0.5); let a32 = a2.powi(3); let c2 = 2.0 * c.sqrt(); let ba_c2 = b * a2 + c2; let v0 = 0.25 * a2 * a2 * b * (2.0 * sabc - c2) + sabc; // TODO: justify and fine-tune this exact constant. if ba_c2 < 1e-13 { // This case happens for Béziers with a sharp kink. v0 } else { v0 + 0.25 * a32 * (4.0 * c * a - b * b) * (((2.0 * a + b) * a2 + 2.0 * sabc) / ba_c2).ln() } } } impl ParamCurveArea for QuadBez { #[inline] fn signed_area(&self) -> f64 { (self.p0.x * (2.0 * self.p1.y + self.p2.y) + 2.0 * self.p1.x * (self.p2.y - self.p0.y) - self.p2.x * (self.p0.y + 2.0 * self.p1.y)) * (1.0 / 6.0) } } impl ParamCurveNearest for QuadBez { /// Find nearest point, using analytical algorithm based on cubic root finding. fn nearest(&self, p: Point, _accuracy: f64) -> (f64, f64) { fn eval_t(p: Point, t_best: &mut f64, r_best: &mut Option, t: f64, p0: Point) { let r = (p0 - p).hypot2(); if r_best.map(|r_best| r < r_best).unwrap_or(true) { *r_best = Some(r); *t_best = t; } } fn try_t( q: &QuadBez, p: Point, t_best: &mut f64, r_best: &mut Option, t: f64, ) -> bool { if t < 0.0 || t > 1.0 { return true; } eval_t(p, t_best, r_best, t, q.eval(t)); false } let d0 = self.p1 - self.p0; let d1 = self.p0.to_vec2() + self.p2.to_vec2() - 2.0 * self.p1.to_vec2(); let d = self.p0 - p; let c0 = d.dot(d0); let c1 = 2.0 * d0.hypot2() + d.dot(d1); let c2 = 3.0 * d1.dot(d0); let c3 = d1.hypot2(); let roots = solve_cubic(c0, c1, c2, c3); let mut r_best = None; let mut t_best = 0.0; let mut need_ends = false; for &t in &roots { need_ends |= try_t(self, p, &mut t_best, &mut r_best, t); } if need_ends { eval_t(p, &mut t_best, &mut r_best, 0.0, self.p0); eval_t(p, &mut t_best, &mut r_best, 1.0, self.p2); } (t_best, r_best.unwrap()) } } impl ParamCurveCurvature for QuadBez {} impl ParamCurveExtrema for QuadBez { fn extrema(&self) -> ArrayVec<[f64; MAX_EXTREMA]> { let mut result = ArrayVec::new(); let d0 = self.p1 - self.p0; let d1 = self.p2 - self.p1; let dd = d1 - d0; if dd.x != 0.0 { let t = -d0.x / dd.x; if t > 0.0 && t < 1.0 { result.push(t); } } if dd.y != 0.0 { let t = -d0.y / dd.y; if t > 0.0 && t < 1.0 { result.push(t); if result.len() == 2 && result[0] > t { result.swap(0, 1); } } } result } } impl Mul for Affine { type Output = QuadBez; #[inline] fn mul(self, other: QuadBez) -> QuadBez { QuadBez { p0: self * other.p0, p1: self * other.p1, p2: self * other.p2, } } } #[cfg(test)] mod tests { use crate::{ Affine, ParamCurve, ParamCurveArclen, ParamCurveArea, ParamCurveDeriv, ParamCurveExtrema, ParamCurveNearest, Point, QuadBez, }; fn assert_near(p0: Point, p1: Point, epsilon: f64) { assert!((p1 - p0).hypot() < epsilon, "{:?} != {:?}", p0, p1); } #[test] fn quadbez_deriv() { let q = QuadBez::new((0.0, 0.0), (0.0, 0.5), (1.0, 1.0)); let deriv = q.deriv(); let n = 10; for i in 0..=n { let t = (i as f64) * (n as f64).recip(); let delta = 1e-6; let p = q.eval(t); let p1 = q.eval(t + delta); let d_approx = (p1 - p) * delta.recip(); let d = deriv.eval(t).to_vec2(); assert!((d - d_approx).hypot() < delta * 2.0); } } #[test] fn quadbez_arclen() { let q = QuadBez::new((0.0, 0.0), (0.0, 0.5), (1.0, 1.0)); let true_arclen = 0.5 * 5.0f64.sqrt() + 0.25 * (2.0 + 5.0f64.sqrt()).ln(); for i in 0..12 { let accuracy = 0.1f64.powi(i); let est = q.arclen(accuracy); let error = est - true_arclen; assert!(error.abs() < accuracy, "{} != {}", est, true_arclen); } } #[test] fn quadbez_arclen_pathological() { let q = QuadBez::new((-1.0, 0.0), (1.03, 0.0), (1.0, 0.0)); let true_arclen = 2.0008737864167325; // A rough empirical calculation let accuracy = 1e-11; let est = q.arclen(accuracy); assert!( (est - true_arclen).abs() < accuracy, "{} != {}", est, true_arclen ); } #[test] fn quadbez_subsegment() { let q = QuadBez::new((3.1, 4.1), (5.9, 2.6), (5.3, 5.8)); let t0 = 0.1; let t1 = 0.8; let qs = q.subsegment(t0..t1); let epsilon = 1e-12; let n = 10; for i in 0..=n { let t = (i as f64) * (n as f64).recip(); let ts = t0 + t * (t1 - t0); assert_near(q.eval(ts), qs.eval(t), epsilon); } } #[test] fn quadbez_raise() { let q = QuadBez::new((3.1, 4.1), (5.9, 2.6), (5.3, 5.8)); let c = q.raise(); let qd = q.deriv(); let cd = c.deriv(); let epsilon = 1e-12; let n = 10; for i in 0..=n { let t = (i as f64) * (n as f64).recip(); assert_near(q.eval(t), c.eval(t), epsilon); assert_near(qd.eval(t), cd.eval(t), epsilon); } } #[test] fn quadbez_signed_area() { // y = 1 - x^2 let q = QuadBez::new((1.0, 0.0), (0.5, 1.0), (0.0, 1.0)); let epsilon = 1e-12; assert!((q.signed_area() - 2.0 / 3.0).abs() < epsilon); assert!(((Affine::rotate(0.5) * q).signed_area() - 2.0 / 3.0).abs() < epsilon); assert!(((Affine::translate((0.0, 1.0)) * q).signed_area() - 3.5 / 3.0).abs() < epsilon); assert!(((Affine::translate((1.0, 0.0)) * q).signed_area() - 3.5 / 3.0).abs() < epsilon); } #[test] fn quadbez_nearest() { fn verify(result: (f64, f64), expected: f64) { assert!( (result.0 - expected).abs() < 1e-6, "got {:?} expected {}", result, expected ); } // y = x^2 let q = QuadBez::new((-1.0, 1.0), (0.0, -1.0), (1.0, 1.0)); verify(q.nearest((0.0, 0.0).into(), 1e-3), 0.5); verify(q.nearest((0.0, 0.1).into(), 1e-3), 0.5); verify(q.nearest((0.0, -0.1).into(), 1e-3), 0.5); verify(q.nearest((0.5, 0.25).into(), 1e-3), 0.75); verify(q.nearest((1.0, 1.0).into(), 1e-3), 1.0); verify(q.nearest((1.1, 1.1).into(), 1e-3), 1.0); verify(q.nearest((-1.1, 1.1).into(), 1e-3), 0.0); let a = Affine::rotate(0.5); verify((a * q).nearest(a * Point::new(0.5, 0.25), 1e-3), 0.75); } // This test exposes a degenerate case in the solver used internally // by the "nearest" calculation - the cubic term is zero. #[test] fn quadbez_nearest_low_order() { fn verify(result: (f64, f64), expected: f64) { assert!( (result.0 - expected).abs() < 1e-6, "got {:?} expected {}", result, expected ); } let q = QuadBez::new((-1.0, 0.0), (0.0, 0.0), (1.0, 0.0)); verify(q.nearest((0.0, 0.0).into(), 1e-3), 0.5); verify(q.nearest((0.0, 1.0).into(), 1e-3), 0.5); } #[test] fn quadbez_extrema() { // y = x^2 let q = QuadBez::new((-1.0, 1.0), (0.0, -1.0), (1.0, 1.0)); let extrema = q.extrema(); assert_eq!(extrema.len(), 1); assert!((extrema[0] - 0.5).abs() < 1e-6); let q = QuadBez::new((0.0, 0.5), (1.0, 1.0), (0.5, 0.0)); let extrema = q.extrema(); assert_eq!(extrema.len(), 2); assert!((extrema[0] - 1.0 / 3.0).abs() < 1e-6); assert!((extrema[1] - 2.0 / 3.0).abs() < 1e-6); // Reverse direction let q = QuadBez::new((0.5, 0.0), (1.0, 1.0), (0.0, 0.5)); let extrema = q.extrema(); assert_eq!(extrema.len(), 2); assert!((extrema[0] - 1.0 / 3.0).abs() < 1e-6); assert!((extrema[1] - 2.0 / 3.0).abs() < 1e-6); } } kurbo-0.7.1/src/rect.rs010064400007650000024000000532371375053465700131430ustar 00000000000000//! A rectangle. use std::fmt; use std::ops::{Add, Sub}; use crate::{Ellipse, Insets, PathEl, Point, RoundedRect, Shape, Size, Vec2}; /// A rectangle. #[derive(Clone, Copy, Default, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Rect { /// The minimum x coordinate (left edge). pub x0: f64, /// The minimum y coordinate (top edge in y-down spaces). pub y0: f64, /// The maximum x coordinate (right edge). pub x1: f64, /// The maximum y coordinate (bottom edge in y-down spaces). pub y1: f64, } impl Rect { /// The empty rectangle at the origin. pub const ZERO: Rect = Rect::new(0., 0., 0., 0.); /// A new rectangle from minimum and maximum coordinates. #[inline] pub const fn new(x0: f64, y0: f64, x1: f64, y1: f64) -> Rect { Rect { x0, y0, x1, y1 } } /// A new rectangle from two points. /// /// The result will have non-negative width and height. #[inline] pub fn from_points(p0: impl Into, p1: impl Into) -> Rect { let p0 = p0.into(); let p1 = p1.into(); Rect::new(p0.x, p0.y, p1.x, p1.y).abs() } /// A new rectangle from origin and size. /// /// The result will have non-negative width and height. #[inline] pub fn from_origin_size(origin: impl Into, size: impl Into) -> Rect { let origin = origin.into(); Rect::from_points(origin, origin + size.into().to_vec2()) } /// A new rectangle from center and size. #[inline] pub fn from_center_size(center: impl Into, size: impl Into) -> Rect { let center = center.into(); let size = 0.5 * size.into(); Rect::new( center.x - size.width, center.y - size.height, center.x + size.width, center.y + size.height, ) } /// Create a new `Rect` with the same size as `self` and a new origin. #[inline] pub fn with_origin(self, origin: impl Into) -> Rect { Rect::from_origin_size(origin, self.size()) } /// Create a new `Rect` with the same origin as `self` and a new size. #[inline] pub fn with_size(self, size: impl Into) -> Rect { Rect::from_origin_size(self.origin(), size) } /// Create a new `Rect` by applying the [`Insets`]. /// /// This will not preserve negative width and height. /// /// # Examples /// /// ``` /// use kurbo::Rect; /// let inset_rect = Rect::new(0., 0., 10., 10.,).inset(2.); /// assert_eq!(inset_rect.width(), 14.0); /// assert_eq!(inset_rect.x0, -2.0); /// assert_eq!(inset_rect.x1, 12.0); /// ``` /// /// [`Insets`]: struct.Insets.html #[inline] pub fn inset(self, insets: impl Into) -> Rect { self + insets.into() } /// The width of the rectangle. /// /// Note: nothing forbids negative width. #[inline] pub fn width(&self) -> f64 { self.x1 - self.x0 } /// The height of the rectangle. /// /// Note: nothing forbids negative height. #[inline] pub fn height(&self) -> f64 { self.y1 - self.y0 } /// Returns the minimum value for the x-coordinate of the rectangle. #[inline] pub fn min_x(&self) -> f64 { self.x0.min(self.x1) } /// Returns the maximum value for the x-coordinate of the rectangle. #[inline] pub fn max_x(&self) -> f64 { self.x0.max(self.x1) } /// Returns the minimum value for the y-coordinate of the rectangle. #[inline] pub fn min_y(&self) -> f64 { self.y0.min(self.y1) } /// Returns the maximum value for the y-coordinate of the rectangle. #[inline] pub fn max_y(&self) -> f64 { self.y0.max(self.y1) } /// The origin of the rectangle. /// /// This is the top left corner in a y-down space and with /// non-negative width and height. #[inline] pub fn origin(&self) -> Point { Point::new(self.x0, self.y0) } /// The size of the rectangle. #[inline] pub fn size(&self) -> Size { Size::new(self.width(), self.height()) } /// The area of the rectangle. #[inline] pub fn area(&self) -> f64 { self.width() * self.height() } /// Whether this rectangle has zero area. /// /// Note: a rectangle with negative area is not considered empty. #[inline] pub fn is_empty(&self) -> bool { self.area() == 0.0 } /// The center point of the rectangle. #[inline] pub fn center(&self) -> Point { Point::new(0.5 * (self.x0 + self.x1), 0.5 * (self.y0 + self.y1)) } /// Returns `true` if `point` lies within `self`. #[inline] pub fn contains(&self, point: Point) -> bool { point.x >= self.x0 && point.x < self.x1 && point.y >= self.y0 && point.y < self.y1 } /// Take absolute value of width and height. /// /// The resulting rect has the same extents as the original, but is /// guaranteed to have non-negative width and height. #[inline] pub fn abs(&self) -> Rect { let Rect { x0, y0, x1, y1 } = *self; Rect::new(x0.min(x1), y0.min(y1), x0.max(x1), y0.max(y1)) } /// The smallest rectangle enclosing two rectangles. /// /// Results are valid only if width and height are non-negative. #[inline] pub fn union(&self, other: Rect) -> Rect { Rect::new( self.x0.min(other.x0), self.y0.min(other.y0), self.x1.max(other.x1), self.y1.max(other.y1), ) } /// Compute the union with one point. /// /// This method includes the perimeter of zero-area rectangles. /// Thus, a succession of `union_pt` operations on a series of /// points yields their enclosing rectangle. /// /// Results are valid only if width and height are non-negative. pub fn union_pt(&self, pt: Point) -> Rect { Rect::new( self.x0.min(pt.x), self.y0.min(pt.y), self.x1.max(pt.x), self.y1.max(pt.y), ) } /// The intersection of two rectangles. /// /// The result is zero-area if either input has negative width or /// height. The result always has non-negative width and height. #[inline] pub fn intersect(&self, other: Rect) -> Rect { let x0 = self.x0.max(other.x0); let y0 = self.y0.max(other.y0); let x1 = self.x1.min(other.x1); let y1 = self.y1.min(other.y1); Rect::new(x0, y0, x1.max(x0), y1.max(y0)) } /// Expand a rectangle by a constant amount in both directions. /// /// The logic simply applies the amount in each direction. If rectangle /// area or added dimensions are negative, this could give odd results. pub fn inflate(&self, width: f64, height: f64) -> Rect { Rect::new( self.x0 - width, self.y0 - height, self.x1 + width, self.y1 + height, ) } /// Returns a new `Rect`, /// with each coordinate value rounded to the nearest integer. /// /// # Examples /// /// ``` /// use kurbo::Rect; /// let rect = Rect::new(3.3, 3.6, 3.0, -3.1).round(); /// assert_eq!(rect.x0, 3.0); /// assert_eq!(rect.y0, 4.0); /// assert_eq!(rect.x1, 3.0); /// assert_eq!(rect.y1, -3.0); /// ``` #[inline] pub fn round(self) -> Rect { Rect::new( self.x0.round(), self.y0.round(), self.x1.round(), self.y1.round(), ) } /// Returns a new `Rect`, /// with each coordinate value rounded up to the nearest integer, /// unless they are already an integer. /// /// # Examples /// /// ``` /// use kurbo::Rect; /// let rect = Rect::new(3.3, 3.6, 3.0, -3.1).ceil(); /// assert_eq!(rect.x0, 4.0); /// assert_eq!(rect.y0, 4.0); /// assert_eq!(rect.x1, 3.0); /// assert_eq!(rect.y1, -3.0); /// ``` #[inline] pub fn ceil(self) -> Rect { Rect::new( self.x0.ceil(), self.y0.ceil(), self.x1.ceil(), self.y1.ceil(), ) } /// Returns a new `Rect`, /// with each coordinate value rounded down to the nearest integer, /// unless they are already an integer. /// /// # Examples /// /// ``` /// use kurbo::Rect; /// let rect = Rect::new(3.3, 3.6, 3.0, -3.1).floor(); /// assert_eq!(rect.x0, 3.0); /// assert_eq!(rect.y0, 3.0); /// assert_eq!(rect.x1, 3.0); /// assert_eq!(rect.y1, -4.0); /// ``` #[inline] pub fn floor(self) -> Rect { Rect::new( self.x0.floor(), self.y0.floor(), self.x1.floor(), self.y1.floor(), ) } /// Returns a new `Rect`, /// with each coordinate value rounded away from the center of the `Rect` /// to the nearest integer, unless they are already an integer. /// That is to say this function will return the smallest possible `Rect` /// with integer coordinates that is a superset of `self`. /// /// # Examples /// /// ``` /// use kurbo::Rect; /// /// // In positive space /// let rect = Rect::new(3.3, 3.6, 5.6, 4.1).expand(); /// assert_eq!(rect.x0, 3.0); /// assert_eq!(rect.y0, 3.0); /// assert_eq!(rect.x1, 6.0); /// assert_eq!(rect.y1, 5.0); /// /// // In both positive and negative space /// let rect = Rect::new(-3.3, -3.6, 5.6, 4.1).expand(); /// assert_eq!(rect.x0, -4.0); /// assert_eq!(rect.y0, -4.0); /// assert_eq!(rect.x1, 6.0); /// assert_eq!(rect.y1, 5.0); /// /// // In negative space /// let rect = Rect::new(-5.6, -4.1, -3.3, -3.6).expand(); /// assert_eq!(rect.x0, -6.0); /// assert_eq!(rect.y0, -5.0); /// assert_eq!(rect.x1, -3.0); /// assert_eq!(rect.y1, -3.0); /// /// // Inverse orientation /// let rect = Rect::new(5.6, -3.6, 3.3, -4.1).expand(); /// assert_eq!(rect.x0, 6.0); /// assert_eq!(rect.y0, -3.0); /// assert_eq!(rect.x1, 3.0); /// assert_eq!(rect.y1, -5.0); /// ``` #[inline] pub fn expand(self) -> Rect { // The compiler optimizer will remove the if branching. let (x0, x1) = if self.x0 < self.x1 { (self.x0.floor(), self.x1.ceil()) } else { (self.x0.ceil(), self.x1.floor()) }; let (y0, y1) = if self.y0 < self.y1 { (self.y0.floor(), self.y1.ceil()) } else { (self.y0.ceil(), self.y1.floor()) }; Rect::new(x0, y0, x1, y1) } /// Returns a new `Rect`, /// with each coordinate value rounded towards the center of the `Rect` /// to the nearest integer, unless they are already an integer. /// That is to say this function will return the biggest possible `Rect` /// with integer coordinates that is a subset of `self`. /// /// # Examples /// /// ``` /// use kurbo::Rect; /// /// // In positive space /// let rect = Rect::new(3.3, 3.6, 5.6, 4.1).trunc(); /// assert_eq!(rect.x0, 4.0); /// assert_eq!(rect.y0, 4.0); /// assert_eq!(rect.x1, 5.0); /// assert_eq!(rect.y1, 4.0); /// /// // In both positive and negative space /// let rect = Rect::new(-3.3, -3.6, 5.6, 4.1).trunc(); /// assert_eq!(rect.x0, -3.0); /// assert_eq!(rect.y0, -3.0); /// assert_eq!(rect.x1, 5.0); /// assert_eq!(rect.y1, 4.0); /// /// // In negative space /// let rect = Rect::new(-5.6, -4.1, -3.3, -3.6).trunc(); /// assert_eq!(rect.x0, -5.0); /// assert_eq!(rect.y0, -4.0); /// assert_eq!(rect.x1, -4.0); /// assert_eq!(rect.y1, -4.0); /// /// // Inverse orientation /// let rect = Rect::new(5.6, -3.6, 3.3, -4.1).trunc(); /// assert_eq!(rect.x0, 5.0); /// assert_eq!(rect.y0, -4.0); /// assert_eq!(rect.x1, 4.0); /// assert_eq!(rect.y1, -4.0); /// ``` #[inline] pub fn trunc(self) -> Rect { // The compiler optimizer will remove the if branching. let (x0, x1) = if self.x0 < self.x1 { (self.x0.ceil(), self.x1.floor()) } else { (self.x0.floor(), self.x1.ceil()) }; let (y0, y1) = if self.y0 < self.y1 { (self.y0.ceil(), self.y1.floor()) } else { (self.y0.floor(), self.y1.ceil()) }; Rect::new(x0, y0, x1, y1) } /// Creates a new [`RoundedRect`] from this `Rect` and the provided /// corner radius. /// /// [`RoundedRect`]: struct.RoundedRect.html #[inline] pub fn to_rounded_rect(self, radius: f64) -> RoundedRect { RoundedRect::from_rect(self, radius) } /// Returns the [`Ellipse`] that is bounded by this `Rect`. /// /// [`Ellipse`]: struct.Ellipse.html #[inline] pub fn to_ellipse(self) -> Ellipse { Ellipse::from_rect(self) } /// The aspect ratio of the `Rect`. /// /// This is defined as the height divided by the width. It measures the /// "squareness" of the rectangle (a value of `1` is square). /// /// If the width is `0` the output will be `sign(y1 - y0) * infinity`. /// /// If The width and height are `0`, the result will be `NaN`. #[inline] pub fn aspect_ratio(&self) -> f64 { self.size().aspect_ratio() } /// Returns the largest possible `Rect` that is fully contained in `self` /// with the given `aspect_ratio`. /// /// The aspect ratio is specified fractionally, as `height / width`. /// /// The resulting rectangle will be centered if it is smaller than the /// input rectangle. /// /// For the special case where the aspect ratio is `1.0`, the resulting /// `Rect` will be square. /// /// # Examples /// /// ``` /// # use kurbo::Rect; /// let outer = Rect::new(0.0, 0.0, 10.0, 20.0); /// let inner = outer.contained_rect_with_aspect_ratio(1.0); /// // The new `Rect` is a square centered at the center of `outer`. /// assert_eq!(inner, Rect::new(0.0, 5.0, 10.0, 15.0)); /// ``` /// pub fn contained_rect_with_aspect_ratio(&self, aspect_ratio: f64) -> Rect { let (width, height) = (self.width(), self.height()); let self_aspect = height / width; // TODO the parameter `1e-9` was chosen quickly and may not be optimal. if (self_aspect - aspect_ratio).abs() < 1e-9 { // short circuit *self } else if self_aspect.abs() < aspect_ratio.abs() { // shrink x to fit let new_width = height * aspect_ratio.recip(); let gap = (width - new_width) * 0.5; let x0 = self.x0 + gap; let x1 = self.x1 - gap; Rect::new(x0, self.y0, x1, self.y1) } else { // shrink y to fit let new_height = width * aspect_ratio; let gap = (height - new_height) * 0.5; let y0 = self.y0 + gap; let y1 = self.y1 - gap; Rect::new(self.x0, y0, self.x1, y1) } } } impl From<(Point, Point)> for Rect { fn from(points: (Point, Point)) -> Rect { Rect::from_points(points.0, points.1) } } impl From<(Point, Size)> for Rect { fn from(params: (Point, Size)) -> Rect { Rect::from_origin_size(params.0, params.1) } } impl Add for Rect { type Output = Rect; #[inline] fn add(self, v: Vec2) -> Rect { Rect::new(self.x0 + v.x, self.y0 + v.y, self.x1 + v.x, self.y1 + v.y) } } impl Sub for Rect { type Output = Rect; #[inline] fn sub(self, v: Vec2) -> Rect { Rect::new(self.x0 - v.x, self.y0 - v.y, self.x1 - v.x, self.y1 - v.y) } } impl Sub for Rect { type Output = Insets; #[inline] fn sub(self, other: Rect) -> Insets { let x0 = other.x0 - self.x0; let y0 = other.y0 - self.y0; let x1 = self.x1 - other.x1; let y1 = self.y1 - other.y1; Insets { x0, y0, x1, y1 } } } #[doc(hidden)] pub struct RectPathIter { rect: Rect, ix: usize, } impl Shape for Rect { type PathElementsIter = RectPathIter; fn path_elements(&self, _tolerance: f64) -> RectPathIter { RectPathIter { rect: *self, ix: 0 } } // It's a bit of duplication having both this and the impl method, but // removing that would require using the trait. We'll leave it for now. #[inline] fn area(&self) -> f64 { Rect::area(self) } #[inline] fn perimeter(&self, _accuracy: f64) -> f64 { 2.0 * (self.width().abs() + self.height().abs()) } /// Note: this function is carefully designed so that if the plane is /// tiled with rectangles, the winding number will be nonzero for exactly /// one of them. #[inline] fn winding(&self, pt: Point) -> i32 { let xmin = self.x0.min(self.x1); let xmax = self.x0.max(self.x1); let ymin = self.y0.min(self.y1); let ymax = self.y0.max(self.y1); if pt.x >= xmin && pt.x < xmax && pt.y >= ymin && pt.y < ymax { if (self.x1 > self.x0) ^ (self.y1 > self.y0) { -1 } else { 1 } } else { 0 } } #[inline] fn bounding_box(&self) -> Rect { self.abs() } #[inline] fn as_rect(&self) -> Option { Some(*self) } } // This is clockwise in a y-down coordinate system for positive area. impl Iterator for RectPathIter { type Item = PathEl; fn next(&mut self) -> Option { self.ix += 1; match self.ix { 1 => Some(PathEl::MoveTo(Point::new(self.rect.x0, self.rect.y0))), 2 => Some(PathEl::LineTo(Point::new(self.rect.x1, self.rect.y0))), 3 => Some(PathEl::LineTo(Point::new(self.rect.x1, self.rect.y1))), 4 => Some(PathEl::LineTo(Point::new(self.rect.x0, self.rect.y1))), 5 => Some(PathEl::ClosePath), _ => None, } } } impl fmt::Debug for Rect { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { if f.alternate() { write!( f, "Rect {{ origin: {:?}, size: {:?} }}", self.origin(), self.size() ) } else { write!( f, "Rect {{ x0: {:?}, y0: {:?}, x1: {:?}, y1: {:?} }}", self.x0, self.y0, self.x1, self.y1 ) } } } impl fmt::Display for Rect { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "Rect {{ ")?; fmt::Display::fmt(&self.origin(), f)?; write!(f, " ")?; fmt::Display::fmt(&self.size(), f)?; write!(f, " }}") } } #[cfg(test)] mod tests { use crate::{Point, Rect, Shape}; fn assert_approx_eq(x: f64, y: f64) { assert!((x - y).abs() < 1e-7); } #[test] fn area_sign() { let r = Rect::new(0.0, 0.0, 10.0, 10.0); let center = r.center(); assert_approx_eq(r.area(), 100.0); assert_eq!(r.winding(center), 1); let p = r.to_path(1e-9); assert_approx_eq(r.area(), p.area()); assert_eq!(r.winding(center), p.winding(center)); let r_flip = Rect::new(0.0, 10.0, 10.0, 0.0); assert_approx_eq(r_flip.area(), -100.0); assert_eq!(r_flip.winding(Point::new(5.0, 5.0)), -1); let p_flip = r_flip.to_path(1e-9); assert_approx_eq(r_flip.area(), p_flip.area()); assert_eq!(r_flip.winding(center), p_flip.winding(center)); } #[test] fn display() { let r = Rect::from_origin_size((10., 12.23214), (22.222222222, 23.1)); assert_eq!( format!("{}", r), "Rect { (10, 12.23214) (22.222222222×23.1) }" ); assert_eq!(format!("{:.2}", r), "Rect { (10.00, 12.23) (22.22×23.10) }"); } /* TODO uncomment when a (possibly approximate) equality test has been decided on #[test] fn rect_from_center_size() { assert_eq!( Rect::from_center_size(Point::new(3.0, 2.0), Size::new(2.0, 4.0)), Rect::new(2.0, 0.0, 4.0, 4.0) ); } */ #[test] fn contained_rect_with_aspect_ratio() { use std::f64; fn case(outer: [f64; 4], aspect_ratio: f64, expected: [f64; 4]) { let outer = Rect::new(outer[0], outer[1], outer[2], outer[3]); let expected = Rect::new(expected[0], expected[1], expected[2], expected[3]); assert_eq!( outer.contained_rect_with_aspect_ratio(aspect_ratio), expected ); } // squares (different point orderings) case([0.0, 0.0, 10.0, 20.0], 1.0, [0.0, 5.0, 10.0, 15.0]); case([0.0, 20.0, 10.0, 0.0], 1.0, [0.0, 5.0, 10.0, 15.0]); case([10.0, 0.0, 0.0, 20.0], 1.0, [10.0, 15.0, 0.0, 5.0]); case([10.0, 20.0, 0.0, 0.0], 1.0, [10.0, 15.0, 0.0, 5.0]); // non-square case([0.0, 0.0, 10.0, 20.0], 0.5, [0.0, 7.5, 10.0, 12.5]); // same aspect ratio case([0.0, 0.0, 10.0, 20.0], 2.0, [0.0, 0.0, 10.0, 20.0]); // negative aspect ratio case([0.0, 0.0, 10.0, 20.0], -1.0, [0.0, 15.0, 10.0, 5.0]); // infinite aspect ratio case([0.0, 0.0, 10.0, 20.0], f64::INFINITY, [5.0, 0.0, 5.0, 20.0]); // zero aspect ratio case([0.0, 0.0, 10.0, 20.0], 0.0, [0.0, 10.0, 10.0, 10.0]); // zero width rect case([0.0, 0.0, 0.0, 20.0], 1.0, [0.0, 10.0, 0.0, 10.0]); // many zeros case([0.0, 0.0, 0.0, 20.0], 0.0, [0.0, 10.0, 0.0, 10.0]); // everything zero case([0.0, 0.0, 0.0, 0.0], 0.0, [0.0, 0.0, 0.0, 0.0]); } #[test] fn aspect_ratio() { let test = Rect::new(0.0, 0.0, 1.0, 1.0); assert!((test.aspect_ratio() - 1.0).abs() < 1e-6); } } kurbo-0.7.1/src/rounded_rect.rs010064400007650000024000000232541374161070200146410ustar 00000000000000//! A rectangle with rounded corners. use crate::{arc::ArcAppendIter, Arc, PathEl, Point, Rect, Shape, Size, Vec2}; use std::f64::consts::{FRAC_PI_2, PI}; /// A rectangle with equally rounded corners. /// /// By construction the rounded rectangle will have /// non-negative dimensions and radius clamped to half size of the rect. /// /// The easiest way to create a `RoundedRect` is often to create a [`Rect`], /// and then call [`to_rounded_rect`]. /// /// [`Rect`]: struct.Rect.html /// [`to_rounded_rect`]: struct.Rect.html#method.to_rounded_rect #[derive(Clone, Copy, Default, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct RoundedRect { /// Coordinates of the rectangle. rect: Rect, /// Radius of all four corners. radius: f64, } impl RoundedRect { /// A new rectangle from minimum and maximum coordinates. /// /// The result will have non-negative width, height and radius. #[inline] pub fn new(x0: f64, y0: f64, x1: f64, y1: f64, radius: f64) -> RoundedRect { RoundedRect::from_rect(Rect::new(x0, y0, x1, y1), radius) } /// A new rounded rectangle from a rectangle and corner radius. /// /// The result will have non-negative width, height and radius. /// /// See also [`Rect::to_rounded_rect`], which offers the same utility. /// /// [`Rect::to_rounded_rect`]: struct.Rect.html#method.to_rounded_rect #[inline] pub fn from_rect(rect: Rect, radius: f64) -> RoundedRect { let rect = rect.abs(); let radius = radius .abs() .min(rect.width() / 2.0) .min(rect.height() / 2.0); RoundedRect { rect, radius } } /// A new rectangle from two [`Point`]s. /// /// The result will have non-negative width, height and radius. /// /// [`Point`]: struct.Point.html #[inline] pub fn from_points(p0: impl Into, p1: impl Into, radius: f64) -> RoundedRect { Rect::from_points(p0, p1).to_rounded_rect(radius) } /// A new rectangle from origin and size. /// /// The result will have non-negative width, height and radius. #[inline] pub fn from_origin_size( origin: impl Into, size: impl Into, radius: f64, ) -> RoundedRect { Rect::from_origin_size(origin, size).to_rounded_rect(radius) } /// The width of the rectangle. #[inline] pub fn width(&self) -> f64 { self.rect.width() } /// The height of the rectangle. #[inline] pub fn height(&self) -> f64 { self.rect.height() } /// Radius of the rounded corners. #[inline] pub fn radius(&self) -> f64 { self.radius } /// The (non-rounded) rectangle. pub fn rect(&self) -> Rect { self.rect } /// The origin of the rectangle. /// /// This is the top left corner in a y-down space. #[inline] pub fn origin(&self) -> Point { self.rect.origin() } /// The center point of the rectangle. #[inline] pub fn center(&self) -> Point { self.rect.center() } } #[doc(hidden)] pub struct RoundedRectPathIter { idx: usize, rect: RectPathIter, arcs: [ArcAppendIter; 4], } impl Shape for RoundedRect { type PathElementsIter = RoundedRectPathIter; fn path_elements(&self, tolerance: f64) -> RoundedRectPathIter { let radius = self.radius(); let radii = Vec2 { x: self.radius, y: self.radius, }; let build_arc_iter = |i, center| { let arc = Arc { center, radii, start_angle: FRAC_PI_2 * i as f64, sweep_angle: FRAC_PI_2, x_rotation: 0.0, }; arc.append_iter(tolerance) }; // Note: order follows the rectangle path iterator. let arcs = [ build_arc_iter( 2, Point { x: self.rect.x0 + radius, y: self.rect.y0 + radius, }, ), build_arc_iter( 3, Point { x: self.rect.x1 - radius, y: self.rect.y0 + radius, }, ), build_arc_iter( 0, Point { x: self.rect.x1 - radius, y: self.rect.y1 - radius, }, ), build_arc_iter( 1, Point { x: self.rect.x0 + radius, y: self.rect.y1 - radius, }, ), ]; let rect = RectPathIter { rect: self.rect, ix: 0, radius: self.radius, }; RoundedRectPathIter { idx: 0, rect, arcs } } #[inline] fn area(&self) -> f64 { let radius = self.radius(); self.rect.area() - (4.0 - PI) * radius * radius } #[inline] fn perimeter(&self, _accuracy: f64) -> f64 { let radius = self.radius(); 2.0 * (self.width() + self.height()) - 8.0 * radius + 2.0 * PI * radius } #[inline] fn winding(&self, mut pt: Point) -> i32 { // The rounded rectangle can be seen as minkowski sum of an inner rectangle // and circle specified by the radius of the corners. let center = self.center(); let radius = self.radius(); let inside_half_width = (self.width() / 2.0 - radius).max(0.0); let inside_half_height = (self.height() / 2.0 - radius).max(0.0); // 1. Translate the point relative to the center of the rectangle. pt.x -= center.x; pt.y -= center.y; // 2. Project point out of the inner rectangle (positive quadrant) // This basically 'substracts' the inner rectangle. let px = (pt.x.abs() - inside_half_width).max(0.0); let py = (pt.y.abs() - inside_half_height).max(0.0); // 3. The test reduced to calculate the winding of the circle. let inside = px * px + py * py <= radius * radius; if inside { 1 } else { 0 } } #[inline] fn bounding_box(&self) -> Rect { self.rect.bounding_box() } #[inline] fn as_rounded_rect(&self) -> Option { Some(*self) } } struct RectPathIter { rect: Rect, radius: f64, ix: usize, } // This is clockwise in a y-down coordinate system for positive area. impl Iterator for RectPathIter { type Item = PathEl; fn next(&mut self) -> Option { self.ix += 1; match self.ix { 1 => Some(PathEl::MoveTo(Point::new( self.rect.x0, self.rect.y0 + self.radius, ))), 2 => Some(PathEl::LineTo(Point::new( self.rect.x1 - self.radius, self.rect.y0, ))), 3 => Some(PathEl::LineTo(Point::new( self.rect.x1, self.rect.y1 - self.radius, ))), 4 => Some(PathEl::LineTo(Point::new( self.rect.x0 + self.radius, self.rect.y1, ))), 5 => Some(PathEl::ClosePath), _ => None, } } } // This is clockwise in a y-down coordinate system for positive area. impl Iterator for RoundedRectPathIter { type Item = PathEl; fn next(&mut self) -> Option { if self.idx > 4 { return None; } // Iterate between rectangle and arc iterators. // Rect iterator will start and end the path. // Initial point set by the rect iterator if self.idx == 0 { self.idx += 1; return self.rect.next(); } // Generate the arc curve elements. // If we reached the end of the arc, add a line towards next arc (rect iterator). match self.arcs[self.idx - 1].next() { Some(elem) => Some(elem), None => { self.idx += 1; self.rect.next() } } } } #[cfg(test)] mod tests { use crate::{Circle, Point, Rect, RoundedRect, Shape}; #[test] fn area() { let epsilon = 1e-9; // Extremum: 0.0 radius corner -> rectangle let rect = Rect::new(0.0, 0.0, 100.0, 100.0); let rounded_rect = RoundedRect::new(0.0, 0.0, 100.0, 100.0, 0.0); assert!((rect.area() - rounded_rect.area()).abs() < epsilon); // Extremum: half-size radius corner -> circle let circle = Circle::new((0.0, 0.0), 50.0); let rounded_rect = RoundedRect::new(0.0, 0.0, 100.0, 100.0, 50.0); assert!((circle.area() - rounded_rect.area()).abs() < epsilon); } #[test] fn winding() { let rect = RoundedRect::new(-5.0, -5.0, 10.0, 20.0, 5.0); assert_eq!(rect.winding(Point::new(0.0, 0.0)), 1); assert_eq!(rect.winding(Point::new(-5.0, 0.0)), 1); // left edge assert_eq!(rect.winding(Point::new(0.0, 20.0)), 1); // bottom edge assert_eq!(rect.winding(Point::new(10.0, 20.0)), 0); // bottom-right corner assert_eq!(rect.winding(Point::new(-10.0, 0.0)), 0); let rect = RoundedRect::new(-10.0, -20.0, 10.0, 20.0, 0.0); // rectangle assert_eq!(rect.winding(Point::new(10.0, 20.0)), 1); // bottom-right corner } #[test] fn bez_conversion() { let rect = RoundedRect::new(-5.0, -5.0, 10.0, 20.0, 5.0); let p = rect.to_path(1e-9); // Note: could be more systematic about tolerance tightness. let epsilon = 1e-7; assert!((rect.area() - p.area()).abs() < epsilon); assert_eq!(p.winding(Point::new(0.0, 0.0)), 1); } } kurbo-0.7.1/src/shape.rs010064400007650000024000000172451374554710500133010ustar 00000000000000//! A generic trait for shapes. use crate::{segments, BezPath, Circle, Line, PathEl, Point, Rect, RoundedRect, Segments}; /// A generic trait for open and closed shapes. /// /// This trait provides conversion from shapes to [`BezPath`]s, as well as /// general geometry functionality like computing [`area`], [`bounding_box`]es, /// and [`winding`] number. /// /// [`BezPath`]: struct.BezPath.html /// [`area`]: #tymethod.area /// [`bounding_box`]: #tymethod.bounding_box /// [`winding`]: #tymethod.winding pub trait Shape: Sized { /// The iterator returned by the [`path_elements`] method. /// /// [`path_elements`]: #tymethod.path_elements type PathElementsIter: Iterator; /// Returns an iterator over this shape expressed as [`PathEl`]s; /// that is, as Bézier path _elements_. /// /// All shapes can be represented as Béziers, but in many situations /// (such as when interfacing with a platform drawing API) there are more /// efficient native types for specific concrete shapes. In this case, /// the user should exhaust the `as_` methods ([`as_rect`], [`as_line`], etc) /// before converting to a [`BezPath`], as those are likely to be more /// efficient. /// /// In many cases, shapes are able to iterate their elements without /// allocating; however creating a a [`BezPath`] object always allocates. /// If you need an owned [`BezPath`] you can use [`to_path`] instead. /// /// # Tolerance /// /// The `tolerance` parameter controls the accuracy of /// conversion of geometric primitives to Bézier curves, as /// curves such as circles cannot be represented exactly but /// only approximated. For drawing as in UI elements, a value /// of 0.1 is appropriate, as it is unlikely to be visible to /// the eye. For scientific applications, a smaller value /// might be appropriate. Note that in general the number of /// cubic Bézier segments scales as `tolerance ^ (-1/6)`. /// /// TODO: When [GAT's] land, the type of this can be changed to /// contain a `&'a self` reference, which would let us take /// iterators from complex shapes without cloning. /// /// [GAT's]: https://github.com/rust-lang/rust/issues/44265 /// [`as_rect`]: #tymethod.as_rect /// [`as_line`]: #tymethod.as_line /// [`to_path`]: #tymethod.to_path fn path_elements(&self, tolerance: f64) -> Self::PathElementsIter; /// Convert to a Bézier path. /// /// This always allocates. It is appropriate when both the source /// shape and the resulting path are to be retained. /// /// If you only need to iterate the elements (such as to convert them to /// drawing commands for a given 2D graphics API) you should prefer /// [`path_elements`], which can avoid allocating where possible. /// /// The `tolerance` parameter is the same as for [`path_elements`]. /// /// [`path_elements`]: #tymethod.path_elements fn to_path(&self, tolerance: f64) -> BezPath { self.path_elements(tolerance).collect() } #[deprecated(since = "0.7.0", note = "Use path_elements instead")] #[doc(hidden)] fn to_bez_path(&self, tolerance: f64) -> Self::PathElementsIter { self.path_elements(tolerance) } /// Convert into a Bézier path. /// /// This allocates in the general case, but is zero-cost if the /// shape is already a [`BezPath`](struct.BezPath.html). /// /// The `tolerance` parameter is the same as for /// [`path_elements()`](#tymethod.path_elements). fn into_path(self, tolerance: f64) -> BezPath { self.to_path(tolerance) } #[deprecated(since = "0.7.0", note = "Use into_path instead")] #[doc(hidden)] fn into_bez_path(self, tolerance: f64) -> BezPath { self.into_path(tolerance) } /// Returns an iterator over this shape expressed as Bézier path /// _segments_ ([`PathSeg`]s). /// /// The allocation behaviour and `tolerance` parameter are the /// same as for [`path_elements()`](#tymethod.path_elements). /// /// [`PathSeg`]: enum.PathSeg.html fn path_segments(&self, tolerance: f64) -> Segments { segments(self.path_elements(tolerance)) } /// Signed area. /// /// This method only produces meaningful results with closed shapes. /// /// The convention for positive area is that y increases when x is /// positive. Thus, it is clockwise when down is increasing y (the /// usual convention for graphics), and anticlockwise when /// up is increasing y (the usual convention for math). fn area(&self) -> f64; /// Total length of perimeter. //FIXME: document the accuracy param fn perimeter(&self, accuracy: f64) -> f64; /// The [winding number] of a point. /// /// This method only produces meaningful results with closed shapes. /// /// The sign of the winding number is consistent with that of [`area`], /// meaning it is +1 when the point is inside a positive area shape /// and -1 when it is inside a negative area shape. Of course, greater /// magnitude values are also possible when the shape is more complex. /// /// [`area`]: #tymethod.area /// [winding number]: https://mathworld.wolfram.com/ContourWindingNumber.html fn winding(&self, pt: Point) -> i32; /// Returns `true` if the [`Point`] is inside this shape. /// /// This is only meaningful for closed shapes. /// /// [`Point`]: struct.Point.html fn contains(&self, pt: Point) -> bool { self.winding(pt) != 0 } /// The smallest rectangle that encloses the shape. fn bounding_box(&self) -> Rect; /// If the shape is a line, make it available. fn as_line(&self) -> Option { None } /// If the shape is a rectangle, make it available. fn as_rect(&self) -> Option { None } /// If the shape is a rounded rectangle, make it available. fn as_rounded_rect(&self) -> Option { None } /// If the shape is a circle, make it available. fn as_circle(&self) -> Option { None } /// If the shape is stored as a slice of path elements, make /// that available. /// /// Note: when GAT's land, a method like `path_elements` would be /// able to iterate through the slice with no extra allocation, /// without making any assumption that storage is contiguous. fn as_path_slice(&self) -> Option<&[PathEl]> { None } } /// Blanket implementation so `impl Shape` will accept owned or reference. impl<'a, T: Shape> Shape for &'a T { type PathElementsIter = T::PathElementsIter; fn path_elements(&self, tolerance: f64) -> Self::PathElementsIter { (*self).path_elements(tolerance) } fn to_path(&self, tolerance: f64) -> BezPath { (*self).to_path(tolerance) } fn path_segments(&self, tolerance: f64) -> Segments { (*self).path_segments(tolerance) } fn area(&self) -> f64 { (*self).area() } fn perimeter(&self, accuracy: f64) -> f64 { (*self).perimeter(accuracy) } fn winding(&self, pt: Point) -> i32 { (*self).winding(pt) } fn bounding_box(&self) -> Rect { (*self).bounding_box() } fn as_circle(&self) -> Option { (*self).as_circle() } fn as_line(&self) -> Option { (*self).as_line() } fn as_rect(&self) -> Option { (*self).as_rect() } fn as_rounded_rect(&self) -> Option { (*self).as_rounded_rect() } fn as_path_slice(&self) -> Option<&[PathEl]> { (*self).as_path_slice() } } kurbo-0.7.1/src/size.rs010064400007650000024000000222341374161070200131330ustar 00000000000000//! A 2D size. use std::fmt; use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Sub, SubAssign}; use crate::common::FloatExt; use crate::{Rect, RoundedRect, Vec2}; /// A 2D size. #[derive(Clone, Copy, Default, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Size { /// The width. pub width: f64, /// The height. pub height: f64, } impl Size { /// A size with zero width or height. pub const ZERO: Size = Size::new(0., 0.); /// Create a new `Size` with the provided `width` and `height`. #[inline] pub const fn new(width: f64, height: f64) -> Self { Size { width, height } } /// Returns the max of `width` and `height`. /// /// # Examples /// /// ``` /// use kurbo::Size; /// let size = Size::new(-10.5, 42.0); /// assert_eq!(size.max_side(), 42.0); /// ``` pub fn max_side(self) -> f64 { self.width.max(self.height) } /// Returns the min of `width` and `height`. /// /// # Examples /// /// ``` /// use kurbo::Size; /// let size = Size::new(-10.5, 42.0); /// assert_eq!(size.min_side(), -10.5); /// ``` pub fn min_side(self) -> f64 { self.width.min(self.height) } /// The area covered by this size. #[inline] pub fn area(self) -> f64 { self.width * self.height } /// Whether this size has zero area. /// /// Note: a size with negative area is not considered empty. #[inline] pub fn is_empty(self) -> bool { self.area() == 0.0 } /// Returns a new size bounded by `min` and `max.` /// /// # Examples /// /// ``` /// use kurbo::Size; /// /// let this = Size::new(0., 100.); /// let min = Size::new(10., 10.,); /// let max = Size::new(50., 50.); /// assert_eq!(this.clamp(min, max), Size::new(10., 50.)) /// ``` pub fn clamp(self, min: Size, max: Size) -> Self { let width = self.width.max(min.width).min(max.width); let height = self.height.max(min.height).min(max.height); Size { width, height } } /// Convert this size into a [`Vec2`], with `width` mapped to `x` and `height` /// mapped to `y`. /// /// [`Vec2`]: struct.Vec2.html #[inline] pub const fn to_vec2(self) -> Vec2 { Vec2::new(self.width, self.height) } /// Returns a new `Size`, /// with `width` and `height` rounded to the nearest integer. /// /// # Examples /// /// ``` /// use kurbo::Size; /// let size_pos = Size::new(3.3, 3.6).round(); /// assert_eq!(size_pos.width, 3.0); /// assert_eq!(size_pos.height, 4.0); /// let size_neg = Size::new(-3.3, -3.6).round(); /// assert_eq!(size_neg.width, -3.0); /// assert_eq!(size_neg.height, -4.0); /// ``` #[inline] pub fn round(self) -> Size { Size::new(self.width.round(), self.height.round()) } /// Returns a new `Size`, /// with `width` and `height` rounded up to the nearest integer, /// unless they are already an integer. /// /// # Examples /// /// ``` /// use kurbo::Size; /// let size_pos = Size::new(3.3, 3.6).ceil(); /// assert_eq!(size_pos.width, 4.0); /// assert_eq!(size_pos.height, 4.0); /// let size_neg = Size::new(-3.3, -3.6).ceil(); /// assert_eq!(size_neg.width, -3.0); /// assert_eq!(size_neg.height, -3.0); /// ``` #[inline] pub fn ceil(self) -> Size { Size::new(self.width.ceil(), self.height.ceil()) } /// Returns a new `Size`, /// with `width` and `height` rounded down to the nearest integer, /// unless they are already an integer. /// /// # Examples /// /// ``` /// use kurbo::Size; /// let size_pos = Size::new(3.3, 3.6).floor(); /// assert_eq!(size_pos.width, 3.0); /// assert_eq!(size_pos.height, 3.0); /// let size_neg = Size::new(-3.3, -3.6).floor(); /// assert_eq!(size_neg.width, -4.0); /// assert_eq!(size_neg.height, -4.0); /// ``` #[inline] pub fn floor(self) -> Size { Size::new(self.width.floor(), self.height.floor()) } /// Returns a new `Size`, /// with `width` and `height` rounded away from zero to the nearest integer, /// unless they are already an integer. /// /// # Examples /// /// ``` /// use kurbo::Size; /// let size_pos = Size::new(3.3, 3.6).expand(); /// assert_eq!(size_pos.width, 4.0); /// assert_eq!(size_pos.height, 4.0); /// let size_neg = Size::new(-3.3, -3.6).expand(); /// assert_eq!(size_neg.width, -4.0); /// assert_eq!(size_neg.height, -4.0); /// ``` #[inline] pub fn expand(self) -> Size { Size::new(self.width.expand(), self.height.expand()) } /// Returns a new `Size`, /// with `width` and `height` rounded down towards zero the nearest integer, /// unless they are already an integer. /// /// # Examples /// /// ``` /// use kurbo::Size; /// let size_pos = Size::new(3.3, 3.6).trunc(); /// assert_eq!(size_pos.width, 3.0); /// assert_eq!(size_pos.height, 3.0); /// let size_neg = Size::new(-3.3, -3.6).trunc(); /// assert_eq!(size_neg.width, -3.0); /// assert_eq!(size_neg.height, -3.0); /// ``` #[inline] pub fn trunc(self) -> Size { Size::new(self.width.trunc(), self.height.trunc()) } /// Returns the aspect ratio of a rectangle with the given size. /// /// If the width is `0`, the output will be `sign(self.height) * infinity`. If The width and /// height are `0`, then the output will be `NaN`. pub fn aspect_ratio(self) -> f64 { self.height / self.width } /// Convert this `Size` into a [`Rect`] with origin `(0.0, 0.0)`. /// /// [`Rect`]: struct.Rect.html #[inline] pub const fn to_rect(self) -> Rect { Rect::new(0., 0., self.width, self.height) } /// Convert this `Size` into a [`RoundedRect`] with origin `(0.0, 0.0)` and /// the provided corner radius. /// /// [`RoundedRect`]: struct.RoundedRect.html #[inline] pub fn to_rounded_rect(self, radius: f64) -> RoundedRect { self.to_rect().to_rounded_rect(radius) } } impl fmt::Debug for Size { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{:?}W×{:?}H", self.width, self.height) } } impl fmt::Display for Size { fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { write!(formatter, "(")?; fmt::Display::fmt(&self.width, formatter)?; write!(formatter, "×")?; fmt::Display::fmt(&self.height, formatter)?; write!(formatter, ")") } } impl MulAssign for Size { #[inline] fn mul_assign(&mut self, other: f64) { *self = Size { width: self.width * other, height: self.height * other, }; } } impl Mul for f64 { type Output = Size; #[inline] fn mul(self, other: Size) -> Size { other * self } } impl Mul for Size { type Output = Size; #[inline] fn mul(self, other: f64) -> Size { Size { width: self.width * other, height: self.height * other, } } } impl DivAssign for Size { #[inline] fn div_assign(&mut self, other: f64) { *self = Size { width: self.width / other, height: self.height / other, }; } } impl Div for Size { type Output = Size; #[inline] fn div(self, other: f64) -> Size { Size { width: self.width / other, height: self.height / other, } } } impl Add for Size { type Output = Size; #[inline] fn add(self, other: Size) -> Size { Size { width: self.width + other.width, height: self.height + other.height, } } } impl AddAssign for Size { #[inline] fn add_assign(&mut self, other: Size) { *self = *self + other; } } impl Sub for Size { type Output = Size; #[inline] fn sub(self, other: Size) -> Size { Size { width: self.width - other.width, height: self.height - other.height, } } } impl SubAssign for Size { #[inline] fn sub_assign(&mut self, other: Size) { *self = *self - other; } } impl From<(f64, f64)> for Size { #[inline] fn from(v: (f64, f64)) -> Size { Size { width: v.0, height: v.1, } } } impl From for (f64, f64) { #[inline] fn from(v: Size) -> (f64, f64) { (v.width, v.height) } } //FIXME: remove for 0.6.0 https://github.com/linebender/kurbo/issues/95 impl Into for Vec2 { #[inline] fn into(self) -> Size { self.to_size() } } #[cfg(test)] mod tests { use super::*; #[test] fn display() { let s = Size::new(-0.12345, 9.87654); assert_eq!(format!("{}", s), "(-0.12345×9.87654)"); let s = Size::new(-0.12345, 9.87654); assert_eq!(format!("{:+6.2}", s), "( -0.12× +9.88)"); } #[test] fn aspect_ratio() { let s = Size::new(1.0, 1.0); assert!((s.aspect_ratio() - 1.0).abs() < 1e-6); } } kurbo-0.7.1/src/svg.rs010064400007650000024000000504111367545551600127760ustar 00000000000000//! SVG path representation. use std::error::Error; use std::f64::consts::PI; use std::fmt::{self, Display, Formatter}; use std::io::{self, Write}; use crate::{Arc, BezPath, ParamCurve, PathEl, PathSeg, Point, Vec2}; // Note: the SVG arc logic is heavily adapted from https://github.com/nical/lyon /// A single SVG arc segment. #[derive(Clone, Copy, Debug)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct SvgArc { /// The arc's start point. pub from: Point, /// The arc's end point. pub to: Point, /// The arc's radii, where the vector's x-component is the radius in the /// positive x direction after applying `x_rotation`. pub radii: Vec2, /// How much the arc is rotated, in radians. pub x_rotation: f64, /// Does this arc sweep through more than π radians? pub large_arc: bool, /// Determines if the arc should begin moving at positive angles. pub sweep: bool, } impl BezPath { /// Create a BezPath with segments corresponding to the sequence of /// `PathSeg`s pub fn from_path_segments(segments: impl Iterator) -> BezPath { let mut path_elements = Vec::new(); let mut current_pos = None; for segment in segments { let start = segment.start(); if Some(start) != current_pos { path_elements.push(PathEl::MoveTo(start)); }; path_elements.push(match segment { PathSeg::Line(l) => PathEl::LineTo(l.p1), PathSeg::Quad(q) => PathEl::QuadTo(q.p1, q.p2), PathSeg::Cubic(c) => PathEl::CurveTo(c.p1, c.p2, c.p3), }); current_pos = Some(segment.end()); } BezPath::from_vec(path_elements) } /// Convert the path to an SVG path string representation. /// /// The current implementation doesn't take any special care to produce a /// short string (reducing precision, using relative movement). pub fn to_svg(&self) -> String { let mut buffer = Vec::new(); self.write_to(&mut buffer).unwrap(); String::from_utf8(buffer).unwrap() } /// Write the SVG representation of this path to the provided buffer. pub fn write_to(&self, mut writer: W) -> io::Result<()> { for el in self.elements() { match *el { PathEl::MoveTo(p) => write!(writer, "M{} {}", p.x, p.y)?, PathEl::LineTo(p) => write!(writer, "L{} {}", p.x, p.y)?, PathEl::QuadTo(p1, p2) => write!(writer, "Q{} {} {} {}", p1.x, p1.y, p2.x, p2.y)?, PathEl::CurveTo(p1, p2, p3) => write!( writer, "C{} {} {} {} {} {}", p1.x, p1.y, p2.x, p2.y, p3.x, p3.y )?, PathEl::ClosePath => write!(writer, "Z")?, } } Ok(()) } /// Try to parse a bezier path from an SVG path element. /// /// This is implemented on a best-effort basis, intended for cases where the /// user controls the source of paths, and is not intended as a replacement /// for a general, robust SVG parser. pub fn from_svg(data: &str) -> Result { let mut lexer = SvgLexer::new(data); let mut path = BezPath::new(); let mut last_cmd = 0; let mut last_ctrl = None; let mut first_pt = Point::ORIGIN; let mut implicit_moveto = None; while let Some(c) = lexer.get_cmd(last_cmd) { if c != b'm' && c != b'M' { if let Some(pt) = implicit_moveto.take() { path.move_to(pt); } } match c { b'm' | b'M' => { implicit_moveto = None; let pt = lexer.get_maybe_relative(c)?; path.move_to(pt); lexer.last_pt = pt; first_pt = pt; last_ctrl = Some(pt); last_cmd = c - (b'M' - b'L'); } b'l' | b'L' => { let pt = lexer.get_maybe_relative(c)?; path.line_to(pt); lexer.last_pt = pt; last_ctrl = Some(pt); last_cmd = c; } b'h' | b'H' => { let mut x = lexer.get_number()?; lexer.opt_comma(); if c == b'h' { x += lexer.last_pt.x; } let pt = Point::new(x, lexer.last_pt.y); path.line_to(pt); lexer.last_pt = pt; last_ctrl = Some(pt); last_cmd = c; } b'v' | b'V' => { let mut y = lexer.get_number()?; lexer.opt_comma(); if c == b'v' { y += lexer.last_pt.y; } let pt = Point::new(lexer.last_pt.x, y); path.line_to(pt); lexer.last_pt = pt; last_ctrl = Some(pt); last_cmd = c; } b'q' | b'Q' => { let p1 = lexer.get_maybe_relative(c)?; let p2 = lexer.get_maybe_relative(c)?; path.quad_to(p1, p2); last_ctrl = Some(p1); lexer.last_pt = p2; last_cmd = c; } b't' | b'T' => { let p1 = match last_ctrl { Some(ctrl) => (2.0 * lexer.last_pt.to_vec2() - ctrl.to_vec2()).to_point(), None => lexer.last_pt, }; let p2 = lexer.get_maybe_relative(c)?; path.quad_to(p1, p2); last_ctrl = Some(p1); lexer.last_pt = p2; last_cmd = c; } b'c' | b'C' => { let p1 = lexer.get_maybe_relative(c)?; let p2 = lexer.get_maybe_relative(c)?; let p3 = lexer.get_maybe_relative(c)?; path.curve_to(p1, p2, p3); last_ctrl = Some(p2); lexer.last_pt = p3; last_cmd = c; } b's' | b'S' => { let p1 = match last_ctrl { Some(ctrl) => (2.0 * lexer.last_pt.to_vec2() - ctrl.to_vec2()).to_point(), None => lexer.last_pt, }; let p2 = lexer.get_maybe_relative(c)?; let p3 = lexer.get_maybe_relative(c)?; path.curve_to(p1, p2, p3); last_ctrl = Some(p2); lexer.last_pt = p3; last_cmd = c; } b'a' | b'A' => { let radii = lexer.get_number_pair()?; let x_rotation = lexer.get_number()?.to_radians(); lexer.opt_comma(); let large_arc = lexer.get_flag()?; lexer.opt_comma(); let sweep = lexer.get_flag()?; lexer.opt_comma(); let p = lexer.get_maybe_relative(c)?; let svg_arc = SvgArc { from: lexer.last_pt, to: p, radii: radii.to_vec2(), x_rotation, large_arc, sweep, }; match Arc::from_svg_arc(&svg_arc) { Some(arc) => { // TODO: consider making tolerance configurable arc.to_cubic_beziers(0.1, |p1, p2, p3| { path.curve_to(p1, p2, p3); }); } None => { path.line_to(p); } } last_ctrl = Some(p); lexer.last_pt = p; last_cmd = c; } b'z' | b'Z' => { path.close_path(); lexer.last_pt = first_pt; implicit_moveto = Some(first_pt); } _ => return Err(SvgParseError::UnknownCommand(c as char)), } } Ok(path) } } /// An error which can be returned when parsing an SVG. #[derive(Debug)] pub enum SvgParseError { /// A number was expected. Wrong, /// The input string ended while still expecting input. UnexpectedEof, /// Encountered an unknown command letter. UnknownCommand(char), } impl Display for SvgParseError { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self { SvgParseError::Wrong => write!(f, "Unable to parse a number"), SvgParseError::UnexpectedEof => write!(f, "Unexpected EOF"), SvgParseError::UnknownCommand(letter) => write!(f, "Unknown command, \"{}\"", letter), } } } impl Error for SvgParseError {} struct SvgLexer<'a> { data: &'a str, ix: usize, pub last_pt: Point, } impl<'a> SvgLexer<'a> { fn new(data: &str) -> SvgLexer { SvgLexer { data, ix: 0, last_pt: Point::ORIGIN, } } fn skip_ws(&mut self) { while let Some(&c) = self.data.as_bytes().get(self.ix) { if !(c == b' ' || c == 9 || c == 10 || c == 12 || c == 13) { break; } self.ix += 1; } } fn get_cmd(&mut self, last_cmd: u8) -> Option { self.skip_ws(); if let Some(c) = self.get_byte() { if (c >= b'a' && c <= b'z') || (c >= b'A' && c <= b'Z') { return Some(c); } else if last_cmd != 0 && (c == b'-' || c == b'.' || (c >= b'0' && c <= b'9')) { // Plausible number start self.unget(); return Some(last_cmd); } else { self.unget(); } } None } fn get_byte(&mut self) -> Option { self.data.as_bytes().get(self.ix).map(|&c| { self.ix += 1; c }) } fn unget(&mut self) { self.ix -= 1; } fn get_number(&mut self) -> Result { self.skip_ws(); let start = self.ix; let c = self.get_byte().ok_or(SvgParseError::UnexpectedEof)?; if !(c == b'-' || c == b'+') { self.unget(); } let mut digit_count = 0; let mut seen_period = false; while let Some(c) = self.get_byte() { if c >= b'0' && c <= b'9' { digit_count += 1; } else if c == b'.' && !seen_period { seen_period = true; } else { self.unget(); break; } } if let Some(c) = self.get_byte() { if c == b'e' || c == b'E' { let mut c = self.get_byte().ok_or(SvgParseError::Wrong)?; if c == b'-' || c == b'+' { c = self.get_byte().ok_or(SvgParseError::Wrong)? } if !(c >= b'0' && c <= b'9') { return Err(SvgParseError::Wrong); } while let Some(c) = self.get_byte() { if !(c >= b'0' && c <= b'9') { self.unget(); break; } } } else { self.unget(); } } if digit_count > 0 { self.data[start..self.ix] .parse() .map_err(|_| SvgParseError::Wrong) } else { Err(SvgParseError::Wrong) } } fn get_flag(&mut self) -> Result { self.skip_ws(); match self.get_byte().ok_or(SvgParseError::UnexpectedEof)? { b'0' => Ok(false), b'1' => Ok(true), _ => Err(SvgParseError::Wrong), } } fn get_number_pair(&mut self) -> Result { let x = self.get_number()?; self.opt_comma(); let y = self.get_number()?; self.opt_comma(); Ok(Point::new(x, y)) } fn get_maybe_relative(&mut self, cmd: u8) -> Result { let pt = self.get_number_pair()?; if cmd >= b'a' && cmd <= b'z' { Ok(self.last_pt + pt.to_vec2()) } else { Ok(pt) } } fn opt_comma(&mut self) { self.skip_ws(); if let Some(c) = self.get_byte() { if c != b',' { self.unget(); } } } } impl SvgArc { /// Checks that arc is actually a straight line. /// /// In this case, it can be replaced with a LineTo. pub fn is_straight_line(&self) -> bool { self.radii.x.abs() <= 1e-5 || self.radii.y.abs() <= 1e-5 || self.from == self.to } } impl Arc { /// Creates an `Arc` from a `SvgArc`. /// /// Returns `None` if `arc` is actually a straight line. pub fn from_svg_arc(arc: &SvgArc) -> Option { // Have to check this first, otherwise `sum_of_sq` will be 0. if arc.is_straight_line() { return None; } let mut rx = arc.radii.x.abs(); let mut ry = arc.radii.y.abs(); let xr = arc.x_rotation % (2.0 * PI); let cos_phi = xr.cos(); let sin_phi = xr.sin(); let hd_x = (arc.from.x - arc.to.x) * 0.5; let hd_y = (arc.from.y - arc.to.y) * 0.5; let hs_x = (arc.from.x + arc.to.x) * 0.5; let hs_y = (arc.from.y + arc.to.y) * 0.5; // F6.5.1 let p = Vec2::new( cos_phi * hd_x + sin_phi * hd_y, -sin_phi * hd_x + cos_phi * hd_y, ); // Sanitize the radii. // If rf > 1 it means the radii are too small for the arc to // possibly connect the end points. In this situation we scale // them up according to the formula provided by the SVG spec. // F6.6.2 let rf = p.x * p.x / (rx * rx) + p.y * p.y / (ry * ry); if rf > 1.0 { let scale = rf.sqrt(); rx *= scale; ry *= scale; } let rxry = rx * ry; let rxpy = rx * p.y; let rypx = ry * p.x; let sum_of_sq = rxpy * rxpy + rypx * rypx; debug_assert!(sum_of_sq != 0.0); // F6.5.2 let sign_coe = if arc.large_arc == arc.sweep { -1.0 } else { 1.0 }; let coe = sign_coe * ((rxry * rxry - sum_of_sq) / sum_of_sq).abs().sqrt(); let transformed_cx = coe * rxpy / ry; let transformed_cy = -coe * rypx / rx; // F6.5.3 let center = Point::new( cos_phi * transformed_cx - sin_phi * transformed_cy + hs_x, sin_phi * transformed_cx + cos_phi * transformed_cy + hs_y, ); let start_v = Vec2::new((p.x - transformed_cx) / rx, (p.y - transformed_cy) / ry); let end_v = Vec2::new((-p.x - transformed_cx) / rx, (-p.y - transformed_cy) / ry); let start_angle = start_v.atan2(); let mut sweep_angle = (end_v.atan2() - start_angle) % (2.0 * PI); if arc.sweep && sweep_angle < 0.0 { sweep_angle += 2.0 * PI; } else if !arc.sweep && sweep_angle > 0.0 { sweep_angle -= 2.0 * PI; } Some(Arc { center, radii: Vec2::new(rx, ry), start_angle, sweep_angle, x_rotation: arc.x_rotation, }) } } #[cfg(test)] mod tests { use crate::{BezPath, CubicBez, Line, ParamCurve, PathSeg, Point, QuadBez, Shape}; #[test] fn test_parse_svg() { let path = BezPath::from_svg("m10 10 100 0 0 100 -100 0z").unwrap(); assert_eq!(path.segments().count(), 4); } #[test] fn test_parse_svg2() { let path = BezPath::from_svg("M3.5 8a.5.5 0 01.5-.5h8a.5.5 0 010 1H4a.5.5 0 01-.5-.5z").unwrap(); assert_eq!(path.segments().count(), 6); } #[test] fn test_parse_svg_arc() { let path = BezPath::from_svg("M 100 100 A 25 25 0 1 0 -25 25 z").unwrap(); assert_eq!(path.segments().count(), 3); } // Regression test for #51 #[test] #[allow(clippy::float_cmp)] fn test_parse_svg_arc_pie() { let path = BezPath::from_svg("M 100 100 h 25 a 25 25 0 1 0 -25 25 z").unwrap(); // Approximate figures, but useful for regression testing assert_eq!(path.area().round(), -1473.0); assert_eq!(path.perimeter(1e-6).round(), 168.0); } #[test] fn test_write_svg_single() { let segments = [CubicBez::new( Point::new(10., 10.), Point::new(20., 20.), Point::new(30., 30.), Point::new(40., 40.), ) .into()]; let path = BezPath::from_path_segments(segments.iter().cloned()); assert_eq!(path.to_svg(), "M10 10C20 20 30 30 40 40"); } #[test] fn test_write_svg_two_nomove() { let segments = [ CubicBez::new( Point::new(10., 10.), Point::new(20., 20.), Point::new(30., 30.), Point::new(40., 40.), ) .into(), CubicBez::new( Point::new(40., 40.), Point::new(30., 30.), Point::new(20., 20.), Point::new(10., 10.), ) .into(), ]; let path = BezPath::from_path_segments(segments.iter().cloned()); assert_eq!(path.to_svg(), "M10 10C20 20 30 30 40 40C30 30 20 20 10 10"); } #[test] fn test_write_svg_two_move() { let segments = [ CubicBez::new( Point::new(10., 10.), Point::new(20., 20.), Point::new(30., 30.), Point::new(40., 40.), ) .into(), CubicBez::new( Point::new(50., 50.), Point::new(30., 30.), Point::new(20., 20.), Point::new(10., 10.), ) .into(), ]; let path = BezPath::from_path_segments(segments.iter().cloned()); assert_eq!( path.to_svg(), "M10 10C20 20 30 30 40 40M50 50C30 30 20 20 10 10" ); } use rand::prelude::*; fn gen_random_path_sequence(rng: &mut impl Rng) -> Vec { const MAX_LENGTH: u32 = 10; let mut elements = vec![]; let mut position = None; let length = rng.gen_range(0, MAX_LENGTH); for _ in 0..length { let should_follow: bool = rand::random(); let kind = rng.gen_range(0, 3); let first = position .filter(|_| should_follow) .unwrap_or_else(|| Point::new(rng.gen(), rng.gen())); let element: PathSeg = match kind { 0 => Line::new(first, Point::new(rng.gen(), rng.gen())).into(), 1 => QuadBez::new( first, Point::new(rng.gen(), rng.gen()), Point::new(rng.gen(), rng.gen()), ) .into(), 2 => CubicBez::new( first, Point::new(rng.gen(), rng.gen()), Point::new(rng.gen(), rng.gen()), Point::new(rng.gen(), rng.gen()), ) .into(), _ => unreachable!(), }; position = Some(element.end()); elements.push(element); } elements } #[test] fn test_serialize_deserialize() { const N_TESTS: u32 = 100; let mut rng = rand::thread_rng(); for _ in 0..N_TESTS { let vec = gen_random_path_sequence(&mut rng); let ser = BezPath::from_path_segments(vec.iter().cloned()).to_svg(); let deser = BezPath::from_svg(&ser).expect("failed deserialization"); let deser_vec = deser.segments().collect::>(); assert_eq!(vec, deser_vec); } } } kurbo-0.7.1/src/translate_scale.rs010064400007650000024000000165411367545551600153510ustar 00000000000000//! A transformation that includes both scale and translation. use std::ops::{Add, AddAssign, Mul, MulAssign, Sub, SubAssign}; use crate::{Affine, Circle, CubicBez, Line, Point, QuadBez, Rect, RoundedRect, Vec2}; /// A transformation including scaling and translation. /// /// If the translation is `(x, y)` and the scale is `s`, then this /// transformation represents this augmented matrix: /// /// ```text /// | s 0 x | /// | 0 s y | /// | 0 0 1 | /// ``` /// /// See [`Affine`](struct.Affine.html) for more details about the /// equivalence with augmented matrices. /// /// Various multiplication ops are defined, and these are all defined /// to be consistent with matrix multiplication. Therefore, /// `TranslateScale * Point` is defined but not the other way around. /// /// Also note that multiplication is not commutative. Thus, /// `TranslateScale::scale(2.0) * TranslateScale::translate(Vec2::new(1.0, 0.0))` /// has a translation of (2, 0), while /// `TranslateScale::translate(Vec2::new(1.0, 0.0)) * TranslateScale::scale(2.0)` /// has a translation of (1, 0). (Both have a scale of 2; also note that /// the first case can be written /// `2.0 * TranslateScale::translate(Vec2::new(1.0, 0.0))` as this case /// has an implicit conversion). /// /// This transformation is less powerful than `Affine`, but can be applied /// to more primitives, especially including [`Rect`](struct.Rect.html). #[derive(Clone, Copy, Debug)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct TranslateScale { translation: Vec2, scale: f64, } impl TranslateScale { /// Create a new transformation from translation and scale. #[inline] pub const fn new(translation: Vec2, scale: f64) -> TranslateScale { TranslateScale { translation, scale } } /// Create a new transformation with scale only. #[inline] pub const fn scale(s: f64) -> TranslateScale { TranslateScale::new(Vec2::ZERO, s) } /// Create a new transformation with translation only. #[inline] pub const fn translate(t: Vec2) -> TranslateScale { TranslateScale::new(t, 1.0) } /// Decompose transformation into translation and scale. pub fn as_tuple(self) -> (Vec2, f64) { (self.translation, self.scale) } /// Compute the inverse transform. /// /// Multiplying a transform with its inverse (either on the /// left or right) results in the identity transform /// (modulo floating point rounding errors). /// /// Produces NaN values when scale is zero. pub fn inverse(self) -> TranslateScale { let scale_recip = self.scale.recip(); TranslateScale { translation: self.translation * -scale_recip, scale: scale_recip, } } } impl Default for TranslateScale { #[inline] fn default() -> TranslateScale { TranslateScale::scale(1.0) } } impl From for Affine { fn from(ts: TranslateScale) -> Affine { let TranslateScale { translation, scale } = ts; Affine::new([scale, 0.0, 0.0, scale, translation.x, translation.y]) } } impl Mul for TranslateScale { type Output = Point; #[inline] fn mul(self, other: Point) -> Point { (self.scale * other.to_vec2()).to_point() + self.translation } } impl Mul for TranslateScale { type Output = TranslateScale; #[inline] fn mul(self, other: TranslateScale) -> TranslateScale { TranslateScale { translation: self.translation + self.scale * other.translation, scale: self.scale * other.scale, } } } impl MulAssign for TranslateScale { #[inline] fn mul_assign(&mut self, other: TranslateScale) { *self = self.mul(other); } } impl Mul for f64 { type Output = TranslateScale; #[inline] fn mul(self, other: TranslateScale) -> TranslateScale { TranslateScale { translation: other.translation * self, scale: other.scale * self, } } } impl Add for TranslateScale { type Output = TranslateScale; #[inline] fn add(self, other: Vec2) -> TranslateScale { TranslateScale { translation: self.translation + other, scale: self.scale, } } } impl Add for Vec2 { type Output = TranslateScale; #[inline] fn add(self, other: TranslateScale) -> TranslateScale { other + self } } impl AddAssign for TranslateScale { #[inline] fn add_assign(&mut self, other: Vec2) { *self = self.add(other); } } impl Sub for TranslateScale { type Output = TranslateScale; #[inline] fn sub(self, other: Vec2) -> TranslateScale { TranslateScale { translation: self.translation - other, scale: self.scale, } } } impl SubAssign for TranslateScale { #[inline] fn sub_assign(&mut self, other: Vec2) { *self = self.sub(other); } } impl Mul for TranslateScale { type Output = Circle; #[inline] fn mul(self, other: Circle) -> Circle { Circle::new(self * other.center, self.scale * other.radius) } } impl Mul for TranslateScale { type Output = Line; #[inline] fn mul(self, other: Line) -> Line { Line::new(self * other.p0, self * other.p1) } } impl Mul for TranslateScale { type Output = Rect; #[inline] fn mul(self, other: Rect) -> Rect { let pt0 = self * Point::new(other.x0, other.y0); let pt1 = self * Point::new(other.x1, other.y1); (pt0, pt1).into() } } impl Mul for TranslateScale { type Output = RoundedRect; #[inline] fn mul(self, other: RoundedRect) -> RoundedRect { RoundedRect::from_rect(self * other.rect(), self.scale * other.radius()) } } impl Mul for TranslateScale { type Output = QuadBez; #[inline] fn mul(self, other: QuadBez) -> QuadBez { QuadBez::new(self * other.p0, self * other.p1, self * other.p2) } } impl Mul for TranslateScale { type Output = CubicBez; #[inline] fn mul(self, other: CubicBez) -> CubicBez { CubicBez::new( self * other.p0, self * other.p1, self * other.p2, self * other.p3, ) } } #[cfg(test)] mod tests { use crate::{Affine, Point, TranslateScale, Vec2}; fn assert_near(p0: Point, p1: Point) { assert!((p1 - p0).hypot() < 1e-9, "{:?} != {:?}", p0, p1); } #[test] fn translate_scale() { let p = Point::new(3.0, 4.0); let ts = TranslateScale::new(Vec2::new(5.0, 6.0), 2.0); assert_near(ts * p, Point::new(11.0, 14.0)); } #[test] fn conversions() { let p = Point::new(3.0, 4.0); let s = 2.0; let t = Vec2::new(5.0, 6.0); let ts = TranslateScale::new(t, s); // Test that conversion to affine is consistent. let a: Affine = ts.into(); assert_near(ts * p, a * p); assert_near((s * p.to_vec2()).to_point(), TranslateScale::scale(s) * p); assert_near(p + t, TranslateScale::translate(t) * p); } #[test] fn inverse() { let p = Point::new(3.0, 4.0); let ts = TranslateScale::new(Vec2::new(5.0, 6.0), 2.0); assert_near(p, (ts * ts.inverse()) * p); assert_near(p, (ts.inverse() * ts) * p); } } kurbo-0.7.1/src/vec2.rs010064400007650000024000000213101367545551600130320ustar 00000000000000//! A simple 2D vector. use std::fmt; use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign}; use crate::common::FloatExt; use crate::{Point, Size}; /// A 2D vector. /// /// This is intended primarily for a vector in the mathematical sense, /// but it can be interpreted as a translation, and converted to and /// from a point (vector relative to the origin) and size. #[derive(Clone, Copy, Default, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Vec2 { /// The x-coordinate. pub x: f64, /// The y-coordinate. pub y: f64, } impl Vec2 { /// The vector (0, 0). pub const ZERO: Vec2 = Vec2::new(0., 0.); /// Create a new vector. #[inline] pub const fn new(x: f64, y: f64) -> Vec2 { Vec2 { x, y } } /// Convert this vector into a `Point`. #[inline] pub const fn to_point(self) -> Point { Point::new(self.x, self.y) } /// Convert this vector into a `Size`. #[inline] pub const fn to_size(self) -> Size { Size::new(self.x, self.y) } /// Create a `Vec2` with the same value for x and y pub(crate) const fn splat(v: f64) -> Self { Vec2 { x: v, y: v } } /// Dot product of two vectors. #[inline] pub fn dot(self, other: Vec2) -> f64 { self.x * other.x + self.y * other.y } /// Cross product of two vectors. /// /// This is signed so that (0, 1) × (1, 0) = 1. #[inline] pub fn cross(self, other: Vec2) -> f64 { self.x * other.y - self.y * other.x } /// Magnitude of vector. #[inline] pub fn hypot(self) -> f64 { self.x.hypot(self.y) } /// Magnitude squared of vector. #[inline] pub fn hypot2(self) -> f64 { self.dot(self) } /// Angle of vector. /// /// If the vector is interpreted as a complex number, this is the argument. /// The angle is expressed in radians. #[inline] pub fn atan2(self) -> f64 { self.y.atan2(self.x) } /// A unit vector of the given angle. /// /// With `th` at zero, the result is the positive X unit vector, and /// at π/2, it is the positive Y unit vector. The angle is expressed /// in radians. /// /// Thus, in a Y-down coordinate system (as is common for graphics), /// it is a clockwise rotation, and in Y-up (traditional for math), it /// is anti-clockwise. This convention is consistent with /// [`Affine::rotate`](struct.Affine.html#method.rotate). #[inline] pub fn from_angle(th: f64) -> Vec2 { Vec2 { x: th.cos(), y: th.sin(), } } /// Linearly interpolate between two vectors. #[inline] pub fn lerp(self, other: Vec2, t: f64) -> Vec2 { self + t * (other - self) } /// Returns a vector of magnitude 1.0 with the same angle as `self`; i.e. /// a unit/direction vector. /// /// This produces `NaN` values when the magnitutde is `0`. #[inline] pub fn normalize(self) -> Vec2 { self / self.hypot() } /// Returns a new `Vec2`, /// with `x` and `y` rounded to the nearest integer. /// /// # Examples /// /// ``` /// use kurbo::Vec2; /// let a = Vec2::new(3.3, 3.6).round(); /// let b = Vec2::new(3.0, -3.1).round(); /// assert_eq!(a.x, 3.0); /// assert_eq!(a.y, 4.0); /// assert_eq!(b.x, 3.0); /// assert_eq!(b.y, -3.0); /// ``` #[inline] pub fn round(self) -> Vec2 { Vec2::new(self.x.round(), self.y.round()) } /// Returns a new `Vec2`, /// with `x` and `y` rounded up to the nearest integer, /// unless they are already an integer. /// /// # Examples /// /// ``` /// use kurbo::Vec2; /// let a = Vec2::new(3.3, 3.6).ceil(); /// let b = Vec2::new(3.0, -3.1).ceil(); /// assert_eq!(a.x, 4.0); /// assert_eq!(a.y, 4.0); /// assert_eq!(b.x, 3.0); /// assert_eq!(b.y, -3.0); /// ``` #[inline] pub fn ceil(self) -> Vec2 { Vec2::new(self.x.ceil(), self.y.ceil()) } /// Returns a new `Vec2`, /// with `x` and `y` rounded down to the nearest integer, /// unless they are already an integer. /// /// # Examples /// /// ``` /// use kurbo::Vec2; /// let a = Vec2::new(3.3, 3.6).floor(); /// let b = Vec2::new(3.0, -3.1).floor(); /// assert_eq!(a.x, 3.0); /// assert_eq!(a.y, 3.0); /// assert_eq!(b.x, 3.0); /// assert_eq!(b.y, -4.0); /// ``` #[inline] pub fn floor(self) -> Vec2 { Vec2::new(self.x.floor(), self.y.floor()) } /// Returns a new `Vec2`, /// with `x` and `y` rounded away from zero to the nearest integer, /// unless they are already an integer. /// /// # Examples /// /// ``` /// use kurbo::Vec2; /// let a = Vec2::new(3.3, 3.6).expand(); /// let b = Vec2::new(3.0, -3.1).expand(); /// assert_eq!(a.x, 4.0); /// assert_eq!(a.y, 4.0); /// assert_eq!(b.x, 3.0); /// assert_eq!(b.y, -4.0); /// ``` #[inline] pub fn expand(self) -> Vec2 { Vec2::new(self.x.expand(), self.y.expand()) } /// Returns a new `Vec2`, /// with `x` and `y` rounded towards zero to the nearest integer, /// unless they are already an integer. /// /// # Examples /// /// ``` /// use kurbo::Vec2; /// let a = Vec2::new(3.3, 3.6).trunc(); /// let b = Vec2::new(3.0, -3.1).trunc(); /// assert_eq!(a.x, 3.0); /// assert_eq!(a.y, 3.0); /// assert_eq!(b.x, 3.0); /// assert_eq!(b.y, -3.0); /// ``` #[inline] pub fn trunc(self) -> Vec2 { Vec2::new(self.x.trunc(), self.y.trunc()) } } impl From<(f64, f64)> for Vec2 { #[inline] fn from(v: (f64, f64)) -> Vec2 { Vec2 { x: v.0, y: v.1 } } } impl From for (f64, f64) { #[inline] fn from(v: Vec2) -> (f64, f64) { (v.x, v.y) } } impl Add for Vec2 { type Output = Vec2; #[inline] fn add(self, other: Vec2) -> Vec2 { Vec2 { x: self.x + other.x, y: self.y + other.y, } } } impl AddAssign for Vec2 { #[inline] fn add_assign(&mut self, other: Vec2) { *self = Vec2 { x: self.x + other.x, y: self.y + other.y, } } } impl Sub for Vec2 { type Output = Vec2; #[inline] fn sub(self, other: Vec2) -> Vec2 { Vec2 { x: self.x - other.x, y: self.y - other.y, } } } impl SubAssign for Vec2 { #[inline] fn sub_assign(&mut self, other: Vec2) { *self = Vec2 { x: self.x - other.x, y: self.y - other.y, } } } impl Mul for Vec2 { type Output = Vec2; #[inline] fn mul(self, other: f64) -> Vec2 { Vec2 { x: self.x * other, y: self.y * other, } } } impl MulAssign for Vec2 { #[inline] fn mul_assign(&mut self, other: f64) { *self = Vec2 { x: self.x * other, y: self.y * other, }; } } impl Mul for f64 { type Output = Vec2; #[inline] fn mul(self, other: Vec2) -> Vec2 { other * self } } impl Div for Vec2 { type Output = Vec2; /// Note: division by a scalar is implemented by multiplying by the reciprocal. /// /// This is more efficient but has different roundoff behavior than division. #[inline] #[allow(clippy::suspicious_arithmetic_impl)] fn div(self, other: f64) -> Vec2 { self * other.recip() } } impl DivAssign for Vec2 { #[inline] fn div_assign(&mut self, other: f64) { self.mul_assign(other.recip()); } } impl Neg for Vec2 { type Output = Vec2; #[inline] fn neg(self) -> Vec2 { Vec2 { x: -self.x, y: -self.y, } } } impl fmt::Display for Vec2 { fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { write!(formatter, "𝐯=(")?; fmt::Display::fmt(&self.x, formatter)?; write!(formatter, ", ")?; fmt::Display::fmt(&self.y, formatter)?; write!(formatter, ")") } } // Conversions to and from mint #[cfg(feature = "mint")] impl From for mint::Vector2 { #[inline] fn from(p: Vec2) -> mint::Vector2 { mint::Vector2 { x: p.x, y: p.y } } } #[cfg(feature = "mint")] impl From> for Vec2 { #[inline] fn from(p: mint::Vector2) -> Vec2 { Vec2 { x: p.x, y: p.y } } } #[cfg(test)] mod tests { use super::*; #[test] fn display() { let v = Vec2::new(1.2332421, 532.10721213123); let s = format!("{:.2}", v); assert_eq!(s.as_str(), "𝐯=(1.23, 532.11)"); } }