kurbo-0.11.1/.cargo_vcs_info.json0000644000000001360000000000100122360ustar { "git": { "sha1": "d948d1e20c9b7feaee6eff609501eec120a04a70" }, "path_in_vcs": "" }kurbo-0.11.1/.github/copyright.sh000064400000000000000000000015161046102023000147350ustar 00000000000000#!/bin/bash # If there are new files with headers that can't match the conditions here, # then the files can be ignored by an additional glob argument via the -g flag. # For example: # -g "!src/special_file.rs" # -g "!src/special_directory" # Check all the standard Rust source files output=$(rg "^// Copyright (19|20)[\d]{2} (.+ and )?the Kurbo Authors( and .+)?$\n^// SPDX-License-Identifier: Apache-2\.0 OR MIT$\n\n" --files-without-match --multiline -g "*.rs" .) if [ -n "$output" ]; then echo -e "The following files lack the correct copyright header:\n" echo $output echo -e "\n\nPlease add the following header:\n" echo "// Copyright $(date +%Y) the Kurbo Authors" echo "// SPDX-License-Identifier: Apache-2.0 OR MIT" echo -e "\n... rest of the file ...\n" exit 1 fi echo "All files have correct copyright headers." exit 0 kurbo-0.11.1/.github/workflows/ci.yml000064400000000000000000000235631046102023000155520ustar 00000000000000env: # We aim to always test with the latest stable Rust toolchain, however we pin to a specific # version like 1.70. Note that we only specify MAJOR.MINOR and not PATCH so that bugfixes still # come automatically. If the version specified here is no longer the latest stable version, # then please feel free to submit a PR that adjusts it along with the potential clippy fixes. RUST_STABLE_VER: "1.79" # In quotes because otherwise (e.g.) 1.70 would be interpreted as 1.7 # The purpose of checking with the minimum supported Rust toolchain is to detect its staleness. # If the compilation fails, then the version specified here needs to be bumped up to reality. # Be sure to also update the rust-version property in the workspace Cargo.toml file, # plus all the README.md files of the affected packages. RUST_MIN_VER: "1.65" # List of packages that will be checked with the minimum supported Rust version. # This should be limited to packages that are intended for publishing. RUST_MIN_VER_PKGS: "-p kurbo" # List of features that depend on the standard library and will be excluded from no_std checks. FEATURES_DEPENDING_ON_STD: "std,default,schemars" # Rationale # # We don't run clippy with --all-targets because then even --lib and --bins are compiled with # dev dependencies enabled, which does not match how they would be compiled by users. # A dev dependency might enable a feature that we need for a regular dependency, # and checking with --all-targets would not find our feature requirements lacking. # This problem still applies to cargo resolver version 2. # Thus we split all the targets into two steps, one with --lib --bins # and another with --tests --benches --examples. # Also, we can't give --lib --bins explicitly because then cargo will error on binary-only packages. # Luckily the default behavior of cargo with no explicit targets is the same but without the error. # # We use cargo-hack for a similar reason. Cargo's --workspace will do feature unification across # the whole workspace. While cargo-hack will instead check each workspace package separately. # # Using cargo-hack also allows us to more easily test the feature matrix of our packages. # We use --each-feature & --optional-deps which will run a separate check for every feature. # # The MSRV jobs run only cargo check because different clippy versions can disagree on goals and # running tests introduces dev dependencies which may require a higher MSRV than the bare package. # # For no_std checks we target x86_64-unknown-none, because this target doesn't support std # and as such will error out if our dependency tree accidentally tries to use std. # https://doc.rust-lang.org/stable/rustc/platform-support/x86_64-unknown-none.html # # We don't save caches in the merge-group cases, because those caches will never be re-used (apart # from the very rare cases where there are multiple PRs in the merge queue). # This is because GitHub doesn't share caches between merge queues and `main`. name: CI on: pull_request: merge_group: # We run on push, even though the commit is the same as when we ran in merge_group. # This allows the cache to be primed. # See https://github.com/orgs/community/discussions/66430 push: branches: - main jobs: fmt: name: cargo fmt runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: install stable toolchain uses: dtolnay/rust-toolchain@master with: toolchain: ${{ env.RUST_STABLE_VER }} components: rustfmt - name: cargo fmt run: cargo fmt --all --check - name: install ripgrep run: | sudo apt update sudo apt install ripgrep - name: check copyright headers run: bash .github/copyright.sh clippy-stable: name: cargo clippy runs-on: ${{ matrix.os }} strategy: matrix: os: [windows-latest, macos-latest, ubuntu-latest] steps: - uses: actions/checkout@v4 - name: restore cache uses: Swatinem/rust-cache@v2 with: save-if: ${{ github.event_name != 'merge_group' }} - name: install stable toolchain uses: dtolnay/rust-toolchain@master with: toolchain: ${{ env.RUST_STABLE_VER }} targets: x86_64-unknown-none components: clippy - name: install cargo-hack uses: taiki-e/install-action@v2 with: tool: cargo-hack - name: cargo clippy (no_std) run: cargo hack clippy --workspace --locked --optional-deps --each-feature --features libm --exclude-features ${{ env.FEATURES_DEPENDING_ON_STD }} --target x86_64-unknown-none -- -D warnings - name: cargo clippy run: cargo hack clippy --workspace --locked --optional-deps --each-feature --features std -- -D warnings - name: cargo clippy (auxiliary) run: cargo hack clippy --workspace --locked --optional-deps --each-feature --features std --tests --benches --examples -- -D warnings clippy-stable-wasm: name: cargo clippy (wasm32) runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: restore cache uses: Swatinem/rust-cache@v2 with: save-if: ${{ github.event_name != 'merge_group' }} - name: install stable toolchain uses: dtolnay/rust-toolchain@master with: toolchain: ${{ env.RUST_STABLE_VER }} targets: wasm32-unknown-unknown components: clippy - name: install cargo-hack uses: taiki-e/install-action@v2 with: tool: cargo-hack - name: cargo clippy run: cargo hack clippy --workspace --locked --target wasm32-unknown-unknown --optional-deps --each-feature --features std -- -D warnings - name: cargo clippy (auxiliary) run: cargo hack clippy --workspace --locked --target wasm32-unknown-unknown --optional-deps --each-feature --features std --tests --benches --examples -- -D warnings test-stable: name: cargo test runs-on: ${{ matrix.os }} strategy: matrix: os: [windows-latest, macos-latest, ubuntu-latest] steps: - uses: actions/checkout@v4 - name: restore cache uses: Swatinem/rust-cache@v2 with: save-if: ${{ github.event_name != 'merge_group' }} - name: install stable toolchain uses: dtolnay/rust-toolchain@master with: toolchain: ${{ env.RUST_STABLE_VER }} - name: cargo test run: cargo test --workspace --locked --all-features test-stable-wasm: name: cargo test (wasm32) runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: restore cache uses: Swatinem/rust-cache@v2 with: save-if: ${{ github.event_name != 'merge_group' }} - name: install stable toolchain uses: dtolnay/rust-toolchain@master with: toolchain: ${{ env.RUST_STABLE_VER }} targets: wasm32-unknown-unknown # TODO: Find a way to make tests work. Until then the tests are merely compiled. - name: cargo test compile run: cargo test --workspace --locked --target wasm32-unknown-unknown --all-features --no-run check-msrv: name: cargo check (msrv) runs-on: ${{ matrix.os }} strategy: matrix: os: [windows-latest, macos-latest, ubuntu-latest] steps: - uses: actions/checkout@v4 - name: restore cache uses: Swatinem/rust-cache@v2 with: save-if: ${{ github.event_name != 'merge_group' }} - name: install msrv toolchain uses: dtolnay/rust-toolchain@master with: toolchain: ${{ env.RUST_MIN_VER }} targets: x86_64-unknown-none - name: install cargo-hack uses: taiki-e/install-action@v2 with: tool: cargo-hack - name: cargo check (no_std) run: cargo hack check ${{ env.RUST_MIN_VER_PKGS }} --locked --optional-deps --each-feature --features libm --exclude-features ${{ env.FEATURES_DEPENDING_ON_STD }} --target x86_64-unknown-none - name: cargo check run: cargo hack check ${{ env.RUST_MIN_VER_PKGS }} --locked --optional-deps --each-feature --features std check-msrv-wasm: name: cargo check (msrv) (wasm32) runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: restore cache uses: Swatinem/rust-cache@v2 with: save-if: ${{ github.event_name != 'merge_group' }} - name: install msrv toolchain uses: dtolnay/rust-toolchain@master with: toolchain: ${{ env.RUST_MIN_VER }} targets: wasm32-unknown-unknown - name: install cargo-hack uses: taiki-e/install-action@v2 with: tool: cargo-hack - name: cargo check run: cargo hack check ${{ env.RUST_MIN_VER_PKGS }} --locked --target wasm32-unknown-unknown --optional-deps --each-feature --features std doc: name: cargo doc # NOTE: We don't have any platform specific docs in this workspace, so we only run on Ubuntu. # If we get per-platform docs (win/macos/linux/wasm32/..) then doc jobs should match that. runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: restore cache uses: Swatinem/rust-cache@v2 with: save-if: ${{ github.event_name != 'merge_group' }} - name: install nightly toolchain uses: dtolnay/rust-toolchain@nightly # We test documentation using nightly to match docs.rs. This prevents potential breakages - name: cargo doc run: cargo doc --workspace --locked --all-features --no-deps --document-private-items -Zunstable-options -Zrustdoc-scrape-examples env: RUSTDOCFLAGS: '--cfg docsrs' # If this fails, consider changing your text or adding something to .typos.toml typos: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: check typos uses: crate-ci/typos@v1.23.2 kurbo-0.11.1/.gitignore000064400000000000000000000000341046102023000130130ustar 00000000000000.vscode /target **/*.rs.bk kurbo-0.11.1/.typos.toml000064400000000000000000000013541046102023000131620ustar 00000000000000# See the configuration reference at # https://github.com/crate-ci/typos/blob/master/docs/reference.md # Corrections take the form of a key/value pair. The key is the incorrect word # and the value is the correct word. If the key and value are the same, the # word is treated as always correct. If the value is an empty string, the word # is treated as always incorrect. # Match Identifier - Case Sensitive [default.extend-identifiers] ba = "ba" ba_c2 = "ba_c2" delt_2 = "delt_2" delt_f = "delt_f" # Match Inside a Word - Case Insensitive [default.extend-words] [files] # Include .github, .cargo, etc. ignore-hidden = false # /.git isn't in .gitignore, because git never tracks it. # Typos doesn't know that, though. extend-exclude = ["/.git"] kurbo-0.11.1/AUTHORS000064400000000000000000000004641046102023000121020ustar 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.11.1/CHANGELOG.md000064400000000000000000000064651046102023000126520ustar 00000000000000 # Changelog The latest published Kurbo release is [0.11.1](#0110-2024-09-12) which was released on 2024-09-12. You can find its changes [documented below](#0111-2024-09-12). ## [Unreleased] This release has an [MSRV][] of 1.65. ## [0.11.1][] (2024-09-12) This release has an [MSRV][] of 1.65. ### Added - Add `From (f32, f32)` for `Point`. ([#339] by [@rsheeter]) - Add `Rect::overlaps` and `Rect::contains_rect`. ([#347] by [@nils-mathieu]) - Add `CubicBez::tangents` ([#288] by [@raphlinus]) - Add `Arc::reversed`. ([#367] by [@waywardmonkeys]) - Add `CircleSegment::inner_arc` and `CircleSegment::outer_arc` ([#368] by [@waywardmonkeys]) - Add `Rect::is_zero_area` and `Size::is_zero_area` and deprecate their `is_empty` methods. ([#370] by [@waywardmonkeys]) - Add `Line::reversed` and `Line::midpoint`. ([#375] by [@waywardmonkeys]) - Allow construction of `Line` from `(Point, Point)` and `(Point, Vec2)`. ([#376] by [@waywardmonkeys]) ### Changed - Move `Self: Sized` bound from `Shape` to methods. ([#340] by [@waywardmonkeys]) - Enable partial SVG path support in `no_std` builds. ([#356] by [@waywardmonkeys]) - Deprecate `BezPath::flatten`, prefer `flatten`. ([#361] by [@waywardmonkeys]) ### Fixed - An edge case in `mindist` was fixed. ([#334] by [@platlas]) - Allow lines in simplify input. ([#343] by [@raphlinus]) - Don't skip first dash in dash pattern. ([#353] by [@dominikh]) - Documentation for `Arc.perimeter` was corrected. ([#354] by [@simoncozens]) - Parsing scientific notation in an SVG path was fixed. ([#365] by [@GabrielDertoni]) ## [0.11.0][] (2024-02-14) This release has an [MSRV][] of 1.65. Note: A changelog was not kept for or before this release [@dominikh]: https://github.com/dominikh [@GabrielDertoni]: https://github.com/GabrielDertoni [@nils-mathieu]: https://github.com/nils-mathieu [@platlas]: https://github.com/platlas [@raphlinus]: https://github.com/raphlinus [@rsheeter]: https://github.com/rsheeter [@simoncozens]: https://github.com/simoncozens [@waywardmonkeys]: https://github.com/waywardmonkeys [#288]: https://github.com/linebender/kurbo/pull/288 [#334]: https://github.com/linebender/kurbo/pull/334 [#339]: https://github.com/linebender/kurbo/pull/339 [#340]: https://github.com/linebender/kurbo/pull/340 [#343]: https://github.com/linebender/kurbo/pull/343 [#347]: https://github.com/linebender/kurbo/pull/347 [#353]: https://github.com/linebender/kurbo/pull/353 [#354]: https://github.com/linebender/kurbo/pull/354 [#356]: https://github.com/linebender/kurbo/pull/356 [#361]: https://github.com/linebender/kurbo/pull/361 [#365]: https://github.com/linebender/kurbo/pull/365 [#367]: https://github.com/linebender/kurbo/pull/367 [#368]: https://github.com/linebender/kurbo/pull/368 [#370]: https://github.com/linebender/kurbo/pull/370 [#375]: https://github.com/linebender/kurbo/pull/375 [#376]: https://github.com/linebender/kurbo/pull/376 [Unreleased]: https://github.com/linebender/kurbo/compare/v0.11.1...HEAD [0.11.0]: https://github.com/linebender/kurbo/releases/tag/v0.11.0 [0.11.1]: https://github.com/linebender/kurbo/releases/tag/v0.11.1 [MSRV]: README.md#minimum-supported-rust-version-msrv kurbo-0.11.1/Cargo.lock0000644000000213410000000000100102120ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "arrayvec" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "bumpalo" version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "byteorder" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "dyn-clone" version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" [[package]] name = "getrandom" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "js-sys", "libc", "wasi", "wasm-bindgen", ] [[package]] name = "itoa" version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "js-sys" version = "0.3.70" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" dependencies = [ "wasm-bindgen", ] [[package]] name = "kurbo" version = "0.11.1" dependencies = [ "arrayvec", "getrandom", "libm", "mint", "rand", "schemars", "serde", "smallvec", ] [[package]] name = "libc" version = "0.2.158" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" [[package]] name = "libm" version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" [[package]] name = "log" version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "memchr" version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "mint" version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e53debba6bda7a793e5f99b8dacf19e626084f525f7829104ba9898f367d85ff" [[package]] name = "once_cell" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "ppv-lite86" version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" dependencies = [ "zerocopy", ] [[package]] name = "proc-macro2" version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ "proc-macro2", ] [[package]] name = "rand" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha", "rand_core", ] [[package]] name = "rand_chacha" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", "rand_core", ] [[package]] name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ "getrandom", ] [[package]] name = "ryu" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "schemars" version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09c024468a378b7e36765cd36702b7a90cc3cba11654f6685c8f233408e89e92" dependencies = [ "dyn-clone", "schemars_derive", "serde", "serde_json", "smallvec", ] [[package]] name = "schemars_derive" version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1eee588578aff73f856ab961cd2f79e36bc45d7ded33a7562adba4667aecc0e" dependencies = [ "proc-macro2", "quote", "serde_derive_internals", "syn", ] [[package]] name = "serde" version = "1.0.209" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.209" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_derive_internals" version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_json" version = "1.0.127" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8043c06d9f82bd7271361ed64f415fe5e12a77fdb52e573e7f06a516dea329ad" dependencies = [ "itoa", "memchr", "ryu", "serde", ] [[package]] name = "smallvec" version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" dependencies = [ "serde", ] [[package]] name = "syn" version = "2.0.76" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "578e081a14e0cefc3279b0472138c513f37b41a08d5a3cca9b6e4e8ceb6cd525" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" dependencies = [ "cfg-if", "once_cell", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" dependencies = [ "proc-macro2", "quote", "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" [[package]] name = "zerocopy" version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ "byteorder", "zerocopy-derive", ] [[package]] name = "zerocopy-derive" version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", "syn", ] kurbo-0.11.1/Cargo.toml0000644000000050140000000000100102340ustar # 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 are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" rust-version = "1.65" name = "kurbo" version = "0.11.1" authors = ["Raph Levien "] build = false autobins = false autoexamples = false autotests = false autobenches = false description = "A 2D curves library" readme = "README.md" keywords = [ "graphics", "curve", "curves", "bezier", "geometry", ] categories = ["graphics"] license = "MIT OR Apache-2.0" repository = "https://github.com/linebender/kurbo" [package.metadata.docs.rs] features = [ "mint", "schemars", "serde", ] [lib] name = "kurbo" path = "src/lib.rs" [[example]] name = "arclen_accuracy" path = "examples/arclen_accuracy.rs" [[example]] name = "circle" path = "examples/circle.rs" [[example]] name = "cubic_arclen" path = "examples/cubic_arclen.rs" [[example]] name = "ellipse" path = "examples/ellipse.rs" [[example]] name = "fit_poly" path = "examples/fit_poly.rs" [[example]] name = "offset" path = "examples/offset.rs" [[example]] name = "quad_intersect" path = "examples/quad_intersect.rs" [[example]] name = "simplify" path = "examples/simplify.rs" [[bench]] name = "cubic_arclen" path = "benches/cubic_arclen.rs" [[bench]] name = "quad_arclen" path = "benches/quad_arclen.rs" [[bench]] name = "quartic" path = "benches/quartic.rs" [[bench]] name = "rect_expand" path = "benches/rect_expand.rs" [dependencies.arrayvec] version = "0.7.6" default-features = false [dependencies.libm] version = "0.2.8" optional = true [dependencies.mint] version = "0.5.9" optional = true [dependencies.schemars] version = "0.8.21" optional = true [dependencies.serde] version = "1.0.209" features = [ "alloc", "derive", ] optional = true default-features = false [dependencies.smallvec] version = "1.13.2" [dev-dependencies.rand] version = "0.8.5" [features] default = ["std"] libm = ["dep:libm"] mint = ["dep:mint"] schemars = [ "schemars/smallvec", "dep:schemars", ] serde = [ "smallvec/serde", "dep:serde", ] std = [] [target.'cfg(target_arch="wasm32")'.dev-dependencies.getrandom] version = "0.2.15" features = ["js"] kurbo-0.11.1/Cargo.toml.orig000064400000000000000000000026311046102023000137170ustar 00000000000000[package] name = "kurbo" version = "0.11.1" authors = ["Raph Levien "] license = "MIT OR Apache-2.0" edition = "2021" # TODO: When this hits 1.74, move lint configuration into this file via a lints table. # Keep in sync with RUST_MIN_VER in .github/workflows/ci.yml, with the README.md file, # and with the MSRV in the `Unreleased` section of CHANGELOG.md. rust-version = "1.65" 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", "schemars", "serde"] [features] default = ["std"] std = [] libm = ["dep:libm"] mint = ["dep:mint"] serde=["smallvec/serde", "dep:serde"] schemars=["schemars/smallvec", "dep:schemars"] [dependencies] smallvec = "1.13.2" [dependencies.arrayvec] version = "0.7.6" default-features = false [dependencies.libm] version = "0.2.8" optional = true [dependencies.mint] version = "0.5.9" optional = true [dependencies.schemars] version = "0.8.21" optional = true [dependencies.serde] version = "1.0.209" optional = true default-features = false features = ["alloc", "derive"] # This is used for research but not really needed; maybe refactor. [dev-dependencies] rand = "0.8.5" [target.'cfg(target_arch="wasm32")'.dev-dependencies] getrandom = { version = "0.2.15", features = ["js"] } kurbo-0.11.1/LICENSE-APACHE000064400000000000000000000261361046102023000127620ustar 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.11.1/LICENSE-MIT000064400000000000000000000020371046102023000124640ustar 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.11.1/README.md000064400000000000000000000061701046102023000123110ustar 00000000000000# kurbo, a Rust 2D curves library [![Build Status](https://github.com/linebender/kurbo/actions/workflows/ci.yml/badge.svg)](https://github.com/linebender/kurbo/actions/workflows/ci.yml) [![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. ## Minimum supported Rust Version (MSRV) This version of Kurbo has been verified to compile with **Rust 1.65** and later. Future versions of Kurbo might increase the Rust version requirement. It will not be treated as a breaking change and as such can even happen with small patch releases.
Click here if compiling fails. As time has passed, some of Kurbo's dependencies could have released versions with a higher Rust requirement. If you encounter a compilation issue due to a dependency and don't want to upgrade your Rust toolchain, then you could downgrade the dependency. ```sh # Use the problematic dependency's name and version cargo update -p package_name --precise 0.1.1 ```
## 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 document any changes in [CHANGELOG.md] as part of your PR, and 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 [CHANGELOG.md]: ./CHANGELOG.md kurbo-0.11.1/benches/cubic_arclen.rs000064400000000000000000000030571046102023000154210ustar 00000000000000// Copyright 2018 the Kurbo Authors // SPDX-License-Identifier: Apache-2.0 OR MIT //! 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.11.1/benches/quad_arclen.rs000064400000000000000000000145231046102023000152660ustar 00000000000000// Copyright 2018 the Kurbo Authors // SPDX-License-Identifier: Apache-2.0 OR MIT //! 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.11.1/benches/quartic.rs000064400000000000000000000013221046102023000144510ustar 00000000000000// Copyright 2022 the Kurbo Authors // SPDX-License-Identifier: Apache-2.0 OR MIT //! Benchmarks of the quartic equation solver. #![cfg(nightly)] #![feature(test)] extern crate test; use kurbo::common::solve_quartic; use test::Bencher; #[bench] fn bench_quartic(bb: &mut Bencher) { let (x1, x2, x3, x4) = (1.0, 2.0, 3.0, 4.0); let a = -(x1 + x2 + x3 + x4); let b = x1 * (x2 + x3) + x2 * (x3 + x4) + x4 * (x1 + x3); let c = -x1 * x2 * (x3 + x4) - x3 * x4 * (x1 + x2); let d = x1 * x2 * x3 * x4; bb.iter(|| { solve_quartic( test::black_box(d), test::black_box(c), test::black_box(b), test::black_box(a), 1.0, ) }) } kurbo-0.11.1/benches/rect_expand.rs000064400000000000000000000032201046102023000152740ustar 00000000000000// Copyright 2020 the Kurbo Authors // SPDX-License-Identifier: Apache-2.0 OR MIT #![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.11.1/clippy.toml000064400000000000000000000007741046102023000132330ustar 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 # Don't warn about these identifiers when using clippy::doc_markdown. doc-valid-idents = ["Direct2D", "PostScript", ".."] kurbo-0.11.1/examples/arclen_accuracy.rs000064400000000000000000000210461046102023000163330ustar 00000000000000// Copyright 2018 the Kurbo Authors // SPDX-License-Identifier: Apache-2.0 OR MIT //! 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.11.1/examples/circle.rs000064400000000000000000000014121046102023000144510ustar 00000000000000// Copyright 2019 the Kurbo Authors // SPDX-License-Identifier: Apache-2.0 OR MIT //! Example of circle #[cfg(feature = "std")] fn main() { use kurbo::{Circle, Shape}; 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!(""); } #[cfg(not(feature = "std"))] fn main() { println!("This example requires the standard library"); } kurbo-0.11.1/examples/cubic_arclen.rs000064400000000000000000000175001046102023000156260ustar 00000000000000// Copyright 2018 the Kurbo Authors // SPDX-License-Identifier: Apache-2.0 OR MIT //! 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.11.1/examples/ellipse.rs000064400000000000000000000015431046102023000146520ustar 00000000000000// Copyright 2020 the Kurbo Authors // SPDX-License-Identifier: Apache-2.0 OR MIT //! Example of ellipse #[cfg(feature = "std")] fn main() { use kurbo::{Ellipse, Shape}; use std::f64::consts::PI; 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!(""); } #[cfg(not(feature = "std"))] fn main() { println!("This example requires the standard library"); } kurbo-0.11.1/examples/fit_poly.rs000064400000000000000000000046031046102023000150420ustar 00000000000000// Copyright 2022 the Kurbo Authors // SPDX-License-Identifier: Apache-2.0 OR MIT //! A test case for the cubic Bézier path simplifier, generating a very //! efficient and accurate SVG file to plot a quartic polynomial. This //! file was used to generate the image for the Wikipedia article on //! quartic polynomials. use std::{io::Write, ops::Range}; use kurbo::{BezPath, CurveFitSample, ParamCurveFit, PathEl, Point, Vec2}; struct MyPoly; fn eval_mypoly(x: f64) -> f64 { (x + 4.) * (x + 1.) * (x - 1.) * (x - 3.) / 14. + 0.5 } fn eval_mypoly_deriv(x: f64) -> f64 { (((4. * x + 3.) * x - 26.) * x - 1.) / 14. } impl ParamCurveFit for MyPoly { fn sample_pt_deriv(&self, t: f64) -> (Point, Vec2) { const S: f64 = 336.; let x = 37. + t * S; let math_x = (x - 220.) / 40.; let y = -eval_mypoly(math_x) * 40. + 260.; let dx = S; let dy = -dx * eval_mypoly_deriv(math_x); (Point::new(x, y), Vec2::new(dx, dy)) } fn sample_pt_tangent(&self, t: f64, _: f64) -> CurveFitSample { let (p, tangent) = self.sample_pt_deriv(t); CurveFitSample { p, tangent } } fn break_cusp(&self, _: Range) -> Option { None } } pub fn to_svg_economical(path: &BezPath) -> String { let mut buffer = Vec::new(); write_to(path, &mut buffer).unwrap(); String::from_utf8(buffer).unwrap() } /// Write the SVG representation of this path to the provided buffer. pub fn write_to(path: &BezPath, mut writer: W) -> std::io::Result<()> { for el in path.elements() { match *el { PathEl::MoveTo(p) => write!(writer, "M{:.2} {:.2}", 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{:.2} {:.2} {:.2} {:.2} {:.2} {:.2}", p1.x, p1.y, p2.x, p2.y, p3.x, p3.y )?, PathEl::ClosePath => write!(writer, "Z")?, } } Ok(()) } fn main() { println!(""); let path = kurbo::fit_to_bezpath_opt(&MyPoly, 0.1); println!( " ", to_svg_economical(&path) ); println!(""); } kurbo-0.11.1/examples/offset.rs000064400000000000000000000014271046102023000145040ustar 00000000000000// Copyright 2022 the Kurbo Authors // SPDX-License-Identifier: Apache-2.0 OR MIT //! A simple example to show an offset curve of a cubic Bézier segment. use kurbo::{offset::CubicOffset, CubicBez, Shape}; fn main() { println!(""); let c = CubicBez::new((100., 100.), (150., 75.), (300., 50.), (400., 200.)); println!( " ", c.to_path(1e-9).to_svg() ); for i in 1..=80 { let co = CubicOffset::new(c, i as f64 * 4.0); let path = kurbo::fit_to_bezpath_opt(&co, 1e-3); println!( " ", path.to_svg() ); } println!(""); } kurbo-0.11.1/examples/quad_intersect.rs000064400000000000000000000054261046102023000162330ustar 00000000000000// Copyright 2022 the Kurbo Authors // SPDX-License-Identifier: Apache-2.0 OR MIT //! Test case that demonstrates quadratic/quadratic intersection using //! new quartic solver. use arrayvec::ArrayVec; use kurbo::{common::solve_quartic, ParamCurve, Point, QuadBez, Shape}; use rand::{thread_rng, Rng}; fn rand_point() -> Point { let mut rng = thread_rng(); Point::new(rng.gen_range(0.0..500.0), rng.gen_range(0.0..500.0)) } fn rand_quad() -> QuadBez { QuadBez::new(rand_point(), rand_point(), rand_point()) } struct ImplicitQuad { x2: f64, xy: f64, y2: f64, x: f64, y: f64, c: f64, } impl ImplicitQuad { fn from_quad(q: QuadBez) -> Self { let Point { x: ax, y: ay } = q.p0; let Point { x: bx, y: by } = q.p1; let Point { x: cx, y: cy } = q.p2; let (u0, u1, u2) = (by - cy, cx - bx, bx * cy - by * cx); let (v0, v1, v2) = (cy - ay, ax - cx, cx * ay - cy * ax); let (w0, w1, w2) = (ay - by, bx - ax, ax * by - ay * bx); ImplicitQuad { x2: 4. * u0 * w0 - v0 * v0, xy: 4. * (u0 * w1 + u1 * w0) - 2. * v0 * v1, y2: 4. * u1 * w1 - v1 * v1, x: 4. * (u0 * w2 + u2 * w0) - 2. * v0 * v2, y: 4. * (u1 * w2 + u2 * w1) - 2. * v1 * v2, c: 4. * u2 * w2 - v2 * v2, } } fn eval(&self, x: f64, y: f64) -> f64 { self.x2 * x * x + self.xy * x * y + self.y2 * y * y + self.x * x + self.y * y + self.c } } fn intersect_quads(q0: QuadBez, q1: QuadBez) -> ArrayVec { let iq = ImplicitQuad::from_quad(q0); let c = q1.p0.to_vec2(); let b = (q1.p1 - q1.p0) * 2.0; let a = c - q1.p1.to_vec2() * 2.0 + q1.p2.to_vec2(); let c0 = iq.eval(c.x, c.y); let c1 = iq.x * b.x + iq.y * b.y + 2. * iq.x2 * (b.x * c.x) + 2. * iq.y2 * (b.y * c.y) + iq.xy * (b.x * c.y + b.y * c.x); let c2 = iq.x * a.x + iq.y * a.y + iq.x2 * (2. * a.x * c.x + b.x * b.x) + iq.xy * (a.x * c.y + b.x * b.y + a.y * c.x) + iq.y2 * (2. * a.y * c.y + b.y * b.y); let c3 = iq.x2 * 2. * a.x * b.x + iq.xy * (a.x * b.y + b.x * a.y) + iq.y2 * 2. * a.y * b.y; let c4 = iq.x2 * a.x * a.x + iq.xy * a.x * a.y + iq.y2 * a.y * a.y; let ts = solve_quartic(c0, c1, c2, c3, c4); println!("ts: {:?}", ts); ts } fn main() { let q0 = rand_quad(); let q1 = rand_quad(); println!( " ", q0.to_path(1e-9).to_svg() ); println!( " ", q1.to_path(1e-9).to_svg() ); for t in intersect_quads(q0, q1) { if (0.0..=1.0).contains(&t) { let p = q1.eval(t); println!(" ", p.x, p.y); } } } kurbo-0.11.1/examples/simplify.rs000064400000000000000000000032321046102023000150460ustar 00000000000000// Copyright 2022 the Kurbo Authors // SPDX-License-Identifier: Apache-2.0 OR MIT //! Test case showing simplification of a Bézier path. In this case, the //! source path is a simple mathematical function, but it should work for //! more general cases as well. use kurbo::{BezPath, Point}; fn plot_fn(f: &dyn Fn(f64) -> f64, d: &dyn Fn(f64) -> f64, xa: f64, xb: f64, n: usize) -> BezPath { let width = 800.; let dx = (xb - xa) / n as f64; let xs = width / (xb - xa); let ys = 250.; let y_origin = 300.; let plot = |x, y| Point::new((x - xa) * xs, y_origin - y * ys); let mut x0 = xa; let mut y0 = f(xa); let mut d0 = d(xa); let mut path = BezPath::new(); path.move_to(plot(x0, y0)); for i in 0..n { let x3 = xa + dx * (i + 1) as f64; let y3 = f(x3); let d3 = d(x3); let x1 = x0 + (1. / 3.) * dx; let x2 = x3 - (1. / 3.) * dx; let y1 = y0 + d0 * (1. / 3.) * dx; let y2 = y3 - d3 * (1. / 3.) * dx; path.curve_to(plot(x1, y1), plot(x2, y2), plot(x3, y3)); x0 = x3; y0 = y3; d0 = d3; } path } fn main() { println!(""); let path = plot_fn(&|x| x.sin(), &|x| x.cos(), -8., 8., 20); println!( " ", path.to_svg() ); let simpl = kurbo::simplify::SimplifyBezPath::new(path); let simplified_path = kurbo::fit_to_bezpath_opt(&simpl, 0.1); println!( " ", simplified_path.to_svg() ); println!(""); } kurbo-0.11.1/rustfmt.toml000064400000000000000000000001071046102023000134250ustar 00000000000000max_width = 100 use_field_init_shorthand = true newline_style = "Unix" kurbo-0.11.1/src/affine.rs000064400000000000000000000441241046102023000134200ustar 00000000000000// Copyright 2018 the Kurbo Authors // SPDX-License-Identifier: Apache-2.0 OR MIT //! Affine transforms. use core::ops::{Mul, MulAssign}; use crate::{Point, Rect, Vec2}; #[cfg(not(feature = "std"))] use crate::common::FloatFuncs; /// A 2D affine transform. #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Affine([f64; 6]); impl Affine { /// The identity transform. pub const IDENTITY: Affine = Affine::scale(1.0); /// 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`](std::ops::Mul) 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, c) = th.sin_cos(); Affine([c, s, -s, c, 0.0, 0.0]) } /// An affine transform representing a rotation of `th` radians about `center`. /// /// See [`Affine::rotate()`] for more info. #[inline] pub fn rotate_about(th: f64, center: Point) -> Affine { let center = center.to_vec2(); Self::translate(-center) .then_rotate(th) .then_translate(center) } /// 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]) } /// An affine transformation representing a skew. /// /// The `skew_x` and `skew_y` parameters represent skew factors for the /// horizontal and vertical directions, respectively. /// /// This is commonly used to generate a faux oblique transform for /// font rendering. In this case, you can slant the glyph 20 degrees /// clockwise in the horizontal direction (assuming a Y-up coordinate /// system): /// /// ``` /// let oblique_transform = kurbo::Affine::skew(20f64.to_radians().tan(), 0.0); /// ``` #[inline] pub fn skew(skew_x: f64, skew_y: f64) -> Affine { Affine([1.0, skew_y, skew_x, 1.0, 0.0, 0.0]) } /// Create an affine transform that represents reflection about the line `point + direction * t, t in (-infty, infty)` /// /// # Examples /// /// ``` /// # use kurbo::{Point, Vec2, Affine}; /// # fn assert_near(p0: Point, p1: Point) { /// # assert!((p1 - p0).hypot() < 1e-9, "{p0:?} != {p1:?}"); /// # } /// let point = Point::new(1., 0.); /// let vec = Vec2::new(1., 1.); /// let map = Affine::reflect(point, vec); /// assert_near(map * Point::new(1., 0.), Point::new(1., 0.)); /// assert_near(map * Point::new(2., 1.), Point::new(2., 1.)); /// assert_near(map * Point::new(2., 2.), Point::new(3., 1.)); /// ``` #[inline] #[must_use] pub fn reflect(point: impl Into, direction: impl Into) -> Self { let point = point.into(); let direction = direction.into(); let n = Vec2 { x: direction.y, y: -direction.x, } .normalize(); // Compute Householder reflection matrix let x2 = n.x * n.x; let xy = n.x * n.y; let y2 = n.y * n.y; // Here we also add in the post translation, because it doesn't require any further calc. let aff = Affine::new([ 1. - 2. * x2, -2. * xy, -2. * xy, 1. - 2. * y2, point.x, point.y, ]); aff.pre_translate(-point.to_vec2()) } /// A [rotation] by `th` followed by `self`. /// /// Equivalent to `self * Affine::rotate(th)` /// /// [rotation]: Affine::rotate #[inline] #[must_use] pub fn pre_rotate(self, th: f64) -> Self { self * Affine::rotate(th) } /// A [rotation] by `th` about `center` followed by `self`. /// /// Equivalent to `self * Affine::rotate_about(th, center)` /// /// [rotation]: Affine::rotate_about #[inline] #[must_use] pub fn pre_rotate_about(self, th: f64, center: Point) -> Self { Affine::rotate_about(th, center) * self } /// A [scale] by `scale` followed by `self`. /// /// Equivalent to `self * Affine::scale(scale)` /// /// [scale]: Affine::scale #[inline] #[must_use] pub fn pre_scale(self, scale: f64) -> Self { self * Affine::scale(scale) } /// A [scale] by `(scale_x, scale_y)` followed by `self`. /// /// Equivalent to `self * Affine::scale_non_uniform(scale_x, scale_y)` /// /// [scale]: Affine::scale_non_uniform #[inline] #[must_use] pub fn pre_scale_non_uniform(self, scale_x: f64, scale_y: f64) -> Self { self * Affine::scale_non_uniform(scale_x, scale_y) } /// A [translation] of `trans` followed by `self`. /// /// Equivalent to `self * Affine::translate(trans)` /// /// [translation]: Affine::translate #[inline] #[must_use] pub fn pre_translate(self, trans: Vec2) -> Self { self * Affine::translate(trans) } /// `self` followed by a [rotation] of `th`. /// /// Equivalent to `Affine::rotate(th) * self` /// /// [rotation]: Affine::rotate #[inline] #[must_use] pub fn then_rotate(self, th: f64) -> Self { Affine::rotate(th) * self } /// `self` followed by a [rotation] of `th` about `center`. /// /// Equivalent to `Affine::rotate_about(th, center) * self` /// /// [rotation]: Affine::rotate_about #[inline] #[must_use] pub fn then_rotate_about(self, th: f64, center: Point) -> Self { Affine::rotate_about(th, center) * self } /// `self` followed by a [scale] of `scale`. /// /// Equivalent to `Affine::scale(scale) * self` /// /// [scale]: Affine::scale #[inline] #[must_use] pub fn then_scale(self, scale: f64) -> Self { Affine::scale(scale) * self } /// `self` followed by a [scale] of `(scale_x, scale_y)`. /// /// Equivalent to `Affine::scale_non_uniform(scale_x, scale_y) * self` /// /// [scale]: Affine::scale_non_uniform #[inline] #[must_use] pub fn then_scale_non_uniform(self, scale_x: f64, scale_y: f64) -> Self { Affine::scale_non_uniform(scale_x, scale_y) * self } /// `self` followed by a translation of `trans`. /// /// Equivalent to `Affine::translate(trans) * self` /// /// [translation]: Affine::translate #[inline] #[must_use] pub fn then_translate(mut self, trans: Vec2) -> Self { self.0[4] += trans.x; self.0[5] += trans.y; self } /// Creates an affine transformation that takes the unit square to the given rectangle. /// /// Useful when you want to draw into the unit square but have your output fill any rectangle. /// In this case push the `Affine` onto the transform stack. pub fn map_unit_square(rect: Rect) -> Affine { Affine([rect.width(), 0., 0., rect.height(), rect.x0, rect.y0]) } /// 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)) } /// Is this map [finite]? /// /// [finite]: f64::is_finite #[inline] pub fn is_finite(&self) -> bool { self.0[0].is_finite() && self.0[1].is_finite() && self.0[2].is_finite() && self.0[3].is_finite() && self.0[4].is_finite() && self.0[5].is_finite() } /// Is this map [NaN]? /// /// [NaN]: f64::is_nan #[inline] pub fn is_nan(&self) -> bool { self.0[0].is_nan() || self.0[1].is_nan() || self.0[2].is_nan() || self.0[3].is_nan() || self.0[4].is_nan() || self.0[5].is_nan() } /// 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 fn translation(self) -> Vec2 { Vec2 { x: self.0[4], y: self.0[5], } } /// Replaces the translation portion of this affine map /// /// The translation can be seen as being applied after the linear part of the map. #[must_use] #[inline] pub fn with_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::IDENTITY } } 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, Vec2}; use std::f64::consts::PI; fn assert_near(p0: Point, p1: Point) { assert!((p1 - p0).hypot() < 1e-9, "{p0:?} != {p1:?}"); } fn affine_assert_near(a0: Affine, a1: Affine) { for i in 0..6 { assert!((a0.0[i] - a1.0[i]).abs() < 1e-9, "{a0:?} != {a1:?}"); } } #[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)); assert_near(Affine::skew(0.0, 0.0) * p, p); assert_near(Affine::skew(2.0, 4.0) * p, Point::new(11.0, 16.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); } #[test] fn reflection() { affine_assert_near( Affine::reflect(Point::ZERO, (1., 0.)), Affine::new([1., 0., 0., -1., 0., 0.]), ); affine_assert_near( Affine::reflect(Point::ZERO, (0., 1.)), Affine::new([-1., 0., 0., 1., 0., 0.]), ); // y = x affine_assert_near( Affine::reflect(Point::ZERO, (1., 1.)), Affine::new([0., 1., 1., 0., 0., 0.]), ); // no translate let point = Point::new(0., 0.); let vec = Vec2::new(1., 1.); let map = Affine::reflect(point, vec); assert_near(map * Point::new(0., 0.), Point::new(0., 0.)); assert_near(map * Point::new(1., 1.), Point::new(1., 1.)); assert_near(map * Point::new(1., 2.), Point::new(2., 1.)); // with translate let point = Point::new(1., 0.); let vec = Vec2::new(1., 1.); let map = Affine::reflect(point, vec); assert_near(map * Point::new(1., 0.), Point::new(1., 0.)); assert_near(map * Point::new(2., 1.), Point::new(2., 1.)); assert_near(map * Point::new(2., 2.), Point::new(3., 1.)); } } kurbo-0.11.1/src/arc.rs000064400000000000000000000156521046102023000127410ustar 00000000000000// Copyright 2019 the Kurbo Authors // SPDX-License-Identifier: Apache-2.0 OR MIT //! An ellipse arc. use crate::{Affine, Ellipse, PathEl, Point, Rect, Shape, Vec2}; use core::{ f64::consts::{FRAC_PI_2, PI}, iter, ops::Mul, }; #[cfg(not(feature = "std"))] use crate::common::FloatFuncs; /// A single elliptical arc segment. #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[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 a new `Arc`. pub fn new( center: impl Into, radii: impl Into, start_angle: f64, sweep_angle: f64, x_rotation: f64, ) -> Self { Self { center: center.into(), radii: radii.into(), start_angle, sweep_angle, x_rotation, } } /// Returns a copy of this `Arc` in the opposite direction. /// /// The new `Arc` will sweep towards the original `Arc`s /// start angle. #[must_use] #[inline] pub fn reversed(&self) -> Arc { Self { center: self.center, radii: self.radii, start_angle: self.start_angle + self.sweep_angle, sweep_angle: -self.sweep_angle, x_rotation: self.x_rotation, } } /// Create an iterator generating Bezier path elements. /// /// The generated elements can be appended 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 ellipse 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. /// /// The closure `p` will be invoked with the control points 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 (angle_sin, angle_cos) = angle.sin_cos(); 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 { let (angle_sin, angle_cos) = angle.sin_cos(); 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> = 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 } /// The perimeter of the arc. /// /// For now we just approximate by using the bezier curve representation. #[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() } } impl Mul for Affine { type Output = Arc; fn mul(self, arc: Arc) -> Self::Output { let ellipse = self * Ellipse::new(arc.center, arc.radii, arc.x_rotation); let center = ellipse.center(); let (radii, rotation) = ellipse.radii_and_rotation(); Arc { center, radii, x_rotation: rotation, start_angle: arc.start_angle, sweep_angle: arc.sweep_angle, } } } #[cfg(test)] mod tests { use super::*; #[test] fn reversed_arc() { let a = Arc::new((0., 0.), (1., 0.), 0., PI, 0.); let f = a.reversed(); // Most fields should be unchanged: assert_eq!(a.center, f.center); assert_eq!(a.radii, f.radii); assert_eq!(a.x_rotation, f.x_rotation); // Sweep angle should be in reverse assert_eq!(a.sweep_angle, -f.sweep_angle); // Reversing it again should result in the original arc assert_eq!(a, f.reversed()); } } kurbo-0.11.1/src/bezpath.rs000064400000000000000000002173731046102023000136350ustar 00000000000000// Copyright 2018 the Kurbo Authors // SPDX-License-Identifier: Apache-2.0 OR MIT //! Bézier paths (up to cubic). #![allow(clippy::many_single_char_names)] use core::iter::{Extend, FromIterator}; use core::mem; use core::ops::{Mul, Range}; use alloc::vec::Vec; use arrayvec::ArrayVec; use crate::common::{solve_cubic, solve_quadratic}; use crate::MAX_EXTREMA; use crate::{ Affine, CubicBez, Line, Nearest, ParamCurve, ParamCurveArclen, ParamCurveArea, ParamCurveExtrema, ParamCurveNearest, Point, QuadBez, Rect, Shape, TranslateScale, Vec2, }; #[cfg(not(feature = "std"))] use crate::common::FloatFuncs; /// 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. /// /// Conceptually, a `BezPath` contains zero or more subpaths. Each subpath /// *always* begins with a `MoveTo`, then has zero or more `LineTo`, `QuadTo`, /// and `CurveTo` elements, and optionally ends with a `ClosePath`. /// /// 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/ /// [`iter`]: BezPath::iter /// [`segments`]: BezPath::segments /// [`flatten`]: flatten /// [`intersect_line`]: PathSeg::intersect_line /// [`segments` free function]: segments /// [`FromIterator`]: std::iter::FromIterator /// [`Extend`]: std::iter::Extend #[derive(Clone, Default, Debug, PartialEq)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[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 = "schemars", derive(schemars::JsonSchema))] #[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 = "schemars", derive(schemars::JsonSchema))] #[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. #[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, } /// The minimum distance between two Bézier curves. pub struct MinDistance { /// The shortest distance between any two points on the two curves. pub distance: f64, /// The position of the nearest point on the first curve, as a parameter. /// /// To resolve this to a [`Point`], use [`ParamCurve::eval`]. /// /// [`ParamCurve::eval`]: crate::ParamCurve::eval pub t1: f64, /// The position of the nearest point on the second curve, as a parameter. /// /// To resolve this to a [`Point`], use [`ParamCurve::eval`]. /// /// [`ParamCurve::eval`]: crate::ParamCurve::eval pub t2: 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 { debug_assert!( v.first().is_none() || matches!(v.first(), Some(PathEl::MoveTo(_))), "BezPath must begin with MoveTo" ); BezPath(v) } /// Removes the last [`PathEl`] from the path and returns it, or `None` if the path is empty. pub fn pop(&mut self) -> Option { self.0.pop() } /// Push a generic path element onto the path. pub fn push(&mut self, el: PathEl) { self.0.push(el); debug_assert!( matches!(self.0.first(), Some(PathEl::MoveTo(_))), "BezPath must begin with MoveTo" ); } /// 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. /// /// Will panic with a debug assert when the path is empty and there is no /// "move to" element on the path. /// /// If `line_to` is called immediately after `close_path` then the current /// subpath starts at the initial point of the previous subpath. pub fn line_to>(&mut self, p: P) { debug_assert!(!self.0.is_empty(), "uninitialized subpath (missing MoveTo)"); self.push(PathEl::LineTo(p.into())); } /// Push a "quad to" element onto the path. /// /// Will panic with a debug assert when the path is empty and there is no /// "move to" element on the path. /// /// If `quad_to` is called immediately after `close_path` then the current /// subpath starts at the initial point of the previous subpath. pub fn quad_to>(&mut self, p1: P, p2: P) { debug_assert!(!self.0.is_empty(), "uninitialized subpath (missing MoveTo)"); self.push(PathEl::QuadTo(p1.into(), p2.into())); } /// Push a "curve to" element onto the path. /// /// Will panic with a debug assert when the path is empty and there is no /// "move to" element on the path. /// /// If `curve_to` is called immediately after `close_path` then the current /// subpath starts at the initial point of the previous subpath. pub fn curve_to>(&mut self, p1: P, p2: P, p3: P) { debug_assert!(!self.0.is_empty(), "uninitialized subpath (missing MoveTo)"); self.push(PathEl::CurveTo(p1.into(), p2.into(), p3.into())); } /// Push a "close path" element onto the path. /// /// Will panic with a debug assert when the path is empty and there is no /// "move to" element on the path. pub fn close_path(&mut self) { debug_assert!(!self.0.is_empty(), "uninitialized subpath (missing MoveTo)"); self.push(PathEl::ClosePath); } /// Get the path elements. pub fn elements(&self) -> &[PathEl] { &self.0 } /// Get the path elements (mut version). pub fn elements_mut(&mut self) -> &mut [PathEl] { &mut self.0 } /// Returns an iterator over the path's elements. pub fn iter(&self) -> impl Iterator + Clone + '_ { self.0.iter().copied() } /// Iterate over the path segments. pub fn segments(&self) -> impl Iterator + Clone + '_ { segments(self.iter()) } /// Shorten the path, keeping the first `len` elements. pub fn truncate(&mut self, len: usize) { self.0.truncate(len); } /// Flatten the path, invoking the callback repeatedly. /// /// See [`flatten`] for more discussion. #[deprecated(since = "0.11.1", note = "use the free function flatten instead")] pub fn flatten(&self, tolerance: f64, callback: impl FnMut(PathEl)) { flatten(self, tolerance, callback); } /// Get the segment at the given element index. /// /// If you need to access all segments, [`segments`] provides a better /// API. This is intended for random access of specific elements, for clients /// that require this specifically. /// /// **note**: This returns the segment that ends at the provided element /// index. In effect this means it is *1-indexed*: since no segment ends at /// the first element (which is presumed to be a `MoveTo`) `get_seg(0)` will /// always return `None`. 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) if start != last => { 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); } } /// Is this path finite? #[inline] pub fn is_finite(&self) -> bool { self.0.iter().all(|v| v.is_finite()) } /// Is this path NaN? #[inline] pub fn is_nan(&self) -> bool { self.0.iter().any(|v| v.is_nan()) } /// Returns a rectangle that conservatively encloses the path. /// /// Unlike the `bounding_box` method, this uses control points directly /// rather than computing tight bounds for curve elements. pub fn control_box(&self) -> Rect { let mut cbox: Option = None; let mut add_pts = |pts: &[Point]| { for pt in pts { cbox = match cbox { Some(cbox) => Some(cbox.union_pt(*pt)), _ => Some(Rect::from_points(*pt, *pt)), }; } }; for &el in self.elements() { match el { PathEl::MoveTo(p0) | PathEl::LineTo(p0) => add_pts(&[p0]), PathEl::QuadTo(p0, p1) => add_pts(&[p0, p1]), PathEl::CurveTo(p0, p1, p2) => add_pts(&[p0, p1, p2]), PathEl::ClosePath => {} } } cbox.unwrap_or_default() } /// Returns a new path with the winding direction of all subpaths reversed. pub fn reverse_subpaths(&self) -> BezPath { let elements = self.elements(); let mut start_ix = 1; let mut start_pt = Point::default(); let mut reversed = BezPath(Vec::with_capacity(elements.len())); // Pending move is used to capture degenerate subpaths that should // remain in the reversed output. let mut pending_move = false; for (ix, el) in elements.iter().enumerate() { match el { PathEl::MoveTo(pt) => { if pending_move { reversed.push(PathEl::MoveTo(start_pt)); } if start_ix < ix { reverse_subpath(start_pt, &elements[start_ix..ix], &mut reversed); } pending_move = true; start_pt = *pt; start_ix = ix + 1; } PathEl::ClosePath => { if start_ix <= ix { reverse_subpath(start_pt, &elements[start_ix..ix], &mut reversed); } reversed.push(PathEl::ClosePath); start_ix = ix + 1; pending_move = false; } _ => { pending_move = false; } } } if start_ix < elements.len() { reverse_subpath(start_pt, &elements[start_ix..], &mut reversed); } else if pending_move { reversed.push(PathEl::MoveTo(start_pt)); } reversed } } /// Helper for reversing a subpath. /// /// The `els` parameter must not contain any `MoveTo` or `ClosePath` elements. fn reverse_subpath(start_pt: Point, els: &[PathEl], reversed: &mut BezPath) { let end_pt = els.last().and_then(|el| el.end_point()).unwrap_or(start_pt); reversed.push(PathEl::MoveTo(end_pt)); for (ix, el) in els.iter().enumerate().rev() { let end_pt = if ix > 0 { els[ix - 1].end_point().unwrap() } else { start_pt }; match el { PathEl::LineTo(_) => reversed.push(PathEl::LineTo(end_pt)), PathEl::QuadTo(c0, _) => reversed.push(PathEl::QuadTo(*c0, end_pt)), PathEl::CurveTo(c0, c1, _) => reversed.push(PathEl::CurveTo(*c1, *c0, end_pt)), _ => panic!("reverse_subpath expects MoveTo and ClosePath to be removed"), } } } 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 from simply iterating over the /// slice, as it returns `PathEl` items, rather than references. impl<'a> IntoIterator for &'a BezPath { type Item = PathEl; type IntoIter = core::iter::Cloned>; fn into_iter(self) -> Self::IntoIter { self.elements().iter().cloned() } } impl IntoIterator for BezPath { type Item = PathEl; type IntoIter = alloc::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. /// /// 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 antialiased 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. /// /// [Flattening quadratic Béziers]: https://raphlinus.github.io/graphics/curves/2019/12/23/flatten-quadbez.html 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 { 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(params, 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`]. /// 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`] function. #[derive(Clone)] pub struct Segments> { elements: I, start_last: Option<(Point, Point)>, } impl> Iterator for Segments { type Item = PathSeg; fn next(&mut self) -> Option { for el in &mut self.elements { // 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), } } fn inv_arclen(&self, arclen: f64, accuracy: f64) -> f64 { match *self { PathSeg::Line(line) => line.inv_arclen(arclen, accuracy), PathSeg::Quad(quad) => quad.inv_arclen(arclen, accuracy), PathSeg::Cubic(cubic) => cubic.inv_arclen(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) -> Nearest { 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 { match *self { PathSeg::Line(line) => line.extrema(), PathSeg::Quad(quad) => quad.extrema(), PathSeg::Cubic(cubic) => cubic.extrema(), } } } impl PathSeg { /// Get the [`PathEl`] that is equivalent to discarding the segment start point. pub fn as_path_el(&self) -> PathEl { match self { PathSeg::Line(line) => PathEl::LineTo(line.p1), PathSeg::Quad(q) => PathEl::QuadTo(q.p1, q.p2), PathSeg::Cubic(c) => PathEl::CurveTo(c.p1, c.p2, c.p3), } } /// 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 (0.0..=1.0).contains(&t) { 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 (0.0..=1.0).contains(&t) { 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 { 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 (-EPSILON..=(1.0 + EPSILON)).contains(&t) { // 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 (0.0..=1.0).contains(&u) { 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 solve_quadratic(c0, c1, c2) { if (-EPSILON..=(1.0 + EPSILON)).contains(&t) { 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 (0.0..=1.0).contains(&u) { 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 solve_cubic(c0, c1, c2, c3) { if (-EPSILON..=(1.0 + EPSILON)).contains(&t) { 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 (0.0..=1.0).contains(&u) { result.push(LineIntersection::new(u, t)); } } } } } result } /// Is this Bezier path finite? #[inline] pub fn is_finite(&self) -> bool { match self { PathSeg::Line(line) => line.is_finite(), PathSeg::Quad(quad_bez) => quad_bez.is_finite(), PathSeg::Cubic(cubic_bez) => cubic_bez.is_finite(), } } /// Is this Bezier path NaN? #[inline] pub fn is_nan(&self) -> bool { match self { PathSeg::Line(line) => line.is_nan(), PathSeg::Quad(quad_bez) => quad_bez.is_nan(), PathSeg::Cubic(cubic_bez) => cubic_bez.is_nan(), } } #[inline] fn as_vec2_vec(&self) -> ArrayVec { let mut a = ArrayVec::new(); match self { PathSeg::Line(l) => { a.push(l.p0.to_vec2()); a.push(l.p1.to_vec2()); } PathSeg::Quad(q) => { a.push(q.p0.to_vec2()); a.push(q.p1.to_vec2()); a.push(q.p2.to_vec2()); } PathSeg::Cubic(c) => { a.push(c.p0.to_vec2()); a.push(c.p1.to_vec2()); a.push(c.p2.to_vec2()); a.push(c.p3.to_vec2()); } }; a } /// Minimum distance between two [`PathSeg`]s. /// /// Returns a tuple of the distance, the path time `t1` of the closest point /// on the first `PathSeg`, and the path time `t2` of the closest point on the /// second `PathSeg`. pub fn min_dist(&self, other: PathSeg, accuracy: f64) -> MinDistance { let (distance, t1, t2) = crate::mindist::min_dist_param( &self.as_vec2_vec(), &other.as_vec2_vec(), (0.0, 1.0), (0.0, 1.0), accuracy, None, ); MinDistance { distance: distance.sqrt(), t1, t2, } } /// Compute endpoint tangents of a path segment. /// /// This version is robust to the path segment not being a regular curve. pub(crate) fn tangents(&self) -> (Vec2, Vec2) { const EPS: f64 = 1e-12; match self { PathSeg::Line(l) => { let d = l.p1 - l.p0; (d, d) } PathSeg::Quad(q) => { let d01 = q.p1 - q.p0; let d0 = if d01.hypot2() > EPS { d01 } else { q.p2 - q.p0 }; let d12 = q.p2 - q.p1; let d1 = if d12.hypot2() > EPS { d12 } else { q.p2 - q.p0 }; (d0, d1) } PathSeg::Cubic(c) => { let d01 = c.p1 - c.p0; let d0 = if d01.hypot2() > EPS { d01 } else { let d02 = c.p2 - c.p0; if d02.hypot2() > EPS { d02 } else { c.p3 - c.p0 } }; let d23 = c.p3 - c.p2; let d1 = if d23.hypot2() > EPS { d23 } else { let d13 = c.p3 - c.p1; if d13.hypot2() > EPS { d13 } else { c.p3 - c.p0 } }; (d0, d1) } } } } impl LineIntersection { fn new(line_t: f64, segment_t: f64) -> Self { LineIntersection { line_t, segment_t } } /// Is this line intersection finite? #[inline] pub fn is_finite(self) -> bool { self.line_t.is_finite() && self.segment_t.is_finite() } /// Is this line intersection NaN? #[inline] pub fn is_nan(self) -> bool { self.line_t.is_nan() || self.segment_t.is_nan() } } // 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<'iter> = core::iter::Copied>; fn path_elements(&self, _tolerance: f64) -> Self::PathElementsIter<'_> { self.0.iter().copied() } 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) } } impl PathEl { /// Is this path element finite? #[inline] pub fn is_finite(&self) -> bool { match self { PathEl::MoveTo(p) => p.is_finite(), PathEl::LineTo(p) => p.is_finite(), PathEl::QuadTo(p, p2) => p.is_finite() && p2.is_finite(), PathEl::CurveTo(p, p2, p3) => p.is_finite() && p2.is_finite() && p3.is_finite(), PathEl::ClosePath => true, } } /// Is this path element NaN? #[inline] pub fn is_nan(&self) -> bool { match self { PathEl::MoveTo(p) => p.is_nan(), PathEl::LineTo(p) => p.is_nan(), PathEl::QuadTo(p, p2) => p.is_nan() || p2.is_nan(), PathEl::CurveTo(p, p2, p3) => p.is_nan() || p2.is_nan() || p3.is_nan(), PathEl::ClosePath => false, } } /// Get the end point of the path element, if it exists. pub fn end_point(&self) -> Option { match self { PathEl::MoveTo(p) => Some(*p), PathEl::LineTo(p1) => Some(*p1), PathEl::QuadTo(_, p2) => Some(*p2), PathEl::CurveTo(_, _, p3) => Some(*p3), _ => None, } } } /// 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<'iter> = core::iter::Copied> where 'a: 'iter; #[inline] fn path_elements(&self, _tolerance: f64) -> Self::PathElementsIter<'_> { self.iter().copied() } 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) } } /// Implements [`Shape`] for an array of [`PathEl`], provided that the first element of the array is /// not a `PathEl::ClosePath`. If it is, several of these functions will panic. /// /// If the array starts with `LineTo`, `QuadTo`, or `CurveTo`, it will be treated as a `MoveTo`. impl Shape for [PathEl; N] { type PathElementsIter<'iter> = core::iter::Copied>; #[inline] fn path_elements(&self, _tolerance: f64) -> Self::PathElementsIter<'_> { self.iter().copied() } 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<'iter> = 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 crate::{Circle, DEFAULT_ACCURACY}; use super::*; fn assert_approx_eq(x: f64, y: f64) { assert!((x - y).abs() < 1e-8, "{x} != {y}"); } #[test] #[should_panic(expected = "uninitialized subpath")] 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] #[should_panic(expected = "uninitialized subpath")] fn test_must_not_start_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(); } #[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); } #[test] fn test_contains() { let mut path = BezPath::new(); path.move_to((0.0, 0.0)); path.line_to((1.0, 1.0)); path.line_to((2.0, 0.0)); path.close_path(); assert_eq!(path.winding(Point::new(1.0, 0.5)), -1); assert!(path.contains(Point::new(1.0, 0.5))); } // get_seg(i) should produce the same results as path_segments().nth(i - 1). #[test] fn test_get_seg() { let circle = Circle::new((10.0, 10.0), 2.0).to_path(DEFAULT_ACCURACY); let segments = circle.path_segments(DEFAULT_ACCURACY).collect::>(); let get_segs = (1..usize::MAX) .map_while(|i| circle.get_seg(i)) .collect::>(); assert_eq!(segments, get_segs); } #[test] fn test_control_box() { // a sort of map ping looking thing drawn with a single cubic // cbox is wildly different than tight box let path = BezPath::from_svg("M200,300 C50,50 350,50 200,300").unwrap(); assert_eq!(Rect::new(50.0, 50.0, 350.0, 300.0), path.control_box()); assert!(path.control_box().area() > path.bounding_box().area()); } #[test] fn test_reverse_unclosed() { let path = BezPath::from_svg("M10,10 Q40,40 60,10 L100,10 C125,10 150,50 125,60").unwrap(); let reversed = path.reverse_subpaths(); assert_eq!( "M125,60 C150,50 125,10 100,10 L60,10 Q40,40 10,10", reversed.to_svg() ); } #[test] fn test_reverse_closed_triangle() { let path = BezPath::from_svg("M100,100 L150,200 L50,200 Z").unwrap(); let reversed = path.reverse_subpaths(); assert_eq!("M50,200 L150,200 L100,100 Z", reversed.to_svg()); } #[test] fn test_reverse_closed_shape() { let path = BezPath::from_svg( "M125,100 Q200,150 175,300 C150,150 50,150 25,300 Q0,150 75,100 L100,50 Z", ) .unwrap(); let reversed = path.reverse_subpaths(); assert_eq!( "M100,50 L75,100 Q0,150 25,300 C50,150 150,150 175,300 Q200,150 125,100 Z", reversed.to_svg() ); } #[test] fn test_reverse_multiple_subpaths() { let svg = "M10,10 Q40,40 60,10 L100,10 C125,10 150,50 125,60 M100,100 L150,200 L50,200 Z M125,100 Q200,150 175,300 C150,150 50,150 25,300 Q0,150 75,100 L100,50 Z"; let expected_svg = "M125,60 C150,50 125,10 100,10 L60,10 Q40,40 10,10 M50,200 L150,200 L100,100 Z M100,50 L75,100 Q0,150 25,300 C50,150 150,150 175,300 Q200,150 125,100 Z"; let path = BezPath::from_svg(svg).unwrap(); let reversed = path.reverse_subpaths(); assert_eq!(expected_svg, reversed.to_svg()); } // https://github.com/fonttools/fonttools/blob/bf265ce49e0cae6f032420a4c80c31d8e16285b8/Tests/pens/reverseContourPen_test.py#L7 #[test] fn test_reverse_lines() { let mut path = BezPath::new(); path.move_to((0.0, 0.0)); path.line_to((1.0, 1.0)); path.line_to((2.0, 2.0)); path.line_to((3.0, 3.0)); path.close_path(); let rev = path.reverse_subpaths(); assert_eq!("M3,3 L2,2 L1,1 L0,0 Z", rev.to_svg()); } #[test] fn test_reverse_multiple_moves() { reverse_test_helper( vec![ PathEl::MoveTo((2.0, 2.0).into()), PathEl::MoveTo((3.0, 3.0).into()), PathEl::ClosePath, PathEl::MoveTo((4.0, 4.0).into()), ], vec![ PathEl::MoveTo((2.0, 2.0).into()), PathEl::MoveTo((3.0, 3.0).into()), PathEl::ClosePath, PathEl::MoveTo((4.0, 4.0).into()), ], ); } // The following are direct port of fonttools' // reverseContourPen_test.py::test_reverse_pen, adapted to rust, excluding // test cases that don't apply because we don't implement // outputImpliedClosingLine=False. // https://github.com/fonttools/fonttools/blob/85c80be/Tests/pens/reverseContourPen_test.py#L6-L467 #[test] fn test_reverse_closed_last_line_not_on_move() { reverse_test_helper( vec![ PathEl::MoveTo((0.0, 0.0).into()), PathEl::LineTo((1.0, 1.0).into()), PathEl::LineTo((2.0, 2.0).into()), PathEl::LineTo((3.0, 3.0).into()), PathEl::ClosePath, ], vec![ PathEl::MoveTo((3.0, 3.0).into()), PathEl::LineTo((2.0, 2.0).into()), PathEl::LineTo((1.0, 1.0).into()), PathEl::LineTo((0.0, 0.0).into()), // closing line NOT implied PathEl::ClosePath, ], ); } #[test] fn test_reverse_closed_last_line_overlaps_move() { reverse_test_helper( vec![ PathEl::MoveTo((0.0, 0.0).into()), PathEl::LineTo((1.0, 1.0).into()), PathEl::LineTo((2.0, 2.0).into()), PathEl::LineTo((0.0, 0.0).into()), PathEl::ClosePath, ], vec![ PathEl::MoveTo((0.0, 0.0).into()), PathEl::LineTo((2.0, 2.0).into()), PathEl::LineTo((1.0, 1.0).into()), PathEl::LineTo((0.0, 0.0).into()), // closing line NOT implied PathEl::ClosePath, ], ); } #[test] fn test_reverse_closed_duplicate_line_following_move() { reverse_test_helper( vec![ PathEl::MoveTo((0.0, 0.0).into()), PathEl::LineTo((0.0, 0.0).into()), PathEl::LineTo((1.0, 1.0).into()), PathEl::LineTo((2.0, 2.0).into()), PathEl::ClosePath, ], vec![ PathEl::MoveTo((2.0, 2.0).into()), PathEl::LineTo((1.0, 1.0).into()), PathEl::LineTo((0.0, 0.0).into()), // duplicate line retained PathEl::LineTo((0.0, 0.0).into()), PathEl::ClosePath, ], ); } #[test] fn test_reverse_closed_two_lines() { reverse_test_helper( vec![ PathEl::MoveTo((0.0, 0.0).into()), PathEl::LineTo((1.0, 1.0).into()), PathEl::ClosePath, ], vec![ PathEl::MoveTo((1.0, 1.0).into()), PathEl::LineTo((0.0, 0.0).into()), // closing line NOT implied PathEl::ClosePath, ], ); } #[test] fn test_reverse_closed_last_curve_overlaps_move() { reverse_test_helper( vec![ PathEl::MoveTo((0.0, 0.0).into()), PathEl::CurveTo((1.0, 1.0).into(), (2.0, 2.0).into(), (3.0, 3.0).into()), PathEl::CurveTo((4.0, 4.0).into(), (5.0, 5.0).into(), (0.0, 0.0).into()), PathEl::ClosePath, ], vec![ PathEl::MoveTo((0.0, 0.0).into()), // no extra lineTo added here PathEl::CurveTo((5.0, 5.0).into(), (4.0, 4.0).into(), (3.0, 3.0).into()), PathEl::CurveTo((2.0, 2.0).into(), (1.0, 1.0).into(), (0.0, 0.0).into()), PathEl::ClosePath, ], ); } #[test] fn test_reverse_closed_last_curve_not_on_move() { reverse_test_helper( vec![ PathEl::MoveTo((0.0, 0.0).into()), PathEl::CurveTo((1.0, 1.0).into(), (2.0, 2.0).into(), (3.0, 3.0).into()), PathEl::CurveTo((4.0, 4.0).into(), (5.0, 5.0).into(), (6.0, 6.0).into()), PathEl::ClosePath, ], vec![ PathEl::MoveTo((6.0, 6.0).into()), // the previously implied line PathEl::CurveTo((5.0, 5.0).into(), (4.0, 4.0).into(), (3.0, 3.0).into()), PathEl::CurveTo((2.0, 2.0).into(), (1.0, 1.0).into(), (0.0, 0.0).into()), PathEl::ClosePath, ], ); } #[test] fn test_reverse_closed_line_curve_line() { reverse_test_helper( vec![ PathEl::MoveTo((0.0, 0.0).into()), PathEl::LineTo((1.0, 1.0).into()), // this line... PathEl::CurveTo((2.0, 2.0).into(), (3.0, 3.0).into(), (4.0, 4.0).into()), PathEl::CurveTo((5.0, 5.0).into(), (6.0, 6.0).into(), (7.0, 7.0).into()), PathEl::ClosePath, ], vec![ PathEl::MoveTo((7.0, 7.0).into()), PathEl::CurveTo((6.0, 6.0).into(), (5.0, 5.0).into(), (4.0, 4.0).into()), PathEl::CurveTo((3.0, 3.0).into(), (2.0, 2.0).into(), (1.0, 1.0).into()), PathEl::LineTo((0.0, 0.0).into()), // ... does NOT become implied PathEl::ClosePath, ], ); } #[test] fn test_reverse_closed_last_quad_overlaps_move() { reverse_test_helper( vec![ PathEl::MoveTo((0.0, 0.0).into()), PathEl::QuadTo((1.0, 1.0).into(), (2.0, 2.0).into()), PathEl::QuadTo((3.0, 3.0).into(), (0.0, 0.0).into()), PathEl::ClosePath, ], vec![ PathEl::MoveTo((0.0, 0.0).into()), // no extra lineTo added here PathEl::QuadTo((3.0, 3.0).into(), (2.0, 2.0).into()), PathEl::QuadTo((1.0, 1.0).into(), (0.0, 0.0).into()), PathEl::ClosePath, ], ); } #[test] fn test_reverse_closed_last_quad_not_on_move() { reverse_test_helper( vec![ PathEl::MoveTo((0.0, 0.0).into()), PathEl::QuadTo((1.0, 1.0).into(), (2.0, 2.0).into()), PathEl::QuadTo((3.0, 3.0).into(), (4.0, 4.0).into()), PathEl::ClosePath, ], vec![ PathEl::MoveTo((4.0, 4.0).into()), // the previously implied line PathEl::QuadTo((3.0, 3.0).into(), (2.0, 2.0).into()), PathEl::QuadTo((1.0, 1.0).into(), (0.0, 0.0).into()), PathEl::ClosePath, ], ); } #[test] fn test_reverse_closed_line_quad_line() { reverse_test_helper( vec![ PathEl::MoveTo((0.0, 0.0).into()), PathEl::LineTo((1.0, 1.0).into()), // this line... PathEl::QuadTo((2.0, 2.0).into(), (3.0, 3.0).into()), PathEl::ClosePath, ], vec![ PathEl::MoveTo((3.0, 3.0).into()), PathEl::QuadTo((2.0, 2.0).into(), (1.0, 1.0).into()), PathEl::LineTo((0.0, 0.0).into()), // ... does NOT become implied PathEl::ClosePath, ], ); } #[test] fn test_reverse_empty() { reverse_test_helper(vec![], vec![]); } #[test] fn test_reverse_single_point() { reverse_test_helper( vec![PathEl::MoveTo((0.0, 0.0).into())], vec![PathEl::MoveTo((0.0, 0.0).into())], ); } #[test] fn test_reverse_single_point_closed() { reverse_test_helper( vec![PathEl::MoveTo((0.0, 0.0).into()), PathEl::ClosePath], vec![PathEl::MoveTo((0.0, 0.0).into()), PathEl::ClosePath], ); } #[test] fn test_reverse_single_line_open() { reverse_test_helper( vec![ PathEl::MoveTo((0.0, 0.0).into()), PathEl::LineTo((1.0, 1.0).into()), ], vec![ PathEl::MoveTo((1.0, 1.0).into()), PathEl::LineTo((0.0, 0.0).into()), ], ); } #[test] fn test_reverse_single_curve_open() { reverse_test_helper( vec![ PathEl::MoveTo((0.0, 0.0).into()), PathEl::CurveTo((1.0, 1.0).into(), (2.0, 2.0).into(), (3.0, 3.0).into()), ], vec![ PathEl::MoveTo((3.0, 3.0).into()), PathEl::CurveTo((2.0, 2.0).into(), (1.0, 1.0).into(), (0.0, 0.0).into()), ], ); } #[test] fn test_reverse_curve_line_open() { reverse_test_helper( vec![ PathEl::MoveTo((0.0, 0.0).into()), PathEl::CurveTo((1.0, 1.0).into(), (2.0, 2.0).into(), (3.0, 3.0).into()), PathEl::LineTo((4.0, 4.0).into()), ], vec![ PathEl::MoveTo((4.0, 4.0).into()), PathEl::LineTo((3.0, 3.0).into()), PathEl::CurveTo((2.0, 2.0).into(), (1.0, 1.0).into(), (0.0, 0.0).into()), ], ); } #[test] fn test_reverse_line_curve_open() { reverse_test_helper( vec![ PathEl::MoveTo((0.0, 0.0).into()), PathEl::LineTo((1.0, 1.0).into()), PathEl::CurveTo((2.0, 2.0).into(), (3.0, 3.0).into(), (4.0, 4.0).into()), ], vec![ PathEl::MoveTo((4.0, 4.0).into()), PathEl::CurveTo((3.0, 3.0).into(), (2.0, 2.0).into(), (1.0, 1.0).into()), PathEl::LineTo((0.0, 0.0).into()), ], ); } #[test] fn test_reverse_duplicate_point_after_move() { // Test case from: https://github.com/googlei18n/cu2qu/issues/51#issue-179370514 // Simplified to only use atomic PathEl::QuadTo (no QuadSplines). reverse_test_helper( vec![ PathEl::MoveTo((848.0, 348.0).into()), PathEl::LineTo((848.0, 348.0).into()), PathEl::QuadTo((848.0, 526.0).into(), (449.0, 704.0).into()), PathEl::QuadTo((848.0, 171.0).into(), (848.0, 348.0).into()), PathEl::ClosePath, ], vec![ PathEl::MoveTo((848.0, 348.0).into()), PathEl::QuadTo((848.0, 171.0).into(), (449.0, 704.0).into()), PathEl::QuadTo((848.0, 526.0).into(), (848.0, 348.0).into()), PathEl::LineTo((848.0, 348.0).into()), PathEl::ClosePath, ], ); } #[test] fn test_reverse_duplicate_point_at_end() { // Test case from: https://github.com/googlefonts/fontmake/issues/572 reverse_test_helper( vec![ PathEl::MoveTo((0.0, 651.0).into()), PathEl::LineTo((0.0, 101.0).into()), PathEl::LineTo((0.0, 101.0).into()), PathEl::LineTo((0.0, 651.0).into()), PathEl::LineTo((0.0, 651.0).into()), PathEl::ClosePath, ], vec![ PathEl::MoveTo((0.0, 651.0).into()), PathEl::LineTo((0.0, 651.0).into()), PathEl::LineTo((0.0, 101.0).into()), PathEl::LineTo((0.0, 101.0).into()), PathEl::LineTo((0.0, 651.0).into()), PathEl::ClosePath, ], ); } fn reverse_test_helper(contour: Vec, expected: Vec) { assert_eq!(BezPath(contour).reverse_subpaths().0, expected); } } kurbo-0.11.1/src/circle.rs000064400000000000000000000264711046102023000134360ustar 00000000000000// Copyright 2019 the Kurbo Authors // SPDX-License-Identifier: Apache-2.0 OR MIT //! Implementation of circle shape. use core::{ f64::consts::{FRAC_PI_2, PI}, iter, ops::{Add, Mul, Sub}, }; use crate::{Affine, Arc, ArcAppendIter, Ellipse, PathEl, Point, Rect, Shape, Vec2}; #[cfg(not(feature = "std"))] use crate::common::FloatFuncs; /// A circle. #[derive(Clone, Copy, Default, Debug, PartialEq)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[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. 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, } } /// Is this circle [finite]? /// /// [finite]: f64::is_finite #[inline] pub fn is_finite(&self) -> bool { self.center.is_finite() && self.radius.is_finite() } /// Is this circle [NaN]? /// /// [NaN]: f64::is_nan #[inline] pub fn is_nan(&self) -> bool { self.center.is_nan() || self.radius.is_nan() } } 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<'iter> = 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 (s0, c0) = th0.sin_cos(); let (s1, c1) = if ix == self.n { (0.0, 1.0) } else { th1.sin_cos() }; 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. #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 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, } } /// Return an arc representing the outer radius. #[must_use] #[inline] pub fn outer_arc(&self) -> Arc { 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, } } /// Return an arc representing the inner radius. /// /// This is [reversed] from the outer arc, so that it is in the /// same direction as the arc that would be drawn (as the path /// elements for this circle segment produce a closed path). /// /// [reversed]: Arc::reversed #[must_use] #[inline] pub fn inner_arc(&self) -> Arc { 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, } } /// Is this circle segment [finite]? /// /// [finite]: f64::is_finite #[inline] pub fn is_finite(&self) -> bool { self.center.is_finite() && self.outer_radius.is_finite() && self.inner_radius.is_finite() && self.start_angle.is_finite() && self.sweep_angle.is_finite() } /// Is this circle segment [NaN]? /// /// [NaN]: f64::is_nan #[inline] pub fn is_nan(&self) -> bool { self.center.is_nan() || self.outer_radius.is_nan() || self.inner_radius.is_nan() || self.start_angle.is_nan() || self.sweep_angle.is_nan() } } 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 = iter::Chain< iter::Chain< iter::Chain, iter::Once>, ArcAppendIter>, iter::Once, >, ArcAppendIter, >; impl Shape for CircleSegment { type PathElementsIter<'iter> = 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(self.outer_arc().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(self.inner_arc().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) } } #[inline] fn point_on_circle(center: Point, radius: f64, angle: f64) -> Point { let (angle_sin, angle_cos) = angle.sin_cos(); center + Vec2 { x: angle_cos * radius, y: angle_sin * radius, } } #[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)); } } kurbo-0.11.1/src/common.rs000064400000000000000000001146331046102023000134630ustar 00000000000000// Copyright 2018 the Kurbo Authors // SPDX-License-Identifier: Apache-2.0 OR MIT //! Common mathematical operations #![allow(missing_docs)] #[cfg(not(feature = "std"))] mod sealed { /// A [sealed trait](https://predr.ag/blog/definitive-guide-to-sealed-traits-in-rust/) /// which stops [`super::FloatFuncs`] from being implemented outside kurbo. This could /// be relaxed in the future if there is are good reasons to allow external impls. /// The benefit from being sealed is that we can add methods without breaking downstream /// implementations. pub trait FloatFuncsSealed {} } use arrayvec::ArrayVec; /// Defines a trait that chooses between libstd or libm implementations of float methods. macro_rules! define_float_funcs { ($( fn $name:ident(self $(,$arg:ident: $arg_ty:ty)*) -> $ret:ty => $lname:ident/$lfname:ident; )+) => { /// Since core doesn't depend upon libm, this provides libm implementations /// of float functions which are typically provided by the std library, when /// the `std` feature is not enabled. /// /// For documentation see the respective functions in the std library. #[cfg(not(feature = "std"))] pub trait FloatFuncs : Sized + sealed::FloatFuncsSealed { /// Special implementation for signum, because libm doesn't have it. fn signum(self) -> Self; $(fn $name(self $(,$arg: $arg_ty)*) -> $ret;)+ } #[cfg(not(feature = "std"))] impl sealed::FloatFuncsSealed for f32 {} #[cfg(not(feature = "std"))] impl FloatFuncs for f32 { #[inline] fn signum(self) -> f32 { if self.is_nan() { f32::NAN } else { 1.0_f32.copysign(self) } } $(fn $name(self $(,$arg: $arg_ty)*) -> $ret { #[cfg(feature = "libm")] return libm::$lfname(self $(,$arg as _)*); #[cfg(not(feature = "libm"))] compile_error!("kurbo requires either the `std` or `libm` feature") })+ } #[cfg(not(feature = "std"))] impl sealed::FloatFuncsSealed for f64 {} #[cfg(not(feature = "std"))] impl FloatFuncs for f64 { #[inline] fn signum(self) -> f64 { if self.is_nan() { f64::NAN } else { 1.0_f64.copysign(self) } } $(fn $name(self $(,$arg: $arg_ty)*) -> $ret { #[cfg(feature = "libm")] return libm::$lname(self $(,$arg as _)*); #[cfg(not(feature = "libm"))] compile_error!("kurbo requires either the `std` or `libm` feature") })+ } } } define_float_funcs! { fn abs(self) -> Self => fabs/fabsf; fn acos(self) -> Self => acos/acosf; fn atan2(self, other: Self) -> Self => atan2/atan2f; fn cbrt(self) -> Self => cbrt/cbrtf; fn ceil(self) -> Self => ceil/ceilf; fn cos(self) -> Self => cos/cosf; fn copysign(self, sign: Self) -> Self => copysign/copysignf; fn floor(self) -> Self => floor/floorf; fn hypot(self, other: Self) -> Self => hypot/hypotf; fn ln(self) -> Self => log/logf; fn log2(self) -> Self => log2/log2f; fn mul_add(self, a: Self, b: Self) -> Self => fma/fmaf; fn powi(self, n: i32) -> Self => pow/powf; fn powf(self, n: Self) -> Self => pow/powf; fn round(self) -> Self => round/roundf; fn sin(self) -> Self => sin/sinf; fn sin_cos(self) -> (Self, Self) => sincos/sincosf; fn sqrt(self) -> Self => sqrt/sqrtf; fn tan(self) -> Self => tan/tanf; fn trunc(self) -> Self => trunc/truncf; } /// 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: /// /// That implementation is in turn based on Jim Blinn's "How to Solve a Cubic /// Equation", which is masterful. /// /// Return 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 { let mut result = ArrayVec::new(); let c3_recip = c3.recip(); const ONETHIRD: f64 = 1. / 3.; let scaled_c2 = c2 * (ONETHIRD * c3_recip); let scaled_c1 = c1 * (ONETHIRD * 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. return solve_quadratic(c0, c1, c2).iter().copied().collect(); } let (c0, c1, c2) = (scaled_c0, scaled_c1, scaled_c2); // (d0, d1, d2) is called "Delta" in article let d0 = (-c2).mul_add(c2, c1); let d1 = (-c1).mul_add(c2, c0); let d2 = c2 * c0 - c1 * c1; // d is called "Discriminant" let d = 4.0 * d0 * d2 - d1 * d1; // de is called "Depressed.x", Depressed.y = d0 let de = (-2.0 * c2).mul_add(d0, d1); // TODO: handle the cases where these intermediate results overflow. if d < 0.0 { let sq = (-0.25 * d).sqrt(); let r = -0.5 * de; let t1 = (r + sq).cbrt() + (r - sq).cbrt(); result.push(t1 - c2); } else if d == 0.0 { let t1 = (-d0).sqrt().copysign(de); result.push(t1 - c2); result.push(-2.0 * t1 - c2); } else { let th = d.sqrt().atan2(-de) * ONETHIRD; // (th_cos, th_sin) is called "CubicRoot" let (th_sin, th_cos) = th.sin_cos(); // (r0, r1, r2) is called "Root" let r0 = th_cos; let ss3 = th_sin * 3.0f64.sqrt(); let r1 = 0.5 * (-th_cos + ss3); let r2 = 0.5 * (-th_cos - ss3); let t = 2.0 * (-d0).sqrt(); result.push(t.mul_add(r0, -c2)); result.push(t.mul_add(r1, -c2)); result.push(t.mul_add(r2, -c2)); } result } /// Find real roots of quadratic equation. /// /// Return 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 { 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 } /// Compute epsilon relative to coefficient. /// /// A helper function from the Orellana and De Michele paper. fn eps_rel(raw: f64, a: f64) -> f64 { if a == 0.0 { raw.abs() } else { ((raw - a) / a).abs() } } /// Find real roots of a quartic equation. /// /// This is a fairly literal implementation of the method described in: /// Algorithm 1010: Boosting Efficiency in Solving Quartic Equations with /// No Compromise in Accuracy, Orellana and De Michele, ACM /// Transactions on Mathematical Software, Vol. 46, No. 2, May 2020. pub fn solve_quartic(c0: f64, c1: f64, c2: f64, c3: f64, c4: f64) -> ArrayVec { if c4 == 0.0 { return solve_cubic(c0, c1, c2, c3).iter().copied().collect(); } if c0 == 0.0 { // Note: appends 0 root at end, doesn't sort. We might want to do that. return solve_cubic(c1, c2, c3, c4) .iter() .copied() .chain(Some(0.0)) .collect(); } let a = c3 / c4; let b = c2 / c4; let c = c1 / c4; let d = c0 / c4; if let Some(result) = solve_quartic_inner(a, b, c, d, false) { return result; } // Do polynomial rescaling const K_Q: f64 = 7.16e76; for rescale in [false, true] { if let Some(result) = solve_quartic_inner( a / K_Q, b / K_Q.powi(2), c / K_Q.powi(3), d / K_Q.powi(4), rescale, ) { return result.iter().map(|x| x * K_Q).collect(); } } // Overflow happened, just return no roots. //println!("overflow, no roots returned"); Default::default() } fn solve_quartic_inner(a: f64, b: f64, c: f64, d: f64, rescale: bool) -> Option> { factor_quartic_inner(a, b, c, d, rescale).map(|quadratics| { quadratics .iter() .flat_map(|(a, b)| solve_quadratic(*b, *a, 1.0)) .collect() }) } /// Factor a quartic into two quadratics. /// /// Attempt to factor a quartic equation into two quadratic equations. Returns `None` either if there /// is overflow (in which case rescaling might succeed) or the factorization would result in /// complex coefficients. /// /// Discussion question: distinguish the two cases in return value? pub fn factor_quartic_inner( a: f64, b: f64, c: f64, d: f64, rescale: bool, ) -> Option> { let calc_eps_q = |a1, b1, a2, b2| { let eps_a = eps_rel(a1 + a2, a); let eps_b = eps_rel(b1 + a1 * a2 + b2, b); let eps_c = eps_rel(b1 * a2 + a1 * b2, c); eps_a + eps_b + eps_c }; let calc_eps_t = |a1, b1, a2, b2| calc_eps_q(a1, b1, a2, b2) + eps_rel(b1 * b2, d); let disc = 9. * a * a - 24. * b; let s = if disc >= 0.0 { -2. * b / (3. * a + disc.sqrt().copysign(a)) } else { -0.25 * a }; let a_prime = a + 4. * s; let b_prime = b + 3. * s * (a + 2. * s); let c_prime = c + s * (2. * b + s * (3. * a + 4. * s)); let d_prime = d + s * (c + s * (b + s * (a + s))); let g_prime; let h_prime; const K_C: f64 = 3.49e102; if rescale { let a_prime_s = a_prime / K_C; let b_prime_s = b_prime / K_C; let c_prime_s = c_prime / K_C; let d_prime_s = d_prime / K_C; g_prime = a_prime_s * c_prime_s - (4. / K_C) * d_prime_s - (1. / 3.) * b_prime_s.powi(2); h_prime = (a_prime_s * c_prime_s + (8. / K_C) * d_prime_s - (2. / 9.) * b_prime_s.powi(2)) * (1. / 3.) * b_prime_s - c_prime_s * (c_prime_s / K_C) - a_prime_s.powi(2) * d_prime_s; } else { g_prime = a_prime * c_prime - 4. * d_prime - (1. / 3.) * b_prime.powi(2); h_prime = (a_prime * c_prime + 8. * d_prime - (2. / 9.) * b_prime.powi(2)) * (1. / 3.) * b_prime - c_prime.powi(2) - a_prime.powi(2) * d_prime; } if !(g_prime.is_finite() && h_prime.is_finite()) { return None; } let phi = depressed_cubic_dominant(g_prime, h_prime); let phi = if rescale { phi * K_C } else { phi }; let l_1 = a * 0.5; let l_3 = (1. / 6.) * b + 0.5 * phi; let delt_2 = c - a * l_3; let d_2_cand_1 = (2. / 3.) * b - phi - l_1 * l_1; let l_2_cand_1 = 0.5 * delt_2 / d_2_cand_1; let l_2_cand_2 = 2. * (d - l_3 * l_3) / delt_2; let d_2_cand_2 = 0.5 * delt_2 / l_2_cand_2; let d_2_cand_3 = d_2_cand_1; let l_2_cand_3 = l_2_cand_2; let mut d_2_best = 0.0; let mut l_2_best = 0.0; let mut eps_l_best = 0.0; for (i, (d_2, l_2)) in [ (d_2_cand_1, l_2_cand_1), (d_2_cand_2, l_2_cand_2), (d_2_cand_3, l_2_cand_3), ] .iter() .enumerate() { let eps_0 = eps_rel(d_2 + l_1 * l_1 + 2. * l_3, b); let eps_1 = eps_rel(2. * (d_2 * l_2 + l_1 * l_3), c); let eps_2 = eps_rel(d_2 * l_2 * l_2 + l_3 * l_3, d); let eps_l = eps_0 + eps_1 + eps_2; if i == 0 || eps_l < eps_l_best { d_2_best = *d_2; l_2_best = *l_2; eps_l_best = eps_l; } } let d_2 = d_2_best; let l_2 = l_2_best; let mut alpha_1; let mut beta_1; let mut alpha_2; let mut beta_2; //println!("phi = {}, d_2 = {}", phi, d_2); if d_2 < 0.0 { let sq = (-d_2).sqrt(); alpha_1 = l_1 + sq; beta_1 = l_3 + sq * l_2; alpha_2 = l_1 - sq; beta_2 = l_3 - sq * l_2; if beta_2.abs() < beta_1.abs() { beta_2 = d / beta_1; } else if beta_2.abs() > beta_1.abs() { beta_1 = d / beta_2; } let cands; if alpha_1.abs() != alpha_2.abs() { if alpha_1.abs() < alpha_2.abs() { let a1_cand_1 = (c - beta_1 * alpha_2) / beta_2; let a1_cand_2 = (b - beta_2 - beta_1) / alpha_2; let a1_cand_3 = a - alpha_2; // Note: cand 3 is first because it is infallible, simplifying logic cands = [ (a1_cand_3, alpha_2), (a1_cand_1, alpha_2), (a1_cand_2, alpha_2), ]; } else { let a2_cand_1 = (c - alpha_1 * beta_2) / beta_1; let a2_cand_2 = (b - beta_2 - beta_1) / alpha_1; let a2_cand_3 = a - alpha_1; cands = [ (alpha_1, a2_cand_3), (alpha_1, a2_cand_1), (alpha_1, a2_cand_2), ]; } let mut eps_q_best = 0.0; for (i, (a1, a2)) in cands.iter().enumerate() { if a1.is_finite() && a2.is_finite() { let eps_q = calc_eps_q(*a1, beta_1, *a2, beta_2); if i == 0 || eps_q < eps_q_best { alpha_1 = *a1; alpha_2 = *a2; eps_q_best = eps_q; } } } } } else if d_2 == 0.0 { let d_3 = d - l_3 * l_3; alpha_1 = l_1; beta_1 = l_3 + (-d_3).sqrt(); alpha_2 = l_1; beta_2 = l_3 - (-d_3).sqrt(); if beta_1.abs() > beta_2.abs() { beta_2 = d / beta_1; } else if beta_2.abs() > beta_1.abs() { beta_1 = d / beta_2; } // TODO: handle case d_2 is very small? } else { // This case means no real roots; in the most general case we might want // to factor into quadratic equations with complex coefficients. return None; } // Newton-Raphson iteration on alpha/beta coeff's. let mut eps_t = calc_eps_t(alpha_1, beta_1, alpha_2, beta_2); for _ in 0..8 { //println!("a1 {} b1 {} a2 {} b2 {}", alpha_1, beta_1, alpha_2, beta_2); //println!("eps_t = {:e}", eps_t); if eps_t == 0.0 { break; } let f_0 = beta_1 * beta_2 - d; let f_1 = beta_1 * alpha_2 + alpha_1 * beta_2 - c; let f_2 = beta_1 + alpha_1 * alpha_2 + beta_2 - b; let f_3 = alpha_1 + alpha_2 - a; let c_1 = alpha_1 - alpha_2; let det_j = beta_1 * beta_1 - beta_1 * (alpha_2 * c_1 + 2. * beta_2) + beta_2 * (alpha_1 * c_1 + beta_2); if det_j == 0.0 { break; } let inv = det_j.recip(); let c_2 = beta_2 - beta_1; let c_3 = beta_1 * alpha_2 - alpha_1 * beta_2; let dz_0 = c_1 * f_0 + c_2 * f_1 + c_3 * f_2 - (beta_1 * c_2 + alpha_1 * c_3) * f_3; let dz_1 = (alpha_1 * c_1 + c_2) * f_0 - beta_1 * c_1 * f_1 - beta_1 * c_2 * f_2 - beta_1 * c_3 * f_3; let dz_2 = -c_1 * f_0 - c_2 * f_1 - c_3 * f_2 + (alpha_2 * c_3 + beta_2 * c_2) * f_3; let dz_3 = -(alpha_2 * c_1 + c_2) * f_0 + beta_2 * c_1 * f_1 + beta_2 * c_2 * f_2 + beta_2 * c_3 * f_3; let a1 = alpha_1 - inv * dz_0; let b1 = beta_1 - inv * dz_1; let a2 = alpha_2 - inv * dz_2; let b2 = beta_2 - inv * dz_3; let new_eps_t = calc_eps_t(a1, b1, a2, b2); // We break if the new eps is equal, paper keeps going if new_eps_t < eps_t { alpha_1 = a1; beta_1 = b1; alpha_2 = a2; beta_2 = b2; eps_t = new_eps_t; } else { //println!("new_eps_t got worse: {:e}", new_eps_t); break; } } Some([(alpha_1, beta_1), (alpha_2, beta_2)].into()) } /// Dominant root of depressed cubic x^3 + gx + h = 0. /// /// Section 2.2 of Orellana and De Michele. // Note: some of the techniques in here might be useful to improve the // cubic solver, and vice versa. fn depressed_cubic_dominant(g: f64, h: f64) -> f64 { let q = (-1. / 3.) * g; let r = 0.5 * h; let phi_0; let k = if q.abs() < 1e102 && r.abs() < 1e154 { None } else if q.abs() < r.abs() { Some(1. - q * (q / r).powi(2)) } else { Some(q.signum() * ((r / q).powi(2) / q - 1.0)) }; if k.is_some() && r == 0.0 { if g > 0.0 { phi_0 = 0.0; } else { phi_0 = (-g).sqrt(); } } else if k.map(|k| k < 0.0).unwrap_or_else(|| r * r < q.powi(3)) { let t = if k.is_some() { r / q / q.sqrt() } else { r / q.powi(3).sqrt() }; phi_0 = -2. * q.sqrt() * (t.abs().acos() * (1. / 3.)).cos().copysign(t); } else { let a = if let Some(k) = k { if q.abs() < r.abs() { -r * (1. + k.sqrt()) } else { -r - (q.abs().sqrt() * q * k.sqrt()).copysign(r) } } else { -r - (r * r - q.powi(3)).sqrt().copysign(r) } .cbrt(); let b = if a == 0.0 { 0.0 } else { q / a }; phi_0 = a + b; } // Refine with Newton-Raphson iteration let mut x = phi_0; let mut f = (x * x + g) * x + h; //println!("g = {:e}, h = {:e}, x = {:e}, f = {:e}", g, h, x, f); const EPS_M: f64 = 2.22045e-16; if f.abs() < EPS_M * x.powi(3).max(g * x).max(h) { return x; } for _ in 0..8 { let delt_f = 3. * x * x + g; if delt_f == 0.0 { break; } let new_x = x - f / delt_f; let new_f = (new_x * new_x + g) * new_x + h; //println!("delt_f = {:e}, new_f = {:e}", delt_f, new_f); if new_f == 0.0 { return new_x; } if new_f.abs() >= f.abs() { break; } x = new_x; f = new_f; } x } /// Solve an arbitrary function for a zero-crossing. /// /// This uses the [ITP method], as described in the paper /// [An Enhancement of the Bisection Method Average Performance Preserving Minmax Optimality]. /// /// The values of `ya` and `yb` are given as arguments rather than /// computed from `f`, as the values may already be known, or they may /// be less expensive to compute as special cases. /// /// It is assumed that `ya < 0.0` and `yb > 0.0`, otherwise unexpected /// results may occur. /// /// The value of `epsilon` must be larger than 2^-63 times `b - a`, /// otherwise integer overflow may occur. The `a` and `b` parameters /// represent the lower and upper bounds of the bracket searched for a /// solution. /// /// The ITP method has tuning parameters. This implementation hardwires /// k2 to 2, both because it avoids an expensive floating point /// exponentiation, and because this value has been tested to work well /// with curve fitting problems. /// /// The `n0` parameter controls the relative impact of the bisection and /// secant components. When it is 0, the number of iterations is /// guaranteed to be no more than the number required by bisection (thus, /// this method is strictly superior to bisection). However, when the /// function is smooth, a value of 1 gives the secant method more of a /// chance to engage, so the average number of iterations is likely /// lower, though there can be one more iteration than bisection in the /// worst case. /// /// The `k1` parameter is harder to characterize, and interested users /// are referred to the paper, as well as encouraged to do empirical /// testing. To match the paper, a value of `0.2 / (b - a)` is /// suggested, and this is confirmed to give good results. /// /// When the function is monotonic, the returned result is guaranteed to /// be within `epsilon` of the zero crossing. For more detailed analysis, /// again see the paper. /// /// [ITP method]: https://en.wikipedia.org/wiki/ITP_Method /// [An Enhancement of the Bisection Method Average Performance Preserving Minmax Optimality]: https://dl.acm.org/doi/10.1145/3423597 #[allow(clippy::too_many_arguments)] pub fn solve_itp( mut f: impl FnMut(f64) -> f64, mut a: f64, mut b: f64, epsilon: f64, n0: usize, k1: f64, mut ya: f64, mut yb: f64, ) -> f64 { let n1_2 = (((b - a) / epsilon).log2().ceil() - 1.0).max(0.0) as usize; let nmax = n0 + n1_2; let mut scaled_epsilon = epsilon * (1u64 << nmax) as f64; while b - a > 2.0 * epsilon { let x1_2 = 0.5 * (a + b); let r = scaled_epsilon - 0.5 * (b - a); let xf = (yb * a - ya * b) / (yb - ya); let sigma = x1_2 - xf; // This has k2 = 2 hardwired for efficiency. let delta = k1 * (b - a).powi(2); let xt = if delta <= (x1_2 - xf).abs() { xf + delta.copysign(sigma) } else { x1_2 }; let xitp = if (xt - x1_2).abs() <= r { xt } else { x1_2 - r.copysign(sigma) }; let yitp = f(xitp); if yitp > 0.0 { b = xitp; yb = yitp; } else if yitp < 0.0 { a = xitp; ya = yitp; } else { return xitp; } scaled_epsilon *= 0.5; } 0.5 * (a + b) } /// A variant ITP solver that allows fallible functions. /// /// Another difference: it returns the bracket that contains the root, /// which may be important if the function has a discontinuity. #[allow(clippy::too_many_arguments)] pub(crate) fn solve_itp_fallible( mut f: impl FnMut(f64) -> Result, mut a: f64, mut b: f64, epsilon: f64, n0: usize, k1: f64, mut ya: f64, mut yb: f64, ) -> Result<(f64, f64), E> { let n1_2 = (((b - a) / epsilon).log2().ceil() - 1.0).max(0.0) as usize; let nmax = n0 + n1_2; let mut scaled_epsilon = epsilon * (1u64 << nmax) as f64; while b - a > 2.0 * epsilon { let x1_2 = 0.5 * (a + b); let r = scaled_epsilon - 0.5 * (b - a); let xf = (yb * a - ya * b) / (yb - ya); let sigma = x1_2 - xf; // This has k2 = 2 hardwired for efficiency. let delta = k1 * (b - a).powi(2); let xt = if delta <= (x1_2 - xf).abs() { xf + delta.copysign(sigma) } else { x1_2 }; let xitp = if (xt - x1_2).abs() <= r { xt } else { x1_2 - r.copysign(sigma) }; let yitp = f(xitp)?; if yitp > 0.0 { b = xitp; yb = yitp; } else if yitp < 0.0 { a = xitp; ya = yitp; } else { return Ok((xitp, xitp)); } scaled_epsilon *= 0.5; } Ok((a, b)) } // 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_4: &[(f64, f64)] = &[ (0.6521451548625461, -0.3399810435848563), (0.6521451548625461, 0.3399810435848563), (0.3478548451374538, -0.8611363115940526), (0.3478548451374538, 0.8611363115940526), ]; 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_6: &[(f64, f64)] = &[ (0.3607615730481386, 0.6612093864662645), (0.3607615730481386, -0.6612093864662645), (0.4679139345726910, -0.2386191860831969), (0.4679139345726910, 0.2386191860831969), (0.1713244923791704, -0.9324695142031521), (0.1713244923791704, 0.9324695142031521), ]; 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_8: &[(f64, f64)] = &[ (0.3626837833783620, -0.1834346424956498), (0.3626837833783620, 0.1834346424956498), (0.3137066458778873, -0.5255324099163290), (0.3137066458778873, 0.5255324099163290), (0.2223810344533745, -0.7966664774136267), (0.2223810344533745, 0.7966664774136267), (0.1012285362903763, -0.9602898564975363), (0.1012285362903763, 0.9602898564975363), ]; pub const GAUSS_LEGENDRE_COEFFS_8_HALF: &[(f64, f64)] = &[ (0.3626837833783620, 0.1834346424956498), (0.3137066458778873, 0.5255324099163290), (0.2223810344533745, 0.7966664774136267), (0.1012285362903763, 0.9602898564975363), ]; 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_16: &[(f64, f64)] = &[ (0.1894506104550685, -0.0950125098376374), (0.1894506104550685, 0.0950125098376374), (0.1826034150449236, -0.2816035507792589), (0.1826034150449236, 0.2816035507792589), (0.1691565193950025, -0.4580167776572274), (0.1691565193950025, 0.4580167776572274), (0.1495959888165767, -0.6178762444026438), (0.1495959888165767, 0.6178762444026438), (0.1246289712555339, -0.7554044083550030), (0.1246289712555339, 0.7554044083550030), (0.0951585116824928, -0.8656312023878318), (0.0951585116824928, 0.8656312023878318), (0.0622535239386479, -0.9445750230732326), (0.0622535239386479, 0.9445750230732326), (0.0271524594117541, -0.9894009349916499), (0.0271524594117541, 0.9894009349916499), ]; // Just the positive x_i values. pub const GAUSS_LEGENDRE_COEFFS_16_HALF: &[(f64, f64)] = &[ (0.1894506104550685, 0.0950125098376374), (0.1826034150449236, 0.2816035507792589), (0.1691565193950025, 0.4580167776572274), (0.1495959888165767, 0.6178762444026438), (0.1246289712555339, 0.7554044083550030), (0.0951585116824928, 0.8656312023878318), (0.0622535239386479, 0.9445750230732326), (0.0271524594117541, 0.9894009349916499), ]; 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), ]; pub const GAUSS_LEGENDRE_COEFFS_24_HALF: &[(f64, f64)] = &[ (0.1279381953467522, 0.0640568928626056), (0.1258374563468283, 0.1911188674736163), (0.1216704729278034, 0.3150426796961634), (0.1155056680537256, 0.4337935076260451), (0.1074442701159656, 0.5454214713888396), (0.0976186521041139, 0.6480936519369755), (0.0861901615319533, 0.7401241915785544), (0.0733464814110803, 0.8200019859739029), (0.0592985849154368, 0.8864155270044011), (0.0442774388174198, 0.9382745520027328), (0.0285313886289337, 0.9747285559713095), (0.0123412297999872, 0.9951872199970213), ]; pub const GAUSS_LEGENDRE_COEFFS_32: &[(f64, f64)] = &[ (0.0965400885147278, -0.0483076656877383), (0.0965400885147278, 0.0483076656877383), (0.0956387200792749, -0.1444719615827965), (0.0956387200792749, 0.1444719615827965), (0.0938443990808046, -0.2392873622521371), (0.0938443990808046, 0.2392873622521371), (0.0911738786957639, -0.3318686022821277), (0.0911738786957639, 0.3318686022821277), (0.0876520930044038, -0.4213512761306353), (0.0876520930044038, 0.4213512761306353), (0.0833119242269467, -0.5068999089322294), (0.0833119242269467, 0.5068999089322294), (0.0781938957870703, -0.5877157572407623), (0.0781938957870703, 0.5877157572407623), (0.0723457941088485, -0.6630442669302152), (0.0723457941088485, 0.6630442669302152), (0.0658222227763618, -0.7321821187402897), (0.0658222227763618, 0.7321821187402897), (0.0586840934785355, -0.7944837959679424), (0.0586840934785355, 0.7944837959679424), (0.0509980592623762, -0.8493676137325700), (0.0509980592623762, 0.8493676137325700), (0.0428358980222267, -0.8963211557660521), (0.0428358980222267, 0.8963211557660521), (0.0342738629130214, -0.9349060759377397), (0.0342738629130214, 0.9349060759377397), (0.0253920653092621, -0.9647622555875064), (0.0253920653092621, 0.9647622555875064), (0.0162743947309057, -0.9856115115452684), (0.0162743947309057, 0.9856115115452684), (0.0070186100094701, -0.9972638618494816), (0.0070186100094701, 0.9972638618494816), ]; pub const GAUSS_LEGENDRE_COEFFS_32_HALF: &[(f64, f64)] = &[ (0.0965400885147278, 0.0483076656877383), (0.0956387200792749, 0.1444719615827965), (0.0938443990808046, 0.2392873622521371), (0.0911738786957639, 0.3318686022821277), (0.0876520930044038, 0.4213512761306353), (0.0833119242269467, 0.5068999089322294), (0.0781938957870703, 0.5877157572407623), (0.0723457941088485, 0.6630442669302152), (0.0658222227763618, 0.7321821187402897), (0.0586840934785355, 0.7944837959679424), (0.0509980592623762, 0.8493676137325700), (0.0428358980222267, 0.8963211557660521), (0.0342738629130214, 0.9349060759377397), (0.0253920653092621, 0.9647622555875064), (0.0162743947309057, 0.9856115115452684), (0.0070186100094701, 0.9972638618494816), ]; #[cfg(test)] mod tests { use crate::common::*; use arrayvec::ArrayVec; fn verify(mut roots: ArrayVec, expected: &[f64]) { assert_eq!(expected.len(), roots.len()); let epsilon = 1e-12; 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), &[ -1.9999999999989995, -1.0000010000848456, -0.9999989999161546, ], ); 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]); } #[test] fn test_solve_quartic() { // These test cases are taken from Orellana and De Michele paper (Table 1). fn test_with_roots(coeffs: [f64; 4], roots: &[f64], rel_err: f64) { // Note: in paper, coefficients are in decreasing order. let mut actual = solve_quartic(coeffs[3], coeffs[2], coeffs[1], coeffs[0], 1.0); actual.sort_by(f64::total_cmp); assert_eq!(actual.len(), roots.len()); for (actual, expected) in actual.iter().zip(roots) { assert!( (actual - expected).abs() < rel_err * expected.abs(), "actual {:e}, expected {:e}, err {:e}", actual, expected, actual - expected ); } } fn test_vieta_roots(x1: f64, x2: f64, x3: f64, x4: f64, roots: &[f64], rel_err: f64) { let a = -(x1 + x2 + x3 + x4); let b = x1 * (x2 + x3) + x2 * (x3 + x4) + x4 * (x1 + x3); let c = -x1 * x2 * (x3 + x4) - x3 * x4 * (x1 + x2); let d = x1 * x2 * x3 * x4; test_with_roots([a, b, c, d], roots, rel_err); } fn test_vieta(x1: f64, x2: f64, x3: f64, x4: f64, rel_err: f64) { test_vieta_roots(x1, x2, x3, x4, &[x1, x2, x3, x4], rel_err); } // case 1 test_vieta(1., 1e3, 1e6, 1e9, 1e-16); // case 2 test_vieta(2., 2.001, 2.002, 2.003, 1e-6); // case 3 test_vieta(1e47, 1e49, 1e50, 1e53, 2e-16); // case 4 test_vieta(-1., 1., 2., 1e14, 1e-16); // case 5 test_vieta(-2e7, -1., 1., 1e7, 1e-16); // case 6 test_with_roots( [-9000002.0, -9999981999998.0, 19999982e6, -2e13], &[-1e6, 1e7], 1e-16, ); // case 7 test_with_roots( [2000011.0, 1010022000028.0, 11110056e6, 2828e10], &[-7., -4.], 1e-16, ); // case 8 test_with_roots( [-100002011.0, 201101022001.0, -102200111000011.0, 11000011e8], &[11., 1e8], 1e-16, ); // cases 9-13 have no real roots // case 14 test_vieta_roots(1000., 1000., 1000., 1000., &[1000., 1000.], 1e-16); // case 15 test_vieta_roots(1e-15, 1000., 1000., 1000., &[1e-15, 1000., 1000.], 1e-15); // case 16 no real roots // case 17 test_vieta(10000., 10001., 10010., 10100., 1e-6); // case 19 test_vieta_roots(1., 1e30, 1e30, 1e44, &[1., 1e30, 1e44], 1e-16); // case 20 // FAILS, error too big test_vieta(1., 1e7, 1e7, 1e14, 1e-7); // case 21 doesn't pick up double root // case 22 test_vieta(1., 10., 1e152, 1e154, 3e-16); // case 23 test_with_roots( [1., 1., 3. / 8., 1e-3], &[-0.497314148060048, -0.00268585193995149], 2e-15, ); // case 24 const S: f64 = 1e30; test_with_roots( [-(1. + 1. / S), 1. / S - S * S, S * S + S, -S], &[-S, 1e-30, 1., S], 2e-16, ); } #[test] fn test_solve_itp() { let f = |x: f64| x.powi(3) - x - 2.0; let x = solve_itp(f, 1., 2., 1e-12, 0, 0.2, f(1.), f(2.)); assert!(f(x).abs() < 6e-12); } #[test] fn test_inv_arclen() { use crate::{ParamCurve, ParamCurveArclen}; let c = crate::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 target = 100.0; let _ = solve_itp( |t| c.subsegment(0.0..t).arclen(1e-9) - target, 0., 1., 1e-6, 1, 0.2, -target, c.arclen(1e-9) - target, ); } } kurbo-0.11.1/src/cubicbez.rs000064400000000000000000001176371046102023000137700ustar 00000000000000// Copyright 2018 the Kurbo Authors // SPDX-License-Identifier: Apache-2.0 OR MIT //! Cubic Bézier segments. use alloc::vec; use alloc::vec::Vec; use core::ops::{Mul, Range}; use crate::MAX_EXTREMA; use crate::{Line, QuadSpline, Vec2}; use arrayvec::ArrayVec; use crate::common::{ solve_quadratic, solve_quartic, GAUSS_LEGENDRE_COEFFS_16_HALF, GAUSS_LEGENDRE_COEFFS_24_HALF, GAUSS_LEGENDRE_COEFFS_8, GAUSS_LEGENDRE_COEFFS_8_HALF, }; use crate::{ Affine, Nearest, ParamCurve, ParamCurveArclen, ParamCurveArea, ParamCurveCurvature, ParamCurveDeriv, ParamCurveExtrema, ParamCurveNearest, PathEl, Point, QuadBez, Rect, Shape, }; #[cfg(not(feature = "std"))] use crate::common::FloatFuncs; const MAX_SPLINE_SPLIT: usize = 100; /// A single cubic Bézier segment. #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[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, } /// Classification result for cusp detection. #[derive(Debug)] pub enum CuspType { /// Cusp is a loop. Loop, /// Cusp has two closely spaced inflection points. DoubleInflection, } 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 } } /// Return a [`QuadSpline`] approximating this cubic Bézier. /// /// Returns `None` if no suitable approximation is found within the given /// tolerance. pub fn approx_spline(&self, accuracy: f64) -> Option { (1..=MAX_SPLINE_SPLIT).find_map(|n| self.approx_spline_n(n, accuracy)) } // Approximate a cubic curve with a quadratic spline of `n` curves fn approx_spline_n(&self, n: usize, accuracy: f64) -> Option { if n == 1 { return self .try_approx_quadratic(accuracy) .map(|quad| QuadSpline::new(vec![quad.p0, quad.p1, quad.p2])); } let mut cubics = self.split_into_n(n); // The above function guarantees that the iterator returns n items, // which is why we're unwrapping things with wild abandon. let mut next_cubic = cubics.next().unwrap(); let mut next_q1: Point = next_cubic.approx_quad_control(0.0); let mut q2 = self.p0; let mut d1 = Vec2::ZERO; let mut spline = vec![self.p0, next_q1]; for i in 1..=n { let current_cubic: CubicBez = next_cubic; let q0 = q2; let q1 = next_q1; q2 = if i < n { next_cubic = cubics.next().unwrap(); next_q1 = next_cubic.approx_quad_control(i as f64 / (n - 1) as f64); spline.push(next_q1); q1.midpoint(next_q1) } else { current_cubic.p3 }; let d0 = d1; d1 = q2.to_vec2() - current_cubic.p3.to_vec2(); if d1.hypot() > accuracy || !CubicBez::new( d0.to_point(), q0.lerp(q1, 2.0 / 3.0) - current_cubic.p1.to_vec2(), q2.lerp(q1, 2.0 / 3.0) - current_cubic.p2.to_vec2(), d1.to_point(), ) .fit_inside(accuracy) { return None; } } spline.push(self.p3); Some(QuadSpline::new(spline)) } fn approx_quad_control(&self, t: f64) -> Point { let p1 = self.p0 + (self.p1 - self.p0) * 1.5; let p2 = self.p3 + (self.p2 - self.p3) * 1.5; p1.lerp(p2, t) } /// Approximate a cubic with a single quadratic /// /// Returns a quadratic approximating the given cubic that maintains /// endpoint tangents if that is within tolerance, or `None` otherwise. fn try_approx_quadratic(&self, accuracy: f64) -> Option { if let Some(q1) = Line::new(self.p0, self.p1).crossing_point(Line::new(self.p2, self.p3)) { let c1 = self.p0.lerp(q1, 2.0 / 3.0); let c2 = self.p3.lerp(q1, 2.0 / 3.0); if !CubicBez::new( Point::ZERO, c1 - self.p1.to_vec2(), c2 - self.p2.to_vec2(), Point::ZERO, ) .fit_inside(accuracy) { return None; } return Some(QuadBez::new(self.p0, q1, self.p3)); } None } fn split_into_n(&self, n: usize) -> impl Iterator { // for certain small values of `n` we precompute all our values. // if we have six or fewer items we precompute them. let mut storage = ArrayVec::<_, 6>::new(); match n { 1 => storage.push(*self), 2 => { let (l, r) = self.subdivide(); storage.try_extend_from_slice(&[r, l]).unwrap(); } 3 => { let (left, mid, right) = self.subdivide_3(); storage.try_extend_from_slice(&[right, mid, left]).unwrap(); } 4 => { let (l, r) = self.subdivide(); let (ll, lr) = l.subdivide(); let (rl, rr) = r.subdivide(); storage.try_extend_from_slice(&[rr, rl, lr, ll]).unwrap(); } 6 => { let (l, r) = self.subdivide(); let (l1, l2, l3) = l.subdivide_3(); let (r1, r2, r3) = r.subdivide_3(); storage .try_extend_from_slice(&[r3, r2, r1, l3, l2, l1]) .unwrap(); } _ => (), } // a limitation of returning 'impl Trait' is that the implementation // can only return a single concrete type; that is you cannot return // Vec::into_iter() from one branch, and then HashSet::into_iter from // another branch. // // This means we have to get a bit fancy, and have a single concrete // type that represents both of our possible cases. let mut storage = if storage.is_empty() { None } else { Some(storage) }; // used in the fallback case let mut i = 0; let (a, b, c, d) = self.parameters(); let dt = 1.0 / n as f64; let delta_2 = dt * dt; let delta_3 = dt * delta_2; core::iter::from_fn(move || { // if storage exists, we use it exclusively if let Some(storage) = storage.as_mut() { return storage.pop(); } // if storage does not exist, we are exclusively working down here. if i >= n { return None; } let t1 = i as f64 * dt; let t1_2 = t1 * t1; let a1 = a * delta_3; let b1 = (3.0 * a * t1 + b) * delta_2; let c1 = (2.0 * b * t1 + c + 3.0 * a * t1_2) * dt; let d1 = a * t1 * t1_2 + b * t1_2 + c * t1 + d; let result = CubicBez::from_parameters(a1, b1, c1, d1); i += 1; Some(result) }) } fn parameters(&self) -> (Vec2, Vec2, Vec2, Vec2) { let c = (self.p1 - self.p0) * 3.0; let b = (self.p2 - self.p1) * 3.0 - c; let d = self.p0.to_vec2(); let a = self.p3.to_vec2() - d - c - b; (a, b, c, d) } /// Rust port of cu2qu [calc_cubic_points](https://github.com/fonttools/fonttools/blob/3b9a73ff8379ab49d3ce35aaaaf04b3a7d9d1655/Lib/fontTools/cu2qu/cu2qu.py#L63-L68) fn from_parameters(a: Vec2, b: Vec2, c: Vec2, d: Vec2) -> Self { let p0 = d.to_point(); let p1 = c.div_exact(3.0).to_point() + d; let p2 = (b + c).div_exact(3.0).to_point() + p1.to_vec2(); let p3 = (a + d + c + b).to_point(); CubicBez::new(p0, p1, p2, p3) } fn subdivide_3(&self) -> (CubicBez, CubicBez, CubicBez) { let (p0, p1, p2, p3) = ( self.p0.to_vec2(), self.p1.to_vec2(), self.p2.to_vec2(), self.p3.to_vec2(), ); // The original Python cu2qu code here does not use division operator to divide by 27 but // instead uses multiplication by the reciprocal 1 / 27. We want to match it exactly // to avoid any floating point differences, hence in this particular case we do not use div_exact. // I could directly use the Vec2 Div trait (also implemented as multiplication by reciprocal) // but I prefer to be explicit here. // Source: https://github.com/fonttools/fonttools/blob/85c80be/Lib/fontTools/cu2qu/cu2qu.py#L215-L218 // See also: https://github.com/linebender/kurbo/issues/272 let one_27th = 27.0_f64.recip(); let mid1 = ((8.0 * p0 + 12.0 * p1 + 6.0 * p2 + p3) * one_27th).to_point(); let deriv1 = (p3 + 3.0 * p2 - 4.0 * p0) * one_27th; let mid2 = ((p0 + 6.0 * p1 + 12.0 * p2 + 8.0 * p3) * one_27th).to_point(); let deriv2 = (4.0 * p3 - 3.0 * p1 - p0) * one_27th; let left = CubicBez::new( self.p0, (2.0 * p0 + p1).div_exact(3.0).to_point(), mid1 - deriv1, mid1, ); let mid = CubicBez::new(mid1, mid1 + deriv1, mid2 - deriv2, mid2); let right = CubicBez::new( mid2, mid2 + deriv2, (p2 + 2.0 * p3).div_exact(3.0).to_point(), self.p3, ); (left, mid, right) } /// Does this curve fit inside the given distance from the origin? /// /// Rust port of cu2qu [cubic_farthest_fit_inside](https://github.com/fonttools/fonttools/blob/3b9a73ff8379ab49d3ce35aaaaf04b3a7d9d1655/Lib/fontTools/cu2qu/cu2qu.py#L281) fn fit_inside(&self, distance: f64) -> bool { if self.p2.to_vec2().hypot() <= distance && self.p1.to_vec2().hypot() <= distance { return true; } let mid = (self.p0.to_vec2() + 3.0 * (self.p1.to_vec2() + self.p2.to_vec2()) + self.p3.to_vec2()) * 0.125; if mid.hypot() > distance { return false; } // Split in two. Note that cu2qu here uses a 3/8 subdivision. I don't know why. let (left, right) = self.subdivide(); left.fit_inside(distance) && right.fit_inside(distance) } /// Is this cubic Bezier curve finite? #[inline] pub fn is_finite(&self) -> bool { self.p0.is_finite() && self.p1.is_finite() && self.p2.is_finite() && self.p3.is_finite() } /// Is this cubic Bezier curve NaN? #[inline] pub fn is_nan(&self) -> bool { self.p0.is_nan() || self.p1.is_nan() || self.p2.is_nan() || self.p3.is_nan() } /// Determine the inflection points. /// /// Return value is t parameter for the inflection points of the curve segment. /// There are a maximum of two for a cubic Bézier. /// /// See /// for the theory. pub fn inflections(&self) -> ArrayVec { let a = self.p1 - self.p0; let b = (self.p2 - self.p1) - a; let c = (self.p3 - self.p0) - 3. * (self.p2 - self.p1); solve_quadratic(a.cross(b), a.cross(c), b.cross(c)) .iter() .copied() .filter(|t| *t >= 0.0 && *t <= 1.0) .collect() } /// Find points on the curve where the tangent line passes through the /// given point. /// /// Result is array of t values such that the tangent line from the curve /// evaluated at that point goes through the argument point. pub fn tangents_to_point(&self, p: Point) -> ArrayVec { let (a, b, c, d_orig) = self.parameters(); let d = d_orig - p.to_vec2(); // coefficients of x(t) \cross x'(t) let c4 = b.cross(a); let c3 = 2.0 * c.cross(a); let c2 = c.cross(b) + 3.0 * d.cross(a); let c1 = 2.0 * d.cross(b); let c0 = d.cross(c); solve_quartic(c0, c1, c2, c3, c4) .iter() .copied() .filter(|t| *t >= 0.0 && *t <= 1.0) .collect() } /// Preprocess a cubic Bézier to ease numerical robustness. /// /// If the cubic Bézier segment has zero or near-zero derivatives, perturb /// the control points to make it easier to process (especially offset and /// stroke), avoiding numerical robustness problems. pub(crate) fn regularize(&self, dimension: f64) -> CubicBez { let mut c = *self; // First step: if control point is too near the endpoint, nudge it away // along the tangent. let dim2 = dimension * dimension; if c.p0.distance_squared(c.p1) < dim2 { let d02 = c.p0.distance_squared(c.p2); if d02 >= dim2 { // TODO: moderate if this would move closer to p3 c.p1 = c.p0.lerp(c.p2, (dim2 / d02).sqrt()); } else { c.p1 = c.p0.lerp(c.p3, 1.0 / 3.0); c.p2 = c.p3.lerp(c.p0, 1.0 / 3.0); return c; } } if c.p3.distance_squared(c.p2) < dim2 { let d13 = c.p1.distance_squared(c.p2); if d13 >= dim2 { // TODO: moderate if this would move closer to p0 c.p2 = c.p3.lerp(c.p1, (dim2 / d13).sqrt()); } else { c.p1 = c.p0.lerp(c.p3, 1.0 / 3.0); c.p2 = c.p3.lerp(c.p0, 1.0 / 3.0); return c; } } if let Some(cusp_type) = self.detect_cusp(dimension) { let d01 = c.p1 - c.p0; let d01h = d01.hypot(); let d23 = c.p3 - c.p2; let d23h = d23.hypot(); match cusp_type { CuspType::Loop => { c.p1 += (dimension / d01h) * d01; c.p2 -= (dimension / d23h) * d23; } CuspType::DoubleInflection => { // Avoid making control distance smaller than dimension if d01h > 2.0 * dimension { c.p1 -= (dimension / d01h) * d01; } if d23h > 2.0 * dimension { c.p2 += (dimension / d23h) * d23; } } } } c } /// Detect whether there is a cusp. /// /// Return a cusp classification if there is a cusp with curvature greater than /// the reciprocal of the given dimension. fn detect_cusp(&self, dimension: f64) -> Option { let d01 = self.p1 - self.p0; let d02 = self.p2 - self.p0; let d03 = self.p3 - self.p0; let d12 = self.p2 - self.p1; let d23 = self.p3 - self.p2; let det_012 = d01.cross(d02); let det_123 = d12.cross(d23); let det_013 = d01.cross(d03); let det_023 = d02.cross(d03); if det_012 * det_123 > 0.0 && det_012 * det_013 < 0.0 && det_012 * det_023 < 0.0 { let q = self.deriv(); // accuracy isn't used for quadratic nearest let nearest = q.nearest(Point::ORIGIN, 1e-9); // detect whether curvature at minimum derivative exceeds 1/dimension, // without division. let d = q.eval(nearest.t); let d2 = q.deriv().eval(nearest.t); let cross = d.to_vec2().cross(d2.to_vec2()); if nearest.distance_sq.powi(3) <= (cross * dimension).powi(2) { let a = 3. * det_012 + det_023 - 2. * det_013; let b = -3. * det_012 + det_013; let c = det_012; let d = b * b - 4. * a * c; if d > 0.0 { return Some(CuspType::DoubleInflection); } else { return Some(CuspType::Loop); } } } None } } /// An iterator for cubic beziers. pub struct CubicBezIter { cubic: CubicBez, ix: usize, } impl Shape for CubicBez { type PathElementsIter<'iter> = 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() } 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, ), ) } #[inline] fn start(&self) -> Point { self.p0 } #[inline] fn end(&self) -> Point { 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(), ) } } fn arclen_quadrature_core(coeffs: &[(f64, f64)], dm: Vec2, dm1: Vec2, dm2: Vec2) -> f64 { coeffs .iter() .map(|&(wi, xi)| { let d = dm + dm2 * (xi * xi); let dpx = (d + dm1 * xi).hypot(); let dmx = (d - dm1 * xi).hypot(); (2.25f64.sqrt() * wi) * (dpx + dmx) }) .sum::() } fn arclen_rec(c: &CubicBez, accuracy: f64, depth: usize) -> f64 { let d03 = c.p3 - c.p0; let d01 = c.p1 - c.p0; let d12 = c.p2 - c.p1; let d23 = c.p3 - c.p2; let lp_lc = d01.hypot() + d12.hypot() + d23.hypot() - d03.hypot(); let dd1 = d12 - d01; let dd2 = d23 - d12; // It might be faster to do direct multiplies, the data dependencies would be shorter. // The following values don't have the factor of 3 for first deriv let dm = 0.25 * (d01 + d23) + 0.5 * d12; // first derivative at midpoint let dm1 = 0.5 * (dd2 + dd1); // second derivative at midpoint let dm2 = 0.25 * (dd2 - dd1); // 0.5 * (third derivative at midpoint) let est = GAUSS_LEGENDRE_COEFFS_8 .iter() .map(|&(wi, xi)| { wi * { let d_norm2 = (dm + dm1 * xi + dm2 * (xi * xi)).hypot2(); let dd_norm2 = (dm1 + dm2 * (2.0 * xi)).hypot2(); dd_norm2 / d_norm2 } }) .sum::(); let est_gauss8_error = (est.powi(3) * 2.5e-6).min(3e-2) * lp_lc; if est_gauss8_error < accuracy { return arclen_quadrature_core(GAUSS_LEGENDRE_COEFFS_8_HALF, dm, dm1, dm2); } let est_gauss16_error = (est.powi(6) * 1.5e-11).min(9e-3) * lp_lc; if est_gauss16_error < accuracy { return arclen_quadrature_core(GAUSS_LEGENDRE_COEFFS_16_HALF, dm, dm1, dm2); } let est_gauss24_error = (est.powi(9) * 3.5e-16).min(3.5e-3) * lp_lc; if est_gauss24_error < accuracy || depth >= 20 { return arclen_quadrature_core(GAUSS_LEGENDRE_COEFFS_24_HALF, dm, dm1, dm2); } let (c0, c1) = c.subdivide(); arclen_rec(&c0, accuracy * 0.5, depth + 1) + arclen_rec(&c1, accuracy * 0.5, depth + 1) } 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 { arclen_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 the nearest point, using subdivision. fn nearest(&self, p: Point, accuracy: f64) -> Nearest { let mut best_r = None; let mut best_t = 0.0; for (t0, t1, q) in self.to_quads(accuracy) { let nearest = q.nearest(p, accuracy); if best_r .map(|best_r| nearest.distance_sq < best_r) .unwrap_or(true) { best_t = t0 + nearest.t * (t1 - t0); best_r = Some(nearest.distance_sq); } } Nearest { t: best_t, distance_sq: best_r.unwrap(), } } } impl ParamCurveCurvature for CubicBez {} impl ParamCurveExtrema for CubicBez { fn extrema(&self) -> ArrayVec { fn one_coord(result: &mut ArrayVec, 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)) } } /// Convert multiple cubic Bézier curves to quadratic splines. /// /// Ensures that the resulting splines have the same number of control points. /// /// Rust port of cu2qu [cubic_approx_quadratic](https://github.com/fonttools/fonttools/blob/3b9a73ff8379ab49d3ce35aaaaf04b3a7d9d1655/Lib/fontTools/cu2qu/cu2qu.py#L322) pub fn cubics_to_quadratic_splines(curves: &[CubicBez], accuracy: f64) -> Option> { let mut result = Vec::new(); let mut split_order = 0; while split_order <= MAX_SPLINE_SPLIT { split_order += 1; result.clear(); for curve in curves { match curve.approx_spline_n(split_order, accuracy) { Some(spline) => result.push(spline), None => break, } } if result.len() == curves.len() { return Some(result); } } None } #[cfg(test)] mod tests { use crate::{ cubics_to_quadratic_splines, Affine, CubicBez, Nearest, ParamCurve, ParamCurveArclen, ParamCurveArea, ParamCurveDeriv, ParamCurveExtrema, ParamCurveNearest, Point, QuadBez, QuadSpline, }; #[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 {accuracy:e}, wanted {actual_arc} got {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 {accuracy:e}, want {actual_arc} got {arc}" ); } #[test] fn cubicbez_inv_arclen_accuracy() { let c = CubicBez::new((0.2, 0.73), (0.35, 1.08), (0.85, 1.08), (1.0, 0.73)); let true_t = c.inv_arclen(0.5, 1e-12); for i in 1..12 { let accuracy = (0.1f64).powi(i); let approx_t = c.inv_arclen(0.5, accuracy); assert!((approx_t - true_t).abs() <= accuracy); } } #[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: Nearest, expected: f64) { assert!( (result.t - expected).abs() < 1e-6, "got {result:?} expected {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 (t0, t1, q) in c.to_quads(accuracy) { 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 {err} wanted {accuracy}"); } } } } #[test] fn cubicbez_approx_spline() { let c1 = CubicBez::new( (550.0, 258.0), (1044.0, 482.0), (2029.0, 1841.0), (1934.0, 1554.0), ); let quad = c1.try_approx_quadratic(344.0); let expected = QuadBez::new( Point::new(550.0, 258.0), Point::new(1673.665720592873, 767.5164401068898), Point::new(1934.0, 1554.0), ); assert!(quad.is_some()); assert_eq!(quad.unwrap(), expected); let quad = c1.try_approx_quadratic(343.0); assert!(quad.is_none()); let spline = c1.approx_spline_n(2, 343.0); assert!(spline.is_some()); let spline = spline.unwrap(); let expected = [ Point::new(550.0, 258.0), Point::new(920.5, 426.0), Point::new(2005.25, 1769.25), Point::new(1934.0, 1554.0), ]; assert_eq!(spline.points().len(), expected.len()); for (got, &wanted) in spline.points().iter().zip(expected.iter()) { assert!(got.distance(wanted) < 5.0); } let spline = c1.approx_spline(5.0); let expected = [ Point::new(550.0, 258.0), Point::new(673.5, 314.0), Point::new(984.8777777777776, 584.2666666666667), Point::new(1312.6305555555557, 927.825), Point::new(1613.1194444444443, 1267.425), Point::new(1842.7055555555555, 1525.8166666666666), Point::new(1957.75, 1625.75), Point::new(1934.0, 1554.0), ]; assert!(spline.is_some()); let spline = spline.unwrap(); assert_eq!(spline.points().len(), expected.len()); for (got, &wanted) in spline.points().iter().zip(expected.iter()) { assert!(got.distance(wanted) < 5.0); } } #[test] fn cubicbez_cubics_to_quadratic_splines() { let curves = vec![ CubicBez::new( (550.0, 258.0), (1044.0, 482.0), (2029.0, 1841.0), (1934.0, 1554.0), ), CubicBez::new( (859.0, 384.0), (1998.0, 116.0), (1596.0, 1772.0), (8.0, 1824.0), ), CubicBez::new( (1090.0, 937.0), (418.0, 1300.0), (125.0, 91.0), (104.0, 37.0), ), ]; let converted = cubics_to_quadratic_splines(&curves, 5.0); assert!(converted.is_some()); let converted = converted.unwrap(); assert_eq!(converted[0].points().len(), 8); assert_eq!(converted[1].points().len(), 8); assert_eq!(converted[2].points().len(), 8); assert!(converted[0].points()[1].distance(Point::new(673.5, 314.0)) < 0.0001); assert!( converted[0].points()[2].distance(Point::new(88639.0 / 90.0, 52584.0 / 90.0)) < 0.0001 ); } #[test] fn cubicbez_approx_spline_div_exact() { // Ensure rounding behavior for division matches fonttools // cu2qu. // See let cubic = CubicBez::new( Point::new(408.0, 321.0), Point::new(408.0, 452.0), Point::new(342.0, 560.0), Point::new(260.0, 560.0), ); let spline = cubic.approx_spline(1.0).unwrap(); assert_eq!( spline.points(), &[ Point::new(408.0, 321.0), // Previous behavior produced 386.49999999999994 for the // y coordinate leading to inconsistent rounding. Point::new(408.0, 386.5), Point::new(368.16666666666663, 495.0833333333333), Point::new(301.0, 560.0), Point::new(260.0, 560.0) ] ); } #[test] fn cubicbez_inflections() { let c = CubicBez::new((0., 0.), (0.8, 1.), (0.2, 1.), (1., 0.)); let inflections = c.inflections(); assert_eq!(inflections.len(), 2); assert!((inflections[0] - 0.311018).abs() < 1e-6); assert!((inflections[1] - 0.688982).abs() < 1e-6); let c = CubicBez::new((0., 0.), (1., 1.), (2., -1.), (3., 0.)); let inflections = c.inflections(); assert_eq!(inflections.len(), 1); assert!((inflections[0] - 0.5).abs() < 1e-6); let c = CubicBez::new((0., 0.), (1., 1.), (2., 1.), (3., 0.)); let inflections = c.inflections(); assert_eq!(inflections.len(), 0); } #[test] fn cubic_to_quadratic_matches_python() { // from https://github.com/googlefonts/fontmake-rs/issues/217 let cubic = CubicBez { p0: (796.0, 319.0).into(), p1: (727.0, 314.0).into(), p2: (242.0, 303.0).into(), p3: (106.0, 303.0).into(), }; // FontTools can approximate this curve successfully in 7 splits, we can too assert!(cubic.approx_spline_n(7, 1.0).is_some()); // FontTools can solve this with accuracy 0.001, we can too assert!(cubics_to_quadratic_splines(&[cubic], 0.001).is_some()); } #[test] fn cubics_to_quadratic_splines_matches_python() { // https://github.com/linebender/kurbo/pull/273 let light = CubicBez::new((378., 608.), (378., 524.), (355., 455.), (266., 455.)); let regular = CubicBez::new((367., 607.), (367., 511.), (338., 472.), (243., 472.)); let bold = CubicBez::new( (372.425, 593.05), (372.425, 524.95), (355.05, 485.95), (274., 485.95), ); let qsplines = cubics_to_quadratic_splines(&[light, regular, bold], 1.0).unwrap(); assert_eq!( qsplines, [ QuadSpline::new(vec![ (378.0, 608.0).into(), (378.0, 566.0).into(), (359.0833333333333, 496.5).into(), (310.5, 455.0).into(), (266.0, 455.0).into(), ]), QuadSpline::new(vec![ (367.0, 607.0).into(), (367.0, 559.0).into(), // Previous behavior produced 496.5 for the y coordinate (344.5833333333333, 499.49999999999994).into(), (290.5, 472.0).into(), (243.0, 472.0).into(), ]), QuadSpline::new(vec![ (372.425, 593.05).into(), (372.425, 559.0).into(), (356.98333333333335, 511.125).into(), (314.525, 485.95).into(), (274.0, 485.95).into(), ]), ] ); } } kurbo-0.11.1/src/ellipse.rs000064400000000000000000000236771046102023000136370ustar 00000000000000// Copyright 2020 the Kurbo Authors // SPDX-License-Identifier: Apache-2.0 OR MIT //! Implementation of ellipse shape. use core::f64::consts::PI; use core::{ iter, ops::{Add, Mul, Sub}, }; use crate::{Affine, Arc, ArcAppendIter, Circle, PathEl, Point, Rect, Shape, Size, Vec2}; #[cfg(not(feature = "std"))] use crate::common::FloatFuncs; /// An ellipse. #[derive(Clone, Copy, Default, Debug, PartialEq)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[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 that 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 axis 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`]. #[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. /// /// [`with_rotation`]: Ellipse::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.with_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.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`]. #[must_use] pub fn with_rotation(self, rotation: f64) -> Ellipse { let scale = self.inner.svd().0; let translation = self.inner.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.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 } /// Returns the radii and the rotation of this ellipse. /// /// Equivalent to `(self.radii(), self.rotation())` but more efficient. pub fn radii_and_rotation(&self) -> (Vec2, f64) { self.inner.svd() } /// Is this ellipse [finite]? /// /// [finite]: f64::is_finite #[inline] pub fn is_finite(&self) -> bool { self.inner.is_finite() } /// Is this ellipse [NaN]? /// /// [NaN]: f64::is_nan #[inline] pub fn is_nan(&self) -> bool { self.inner.is_nan() } #[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 ellipse. #[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 ellipse. #[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> = 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.11.1/src/fit.rs000064400000000000000000000645221046102023000127560ustar 00000000000000// Copyright 2022 the Kurbo Authors // SPDX-License-Identifier: Apache-2.0 OR MIT //! An implementation of cubic Bézier curve fitting based on a quartic //! solver making signed area and moment match the source curve. use core::ops::Range; use alloc::vec::Vec; use arrayvec::ArrayVec; use crate::{ common::{ factor_quartic_inner, solve_cubic, solve_itp_fallible, solve_quadratic, GAUSS_LEGENDRE_COEFFS_16, }, Affine, BezPath, CubicBez, Line, ParamCurve, ParamCurveArclen, ParamCurveNearest, Point, Vec2, }; #[cfg(not(feature = "std"))] use crate::common::FloatFuncs; /// The source curve for curve fitting. /// /// This trait is a general representation of curves to be used as input to a curve /// fitting process. It can represent source curves with cusps and corners, though /// if the corners are known in advance, it may be better to run curve fitting on /// subcurves bounded by the corners. /// /// The trait primarily works by sampling the source curve and computing the position /// and derivative at each sample. Those derivatives are then used for multiple /// sub-tasks, including ensuring G1 continuity at subdivision points, computing the /// area and moment of the curve for curve fitting, and casting rays for evaluation /// of a distance metric to test accuracy. /// /// A major motivation is computation of offset curves, which often have cusps, but /// the presence and location of those cusps is not generally known. It is also /// intended for conversion between curve types (for example, piecewise Euler spiral /// or NURBS), and distortion effects such as perspective transform. /// /// Note general similarities to [`ParamCurve`] but also important differences. /// Instead of separate [`eval`] and evaluation of derivative, have a single /// [`sample_pt_deriv`] method which can be more efficient and also handles cusps more /// robustly. Also there is no method for subsegment, as that is not needed and /// would be annoying to implement. /// /// [`ParamCurve`]: crate::ParamCurve /// [`eval`]: crate::ParamCurve::eval /// [`sample_pt_deriv`]: ParamCurveFit::sample_pt_deriv pub trait ParamCurveFit { /// Evaluate the curve and its tangent at parameter `t`. /// /// For a regular curve (one not containing a cusp or corner), the /// derivative is a good choice for the tangent vector and the `sign` /// parameter can be ignored. Otherwise, the `sign` parameter selects which /// side of the discontinuity the tangent will be sampled from. /// /// Generally `t` is in the range [0..1]. fn sample_pt_tangent(&self, t: f64, sign: f64) -> CurveFitSample; /// Evaluate the point and derivative at parameter `t`. /// /// In curves with cusps, the derivative can go to zero. fn sample_pt_deriv(&self, t: f64) -> (Point, Vec2); /// Compute moment integrals. /// /// This method computes the integrals of y dx, x y dx, and y^2 dx over the /// length of this curve. From these integrals it is fairly straightforward /// to derive the moments needed for curve fitting. /// /// A default implementation is proved which does quadrature integration /// with Green's theorem, in terms of samples evaluated with /// [`sample_pt_deriv`]. /// /// [`sample_pt_deriv`]: ParamCurveFit::sample_pt_deriv fn moment_integrals(&self, range: Range) -> (f64, f64, f64) { let t0 = 0.5 * (range.start + range.end); let dt = 0.5 * (range.end - range.start); let (a, x, y) = GAUSS_LEGENDRE_COEFFS_16 .iter() .map(|(wi, xi)| { let t = t0 + xi * dt; let (p, d) = self.sample_pt_deriv(t); let a = wi * d.x * p.y; let x = p.x * a; let y = p.y * a; (a, x, y) }) .fold((0.0, 0.0, 0.0), |(a0, x0, y0), (a1, x1, y1)| { (a0 + a1, x0 + x1, y0 + y1) }); (a * dt, x * dt, y * dt) } /// Find a cusp or corner within the given range. /// /// If the range contains a corner or cusp, return it. If there is more /// than one such discontinuity, any can be reported, as the function will /// be called repeatedly after subdivision of the range. /// /// Do not report cusps at the endpoints of the range, as this may cause /// potentially infinite subdivision. In particular, when a cusp is reported /// and this method is called on a subdivided range bounded by the reported /// cusp, then the subsequent call should not report a cusp there. /// /// The definition of what exactly constitutes a cusp is somewhat loose. /// If a cusp is missed, then the curve fitting algorithm will attempt to /// fit the curve with a smooth curve, which is generally not a disaster /// will usually result in more subdivision. Conversely, it might be useful /// to report near-cusps, specifically points of curvature maxima where the /// curvature is large but still mathematically finite. fn break_cusp(&self, range: Range) -> Option; } /// A sample point of a curve for fitting. pub struct CurveFitSample { /// A point on the curve at the sample location. pub p: Point, /// A vector tangent to the curve at the sample location. pub tangent: Vec2, } impl CurveFitSample { /// Intersect a ray orthogonal to the tangent with the given cubic. /// /// Returns a vector of `t` values on the cubic. fn intersect(&self, c: CubicBez) -> ArrayVec { let p1 = 3.0 * (c.p1 - c.p0); let p2 = 3.0 * c.p2.to_vec2() - 6.0 * c.p1.to_vec2() + 3.0 * c.p0.to_vec2(); let p3 = (c.p3 - c.p0) - 3.0 * (c.p2 - c.p1); let c0 = (c.p0 - self.p).dot(self.tangent); let c1 = p1.dot(self.tangent); let c2 = p2.dot(self.tangent); let c3 = p3.dot(self.tangent); solve_cubic(c0, c1, c2, c3) .into_iter() .filter(|t| (0.0..=1.0).contains(t)) .collect() } } /// Generate a Bézier path that fits the source curve. /// /// This function is still experimental and the signature might change; it's possible /// it might become a method on the [`ParamCurveFit`] trait. /// /// This function recursively subdivides the curve in half by the parameter when the /// accuracy is not met. That gives a reasonably optimized result but not necessarily /// the minimum number of segments. /// /// In general, the resulting Bézier path should have a Fréchet distance less than /// the provided `accuracy` parameter. However, this is not a rigorous guarantee, as /// the error metric is computed approximately. /// /// This function is intended for use when the source curve is piecewise continuous, /// with the discontinuities reported by the cusp method. In applications (such as /// stroke expansion) where this property may not hold, it is up to the client to /// detect and handle such cases. Even so, best effort is made to avoid infinite /// subdivision. /// /// When a higher degree of optimization is desired (at considerably more runtime cost), /// consider [`fit_to_bezpath_opt`] instead. pub fn fit_to_bezpath(source: &impl ParamCurveFit, accuracy: f64) -> BezPath { let mut path = BezPath::new(); fit_to_bezpath_rec(source, 0.0..1.0, accuracy, &mut path); path } // Discussion question: possibly should take endpoint samples, to avoid // duplication of that work. fn fit_to_bezpath_rec( source: &impl ParamCurveFit, range: Range, accuracy: f64, path: &mut BezPath, ) { let start = range.start; let end = range.end; let start_p = source.sample_pt_tangent(range.start, 1.0).p; let end_p = source.sample_pt_tangent(range.end, -1.0).p; if start_p.distance_squared(end_p) <= accuracy * accuracy { if let Some((c, _)) = try_fit_line(source, accuracy, range, start_p, end_p) { if path.is_empty() { path.move_to(c.p0); } path.curve_to(c.p1, c.p2, c.p3); return; } } let t = if let Some(t) = source.break_cusp(start..end) { t } else if let Some((c, _)) = fit_to_cubic(source, start..end, accuracy) { if path.is_empty() { path.move_to(c.p0); } path.curve_to(c.p1, c.p2, c.p3); return; } else { // A smarter approach is possible than midpoint subdivision, but would be // a significant increase in complexity. 0.5 * (start + end) }; if t == start || t == end { // infinite recursion, just draw a line let p1 = start_p.lerp(end_p, 1.0 / 3.0); let p2 = end_p.lerp(start_p, 1.0 / 3.0); if path.is_empty() { path.move_to(start_p); } path.curve_to(p1, p2, end_p); return; } fit_to_bezpath_rec(source, start..t, accuracy, path); fit_to_bezpath_rec(source, t..end, accuracy, path); } const N_SAMPLE: usize = 20; /// An acceleration structure for estimating curve distance struct CurveDist { samples: ArrayVec, arcparams: ArrayVec, range: Range, /// A "spicy" curve is one with potentially extreme curvature variation, /// so use arc length measurement for better accuracy. spicy: bool, } impl CurveDist { fn from_curve(source: &impl ParamCurveFit, range: Range) -> Self { let step = (range.end - range.start) * (1.0 / (N_SAMPLE + 1) as f64); let mut last_tan = None; let mut spicy = false; const SPICY_THRESH: f64 = 0.2; let mut samples = ArrayVec::new(); for i in 0..N_SAMPLE + 2 { let sample = source.sample_pt_tangent(range.start + i as f64 * step, 1.0); if let Some(last_tan) = last_tan { let cross = sample.tangent.cross(last_tan); let dot = sample.tangent.dot(last_tan); if cross.abs() > SPICY_THRESH * dot.abs() { spicy = true; } } last_tan = Some(sample.tangent); if i > 0 && i < N_SAMPLE + 1 { samples.push(sample); } } CurveDist { samples, arcparams: Default::default(), range, spicy, } } fn compute_arc_params(&mut self, source: &impl ParamCurveFit) { const N_SUBSAMPLE: usize = 10; let (start, end) = (self.range.start, self.range.end); let dt = (end - start) * (1.0 / ((N_SAMPLE + 1) * N_SUBSAMPLE) as f64); let mut arclen = 0.0; for i in 0..N_SAMPLE + 1 { for j in 0..N_SUBSAMPLE { let t = start + dt * ((i * N_SUBSAMPLE + j) as f64 + 0.5); let (_, deriv) = source.sample_pt_deriv(t); arclen += deriv.hypot(); } if i < N_SAMPLE { self.arcparams.push(arclen); } } let arclen_inv = arclen.recip(); for x in &mut self.arcparams { *x *= arclen_inv; } } /// Evaluate distance based on arc length parametrization fn eval_arc(&self, c: CubicBez, acc2: f64) -> Option { // TODO: this could perhaps be tuned. const EPS: f64 = 1e-9; let c_arclen = c.arclen(EPS); let mut max_err2 = 0.0; for (sample, s) in self.samples.iter().zip(&self.arcparams) { let t = c.inv_arclen(c_arclen * s, EPS); let err = sample.p.distance_squared(c.eval(t)); max_err2 = err.max(max_err2); if max_err2 > acc2 { return None; } } Some(max_err2) } /// Evaluate distance to a cubic approximation. /// /// If distance exceeds stated accuracy, can return `None`. Note that /// `acc2` is the square of the target. /// /// Returns the square of the error, which is intended to be a good /// approximation of the Fréchet distance. fn eval_ray(&self, c: CubicBez, acc2: f64) -> Option { let mut max_err2 = 0.0; for sample in &self.samples { let mut best = acc2 + 1.0; for t in sample.intersect(c) { let err = sample.p.distance_squared(c.eval(t)); best = best.min(err); } max_err2 = best.max(max_err2); if max_err2 > acc2 { return None; } } Some(max_err2) } fn eval_dist(&mut self, source: &impl ParamCurveFit, c: CubicBez, acc2: f64) -> Option { // Always compute cheaper distance, hoping for early-out. let ray_dist = self.eval_ray(c, acc2)?; if !self.spicy { return Some(ray_dist); } if self.arcparams.is_empty() { self.compute_arc_params(source); } self.eval_arc(c, acc2) } } /// As described in [Simplifying Bézier paths], strictly optimizing for /// Fréchet distance can create bumps. The problem is curves with long /// control arms (distance from the control point to the corresponding /// endpoint). We mitigate that by applying a penalty as a multiplier to /// the measured error (approximate Fréchet distance). This is ReLU-like, /// with a value of 1.0 below the elbow, and a given slope above it. The /// values here have been determined empirically to give good results. /// /// [Simplifying Bézier paths]: /// https://raphlinus.github.io/curves/2023/04/18/bezpath-simplify.html const D_PENALTY_ELBOW: f64 = 0.65; const D_PENALTY_SLOPE: f64 = 2.0; /// Try fitting a line. /// /// This is especially useful for very short chords, in which the standard /// cubic fit is not numerically stable. The tangents are not considered, so /// it's useful in cusp and near-cusp situations where the tangents are not /// reliable, as well. /// /// Returns the line raised to a cubic and the error, if within tolerance. fn try_fit_line( source: &impl ParamCurveFit, accuracy: f64, range: Range, start: Point, end: Point, ) -> Option<(CubicBez, f64)> { let acc2 = accuracy * accuracy; let chord_l = Line::new(start, end); const SHORT_N: usize = 7; let mut max_err2 = 0.0; let dt = (range.end - range.start) / (SHORT_N + 1) as f64; for i in 0..SHORT_N { let t = range.start + (i + 1) as f64 * dt; let p = source.sample_pt_deriv(t).0; let err2 = chord_l.nearest(p, accuracy).distance_sq; if err2 > acc2 { // Not in tolerance; likely subdivision will help. return None; } max_err2 = err2.max(max_err2); } let p1 = start.lerp(end, 1.0 / 3.0); let p2 = end.lerp(start, 1.0 / 3.0); let c = CubicBez::new(start, p1, p2, end); Some((c, max_err2)) } /// Fit a single cubic to a range of the source curve. /// /// Returns the cubic segment and the square of the error. /// Discussion question: should this be a method on the trait instead? pub fn fit_to_cubic( source: &impl ParamCurveFit, range: Range, accuracy: f64, ) -> Option<(CubicBez, f64)> { let start = source.sample_pt_tangent(range.start, 1.0); let end = source.sample_pt_tangent(range.end, -1.0); let d = end.p - start.p; let chord2 = d.hypot2(); let acc2 = accuracy * accuracy; if chord2 <= acc2 { // Special case very short chords; try to fit a line. return try_fit_line(source, accuracy, range, start.p, end.p); } let th = d.atan2(); fn mod_2pi(th: f64) -> f64 { let th_scaled = th * core::f64::consts::FRAC_1_PI * 0.5; core::f64::consts::PI * 2.0 * (th_scaled - th_scaled.round()) } let th0 = mod_2pi(start.tangent.atan2() - th); let th1 = mod_2pi(th - end.tangent.atan2()); let (mut area, mut x, mut y) = source.moment_integrals(range.clone()); let (x0, y0) = (start.p.x, start.p.y); let (dx, dy) = (d.x, d.y); // Subtract off area of chord area -= dx * (y0 + 0.5 * dy); // `area` is signed area of closed curve segment. // This quantity is invariant to translation and rotation. // Subtract off moment of chord let dy_3 = dy * (1. / 3.); x -= dx * (x0 * y0 + 0.5 * (x0 * dy + y0 * dx) + dy_3 * dx); y -= dx * (y0 * y0 + y0 * dy + dy_3 * dy); // Translate start point to origin; convert raw integrals to moments. x -= x0 * area; y = 0.5 * y - y0 * area; // Rotate into place (this also scales up by chordlength for efficiency). let moment = d.x * x + d.y * y; // `moment` is the chordlength times the x moment of the curve translated // so its start point is on the origin, and rotated so its end point is on the // x axis. let chord2_inv = chord2.recip(); let unit_area = area * chord2_inv; let mx = moment * chord2_inv.powi(2); // `unit_area` is signed area scaled to unit chord; `mx` is scaled x moment let chord = chord2.sqrt(); let aff = Affine::translate(start.p.to_vec2()) * Affine::rotate(th) * Affine::scale(chord); let mut curve_dist = CurveDist::from_curve(source, range); let mut best_c = None; let mut best_err2 = None; for (cand, d0, d1) in cubic_fit(th0, th1, unit_area, mx) { let c = aff * cand; if let Some(err2) = curve_dist.eval_dist(source, c, acc2) { fn scale_f(d: f64) -> f64 { 1.0 + (d - D_PENALTY_ELBOW).max(0.0) * D_PENALTY_SLOPE } let scale = scale_f(d0).max(scale_f(d1)).powi(2); let err2 = err2 * scale; if err2 < acc2 && best_err2.map(|best| err2 < best).unwrap_or(true) { best_c = Some(c); best_err2 = Some(err2); } } } match (best_c, best_err2) { (Some(c), Some(err2)) => Some((c, err2)), _ => None, } } /// Returns curves matching area and moment, given unit chord. fn cubic_fit(th0: f64, th1: f64, area: f64, mx: f64) -> ArrayVec<(CubicBez, f64, f64), 4> { // Note: maybe we want to take unit vectors instead of angle? Shouldn't // matter much either way though. let (s0, c0) = th0.sin_cos(); let (s1, c1) = th1.sin_cos(); let a4 = -9. * c0 * (((2. * s1 * c1 * c0 + s0 * (2. * c1 * c1 - 1.)) * c0 - 2. * s1 * c1) * c0 - c1 * c1 * s0); let a3 = 12. * ((((c1 * (30. * area * c1 - s1) - 15. * area) * c0 + 2. * s0 - c1 * s0 * (c1 + 30. * area * s1)) * c0 + c1 * (s1 - 15. * area * c1)) * c0 - s0 * c1 * c1); let a2 = 12. * ((((70. * mx + 15. * area) * s1 * s1 + c1 * (9. * s1 - 70. * c1 * mx - 5. * c1 * area)) * c0 - 5. * s0 * s1 * (3. * s1 - 4. * c1 * (7. * mx + area))) * c0 - c1 * (9. * s1 - 70. * c1 * mx - 5. * c1 * area)); let a1 = 16. * (((12. * s0 - 5. * c0 * (42. * mx - 17. * area)) * s1 - 70. * c1 * (3. * mx - area) * s0 - 75. * c0 * c1 * area * area) * s1 - 75. * c1 * c1 * area * area * s0); let a0 = 80. * s1 * (42. * s1 * mx - 25. * area * (s1 - c1 * area)); // TODO: "roots" is not a good name for this variable, as it also contains // the real part of complex conjugate pairs. let mut roots = ArrayVec::::new(); const EPS: f64 = 1e-12; if a4.abs() > EPS { let a = a3 / a4; let b = a2 / a4; let c = a1 / a4; let d = a0 / a4; if let Some(quads) = factor_quartic_inner(a, b, c, d, false) { for (qc1, qc0) in quads { let qroots = solve_quadratic(qc0, qc1, 1.0); if qroots.is_empty() { // Real part of pair of complex roots roots.push(-0.5 * qc1); } else { roots.extend(qroots); } } } } else if a3.abs() > EPS { roots.extend(solve_cubic(a0, a1, a2, a3)); } else if a2.abs() > EPS || a1.abs() > EPS || a0.abs() > EPS { roots.extend(solve_quadratic(a0, a1, a2)); } else { return [( CubicBez::new((0.0, 0.0), (1. / 3., 0.0), (2. / 3., 0.0), (1., 0.0)), 1f64 / 3., 1f64 / 3., )] .into_iter() .collect(); } let s01 = s0 * c1 + s1 * c0; roots .iter() .filter_map(|&d0| { let (d0, d1) = if d0 > 0.0 { let d1 = (d0 * s0 - area * (10. / 3.)) / (0.5 * d0 * s01 - s1); if d1 > 0.0 { (d0, d1) } else { (s1 / s01, 0.0) } } else { (0.0, s0 / s01) }; // We could implement a maximum d value here. if d0 >= 0.0 && d1 >= 0.0 { Some(( CubicBez::new( (0.0, 0.0), (d0 * c0, d0 * s0), (1.0 - d1 * c1, d1 * s1), (1.0, 0.0), ), d0, d1, )) } else { None } }) .collect() } /// Generate a highly optimized Bézier path that fits the source curve. /// /// This function is still experimental and the signature might change; it's possible /// it might become a method on the [`ParamCurveFit`] trait. /// /// This function is considerably slower than [`fit_to_bezpath`], as it computes /// optimal subdivision points. Its result is expected to be very close to the optimum /// possible Bézier path for the source curve, in that it has a minimal number of curve /// segments, and a minimal error over all paths with that number of segments. /// /// See [`fit_to_bezpath`] for an explanation of the `accuracy` parameter. pub fn fit_to_bezpath_opt(source: &impl ParamCurveFit, accuracy: f64) -> BezPath { let mut cusps = Vec::new(); let mut path = BezPath::new(); let mut t0 = 0.0; loop { let t1 = cusps.last().copied().unwrap_or(1.0); match fit_to_bezpath_opt_inner(source, accuracy, t0..t1, &mut path) { Some(t) => cusps.push(t), None => match cusps.pop() { Some(t) => t0 = t, None => break, }, } } path } /// Fit a range without cusps. /// /// On Ok return, range has been added to the path. On Err return, report a cusp (and don't /// mutate path). fn fit_to_bezpath_opt_inner( source: &impl ParamCurveFit, accuracy: f64, range: Range, path: &mut BezPath, ) -> Option { if let Some(t) = source.break_cusp(range.clone()) { return Some(t); } let err; if let Some((c, err2)) = fit_to_cubic(source, range.clone(), accuracy) { err = err2.sqrt(); if err < accuracy { if range.start == 0.0 { path.move_to(c.p0); } path.curve_to(c.p1, c.p2, c.p3); return None; } } else { err = 2.0 * accuracy; } let (mut t0, t1) = (range.start, range.end); let mut n = 0; let last_err; loop { n += 1; match fit_opt_segment(source, accuracy, t0..t1) { FitResult::ParamVal(t) => t0 = t, FitResult::SegmentError(err) => { last_err = err; break; } FitResult::CuspFound(t) => return Some(t), } } t0 = range.start; const EPS: f64 = 1e-9; let f = |x| fit_opt_err_delta(source, x, accuracy, t0..t1, n); let k1 = 0.2 / accuracy; let ya = -err; let yb = accuracy - last_err; let (_, x) = match solve_itp_fallible(f, 0.0, accuracy, EPS, 1, k1, ya, yb) { Ok(x) => x, Err(t) => return Some(t), }; //println!("got fit with n={}, err={}", n, x); let path_len = path.elements().len(); for i in 0..n { let t1 = if i < n - 1 { match fit_opt_segment(source, x, t0..range.end) { FitResult::ParamVal(t1) => t1, FitResult::SegmentError(_) => range.end, FitResult::CuspFound(t) => { path.truncate(path_len); return Some(t); } } } else { range.end }; let (c, _) = fit_to_cubic(source, t0..t1, accuracy).unwrap(); if i == 0 && range.start == 0.0 { path.move_to(c.p0); } path.curve_to(c.p1, c.p2, c.p3); t0 = t1; if t0 == range.end { // This is unlikely but could happen when not monotonic. break; } } None } fn measure_one_seg(source: &impl ParamCurveFit, range: Range, limit: f64) -> Option { fit_to_cubic(source, range, limit).map(|(_, err2)| err2.sqrt()) } enum FitResult { /// The parameter (`t`) value that meets the desired accuracy. ParamVal(f64), /// Error of the measured segment. SegmentError(f64), /// The parameter value where a cusp was found. CuspFound(f64), } fn fit_opt_segment(source: &impl ParamCurveFit, accuracy: f64, range: Range) -> FitResult { if let Some(t) = source.break_cusp(range.clone()) { return FitResult::CuspFound(t); } let missing_err = accuracy * 2.0; let err = measure_one_seg(source, range.clone(), accuracy).unwrap_or(missing_err); if err <= accuracy { return FitResult::SegmentError(err); } let (t0, t1) = (range.start, range.end); let f = |x| { if let Some(t) = source.break_cusp(range.clone()) { return Err(t); } let err = measure_one_seg(source, t0..x, accuracy).unwrap_or(missing_err); Ok(err - accuracy) }; const EPS: f64 = 1e-9; let k1 = 2.0 / (t1 - t0); match solve_itp_fallible(f, t0, t1, EPS, 1, k1, -accuracy, err - accuracy) { Ok((t1, _)) => FitResult::ParamVal(t1), Err(t) => FitResult::CuspFound(t), } } // Ok result is delta error (accuracy - error of last seg). // Err result is a cusp. fn fit_opt_err_delta( source: &impl ParamCurveFit, accuracy: f64, limit: f64, range: Range, n: usize, ) -> Result { let (mut t0, t1) = (range.start, range.end); for _ in 0..n - 1 { t0 = match fit_opt_segment(source, accuracy, t0..t1) { FitResult::ParamVal(t0) => t0, // In this case, n - 1 will work, which of course means the error is highly // non-monotonic. We should probably harvest that solution. FitResult::SegmentError(_) => return Ok(accuracy), FitResult::CuspFound(t) => return Err(t), } } let err = measure_one_seg(source, t0..t1, limit).unwrap_or(accuracy * 2.0); Ok(accuracy - err) } kurbo-0.11.1/src/insets.rs000064400000000000000000000200121046102023000134630ustar 00000000000000// Copyright 2019 the Kurbo Authors // SPDX-License-Identifier: Apache-2.0 OR MIT //! A description of the distances between the edges of two rectangles. use core::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 rectangle 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()); /// ``` #[derive(Clone, Copy, Default, Debug, PartialEq)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[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 { /// Zeroed 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 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())); /// ``` /// /// [`x_value`]: Insets::x_value /// [`y_value`]: Insets::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), } } /// Are these insets finite? #[inline] pub fn is_finite(&self) -> bool { self.x0.is_finite() && self.y0.is_finite() && self.x1.is_finite() && self.y1.is_finite() } /// Are these insets NaN? #[inline] pub fn is_nan(&self) -> bool { self.x0.is_nan() || self.y0.is_nan() || self.x1.is_nan() || self.y1.is_nan() } } 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.11.1/src/lib.rs000064400000000000000000000115071046102023000127350ustar 00000000000000// Copyright 2018 the Kurbo Authors // SPDX-License-Identifier: Apache-2.0 OR MIT //! 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. //! It can be used as "vocabulary types" for representing curves and paths, and //! also contains a number of computational geometry methods. //! //! # 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 //! /// perimeter, 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 nearest = segment.nearest(pt, DESIRED_ACCURACY); //! if best.map(|(_, best_d)| nearest.distance_sq < best_d).unwrap_or(true) { //! best = Some((segment.eval(nearest.t), nearest.distance_sq)) //! } //! } //! 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); //! ``` //! //! # Features //! //! This crate either uses the standard library or the [`libm`] crate for //! math functionality. The `std` feature is enabled by default, but can be //! disabled, as long as the `libm` feature is enabled. This is useful for //! `no_std` environments. However, note that the `libm` crate is not as //! efficient as the standard library, and that this crate still uses the //! `alloc` crate regardless. //! //! [`libm`]: https://docs.rs/libm #![forbid(unsafe_code)] #![deny(missing_docs, clippy::trivially_copy_pass_by_ref)] #![warn(clippy::doc_markdown, rustdoc::broken_intra_doc_links)] #![warn(clippy::semicolon_if_nothing_returned)] #![warn(unused_qualifications)] #![allow( clippy::unreadable_literal, clippy::many_single_char_names, clippy::excessive_precision, clippy::bool_to_int_with_if )] #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![cfg_attr(all(not(feature = "std"), not(test)), no_std)] #[cfg(not(any(feature = "std", feature = "libm")))] compile_error!("kurbo requires either the `std` or `libm` feature"); extern crate alloc; mod affine; mod arc; mod bezpath; mod circle; pub mod common; mod cubicbez; mod ellipse; mod fit; mod insets; mod line; mod mindist; pub mod offset; mod param_curve; mod point; mod quadbez; mod quadspline; mod rect; mod rounded_rect; mod rounded_rect_radii; mod shape; pub mod simplify; mod size; mod stroke; mod svg; mod translate_scale; mod vec2; pub use crate::affine::Affine; pub use crate::arc::{Arc, ArcAppendIter}; pub use crate::bezpath::{ flatten, segments, BezPath, LineIntersection, MinDistance, PathEl, PathSeg, PathSegIter, Segments, }; pub use crate::circle::{Circle, CirclePathIter, CircleSegment}; pub use crate::cubicbez::{cubics_to_quadratic_splines, CubicBez, CubicBezIter, CuspType}; pub use crate::ellipse::Ellipse; pub use crate::fit::{ fit_to_bezpath, fit_to_bezpath_opt, fit_to_cubic, CurveFitSample, ParamCurveFit, }; pub use crate::insets::Insets; pub use crate::line::{ConstPoint, Line, LinePathIter}; pub use crate::param_curve::{ Nearest, ParamCurve, ParamCurveArclen, ParamCurveArea, ParamCurveCurvature, ParamCurveDeriv, ParamCurveExtrema, ParamCurveNearest, DEFAULT_ACCURACY, MAX_EXTREMA, }; pub use crate::point::Point; pub use crate::quadbez::{QuadBez, QuadBezIter}; pub use crate::quadspline::QuadSpline; pub use crate::rect::{Rect, RectPathIter}; pub use crate::rounded_rect::{RoundedRect, RoundedRectPathIter}; pub use crate::rounded_rect_radii::RoundedRectRadii; pub use crate::shape::Shape; pub use crate::size::Size; pub use crate::stroke::{ dash, stroke, Cap, DashIterator, Dashes, Join, Stroke, StrokeOptLevel, StrokeOpts, }; pub use crate::svg::{SvgArc, SvgParseError}; pub use crate::translate_scale::TranslateScale; pub use crate::vec2::Vec2; kurbo-0.11.1/src/line.rs000064400000000000000000000211531046102023000131140ustar 00000000000000// Copyright 2018 the Kurbo Authors // SPDX-License-Identifier: Apache-2.0 OR MIT //! Lines. use core::ops::{Add, Mul, Range, Sub}; use arrayvec::ArrayVec; use crate::{ Affine, Nearest, 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 = "schemars", derive(schemars::JsonSchema))] #[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(), } } /// Returns a copy of this `Line` with the end points swapped so that it /// points in the opposite direction. #[must_use] #[inline] pub fn reversed(&self) -> Line { Self { p0: self.p1, p1: self.p0, } } /// The length of the line. #[inline] pub fn length(self) -> f64 { self.arclen(DEFAULT_ACCURACY) } /// The midpoint of the line. /// /// This is the same as calling [`Point::midpoint`] with /// the endpoints of this line. #[must_use] #[inline] pub fn midpoint(&self) -> Point { self.p0.midpoint(self.p1) } /// Computes the point where two lines, if extended to infinity, would cross. pub fn crossing_point(self, other: Line) -> Option { let ab = self.p1 - self.p0; let cd = other.p1 - other.p0; let pcd = ab.cross(cd); if pcd == 0.0 { return None; } let h = ab.cross(self.p0 - other.p0) / pcd; Some(other.p0 + cd * h) } /// Is this line `finite`? /// /// [finite]: f64::is_finite #[inline] pub fn is_finite(self) -> bool { self.p0.is_finite() && self.p1.is_finite() } /// Is this line `NaN`? /// /// [NaN]: f64::is_nan #[inline] pub fn is_nan(self) -> bool { self.p0.is_nan() || self.p1.is_nan() } } impl From<(Point, Point)> for Line { fn from((from, to): (Point, Point)) -> Self { Line::new(from, to) } } impl From<(Point, Vec2)> for Line { fn from((origin, displacement): (Point, Vec2)) -> Self { Line::new(origin, origin + displacement) } } impl ParamCurve for Line { #[inline] fn eval(&self, t: f64) -> Point { self.p0.lerp(self.p1, t) } #[inline] fn subsegment(&self, range: Range) -> Line { Line { p0: self.eval(range.start), p1: self.eval(range.end), } } #[inline] fn start(&self) -> Point { self.p0 } #[inline] fn end(&self) -> Point { self.p1 } } 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() } #[inline] fn inv_arclen(&self, arclen: f64, _accuracy: f64) -> f64 { arclen / (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) -> Nearest { let d = self.p1 - self.p0; let dotp = d.dot(p - self.p0); let d_squared = d.dot(d); let (t, distance_sq) = 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) }; Nearest { distance_sq, t } } } impl ParamCurveCurvature for Line { #[inline] fn curvature(&self, _t: f64) -> f64 { 0.0 } } impl ParamCurveExtrema for Line { #[inline] fn extrema(&self) -> ArrayVec { ArrayVec::new() } } /// A trivial "curve" that is just a constant. #[derive(Clone, Copy, Debug)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct ConstPoint(Point); impl ConstPoint { /// Is this point [finite]? /// /// [finite]: f64::is_finite #[inline] pub fn is_finite(self) -> bool { self.0.is_finite() } /// Is this point [NaN]? /// /// [NaN]: f64::is_nan #[inline] pub fn is_nan(self) -> bool { self.0.is_nan() } } 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 } #[inline] fn inv_arclen(&self, _arclen: f64, _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<'iter> = 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, Point}; #[test] fn line_reversed() { let l = Line::new((0.0, 0.0), (1.0, 1.0)); let f = l.reversed(); assert_eq!(l.p0, f.p1); assert_eq!(l.p1, f.p0); // Reversing it again should result in the original line assert_eq!(l, f.reversed()); } #[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); } #[test] fn line_midpoint() { let l = Line::new((0.0, 0.0), (2.0, 4.0)); assert_eq!(l.midpoint(), Point::new(1.0, 2.0)); } #[test] fn line_is_finite() { assert!((Line { p0: Point { x: 0., y: 0. }, p1: Point { x: 1., y: 1. } }) .is_finite()); assert!(!(Line { p0: Point { x: 0., y: 0. }, p1: Point { x: f64::INFINITY, y: 1. } }) .is_finite()); assert!(!(Line { p0: Point { x: 0., y: 0. }, p1: Point { x: 0., y: f64::INFINITY } }) .is_finite()); } } kurbo-0.11.1/src/mindist.rs000064400000000000000000000230571046102023000136410ustar 00000000000000// Copyright 2021 the Kurbo Authors // SPDX-License-Identifier: Apache-2.0 OR MIT //! Minimum distance between two Bézier curves //! //! This implements the algorithm in "Computing the minimum distance between //! two Bézier curves", Chen et al., *Journal of Computational and Applied //! Mathematics* 229(2009), 294-301 use crate::Vec2; use core::cmp::Ordering; #[cfg(not(feature = "std"))] use crate::common::FloatFuncs; pub(crate) fn min_dist_param( bez1: &[Vec2], bez2: &[Vec2], u: (f64, f64), v: (f64, f64), epsilon: f64, best_alpha: Option, ) -> (f64, f64, f64) { assert!(!bez1.is_empty() && !bez2.is_empty()); let n = bez1.len() - 1; let m = bez2.len() - 1; let (umin, umax) = u; let (vmin, vmax) = v; let umid = (umin + umax) / 2.0; let vmid = (vmin + vmax) / 2.0; let svalues: [(f64, f64, f64); 4] = [ (S(umin, vmin, bez1, bez2), umin, vmin), (S(umin, vmax, bez1, bez2), umin, vmax), (S(umax, vmin, bez1, bez2), umax, vmin), (S(umax, vmax, bez1, bez2), umax, vmax), ]; let alpha: f64 = svalues.iter().map(|(a, _, _)| *a).reduce(f64::min).unwrap(); if let Some(best) = best_alpha { if alpha > best { return (alpha, umid, vmid); } } if (umax - umin).abs() < epsilon || (vmax - vmin).abs() < epsilon { return (alpha, umid, vmid); } // Property one: D(r>k) > alpha let mut is_outside = true; let mut min_drk = None; let mut min_ij = None; for r in 0..(2 * n) { for k in 0..(2 * m) { let d_rk = D_rk(r, k, bez1, bez2); if d_rk < alpha { is_outside = false; } if min_drk.is_none() || Some(d_rk) < min_drk { min_drk = Some(d_rk); min_ij = Some((r, k)); } } } if is_outside { return (alpha, umid, vmid); } // Property two: boundary check let mut at_boundary0on_bez1 = true; let mut at_boundary1on_bez1 = true; let mut at_boundary0on_bez2 = true; let mut at_boundary1on_bez2 = true; for i in 0..2 * n { for j in 0..2 * m { let dij = D_rk(i, j, bez1, bez2); let dkj = D_rk(0, j, bez1, bez2); if dij < dkj { at_boundary0on_bez1 = false; } let dkj = D_rk(2 * n, j, bez1, bez2); if dij < dkj { at_boundary1on_bez1 = false; } let dkj = D_rk(i, 0, bez1, bez2); if dij < dkj { at_boundary0on_bez2 = false; } let dkj = D_rk(i, 2 * m, bez1, bez2); if dij < dkj { at_boundary1on_bez2 = false; } } } if at_boundary0on_bez1 && at_boundary0on_bez2 { return svalues[0]; } if at_boundary0on_bez1 && at_boundary1on_bez2 { return svalues[1]; } if at_boundary1on_bez1 && at_boundary0on_bez2 { return svalues[2]; } if at_boundary1on_bez1 && at_boundary1on_bez2 { return svalues[3]; } let (min_i, min_j) = min_ij.unwrap(); let new_umid = umin + (umax - umin) * (min_i as f64 / (2 * n) as f64); let new_vmid = vmin + (vmax - vmin) * (min_j as f64 / (2 * m) as f64); // Subdivide let results = [ min_dist_param( bez1, bez2, (umin, new_umid), (vmin, new_vmid), epsilon, Some(alpha), ), min_dist_param( bez1, bez2, (umin, new_umid), (new_vmid, vmax), epsilon, Some(alpha), ), min_dist_param( bez1, bez2, (new_umid, umax), (vmin, new_vmid), epsilon, Some(alpha), ), min_dist_param( bez1, bez2, (new_umid, umax), (new_vmid, vmax), epsilon, Some(alpha), ), ]; *results .iter() .min_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(Ordering::Less)) .unwrap() } // $ S(u,v)=\sum_{r=0}^{2n} \sum _{k=0}^{2m} D_{r,k} B_{2n}^r(u) B_{2m}^k(v) $ #[allow(non_snake_case)] fn S(u: f64, v: f64, bez1: &[Vec2], bez2: &[Vec2]) -> f64 { let n = bez1.len() - 1; let m = bez2.len() - 1; let mut summand = 0.0; for r in 0..=2 * n { for k in 0..=2 * m { summand += D_rk(r, k, bez1, bez2) * basis_function(2 * n, r, u) * basis_function(2 * m, k, v); } } summand } // $ C_{r,k} = ( \sum_{i=\theta}^\upsilon P_i C_n^i C_n^{r-i} / C_{2n}^r ) \cdot (\sum_{j=\sigma}^\varsigma Q_j C_m^j C_m^{k-j} / C_{2m}^k ) $ #[allow(non_snake_case)] fn C_rk(r: usize, k: usize, bez1: &[Vec2], bez2: &[Vec2]) -> f64 { let n = bez1.len() - 1; let upsilon = r.min(n); let theta = r - n.min(r); let mut left: Vec2 = Vec2::ZERO; for (i, &item) in bez1.iter().enumerate().take(upsilon + 1).skip(theta) { left += item * (choose(n, i) * choose(n, r - i)) as f64 / (choose(2 * n, r) as f64); } let m = bez2.len() - 1; let varsigma = k.min(m); let sigma = k - m.min(k); let mut right: Vec2 = Vec2::ZERO; for (j, &item) in bez2.iter().enumerate().take(varsigma + 1).skip(sigma) { right += item * (choose(m, j) * choose(m, k - j)) as f64 / (choose(2 * m, k) as f64); } left.dot(right) } // $ A_r=\sum _{i=\theta} ^\upsilon (P_i \cdot P_{r-i}) C_n^i C_n^{r-i} / C_{2n}^r $ // ($ B_k $ is just the same as $ A_r $ but for the other curve.) #[allow(non_snake_case)] fn A_r(r: usize, p: &[Vec2]) -> f64 { let n = p.len() - 1; let upsilon = r.min(n); let theta = r - n.min(r); (theta..=upsilon) .map(|i| { let dot = p[i].dot(p[r - i]); // These are bounds checked by the sum limits let factor = (choose(n, i) * choose(n, r - i)) as f64 / (choose(2 * n, r) as f64); dot * factor }) .sum() } #[allow(non_snake_case)] fn D_rk(r: usize, k: usize, bez1: &[Vec2], bez2: &[Vec2]) -> f64 { // In the paper, B_k is used for the second factor, but it's the same thing A_r(r, bez1) + A_r(k, bez2) - 2.0 * C_rk(r, k, bez1, bez2) } // Bezier basis function fn basis_function(n: usize, i: usize, u: f64) -> f64 { choose(n, i) as f64 * (1.0 - u).powi((n - i) as i32) * u.powi(i as i32) } // Binomial co-efficient, but returning zeros for values outside of domain fn choose(n: usize, k: usize) -> u32 { let mut n = n; if k > n { return 0; } let mut p = 1; for i in 1..=(n - k) { p *= n; p /= i; n -= 1; } p as u32 } #[cfg(test)] mod tests { use crate::mindist::A_r; use crate::mindist::{choose, D_rk}; use crate::param_curve::ParamCurve; use crate::{CubicBez, Line, PathSeg, Vec2}; #[test] fn test_choose() { assert_eq!(choose(6, 0), 1); assert_eq!(choose(6, 1), 6); assert_eq!(choose(6, 2), 15); } #[test] fn test_d_rk() { let bez1 = vec![ Vec2::new(129.0, 139.0), Vec2::new(190.0, 139.0), Vec2::new(201.0, 364.0), Vec2::new(90.0, 364.0), ]; let bez2 = vec![ Vec2::new(309.0, 159.0), Vec2::new(178.0, 159.0), Vec2::new(215.0, 408.0), Vec2::new(309.0, 408.0), ]; let b = A_r(1, &bez2); assert!((b - 80283.0).abs() < 0.005, "B_1(Q)={b:?}"); let d = D_rk(0, 1, &bez1, &bez2); assert!((d - 9220.0).abs() < 0.005, "D={d:?}"); } #[test] fn test_mindist() { let bez1 = PathSeg::Cubic(CubicBez::new( (129.0, 139.0), (190.0, 139.0), (201.0, 364.0), (90.0, 364.0), )); let bez2 = PathSeg::Cubic(CubicBez::new( (309.0, 159.0), (178.0, 159.0), (215.0, 408.0), (309.0, 408.0), )); let mindist = bez1.min_dist(bez2, 0.001); assert!((mindist.distance - 50.9966).abs() < 0.5); } #[test] fn test_overflow() { let bez1 = PathSeg::Cubic(CubicBez::new( (232.0, 126.0), (134.0, 126.0), (139.0, 232.0), (141.0, 301.0), )); let bez2 = PathSeg::Line(Line::new((359.0, 416.0), (367.0, 755.0))); let mindist = bez1.min_dist(bez2, 0.001); assert!((mindist.distance - 246.4731222669117).abs() < 0.5); } #[test] fn test_out_of_order() { let bez1 = PathSeg::Cubic(CubicBez::new( (287.0, 182.0), (346.0, 277.0), (356.0, 299.0), (359.0, 416.0), )); let bez2 = PathSeg::Line(Line::new((141.0, 301.0), (152.0, 709.0))); let mindist1 = bez1.min_dist(bez2, 0.5); let mindist2 = bez2.min_dist(bez1, 0.5); assert!((mindist1.distance - mindist2.distance).abs() < 0.5); } #[test] fn test_line_curve() { let line = PathSeg::Line(Line::new((929.0, 335.0), (911.0, 340.0))); let line_as_bez = PathSeg::Cubic(CubicBez::new( line.eval(0.0), line.eval(1.0 / 3.0), line.eval(2.0 / 3.0), line.eval(1.0), )); let bez2 = PathSeg::Cubic(CubicBez::new( (1052.0, 401.0), (1048.0, 305.0), (1046.0, 216.0), (1054.0, 146.0), )); let mindist_as_bez = line_as_bez.min_dist(bez2, 0.5); let mindist_as_line = line.min_dist(bez2, 0.5); assert!((mindist_as_line.distance - mindist_as_bez.distance).abs() < 0.5); } } kurbo-0.11.1/src/offset.rs000064400000000000000000000167441046102023000134650ustar 00000000000000// Copyright 2022 the Kurbo Authors // SPDX-License-Identifier: Apache-2.0 OR MIT //! Computation of offset curves of cubic Béziers, based on a curve fitting //! approach. //! //! See the [Parallel curves of cubic Béziers] blog post for a discussion of how //! this algorithm works and what kind of results can be expected. In general, it //! is expected to perform much better than most published algorithms. The number //! of curve segments needed to attain a given accuracy scales as O(n^6) with //! accuracy. //! //! In general, to compute the offset curve (also known as parallel curve) of //! a cubic Bézier segment, create a [`CubicOffset`] struct with the curve //! segment and offset, then use [`fit_to_bezpath`] or [`fit_to_bezpath_opt`] //! depending on how much time to spend optimizing the resulting path. //! //! [`fit_to_bezpath`]: crate::fit_to_bezpath //! [`fit_to_bezpath_opt`]: crate::fit_to_bezpath_opt //! [Parallel curves of cubic Béziers]: https://raphlinus.github.io/curves/2022/09/09/parallel-beziers.html use core::ops::Range; #[cfg(not(feature = "std"))] use crate::common::FloatFuncs; use crate::{ common::solve_itp, CubicBez, CurveFitSample, ParamCurve, ParamCurveDeriv, ParamCurveFit, Point, QuadBez, Vec2, }; /// The offset curve of a cubic Bézier. /// /// This is a representation of the offset curve of a cubic Bézier segment, for /// purposes of curve fitting. /// /// See the [module-level documentation] for a bit more discussion of the approach, /// and how this struct is to be used. /// /// [module-level documentation]: crate::offset pub struct CubicOffset { /// Source curve. c: CubicBez, /// Derivative of source curve. q: QuadBez, /// Offset. d: f64, // c0 + c1 t + c2 t^2 is the cross product of second and first // derivatives of the underlying cubic, multiplied by offset (for // computing cusp). c0: f64, c1: f64, c2: f64, } impl CubicOffset { /// Create a new curve from Bézier segment and offset. /// /// This method should only be used if the Bézier is smooth. Use /// [`new_regularized`] instead to deal with a wider range of inputs. /// /// [`new_regularized`]: Self::new_regularized pub fn new(c: CubicBez, d: f64) -> Self { let q = c.deriv(); let d0 = q.p0.to_vec2(); let d1 = 2.0 * (q.p1 - q.p0); let d2 = q.p0.to_vec2() - 2.0 * q.p1.to_vec2() + q.p2.to_vec2(); CubicOffset { c, q, d, c0: d * d1.cross(d0), c1: d * 2.0 * d2.cross(d0), c2: d * d2.cross(d1), } } /// Create a new curve from Bézier segment and offset, with numerical robustness tweaks. /// /// The dimension represents a minimum feature size; the regularization is allowed to /// perturb the curve by this amount in order to improve the robustness. pub fn new_regularized(c: CubicBez, d: f64, dimension: f64) -> Self { Self::new(c.regularize(dimension), d) } fn eval_offset(&self, t: f64) -> Vec2 { let dp = self.q.eval(t).to_vec2(); let norm = Vec2::new(-dp.y, dp.x); // TODO: deal with hypot = 0 norm * self.d / dp.hypot() } fn eval(&self, t: f64) -> Point { // Point on source curve. self.c.eval(t) + self.eval_offset(t) } /// Evaluate derivative of curve. fn eval_deriv(&self, t: f64) -> Vec2 { self.cusp_sign(t) * self.q.eval(t).to_vec2() } // Compute a function which has a zero-crossing at cusps, and is // positive at low curvatures on the source curve. fn cusp_sign(&self, t: f64) -> f64 { let ds2 = self.q.eval(t).to_vec2().hypot2(); ((self.c2 * t + self.c1) * t + self.c0) / (ds2 * ds2.sqrt()) + 1.0 } } impl ParamCurveFit for CubicOffset { fn sample_pt_tangent(&self, t: f64, sign: f64) -> CurveFitSample { let p = self.eval(t); const CUSP_EPS: f64 = 1e-8; let mut cusp = self.cusp_sign(t); if cusp.abs() < CUSP_EPS { // This is a numerical derivative, which is probably good enough // for all practical purposes, but an analytical derivative would // be more elegant. // // Also, we're not dealing with second or higher order cusps. cusp = sign * (self.cusp_sign(t + CUSP_EPS) - self.cusp_sign(t - CUSP_EPS)); } let tangent = self.q.eval(t).to_vec2() * cusp.signum(); CurveFitSample { p, tangent } } fn sample_pt_deriv(&self, t: f64) -> (Point, Vec2) { (self.eval(t), self.eval_deriv(t)) } fn break_cusp(&self, range: Range) -> Option { const CUSP_EPS: f64 = 1e-8; // When an endpoint is on (or very near) a cusp, move just far enough // away from the cusp that we're confident we have the right sign. let break_cusp_help = |mut x, mut d| { let mut cusp = self.cusp_sign(x); while cusp.abs() < CUSP_EPS && d < 1.0 { x += d; let old_cusp = cusp; cusp = self.cusp_sign(x); if cusp.abs() > old_cusp.abs() { break; } d *= 2.0; } (x, cusp) }; let (a, cusp0) = break_cusp_help(range.start, 1e-12); let (b, cusp1) = break_cusp_help(range.end, -1e-12); if a >= b || cusp0 * cusp1 >= 0.0 { // Discussion point: maybe we should search for double cusps in the interior // of the range. return None; } let s = cusp1.signum(); let f = |t| s * self.cusp_sign(t); let k1 = 0.2 / (b - a); const ITP_EPS: f64 = 1e-12; let x = solve_itp(f, a, b, ITP_EPS, 1, k1, s * cusp0, s * cusp1); Some(x) } } #[cfg(test)] mod tests { use super::CubicOffset; use crate::{fit_to_bezpath, fit_to_bezpath_opt, CubicBez, PathEl}; // This test tries combinations of parameters that have caused problems in the past. #[test] fn pathological_curves() { let curve = CubicBez { p0: (-1236.3746269978635, 152.17981429574826).into(), p1: (-1175.18662093517, 108.04721798590596).into(), p2: (-1152.142883879584, 105.76260301083356).into(), p3: (-1151.842639804639, 105.73040758939104).into(), }; let offset = 3603.7267536453924; let accuracy = 0.1; let offset_path = CubicOffset::new(curve, offset); let path = fit_to_bezpath_opt(&offset_path, accuracy); assert!(matches!(path.iter().next(), Some(PathEl::MoveTo(_)))); let path_opt = fit_to_bezpath(&offset_path, accuracy); assert!(matches!(path_opt.iter().next(), Some(PathEl::MoveTo(_)))); } /// Cubic offset that used to trigger infinite recursion. #[test] fn infinite_recursion() { const DIM_TUNE: f64 = 0.25; const TOLERANCE: f64 = 0.1; let c = CubicBez::new( (1096.2962962962963, 593.90243902439033), (1043.6213991769548, 593.90243902439033), (1030.4526748971193, 593.90243902439033), (1056.7901234567901, 593.90243902439033), ); let co = CubicOffset::new_regularized(c, -0.5, DIM_TUNE * TOLERANCE); fit_to_bezpath(&co, TOLERANCE); } #[test] fn test_cubic_offset_simple_line() { let cubic = CubicBez::new((0., 0.), (10., 0.), (20., 0.), (30., 0.)); let offset = CubicOffset::new(cubic, 5.); let _optimized = fit_to_bezpath(&offset, 1e-6); } } kurbo-0.11.1/src/param_curve.rs000064400000000000000000000166671046102023000145070ustar 00000000000000// Copyright 2018 the Kurbo Authors // SPDX-License-Identifier: Apache-2.0 OR MIT //! A trait for curves parametrized by a scalar. use core::ops::Range; use arrayvec::ArrayVec; use crate::{common, Point, Rect}; #[cfg(not(feature = "std"))] use crate::common::FloatFuncs; /// 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 arc length from the start. /// /// This implementation uses the IPT method, as provided by /// [`common::solve_itp`]. This is as robust as bisection but /// typically converges faster. In addition, the method takes /// care to compute arc lengths of increasingly smaller segments /// of the curve, as that is likely faster than repeatedly /// computing the arc length of the segment starting at t=0. fn inv_arclen(&self, arclen: f64, accuracy: f64) -> f64 { if arclen <= 0.0 { return 0.0; } let total_arclen = self.arclen(accuracy); if arclen >= total_arclen { return 1.0; } let mut t_last = 0.0; let mut arclen_last = 0.0; let epsilon = accuracy / total_arclen; let n = 1.0 - epsilon.log2().ceil().min(0.0); let inner_accuracy = accuracy / n; let f = |t: f64| { let (range, dir) = if t > t_last { (t_last..t, 1.0) } else { (t..t_last, -1.0) }; let arc = self.subsegment(range).arclen(inner_accuracy); arclen_last += arc * dir; t_last = t; arclen_last - arclen }; common::solve_itp(f, 0.0, 1.0, epsilon, 1, 0.2, -arclen, total_arclen - arclen) } } /// 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; } /// The nearest position on a curve to some point. /// /// This is returned by [`ParamCurveNearest::nearest`] #[derive(Debug, Clone, Copy)] pub struct Nearest { /// The square of the distance from the nearest position on the curve /// to the given point. pub distance_sq: f64, /// The position on the curve of the nearest point, as a parameter. /// /// To resolve this to a [`Point`], use [`ParamCurve::eval`]. pub t: f64, } /// A parametrized curve that reports the nearest point. pub trait ParamCurveNearest { /// Find the position on the curve that is nearest to the given point. /// /// This returns a [`Nearest`] struct that contains information about /// the position. fn nearest(&self, p: Point, accuracy: f64) -> Nearest; } /// 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; /// Return parameter ranges, each of which is monotonic within the range. fn extrema_ranges(&self) -> ArrayVec, { 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.11.1/src/point.rs000064400000000000000000000221771046102023000133250ustar 00000000000000// Copyright 2019 the Kurbo Authors // SPDX-License-Identifier: Apache-2.0 OR MIT //! A 2D point. use core::fmt; use core::ops::{Add, AddAssign, Sub, SubAssign}; use crate::common::FloatExt; use crate::Vec2; #[cfg(not(feature = "std"))] use crate::common::FloatFuncs; /// A 2D point. /// /// This type represents a point in 2D space. It has the same layout as [`Vec2`][crate::Vec2], but /// its meaning is different: `Vec2` represents a change in location (for example velocity). /// /// In general, `kurbo` overloads math operators where it makes sense, for example implementing /// `Affine * Point` as the point under the affine transformation. However `Point + Point` and /// `f64 * Point` are not implemented, because the operations do not make geometric sense. If you /// need to apply these operations, then 1) check what you're doing makes geometric sense, then 2) /// use [`Point::to_vec2`] to convert the point to a `Vec2`. #[derive(Clone, Copy, Default, PartialEq)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[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. /// /// See [`Vec2::hypot`] for the same operation on [`Vec2`]. #[inline] pub fn distance(self, other: Point) -> f64 { (self - other).hypot() } /// Squared Euclidean distance. /// /// See [`Vec2::hypot2`] for the same operation on [`Vec2`]. #[inline] pub fn distance_squared(self, other: Point) -> f64 { (self - other).hypot2() } /// 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); /// ``` /// /// [rounded]: f64::round #[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); /// ``` /// /// [rounded up]: f64::ceil #[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); /// ``` /// /// [rounded down]: f64::floor #[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); /// ``` /// /// [rounded away]: FloatExt::expand #[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); /// ``` /// /// [rounded towards]: f64::trunc #[inline] pub fn trunc(self) -> Point { Point::new(self.x.trunc(), self.y.trunc()) } /// Is this point [finite]? /// /// [finite]: f64::is_finite #[inline] pub fn is_finite(self) -> bool { self.x.is_finite() && self.y.is_finite() } /// Is this point [`NaN`]? /// /// [`NaN`]: f64::is_nan #[inline] pub fn is_nan(self) -> bool { self.x.is_nan() || self.y.is_nan() } } impl From<(f32, f32)> for Point { #[inline] fn from(v: (f32, f32)) -> Point { Point { x: v.0 as f64, y: v.1 as f64, } } } 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(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 } } } #[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!("{p:.2}"), "(0.12, 9.88)"); } } kurbo-0.11.1/src/quadbez.rs000064400000000000000000000406051046102023000136230ustar 00000000000000// Copyright 2018 the Kurbo Authors // SPDX-License-Identifier: Apache-2.0 OR MIT //! Quadratic Bézier segments. use core::ops::{Mul, Range}; use arrayvec::ArrayVec; use crate::common::solve_cubic; use crate::MAX_EXTREMA; use crate::{ Affine, CubicBez, Line, Nearest, ParamCurve, ParamCurveArclen, ParamCurveArea, ParamCurveCurvature, ParamCurveDeriv, ParamCurveExtrema, ParamCurveNearest, PathEl, Point, Rect, Shape, }; #[cfg(not(feature = "std"))] use crate::common::FloatFuncs; /// A single quadratic Bézier segment. #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[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 } /// Is this quadratic Bezier curve finite? #[inline] pub fn is_finite(&self) -> bool { self.p0.is_finite() && self.p1.is_finite() && self.p2.is_finite() } /// Is this quadratic Bezier curve NaN? #[inline] pub fn is_nan(&self) -> bool { self.p0.is_nan() || self.p1.is_nan() || self.p2.is_nan() } } /// An iterator for quadratic beziers. pub struct QuadBezIter { quad: QuadBez, ix: usize, } impl Shape for QuadBez { type PathElementsIter<'iter> = 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() } 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 } } /// 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), ) } #[inline] fn start(&self) -> Point { self.p0 } #[inline] fn end(&self) -> Point { self.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 the nearest point, using analytical algorithm based on cubic root finding. fn nearest(&self, p: Point, _accuracy: f64) -> Nearest { 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 !(0.0..=1.0).contains(&t) { 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; if roots.is_empty() { need_ends = true; } 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); } Nearest { t: t_best, distance_sq: r_best.unwrap(), } } } impl ParamCurveCurvature for QuadBez {} impl ParamCurveExtrema for QuadBez { fn extrema(&self) -> ArrayVec { 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, Nearest, 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); } fn verify(result: Nearest, expected: f64) { assert!( (result.t - expected).abs() < 1e-6, "got {result:?} expected {expected}" ); } #[test] fn quadbez_nearest() { // 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() { 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_nearest_rounding_panic() { let quad = QuadBez::new( (-1.0394736842105263, 0.0), (0.8210526315789474, -1.511111111111111), (0.0, 1.9333333333333333), ); let test = Point::new(-1.7976931348623157e308, 0.8571428571428571); // accuracy ignored let _res = quad.nearest(test, 1e-6); // if we got here then we didn't panic } #[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.11.1/src/quadspline.rs000064400000000000000000000060671046102023000143410ustar 00000000000000// Copyright 2021 the Kurbo Authors // SPDX-License-Identifier: Apache-2.0 OR MIT //! Quadratic Bézier splines. use crate::Point; use crate::QuadBez; use alloc::vec::Vec; /// A quadratic Bézier spline in [B-spline](https://en.wikipedia.org/wiki/B-spline) format. #[derive(Clone, Debug, PartialEq)] pub struct QuadSpline(Vec); impl QuadSpline { /// Construct a new `QuadSpline` from an array of [`Point`]s. #[inline] pub fn new(points: Vec) -> Self { Self(points) } /// Return the spline's control [`Point`]s. #[inline] pub fn points(&self) -> &[Point] { &self.0 } /// Return an iterator over the implied [`QuadBez`] sequence. /// /// The returned quads are guaranteed to be G1 continuous. #[inline] pub fn to_quads(&self) -> impl Iterator + '_ { ToQuadBez { idx: 0, points: &self.0, } } } struct ToQuadBez<'a> { idx: usize, points: &'a Vec, } impl<'a> Iterator for ToQuadBez<'a> { type Item = QuadBez; fn next(&mut self) -> Option { let [mut p0, p1, mut p2]: [Point; 3] = self.points.get(self.idx..=self.idx + 2)?.try_into().ok()?; if self.idx != 0 { p0 = p0.midpoint(p1); } if self.idx + 2 < self.points.len() - 1 { p2 = p1.midpoint(p2); } self.idx += 1; Some(QuadBez { p0, p1, p2 }) } } #[cfg(test)] mod tests { use crate::{Point, QuadBez, QuadSpline}; #[test] pub fn no_points_no_quads() { assert!(QuadSpline::new(Vec::new()).to_quads().next().is_none()); } #[test] pub fn one_point_no_quads() { assert!(QuadSpline::new(vec![Point::new(1.0, 1.0)]) .to_quads() .next() .is_none()); } #[test] pub fn two_points_no_quads() { assert!( QuadSpline::new(vec![Point::new(1.0, 1.0), Point::new(1.0, 1.0)]) .to_quads() .next() .is_none() ); } #[test] pub fn three_points_same_quad() { let p0 = Point::new(1.0, 1.0); let p1 = Point::new(2.0, 2.0); let p2 = Point::new(3.0, 3.0); assert_eq!( vec![QuadBez { p0, p1, p2 }], QuadSpline::new(vec![p0, p1, p2]) .to_quads() .collect::>() ); } #[test] pub fn four_points_implicit_on_curve() { let p0 = Point::new(1.0, 1.0); let p1 = Point::new(3.0, 3.0); let p2 = Point::new(5.0, 5.0); let p3 = Point::new(8.0, 8.0); assert_eq!( vec![ QuadBez { p0, p1, p2: p1.midpoint(p2) }, QuadBez { p0: p1.midpoint(p2), p1: p2, p2: p3 } ], QuadSpline::new(vec![p0, p1, p2, p3]) .to_quads() .collect::>() ); } } kurbo-0.11.1/src/rect.rs000064400000000000000000000655461046102023000131400ustar 00000000000000// Copyright 2019 the Kurbo Authors // SPDX-License-Identifier: Apache-2.0 OR MIT //! A rectangle. use core::fmt; use core::ops::{Add, Sub}; use crate::{Ellipse, Insets, PathEl, Point, RoundedRect, RoundedRectRadii, Shape, Size, Vec2}; #[cfg(not(feature = "std"))] use crate::common::FloatFuncs; /// A rectangle. #[derive(Clone, Copy, Default, PartialEq)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[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); /// ``` #[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. #[doc(alias = "is_empty")] #[inline] pub fn is_zero_area(&self) -> bool { self.area() == 0.0 } /// Whether this rectangle has zero area. /// /// Note: a rectangle with negative area is not considered empty. #[inline] #[deprecated(since = "0.11.1", note = "use is_zero_area instead")] pub fn is_empty(&self) -> bool { self.is_zero_area() } /// 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. /// /// If you want to determine whether two rectangles intersect, use the /// [`overlaps`] method instead. /// /// [`overlaps`]: Rect::overlaps #[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)) } /// Determines whether this rectangle overlaps with another in any way. /// /// Note that the edge of the rectangle is considered to be part of itself, meaning /// that two rectangles that share an edge are considered to overlap. /// /// Returns `true` if the rectangles overlap, `false` otherwise. /// /// If you want to compute the *intersection* of two rectangles, use the /// [`intersect`] method instead. /// /// [`intersect`]: Rect::intersect /// /// # Examples /// /// ``` /// use kurbo::Rect; /// /// let rect1 = Rect::new(0.0, 0.0, 10.0, 10.0); /// let rect2 = Rect::new(5.0, 5.0, 15.0, 15.0); /// assert!(rect1.overlaps(rect2)); /// /// let rect1 = Rect::new(0.0, 0.0, 10.0, 10.0); /// let rect2 = Rect::new(10.0, 0.0, 20.0, 10.0); /// assert!(rect1.overlaps(rect2)); /// ``` #[inline] pub fn overlaps(&self, other: Rect) -> bool { self.x0 <= other.x1 && self.x1 >= other.x0 && self.y0 <= other.y1 && self.y1 >= other.y0 } /// Returns whether this rectangle contains another rectangle. /// /// A rectangle is considered to contain another rectangle if the other /// rectangle is fully enclosed within the bounds of this rectangle. /// /// # Examples /// /// ``` /// use kurbo::Rect; /// /// let rect1 = Rect::new(0.0, 0.0, 10.0, 10.0); /// let rect2 = Rect::new(2.0, 2.0, 4.0, 4.0); /// assert!(rect1.contains_rect(rect2)); /// ``` /// /// Two equal rectangles are considered to contain each other. /// /// ``` /// use kurbo::Rect; /// /// let rect = Rect::new(0.0, 0.0, 10.0, 10.0); /// assert!(rect.contains_rect(rect)); /// ``` #[inline] pub fn contains_rect(&self, other: Rect) -> bool { self.x0 <= other.x0 && self.y0 <= other.y0 && self.x1 >= other.x1 && self.y1 >= other.y1 } /// 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); /// ``` /// /// [rounded]: f64::round #[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); /// ``` /// /// [rounded up]: f64::ceil #[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); /// ``` /// /// [rounded down]: f64::floor #[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) } /// Scales the `Rect` by `factor` with respect to the origin (the point `(0, 0)`). /// /// # Examples /// /// ``` /// use kurbo::Rect; /// /// let rect = Rect::new(2., 2., 4., 6.).scale_from_origin(2.); /// assert_eq!(rect.x0, 4.); /// assert_eq!(rect.x1, 8.); /// ``` #[inline] pub fn scale_from_origin(self, factor: f64) -> Rect { Rect { x0: self.x0 * factor, y0: self.y0 * factor, x1: self.x1 * factor, y1: self.y1 * factor, } } /// Creates a new [`RoundedRect`] from this `Rect` and the provided /// corner [radius](RoundedRectRadii). #[inline] pub fn to_rounded_rect(self, radii: impl Into) -> RoundedRect { RoundedRect::from_rect(self, radii) } /// Returns the [`Ellipse`] that is bounded by this `Rect`. #[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) } } /// Is this rectangle [finite]? /// /// [finite]: f64::is_finite #[inline] pub fn is_finite(&self) -> bool { self.x0.is_finite() && self.x1.is_finite() && self.y0.is_finite() && self.y1.is_finite() } /// Is this rectangle [NaN]? /// /// [NaN]: f64::is_nan #[inline] pub fn is_nan(&self) -> bool { self.x0.is_nan() || self.y0.is_nan() || self.x1.is_nan() || self.y1.is_nan() } } 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<'iter> = 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) } #[inline] fn contains(&self, pt: Point) -> bool { self.contains(pt) } } // 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!("{r:.2}"), "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() { 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); } #[test] fn contained_rect_overlaps() { let outer = Rect::new(0.0, 0.0, 10.0, 10.0); let inner = Rect::new(2.0, 2.0, 4.0, 4.0); assert!(outer.overlaps(inner)); } #[test] fn overlapping_rect_overlaps() { let a = Rect::new(0.0, 0.0, 10.0, 10.0); let b = Rect::new(5.0, 5.0, 15.0, 15.0); assert!(a.overlaps(b)); } #[test] fn disjoint_rect_overlaps() { let a = Rect::new(0.0, 0.0, 10.0, 10.0); let b = Rect::new(11.0, 11.0, 15.0, 15.0); assert!(!a.overlaps(b)); } #[test] fn sharing_edge_overlaps() { let a = Rect::new(0.0, 0.0, 10.0, 10.0); let b = Rect::new(10.0, 0.0, 20.0, 10.0); assert!(a.overlaps(b)); } // Test the two other directions in case there is a bug that only appears in one direction. #[test] fn disjoint_rect_overlaps_negative() { let a = Rect::new(0.0, 0.0, 10.0, 10.0); let b = Rect::new(-10.0, -10.0, -5.0, -5.0); assert!(!a.overlaps(b)); } #[test] fn contained_rectangle_contains() { let outer = Rect::new(0.0, 0.0, 10.0, 10.0); let inner = Rect::new(2.0, 2.0, 4.0, 4.0); assert!(outer.contains_rect(inner)); } #[test] fn overlapping_rectangle_contains() { let outer = Rect::new(0.0, 0.0, 10.0, 10.0); let inner = Rect::new(5.0, 5.0, 15.0, 15.0); assert!(!outer.contains_rect(inner)); } #[test] fn disjoint_rectangle_contains() { let outer = Rect::new(0.0, 0.0, 10.0, 10.0); let inner = Rect::new(11.0, 11.0, 15.0, 15.0); assert!(!outer.contains_rect(inner)); } } kurbo-0.11.1/src/rounded_rect.rs000064400000000000000000000345721046102023000146530ustar 00000000000000// Copyright 2019 the Kurbo Authors // SPDX-License-Identifier: Apache-2.0 OR MIT //! A rectangle with rounded corners. use core::f64::consts::{FRAC_PI_2, FRAC_PI_4}; use core::ops::{Add, Sub}; use crate::{arc::ArcAppendIter, Arc, PathEl, Point, Rect, RoundedRectRadii, Shape, Size, Vec2}; #[cfg(not(feature = "std"))] use crate::common::FloatFuncs; /// A rectangle with equally rounded corners. /// /// By construction the rounded rectangle will have /// non-negative dimensions and radii 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`]. /// /// ``` /// use kurbo::{RoundedRect, RoundedRectRadii}; /// /// // Create a rounded rectangle with a single radius for all corners: /// RoundedRect::new(0.0, 0.0, 10.0, 10.0, 5.0); /// /// // Or, specify different radii for each corner, clockwise from the top-left: /// RoundedRect::new(0.0, 0.0, 10.0, 10.0, (1.0, 2.0, 3.0, 4.0)); /// ``` /// /// [`to_rounded_rect`]: Rect::to_rounded_rect #[derive(Clone, Copy, Default, Debug, PartialEq)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct RoundedRect { /// Coordinates of the rectangle. rect: Rect, /// Radius of all four corners. radii: RoundedRectRadii, } impl RoundedRect { /// A new rectangle from minimum and maximum coordinates. /// /// The result will have non-negative width, height and radii. #[inline] pub fn new( x0: f64, y0: f64, x1: f64, y1: f64, radii: impl Into, ) -> RoundedRect { RoundedRect::from_rect(Rect::new(x0, y0, x1, y1), radii) } /// A new rounded rectangle from a rectangle and corner radii. /// /// The result will have non-negative width, height and radii. /// /// See also [`Rect::to_rounded_rect`], which offers the same utility. #[inline] pub fn from_rect(rect: Rect, radii: impl Into) -> RoundedRect { let rect = rect.abs(); let shortest_side_length = (rect.width()).min(rect.height()); let radii = radii.into().abs().clamp(shortest_side_length / 2.0); RoundedRect { rect, radii } } /// A new rectangle from two [`Point`]s. /// /// The result will have non-negative width, height and radius. #[inline] pub fn from_points( p0: impl Into, p1: impl Into, radii: impl Into, ) -> RoundedRect { Rect::from_points(p0, p1).to_rounded_rect(radii) } /// 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, radii: impl Into, ) -> RoundedRect { Rect::from_origin_size(origin, size).to_rounded_rect(radii) } /// 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() } /// Radii of the rounded corners. #[inline] pub fn radii(&self) -> RoundedRectRadii { self.radii } /// 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() } /// Is this rounded rectangle finite? #[inline] pub fn is_finite(&self) -> bool { self.rect.is_finite() && self.radii.is_finite() } /// Is this rounded rectangle NaN? #[inline] pub fn is_nan(&self) -> bool { self.rect.is_nan() || self.radii.is_nan() } } #[doc(hidden)] pub struct RoundedRectPathIter { idx: usize, rect: RectPathIter, arcs: [ArcAppendIter; 4], } impl Shape for RoundedRect { type PathElementsIter<'iter> = RoundedRectPathIter; fn path_elements(&self, tolerance: f64) -> RoundedRectPathIter { let radii = self.radii(); let build_arc_iter = |i, center, ellipse_radii| { let arc = Arc { center, radii: ellipse_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 + radii.top_left, y: self.rect.y0 + radii.top_left, }, Vec2 { x: radii.top_left, y: radii.top_left, }, ), build_arc_iter( 3, Point { x: self.rect.x1 - radii.top_right, y: self.rect.y0 + radii.top_right, }, Vec2 { x: radii.top_right, y: radii.top_right, }, ), build_arc_iter( 0, Point { x: self.rect.x1 - radii.bottom_right, y: self.rect.y1 - radii.bottom_right, }, Vec2 { x: radii.bottom_right, y: radii.bottom_right, }, ), build_arc_iter( 1, Point { x: self.rect.x0 + radii.bottom_left, y: self.rect.y1 - radii.bottom_left, }, Vec2 { x: radii.bottom_left, y: radii.bottom_left, }, ), ]; let rect = RectPathIter { rect: self.rect, ix: 0, radii, }; RoundedRectPathIter { idx: 0, rect, arcs } } #[inline] fn area(&self) -> f64 { // A corner is a quarter-circle, i.e. // .............# // . ###### // . ######### // . ########### // . ############ // .############# // ############## // |-----r------| // For each corner, we need to subtract the square that bounds this // quarter-circle, and add back in the area of quarter circle. let radii = self.radii(); // Start with the area of the bounding rectangle. For each corner, // subtract the area of the corner under the quarter-circle, and add // back the area of the quarter-circle. self.rect.area() + [ radii.top_left, radii.top_right, radii.bottom_right, radii.bottom_left, ] .iter() .map(|radius| (FRAC_PI_4 - 1.0) * radius * radius) .sum::() } #[inline] fn perimeter(&self, _accuracy: f64) -> f64 { // A corner is a quarter-circle, i.e. // .............# // . # // . # // . # // . # // .# // # // |-----r------| // If we start with the bounding rectangle, then subtract 2r (the // straight edge outside the circle) and add 1/4 * pi * (2r) (the // perimeter of the quarter-circle) for each corner with radius r, we // get the perimeter of the shape. let radii = self.radii(); // Start with the full perimeter. For each corner, subtract the // border surrounding the rounded corner and add the quarter-circle // perimeter. self.rect.perimeter(1.0) + ([ radii.top_left, radii.top_right, radii.bottom_right, radii.bottom_left, ]) .iter() .map(|radius| (-2.0 + FRAC_PI_2) * radius) .sum::() } #[inline] fn winding(&self, mut pt: Point) -> i32 { let center = self.center(); // 1. Translate the point relative to the center of the rectangle. pt.x -= center.x; pt.y -= center.y; // 2. Pick a radius value to use based on which quadrant the point is // in. let radii = self.radii(); let radius = match pt { pt if pt.x < 0.0 && pt.y < 0.0 => radii.top_left, pt if pt.x >= 0.0 && pt.y < 0.0 => radii.top_right, pt if pt.x >= 0.0 && pt.y >= 0.0 => radii.bottom_right, pt if pt.x < 0.0 && pt.y >= 0.0 => radii.bottom_left, _ => 0.0, }; // 3. This is the width and height of a rectangle with one corner at // the center of the rounded rectangle, and another corner at the // center of the relevant corner circle. let inside_half_width = (self.width() / 2.0 - radius).max(0.0); let inside_half_height = (self.height() / 2.0 - radius).max(0.0); // 4. Three things are happening here. // // First, the x- and y-values are being reflected into the positive // (bottom-right quadrant). The radius has already been determined, // so it doesn't matter what quadrant is used. // // After reflecting, the points are clamped so that their x- and y- // values can't be lower than the x- and y- values of the center of // the corner circle, and the coordinate system is transformed // again, putting (0, 0) at the center of the corner circle. let px = (pt.x.abs() - inside_half_width).max(0.0); let py = (pt.y.abs() - inside_half_height).max(0.0); // 5. The transforms above clamp all input points such that they will // be inside the rounded rectangle if the corresponding output point // (px, py) is inside a circle centered around the origin with the // given radius. 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, radii: RoundedRectRadii, 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.radii.top_left, ))), 2 => Some(PathEl::LineTo(Point::new( self.rect.x1 - self.radii.top_right, self.rect.y0, ))), 3 => Some(PathEl::LineTo(Point::new( self.rect.x1, self.rect.y1 - self.radii.bottom_right, ))), 4 => Some(PathEl::LineTo(Point::new( self.rect.x0 + self.radii.bottom_left, 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() } } } } impl Add for RoundedRect { type Output = RoundedRect; #[inline] fn add(self, v: Vec2) -> RoundedRect { RoundedRect::from_rect(self.rect + v, self.radii) } } impl Sub for RoundedRect { type Output = RoundedRect; #[inline] fn sub(self, v: Vec2) -> RoundedRect { RoundedRect::from_rect(self.rect - v, self.radii) } } #[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, 5.0, 5.0, 0.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(-5.0, 20.0)), 1); // bottom-left corner (has a radius of 0) 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.11.1/src/rounded_rect_radii.rs000064400000000000000000000076041046102023000160170ustar 00000000000000// Copyright 2021 the Kurbo Authors // SPDX-License-Identifier: Apache-2.0 OR MIT //! A description of the radii for each corner of a rounded rectangle. use core::convert::From; #[cfg(not(feature = "std"))] use crate::common::FloatFuncs; /// Radii for each corner of a rounded rectangle. /// /// The use of `top` as in `top_left` assumes a y-down coordinate space. Piet /// (and Druid by extension) uses a y-down coordinate space, but Kurbo also /// supports a y-up coordinate space, in which case `top_left` would actually /// refer to the bottom-left corner, and vice versa. Top may not always /// actually be the top, but `top` corners will always have a smaller y-value /// than `bottom` corners. #[derive(Clone, Copy, Default, Debug, PartialEq)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct RoundedRectRadii { /// The radius of the top-left corner. pub top_left: f64, /// The radius of the top-right corner. pub top_right: f64, /// The radius of the bottom-right corner. pub bottom_right: f64, /// The radius of the bottom-left corner. pub bottom_left: f64, } impl RoundedRectRadii { /// Create a new `RoundedRectRadii`. This function takes radius values for /// the four corners. The argument order is `top_left`, `top_right`, /// `bottom_right`, `bottom_left`, or clockwise starting from `top_left`. pub const fn new(top_left: f64, top_right: f64, bottom_right: f64, bottom_left: f64) -> Self { RoundedRectRadii { top_left, top_right, bottom_right, bottom_left, } } /// Create a new `RoundedRectRadii` from a single radius. The `radius` /// argument will be set as the radius for all four corners. pub const fn from_single_radius(radius: f64) -> Self { RoundedRectRadii { top_left: radius, top_right: radius, bottom_right: radius, bottom_left: radius, } } /// Takes the absolute value of all corner radii. pub fn abs(&self) -> Self { RoundedRectRadii::new( self.top_left.abs(), self.top_right.abs(), self.bottom_right.abs(), self.bottom_left.abs(), ) } /// For each corner, takes the min of that corner's radius and `max`. pub fn clamp(&self, max: f64) -> Self { RoundedRectRadii::new( self.top_left.min(max), self.top_right.min(max), self.bottom_right.min(max), self.bottom_left.min(max), ) } /// Returns `true` if all radius values are finite. pub fn is_finite(&self) -> bool { self.top_left.is_finite() && self.top_right.is_finite() && self.bottom_right.is_finite() && self.bottom_left.is_finite() } /// Returns `true` if any corner radius value is NaN. pub fn is_nan(&self) -> bool { self.top_left.is_nan() || self.top_right.is_nan() || self.bottom_right.is_nan() || self.bottom_left.is_nan() } /// If all radii are equal, returns the value of the radii. Otherwise, /// returns `None`. pub fn as_single_radius(&self) -> Option { let epsilon = 1e-9; if (self.top_left - self.top_right).abs() < epsilon && (self.top_right - self.bottom_right).abs() < epsilon && (self.bottom_right - self.bottom_left).abs() < epsilon { Some(self.top_left) } else { None } } } impl From for RoundedRectRadii { fn from(radius: f64) -> Self { RoundedRectRadii::from_single_radius(radius) } } impl From<(f64, f64, f64, f64)> for RoundedRectRadii { fn from(radii: (f64, f64, f64, f64)) -> Self { RoundedRectRadii::new(radii.0, radii.1, radii.2, radii.3) } } kurbo-0.11.1/src/shape.rs000064400000000000000000000171151046102023000132700ustar 00000000000000// Copyright 2019 the Kurbo Authors // SPDX-License-Identifier: Apache-2.0 OR MIT //! 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. /// /// [`area`]: Shape::area /// [`bounding_box`]: Shape::bounding_box /// [`winding`]: Shape::winding pub trait Shape { /// The iterator returned by the [`path_elements`] method. /// /// [`path_elements`]: Shape::path_elements type PathElementsIter<'iter>: Iterator + 'iter where Self: 'iter; /// 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 [`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)`. /// /// [`as_rect`]: Shape::as_rect /// [`as_line`]: Shape::as_line /// [`to_path`]: Shape::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`]: Shape::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`]. /// /// The `tolerance` parameter is the same as for [`path_elements()`]. /// /// [`path_elements()`]: Shape::path_elements fn into_path(self, tolerance: f64) -> BezPath where Self: Sized, { self.to_path(tolerance) } #[deprecated(since = "0.7.0", note = "Use into_path instead")] #[doc(hidden)] fn into_bez_path(self, tolerance: f64) -> BezPath where Self: Sized, { 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()`] /// /// [`PathSeg`]: crate::PathSeg /// [`path_elements()`]: Shape::path_elements 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`]: Shape::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. 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<'iter> = T::PathElementsIter<'iter> where T: 'iter, 'a: 'iter; 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_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_circle(&self) -> Option { (*self).as_circle() } fn as_path_slice(&self) -> Option<&[PathEl]> { (*self).as_path_slice() } } kurbo-0.11.1/src/simplify.rs000064400000000000000000000312761046102023000140300ustar 00000000000000// Copyright 2022 the Kurbo Authors // SPDX-License-Identifier: Apache-2.0 OR MIT //! Simplification of a Bézier path. //! //! This module is currently experimental. //! //! The methods in this module create a `SimplifyBezPath` object, which can then //! be fed to [`fit_to_bezpath`] or [`fit_to_bezpath_opt`] depending on the degree //! of optimization desired. //! //! The implementation uses a number of techniques to achieve high performance and //! accuracy. The parameter (generally written `t`) evenly divides the curve segments //! in the original, so sampling can be done in constant time. The derivatives are //! computed analytically, as that is straightforward with Béziers. //! //! The areas and moments are computed analytically (using Green's theorem), and //! the prefix sum is stored. Thus, it is possible to analytically compute the area //! and moment of any subdivision of the curve, also in constant time, by taking //! the difference of two stored prefix sum values, then fixing up the subsegments. //! //! A current limitation (hoped to be addressed in the future) is that non-regular //! cubic segments may have tangents computed incorrectly. This can easily happen, //! for example when setting a control point equal to an endpoint. //! //! In addition, this method does not report corners (adjoining segments where the //! tangents are not continuous). It is not clear whether it's best to handle such //! cases here, or in client code. //! //! [`fit_to_bezpath`]: crate::fit_to_bezpath //! [`fit_to_bezpath_opt`]: crate::fit_to_bezpath_opt use alloc::vec::Vec; use core::ops::Range; #[cfg(not(feature = "std"))] use crate::common::FloatFuncs; use crate::{ fit_to_bezpath, fit_to_bezpath_opt, BezPath, CubicBez, CurveFitSample, Line, ParamCurve, ParamCurveDeriv, ParamCurveFit, PathEl, PathSeg, Point, QuadBez, Vec2, }; /// A Bézier path which has been prepared for simplification. /// /// See the [module-level documentation] for a bit more discussion of the approach, /// and how this struct is to be used. /// /// [module-level documentation]: crate::simplify pub struct SimplifyBezPath(Vec); struct SimplifyCubic { c: CubicBez, // The inclusive prefix sum of the moment integrals moments: (f64, f64, f64), } /// Options for simplifying paths. pub struct SimplifyOptions { /// The tangent of the minimum angle below which the path is considered smooth. angle_thresh: f64, opt_level: SimplifyOptLevel, } /// Optimization level for simplification. pub enum SimplifyOptLevel { /// Subdivide; faster but not as optimized results. Subdivide, /// Optimize subdivision points. Optimize, } impl Default for SimplifyOptions { fn default() -> Self { let opt_level = SimplifyOptLevel::Subdivide; SimplifyOptions { angle_thresh: 1e-3, opt_level, } } } #[doc(hidden)] /// Compute moment integrals. /// /// This is exposed for testing purposes but is an internal detail. We can /// add to the public, documented interface if there is a use case. pub fn moment_integrals(c: CubicBez) -> (f64, f64, f64) { let (x0, y0) = (c.p0.x, c.p0.y); let (x1, y1) = (c.p1.x - x0, c.p1.y - y0); let (x2, y2) = (c.p2.x - x0, c.p2.y - y0); let (x3, y3) = (c.p3.x - x0, c.p3.y - y0); let r0 = 3. * x1; let r1 = 3. * y1; let r2 = x2 * y3; let r3 = x3 * y2; let r4 = x3 * y3; let r5 = 27. * y1; let r6 = x1 * x2; let r7 = 27. * y2; let r8 = 45. * r2; let r9 = 18. * x3; let r10 = x1 * y1; let r11 = 30. * x1; let r12 = 45. * x3; let r13 = x2 * y1; let r14 = 45. * r3; let r15 = x1.powi(2); let r16 = 18. * y3; let r17 = x2.powi(2); let r18 = 45. * y3; let r19 = x3.powi(2); let r20 = 30. * y1; let r21 = y2.powi(2); let r22 = y3.powi(2); let r23 = y1.powi(2); let a = -r0 * y2 - r0 * y3 + r1 * x2 + r1 * x3 - 6. * r2 + 6. * r3 + 10. * r4; // Scale and add chord let lift = x3 * y0; let area = a * 0.05 + lift; let x = r10 * r9 - r11 * r4 + r12 * r13 + r14 * x2 - r15 * r16 - r15 * r7 - r17 * r18 + r17 * r5 + r19 * r20 + 105. * r19 * y2 + 280. * r19 * y3 - 105. * r2 * x3 + r5 * r6 - r6 * r7 - r8 * x1; let y = -r10 * r16 - r10 * r7 - r11 * r22 + r12 * r21 + r13 * r7 + r14 * y1 - r18 * x1 * y2 + r20 * r4 - 27. * r21 * x1 - 105. * r22 * x2 + 140. * r22 * x3 + r23 * r9 + 27. * r23 * x2 + 105. * r3 * y3 - r8 * y2; let mx = x * (1. / 840.) + x0 * area + 0.5 * x3 * lift; let my = y * (1. / 420.) + y0 * a * 0.1 + y0 * lift; (area, mx, my) } impl SimplifyBezPath { /// Set up a new Bézier path for simplification. /// /// Currently this is not dealing with discontinuities at all, but it /// could be extended to do so. pub fn new(path: impl IntoIterator) -> Self { let (mut a, mut x, mut y) = (0.0, 0.0, 0.0); let els = crate::segments(path) .map(|seg| { let c = seg.to_cubic(); let (ai, xi, yi) = moment_integrals(c); a += ai; x += xi; y += yi; SimplifyCubic { c, moments: (a, x, y), } }) .collect(); SimplifyBezPath(els) } /// Resolve a `t` value to a cubic. /// /// Also return the resulting `t` value for the selected cubic. fn scale(&self, t: f64) -> (usize, f64) { let t_scale = t * self.0.len() as f64; let t_floor = t_scale.floor(); (t_floor as usize, t_scale - t_floor) } fn moment_integrals(&self, i: usize, range: Range) -> (f64, f64, f64) { if range.end == range.start { (0.0, 0.0, 0.0) } else { moment_integrals(self.0[i].c.subsegment(range)) } } } impl ParamCurveFit for SimplifyBezPath { fn sample_pt_deriv(&self, t: f64) -> (Point, Vec2) { let (mut i, mut t0) = self.scale(t); let n = self.0.len(); if i == n { i -= 1; t0 = 1.0; } let c = self.0[i].c; (c.eval(t0), c.deriv().eval(t0).to_vec2() * n as f64) } fn sample_pt_tangent(&self, t: f64, _: f64) -> CurveFitSample { let (mut i, mut t0) = self.scale(t); if i == self.0.len() { i -= 1; t0 = 1.0; } let c = self.0[i].c; let p = c.eval(t0); let tangent = c.deriv().eval(t0).to_vec2(); CurveFitSample { p, tangent } } // We could use the default implementation, but provide our own, mostly // because it is possible to efficiently provide an analytically accurate // answer. fn moment_integrals(&self, range: Range) -> (f64, f64, f64) { let (i0, t0) = self.scale(range.start); let (i1, t1) = self.scale(range.end); if i0 == i1 { self.moment_integrals(i0, t0..t1) } else { let (a0, x0, y0) = self.moment_integrals(i0, t0..1.0); let (a1, x1, y1) = self.moment_integrals(i1, 0.0..t1); let (mut a, mut x, mut y) = (a0 + a1, x0 + x1, y0 + y1); if i1 > i0 + 1 { let (a2, x2, y2) = self.0[i0].moments; let (a3, x3, y3) = self.0[i1 - 1].moments; a += a3 - a2; x += x3 - x2; y += y3 - y2; } (a, x, y) } } fn break_cusp(&self, _: Range) -> Option { None } } #[derive(Default)] struct SimplifyState { queue: BezPath, result: BezPath, needs_moveto: bool, } impl SimplifyState { fn add_seg(&mut self, seg: PathSeg) { if self.queue.is_empty() { self.queue.move_to(seg.start()); } match seg { PathSeg::Line(l) => self.queue.line_to(l.p1), PathSeg::Quad(q) => self.queue.quad_to(q.p1, q.p2), PathSeg::Cubic(c) => self.queue.curve_to(c.p1, c.p2, c.p3), } } fn flush(&mut self, accuracy: f64, options: &SimplifyOptions) { if self.queue.is_empty() { return; } if self.queue.elements().len() == 2 { // Queue is just one segment (count is moveto + primitive) // Just output the segment, no simplification is possible. self.result .extend(self.queue.iter().skip(!self.needs_moveto as usize)); } else { let s = SimplifyBezPath::new(&self.queue); let b = match options.opt_level { SimplifyOptLevel::Subdivide => fit_to_bezpath(&s, accuracy), SimplifyOptLevel::Optimize => fit_to_bezpath_opt(&s, accuracy), }; self.result .extend(b.iter().skip(!self.needs_moveto as usize)); } self.needs_moveto = false; self.queue.truncate(0); } } /// Simplify a Bézier path. /// /// This function simplifies an arbitrary Bézier path; it is designed to handle /// multiple subpaths and also corners. /// /// The underlying curve-fitting approach works best if the source path is very /// smooth. If it contains higher frequency noise, then results may be poor, as /// the resulting curve matches the original with G1 continuity at each subdivision /// point, and also preserves the area. For such inputs, consider some form of /// smoothing or low-pass filtering before simplification. In particular, if the /// input is derived from a sequence of points, consider fitting a smooth spline. /// /// We may add such capabilities in the future, possibly as opt-in smoothing /// specified through the options. pub fn simplify_bezpath( path: impl IntoIterator, accuracy: f64, options: &SimplifyOptions, ) -> BezPath { let mut last_pt = None; let mut last_seg: Option = None; let mut state = SimplifyState::default(); for el in path { let mut this_seg = None; match el { PathEl::MoveTo(p) => { state.flush(accuracy, options); state.needs_moveto = true; last_pt = Some(p); } PathEl::LineTo(p) => { let last = last_pt.unwrap(); if last == p { continue; } this_seg = Some(PathSeg::Line(Line::new(last, p))); } PathEl::QuadTo(p1, p2) => { let last = last_pt.unwrap(); if last == p1 && last == p2 { continue; } this_seg = Some(PathSeg::Quad(QuadBez::new(last, p1, p2))); } PathEl::CurveTo(p1, p2, p3) => { let last = last_pt.unwrap(); if last == p1 && last == p2 && last == p3 { continue; } this_seg = Some(PathSeg::Cubic(CubicBez::new(last, p1, p2, p3))); } PathEl::ClosePath => { state.flush(accuracy, options); state.result.close_path(); state.needs_moveto = true; last_seg = None; continue; } } if let Some(seg) = this_seg { if let Some(last) = last_seg { let last_tan = last.tangents().1; let this_tan = seg.tangents().0; if last_tan.cross(this_tan).abs() > last_tan.dot(this_tan).abs() * options.angle_thresh { state.flush(accuracy, options); } } last_pt = Some(seg.end()); state.add_seg(seg); } last_seg = this_seg; } state.flush(accuracy, options); state.result } impl SimplifyOptions { /// Set optimization level. pub fn opt_level(mut self, level: SimplifyOptLevel) -> Self { self.opt_level = level; self } /// Set angle threshold. /// /// The tangent of the angle below which joins are considered smooth and /// not corners. The default is approximately 1 milliradian. pub fn angle_thresh(mut self, thresh: f64) -> Self { self.angle_thresh = thresh; self } } #[cfg(test)] mod tests { use crate::BezPath; use super::{simplify_bezpath, SimplifyOptions}; #[test] fn simplify_lines_corner() { // Make sure lines are passed through unchanged if there is a corner. let mut path = BezPath::new(); path.move_to((1., 2.)); path.line_to((3., 4.)); path.line_to((10., 5.)); let options = SimplifyOptions::default(); let simplified = simplify_bezpath(path.clone(), 1.0, &options); assert_eq!(path, simplified); } } kurbo-0.11.1/src/size.rs000064400000000000000000000236041046102023000131420ustar 00000000000000// Copyright 2019 the Kurbo Authors // SPDX-License-Identifier: Apache-2.0 OR MIT //! A 2D size. use core::fmt; use core::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Sub, SubAssign}; use crate::common::FloatExt; use crate::{Rect, RoundedRect, RoundedRectRadii, Vec2}; #[cfg(not(feature = "std"))] use crate::common::FloatFuncs; /// A 2D size. #[derive(Clone, Copy, Default, PartialEq)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[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. #[doc(alias = "is_empty")] #[inline] pub fn is_zero_area(self) -> bool { self.area() == 0.0 } /// Whether this size has zero area. /// /// Note: a size with negative area is not considered empty. #[inline] #[deprecated(since = "0.11.1", note = "use is_zero_area instead")] pub fn is_empty(self) -> bool { self.is_zero_area() } /// 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`. #[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); /// ``` /// /// [rounded]: f64::round #[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); /// ``` /// /// [rounded up]: f64::ceil #[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); /// ``` /// /// [rounded down]: f64::floor #[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); /// ``` /// /// [rounded away]: FloatExt::expand #[inline] pub fn expand(self) -> Size { Size::new(self.width.expand(), self.height.expand()) } /// Returns a new `Size`, /// with `width` and `height` [rounded towards] zero to 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); /// ``` /// /// [rounded towards]: f64::trunc #[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)`. #[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. #[inline] pub fn to_rounded_rect(self, radii: impl Into) -> RoundedRect { self.to_rect().to_rounded_rect(radii) } /// Is this size [finite]? /// /// [finite]: f64::is_finite #[inline] pub fn is_finite(self) -> bool { self.width.is_finite() && self.height.is_finite() } /// Is this size [NaN]? /// /// [NaN]: f64::is_nan #[inline] pub fn is_nan(self) -> bool { self.width.is_nan() || self.height.is_nan() } } 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) } } #[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!("{s:+6.2}"), "( -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.11.1/src/stroke.rs000064400000000000000000000766311046102023000135070ustar 00000000000000// Copyright 2023 the Kurbo Authors // SPDX-License-Identifier: Apache-2.0 OR MIT use core::{borrow::Borrow, f64::consts::PI}; use alloc::vec::Vec; use smallvec::SmallVec; #[cfg(not(feature = "std"))] use crate::common::FloatFuncs; use crate::{ common::solve_quadratic, fit_to_bezpath, fit_to_bezpath_opt, offset::CubicOffset, Affine, Arc, BezPath, CubicBez, Line, ParamCurve, ParamCurveArclen, PathEl, PathSeg, Point, QuadBez, Vec2, }; /// Defines the connection between two segments of a stroke. #[derive(Copy, Clone, PartialEq, Eq, Debug)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum Join { /// A straight line connecting the segments. Bevel, /// The segments are extended to their natural intersection point. Miter, /// An arc between the segments. Round, } /// Defines the shape to be drawn at the ends of a stroke. #[derive(Copy, Clone, PartialEq, Eq, Debug)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum Cap { /// Flat cap. Butt, /// Square cap with dimensions equal to half the stroke width. Square, /// Rounded cap with radius equal to half the stroke width. Round, } /// Describes the visual style of a stroke. #[derive(Clone, Debug)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Stroke { /// Width of the stroke. pub width: f64, /// Style for connecting segments of the stroke. pub join: Join, /// Limit for miter joins. pub miter_limit: f64, /// Style for capping the beginning of an open subpath. pub start_cap: Cap, /// Style for capping the end of an open subpath. pub end_cap: Cap, /// Lengths of dashes in alternating on/off order. pub dash_pattern: Dashes, /// Offset of the first dash. pub dash_offset: f64, } /// Options for path stroking. pub struct StrokeOpts { opt_level: StrokeOptLevel, } /// Optimization level for computing pub enum StrokeOptLevel { /// Adaptively subdivide segments in half. Subdivide, /// Compute optimized subdivision points to minimize error. Optimized, } impl Default for StrokeOpts { fn default() -> Self { let opt_level = StrokeOptLevel::Subdivide; StrokeOpts { opt_level } } } impl Default for Stroke { fn default() -> Self { Self { width: 1.0, join: Join::Round, miter_limit: 4.0, start_cap: Cap::Round, end_cap: Cap::Round, dash_pattern: Default::default(), dash_offset: 0.0, } } } impl Stroke { /// Creates a new stroke with the specified width. pub fn new(width: f64) -> Self { Self { width, ..Default::default() } } /// Builder method for setting the join style. pub fn with_join(mut self, join: Join) -> Self { self.join = join; self } /// Builder method for setting the limit for miter joins. pub fn with_miter_limit(mut self, limit: f64) -> Self { self.miter_limit = limit; self } /// Builder method for setting the cap style for the start of the stroke. pub fn with_start_cap(mut self, cap: Cap) -> Self { self.start_cap = cap; self } /// Builder method for setting the cap style for the end of the stroke. pub fn with_end_cap(mut self, cap: Cap) -> Self { self.end_cap = cap; self } /// Builder method for setting the cap style. pub fn with_caps(mut self, cap: Cap) -> Self { self.start_cap = cap; self.end_cap = cap; self } /// Builder method for setting the dashing parameters. pub fn with_dashes

(mut self, offset: f64, pattern: P) -> Self where P: IntoIterator, P::Item: Borrow, { self.dash_offset = offset; self.dash_pattern.clear(); self.dash_pattern .extend(pattern.into_iter().map(|dash| *dash.borrow())); self } } impl StrokeOpts { /// Set optimization level for computing stroke outlines. pub fn opt_level(mut self, opt_level: StrokeOptLevel) -> Self { self.opt_level = opt_level; self } } /// Collection of values representing lengths in a dash pattern. pub type Dashes = SmallVec<[f64; 4]>; /// Internal structure used for creating strokes. #[derive(Default)] struct StrokeCtx { // As a possible future optimization, we might not need separate storage // for forward and backward paths, we can add forward to the output in-place. // However, this structure is clearer and the cost fairly modest. output: BezPath, forward_path: BezPath, backward_path: BezPath, start_pt: Point, start_norm: Vec2, start_tan: Vec2, last_pt: Point, last_tan: Vec2, // Precomputation of the join threshold, to optimize per-join logic. // If hypot < (hypot + dot) * join_thresh, omit join altogether. join_thresh: f64, } /// Expand a stroke into a fill. /// /// The `tolerance` parameter controls the accuracy of the result. In general, /// the number of subdivisions in the output scales to the -1/6 power of the /// parameter, for example making it 1/64 as big generates twice as many /// segments. The appropriate value depends on the application; if the result /// of the stroke will be scaled up, a smaller value is needed. /// /// This method attempts a fairly high degree of correctness, but ultimately /// is based on computing parallel curves and adding joins and caps, rather than /// computing the rigorously correct parallel sweep (which requires evolutes in /// the general case). See [Nehab 2020] for more discussion. /// /// [Nehab 2020]: https://dl.acm.org/doi/10.1145/3386569.3392392 pub fn stroke( path: impl IntoIterator, style: &Stroke, opts: &StrokeOpts, tolerance: f64, ) -> BezPath { if style.dash_pattern.is_empty() { stroke_undashed(path, style, tolerance, opts) } else { let dashed = dash(path.into_iter(), style.dash_offset, &style.dash_pattern); stroke_undashed(dashed, style, tolerance, opts) } } /// Version of stroke expansion for styles with no dashes. fn stroke_undashed( path: impl IntoIterator, style: &Stroke, tolerance: f64, opts: &StrokeOpts, ) -> BezPath { let mut ctx = StrokeCtx { join_thresh: 2.0 * tolerance / style.width, ..Default::default() }; for el in path { let p0 = ctx.last_pt; match el { PathEl::MoveTo(p) => { ctx.finish(style); ctx.start_pt = p; ctx.last_pt = p; } PathEl::LineTo(p1) => { if p1 != p0 { let tangent = p1 - p0; ctx.do_join(style, tangent); ctx.last_tan = tangent; ctx.do_line(style, tangent, p1); } } PathEl::QuadTo(p1, p2) => { if p1 != p0 || p2 != p0 { let q = QuadBez::new(p0, p1, p2); let (tan0, tan1) = PathSeg::Quad(q).tangents(); ctx.do_join(style, tan0); ctx.do_cubic(style, q.raise(), tolerance, opts); ctx.last_tan = tan1; } } PathEl::CurveTo(p1, p2, p3) => { if p1 != p0 || p2 != p0 || p3 != p0 { let c = CubicBez::new(p0, p1, p2, p3); let (tan0, tan1) = PathSeg::Cubic(c).tangents(); ctx.do_join(style, tan0); ctx.do_cubic(style, c, tolerance, opts); ctx.last_tan = tan1; } } PathEl::ClosePath => { if p0 != ctx.start_pt { let tangent = ctx.start_pt - p0; ctx.do_join(style, tangent); ctx.last_tan = tangent; ctx.do_line(style, tangent, ctx.start_pt); } ctx.finish_closed(style); } } } ctx.finish(style); ctx.output } fn round_cap(out: &mut BezPath, tolerance: f64, center: Point, norm: Vec2) { round_join(out, tolerance, center, norm, PI); } fn round_join(out: &mut BezPath, tolerance: f64, center: Point, norm: Vec2, angle: f64) { let a = Affine::new([norm.x, norm.y, -norm.y, norm.x, center.x, center.y]); let arc = Arc::new(Point::ORIGIN, (1.0, 1.0), PI - angle, angle, 0.0); arc.to_cubic_beziers(tolerance, |p1, p2, p3| out.curve_to(a * p1, a * p2, a * p3)); } fn round_join_rev(out: &mut BezPath, tolerance: f64, center: Point, norm: Vec2, angle: f64) { let a = Affine::new([norm.x, norm.y, norm.y, -norm.x, center.x, center.y]); let arc = Arc::new(Point::ORIGIN, (1.0, 1.0), PI - angle, angle, 0.0); arc.to_cubic_beziers(tolerance, |p1, p2, p3| out.curve_to(a * p1, a * p2, a * p3)); } fn square_cap(out: &mut BezPath, close: bool, center: Point, norm: Vec2) { let a = Affine::new([norm.x, norm.y, -norm.y, norm.x, center.x, center.y]); out.line_to(a * Point::new(1.0, 1.0)); out.line_to(a * Point::new(-1.0, 1.0)); if close { out.close_path(); } else { out.line_to(a * Point::new(-1.0, 0.0)); } } fn extend_reversed(out: &mut BezPath, elements: &[PathEl]) { for i in (1..elements.len()).rev() { let end = elements[i - 1].end_point().unwrap(); match elements[i] { PathEl::LineTo(_) => out.line_to(end), PathEl::QuadTo(p1, _) => out.quad_to(p1, end), PathEl::CurveTo(p1, p2, _) => out.curve_to(p2, p1, end), _ => unreachable!(), } } } fn fit_with_opts(co: &CubicOffset, tolerance: f64, opts: &StrokeOpts) -> BezPath { match opts.opt_level { StrokeOptLevel::Subdivide => fit_to_bezpath(co, tolerance), StrokeOptLevel::Optimized => fit_to_bezpath_opt(co, tolerance), } } impl StrokeCtx { /// Append forward and backward paths to output. fn finish(&mut self, style: &Stroke) { // TODO: scale let tolerance = 1e-3; if self.forward_path.is_empty() { return; } self.output.extend(&self.forward_path); let back_els = self.backward_path.elements(); let return_p = back_els[back_els.len() - 1].end_point().unwrap(); let d = self.last_pt - return_p; match style.end_cap { Cap::Butt => self.output.line_to(return_p), Cap::Round => round_cap(&mut self.output, tolerance, self.last_pt, d), Cap::Square => square_cap(&mut self.output, false, self.last_pt, d), } extend_reversed(&mut self.output, back_els); match style.start_cap { Cap::Butt => self.output.close_path(), Cap::Round => round_cap(&mut self.output, tolerance, self.start_pt, self.start_norm), Cap::Square => square_cap(&mut self.output, true, self.start_pt, self.start_norm), } self.forward_path.truncate(0); self.backward_path.truncate(0); } /// Finish a closed path fn finish_closed(&mut self, style: &Stroke) { if self.forward_path.is_empty() { return; } self.do_join(style, self.start_tan); self.output.extend(&self.forward_path); self.output.close_path(); let back_els = self.backward_path.elements(); let last_pt = back_els[back_els.len() - 1].end_point().unwrap(); self.output.move_to(last_pt); extend_reversed(&mut self.output, back_els); self.output.close_path(); self.forward_path.truncate(0); self.backward_path.truncate(0); } fn do_join(&mut self, style: &Stroke, tan0: Vec2) { // TODO: scale let tolerance = 1e-3; let scale = 0.5 * style.width / tan0.hypot(); let norm = scale * Vec2::new(-tan0.y, tan0.x); let p0 = self.last_pt; if self.forward_path.elements().is_empty() { self.forward_path.move_to(p0 - norm); self.backward_path.move_to(p0 + norm); self.start_tan = tan0; self.start_norm = norm; } else { let ab = self.last_tan; let cd = tan0; let cross = ab.cross(cd); let dot = ab.dot(cd); let hypot = cross.hypot(dot); // possible TODO: a minor speedup could be squaring both sides if dot <= 0.0 || cross.abs() >= hypot * self.join_thresh { match style.join { Join::Bevel => { self.forward_path.line_to(p0 - norm); self.backward_path.line_to(p0 + norm); } Join::Miter => { if 2.0 * hypot < (hypot + dot) * style.miter_limit.powi(2) { // TODO: maybe better to store last_norm or derive from path? let last_scale = 0.5 * style.width / ab.hypot(); let last_norm = last_scale * Vec2::new(-ab.y, ab.x); if cross > 0.0 { let fp_last = p0 - last_norm; let fp_this = p0 - norm; let h = ab.cross(fp_this - fp_last) / cross; let miter_pt = fp_this - cd * h; self.forward_path.line_to(miter_pt); } else if cross < 0.0 { let fp_last = p0 + last_norm; let fp_this = p0 + norm; let h = ab.cross(fp_this - fp_last) / cross; let miter_pt = fp_this - cd * h; self.backward_path.line_to(miter_pt); } } self.forward_path.line_to(p0 - norm); self.backward_path.line_to(p0 + norm); } Join::Round => { let angle = cross.atan2(dot); if angle > 0.0 { self.backward_path.line_to(p0 + norm); round_join(&mut self.forward_path, tolerance, p0, norm, angle); } else { self.forward_path.line_to(p0 - norm); round_join_rev(&mut self.backward_path, tolerance, p0, -norm, -angle); } } } } } } fn do_line(&mut self, style: &Stroke, tangent: Vec2, p1: Point) { let scale = 0.5 * style.width / tangent.hypot(); let norm = scale * Vec2::new(-tangent.y, tangent.x); self.forward_path.line_to(p1 - norm); self.backward_path.line_to(p1 + norm); self.last_pt = p1; } fn do_cubic(&mut self, style: &Stroke, c: CubicBez, tolerance: f64, opts: &StrokeOpts) { // First, detect degenerate linear case // Ordinarily, this is the direction of the chord, but if the chord is very // short, we take the longer control arm. let chord = c.p3 - c.p0; let mut chord_ref = chord; let mut chord_ref_hypot2 = chord_ref.hypot2(); let d01 = c.p1 - c.p0; if d01.hypot2() > chord_ref_hypot2 { chord_ref = d01; chord_ref_hypot2 = chord_ref.hypot2(); } let d23 = c.p3 - c.p2; if d23.hypot2() > chord_ref_hypot2 { chord_ref = d23; chord_ref_hypot2 = chord_ref.hypot2(); } // Project Bézier onto chord let p0 = c.p0.to_vec2().dot(chord_ref); let p1 = c.p1.to_vec2().dot(chord_ref); let p2 = c.p2.to_vec2().dot(chord_ref); let p3 = c.p3.to_vec2().dot(chord_ref); const ENDPOINT_D: f64 = 0.01; if p3 <= p0 || p1 > p2 || p1 < p0 + ENDPOINT_D * (p3 - p0) || p2 > p3 - ENDPOINT_D * (p3 - p0) { // potentially a cusp inside let x01 = d01.cross(chord_ref); let x23 = d23.cross(chord_ref); let x03 = chord.cross(chord_ref); let thresh = tolerance.powi(2) * chord_ref_hypot2; if x01 * x01 < thresh && x23 * x23 < thresh && x03 * x03 < thresh { // control points are nearly co-linear let midpoint = c.p0.midpoint(c.p3); // Mapping back from projection of reference chord let ref_vec = chord_ref / chord_ref_hypot2; let ref_pt = midpoint - 0.5 * (p0 + p3) * ref_vec; self.do_linear(style, c, [p0, p1, p2, p3], ref_pt, ref_vec); return; } } // A tuning parameter for regularization. A value too large may distort the curve, // while a value too small may fail to generate smooth curves. This is a somewhat // arbitrary value, and should be revisited. const DIM_TUNE: f64 = 0.25; let dimension = tolerance * DIM_TUNE; let co = CubicOffset::new_regularized(c, -0.5 * style.width, dimension); let forward = fit_with_opts(&co, tolerance, opts); self.forward_path.extend(forward.into_iter().skip(1)); let co = CubicOffset::new_regularized(c, 0.5 * style.width, dimension); let backward = fit_with_opts(&co, tolerance, opts); self.backward_path.extend(backward.into_iter().skip(1)); self.last_pt = c.p3; } /// Do a cubic which is actually linear. /// /// The `p` argument is the control points projected to the reference chord. /// The ref arguments are the inverse map of a projection back to the client /// coordinate space. fn do_linear( &mut self, style: &Stroke, c: CubicBez, p: [f64; 4], ref_pt: Point, ref_vec: Vec2, ) { // Always do round join, to model cusp as limit of finite curvature (see Nehab). let style = Stroke::new(style.width).with_join(Join::Round); // Tangents of endpoints (for connecting to joins) let (tan0, tan1) = PathSeg::Cubic(c).tangents(); self.last_tan = tan0; // find cusps let c0 = p[1] - p[0]; let c1 = 2.0 * p[2] - 4.0 * p[1] + 2.0 * p[0]; let c2 = p[3] - 3.0 * p[2] + 3.0 * p[1] - p[0]; let roots = solve_quadratic(c0, c1, c2); // discard cusps right at endpoints const EPSILON: f64 = 1e-6; for t in roots { if t > EPSILON && t < 1.0 - EPSILON { let mt = 1.0 - t; let z = mt * (mt * mt * p[0] + 3.0 * t * (mt * p[1] + t * p[2])) + t * t * t * p[3]; let p = ref_pt + z * ref_vec; let tan = p - self.last_pt; self.do_join(&style, tan); self.do_line(&style, tan, p); self.last_tan = tan; } } let tan = c.p3 - self.last_pt; self.do_join(&style, tan); self.do_line(&style, tan, c.p3); self.last_tan = tan; self.do_join(&style, tan1); } } /// An implementation of dashing as an iterator-to-iterator transformation. #[doc(hidden)] pub struct DashIterator<'a, T> { inner: T, input_done: bool, closepath_pending: bool, dashes: &'a [f64], dash_ix: usize, init_dash_ix: usize, init_dash_remaining: f64, init_is_active: bool, is_active: bool, state: DashState, current_seg: PathSeg, t: f64, dash_remaining: f64, seg_remaining: f64, start_pt: Point, last_pt: Point, stash: Vec, stash_ix: usize, } #[derive(PartialEq, Eq)] enum DashState { NeedInput, ToStash, Working, FromStash, } impl<'a, T: Iterator> Iterator for DashIterator<'a, T> { type Item = PathEl; fn next(&mut self) -> Option { loop { match self.state { DashState::NeedInput => { if self.input_done { return None; } self.get_input(); if self.input_done { return None; } self.state = DashState::ToStash; } DashState::ToStash => { if let Some(el) = self.step() { self.stash.push(el); } } DashState::Working => { if let Some(el) = self.step() { return Some(el); } } DashState::FromStash => { if let Some(el) = self.stash.get(self.stash_ix) { self.stash_ix += 1; return Some(*el); } else { self.stash.clear(); self.stash_ix = 0; if self.input_done { return None; } if self.closepath_pending { self.closepath_pending = false; self.state = DashState::NeedInput; } else { self.state = DashState::ToStash; } } } } } } } fn seg_to_el(el: &PathSeg) -> PathEl { match el { 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), } } const DASH_ACCURACY: f64 = 1e-6; /// Create a new dashing iterator. /// /// Handling of dashes is fairly orthogonal to stroke expansion. This iterator /// is an internal detail of the stroke expansion logic, but is also available /// separately, and is expected to be useful when doing stroke expansion on /// GPU. /// /// It is implemented as an iterator-to-iterator transform. Because it consumes /// the input sequentially and produces consistent output with correct joins, /// it requires internal state and may allocate. /// /// Accuracy is currently hard-coded to 1e-6. This is better than generally /// expected, and care is taken to get cusps correct, among other things. pub fn dash<'a>( inner: impl Iterator + 'a, dash_offset: f64, dashes: &'a [f64], ) -> impl Iterator + 'a { dash_impl(inner, dash_offset, dashes) } // This is only a separate function to make `DashIterator::new()` typecheck. fn dash_impl>( inner: T, dash_offset: f64, dashes: &[f64], ) -> DashIterator { let mut dash_ix = 0; let mut dash_remaining = dashes[dash_ix] - dash_offset; let mut is_active = true; // Find place in dashes array for initial offset. while dash_remaining < 0.0 { dash_ix = (dash_ix + 1) % dashes.len(); dash_remaining += dashes[dash_ix]; is_active = !is_active; } DashIterator { inner, input_done: false, closepath_pending: false, dashes, dash_ix, init_dash_ix: dash_ix, init_dash_remaining: dash_remaining, init_is_active: is_active, is_active, state: DashState::NeedInput, current_seg: PathSeg::Line(Line::new(Point::ORIGIN, Point::ORIGIN)), t: 0.0, dash_remaining, seg_remaining: 0.0, start_pt: Point::ORIGIN, last_pt: Point::ORIGIN, stash: Vec::new(), stash_ix: 0, } } impl<'a, T: Iterator> DashIterator<'a, T> { #[doc(hidden)] #[deprecated(since = "0.10.4", note = "use dash() instead")] pub fn new(inner: T, dash_offset: f64, dashes: &'a [f64]) -> Self { dash_impl(inner, dash_offset, dashes) } fn get_input(&mut self) { loop { if self.closepath_pending { self.handle_closepath(); break; } let Some(next_el) = self.inner.next() else { self.input_done = true; self.state = DashState::FromStash; return; }; let p0 = self.last_pt; match next_el { PathEl::MoveTo(p) => { if !self.stash.is_empty() { self.state = DashState::FromStash; } self.start_pt = p; self.last_pt = p; self.reset_phase(); continue; } PathEl::LineTo(p1) => { let l = Line::new(p0, p1); self.seg_remaining = l.arclen(DASH_ACCURACY); self.current_seg = PathSeg::Line(l); self.last_pt = p1; } PathEl::QuadTo(p1, p2) => { let q = QuadBez::new(p0, p1, p2); self.seg_remaining = q.arclen(DASH_ACCURACY); self.current_seg = PathSeg::Quad(q); self.last_pt = p2; } PathEl::CurveTo(p1, p2, p3) => { let c = CubicBez::new(p0, p1, p2, p3); self.seg_remaining = c.arclen(DASH_ACCURACY); self.current_seg = PathSeg::Cubic(c); self.last_pt = p3; } PathEl::ClosePath => { self.closepath_pending = true; if p0 != self.start_pt { let l = Line::new(p0, self.start_pt); self.seg_remaining = l.arclen(DASH_ACCURACY); self.current_seg = PathSeg::Line(l); self.last_pt = self.start_pt; } else { self.handle_closepath(); } } } break; } self.t = 0.0; } /// Move arc length forward to next event. fn step(&mut self) -> Option { let mut result = None; if self.state == DashState::ToStash && self.stash.is_empty() { if self.is_active { result = Some(PathEl::MoveTo(self.current_seg.start())); } else { self.state = DashState::Working; } } else if self.dash_remaining < self.seg_remaining { // next transition is a dash transition let seg = self.current_seg.subsegment(self.t..1.0); let t1 = seg.inv_arclen(self.dash_remaining, DASH_ACCURACY); if self.is_active { let subseg = seg.subsegment(0.0..t1); result = Some(seg_to_el(&subseg)); self.state = DashState::Working; } else { let p = seg.eval(t1); result = Some(PathEl::MoveTo(p)); } self.is_active = !self.is_active; self.t += t1 * (1.0 - self.t); self.seg_remaining -= self.dash_remaining; self.dash_ix += 1; if self.dash_ix == self.dashes.len() { self.dash_ix = 0; } self.dash_remaining = self.dashes[self.dash_ix]; } else { if self.is_active { let seg = self.current_seg.subsegment(self.t..1.0); result = Some(seg_to_el(&seg)); } self.dash_remaining -= self.seg_remaining; self.get_input(); } result } fn handle_closepath(&mut self) { if self.state == DashState::ToStash { // Have looped back without breaking a dash, just play it back self.stash.push(PathEl::ClosePath); } else if self.is_active { // connect with path in stash, skip MoveTo. self.stash_ix = 1; } self.state = DashState::FromStash; self.reset_phase(); } fn reset_phase(&mut self) { self.dash_ix = self.init_dash_ix; self.dash_remaining = self.init_dash_remaining; self.is_active = self.init_is_active; } } #[cfg(test)] mod tests { use crate::{ dash, segments, stroke, Cap::Butt, CubicBez, Join::Miter, Line, PathSeg, Shape, Stroke, }; // A degenerate stroke with a cusp at the endpoint. #[test] fn pathological_stroke() { let curve = CubicBez::new( (602.469, 286.585), (641.975, 286.585), (562.963, 286.585), (562.963, 286.585), ); let path = curve.into_path(0.1); let stroke_style = Stroke::new(1.); let stroked = stroke(path, &stroke_style, &Default::default(), 0.001); assert!(stroked.is_finite()); } // Test cases adapted from https://github.com/linebender/vello/pull/388 #[test] fn broken_strokes() { let broken_cubics = [ [ (465.24423, 107.11105), (475.50754, 107.11105), (475.50754, 107.11105), (475.50754, 107.11105), ], [(0., -0.01), (128., 128.001), (128., -0.01), (0., 128.001)], // Near-cusp [(0., 0.), (0., -10.), (0., -10.), (0., 10.)], // Flat line with 180 [(10., 0.), (0., 0.), (20., 0.), (10., 0.)], // Flat line with 2 180s [(39., -39.), (40., -40.), (40., -40.), (0., 0.)], // Flat diagonal with 180 [(40., 40.), (0., 0.), (200., 200.), (0., 0.)], // Diag w/ an internal 180 [(0., 0.), (1e-2, 0.), (-1e-2, 0.), (0., 0.)], // Circle // Flat line with no turns: [ (400.75, 100.05), (400.75, 100.05), (100.05, 300.95), (100.05, 300.95), ], [(0.5, 0.), (0., 0.), (20., 0.), (10., 0.)], // Flat line with 2 180s [(10., 0.), (0., 0.), (10., 0.), (10., 0.)], // Flat line with a 180 ]; let stroke_style = Stroke::new(30.).with_caps(Butt).with_join(Miter); for cubic in &broken_cubics { let path = CubicBez::new(cubic[0], cubic[1], cubic[2], cubic[3]).into_path(0.1); let stroked = stroke(path, &stroke_style, &Default::default(), 0.001); assert!(stroked.is_finite()); } } #[test] fn dash_sequence() { let shape = Line::new((0.0, 0.0), (21.0, 0.0)); let dashes = [1., 5., 2., 5.]; let expansion = [ PathSeg::Line(Line::new((6., 0.), (8., 0.))), PathSeg::Line(Line::new((13., 0.), (14., 0.))), PathSeg::Line(Line::new((19., 0.), (21., 0.))), PathSeg::Line(Line::new((0., 0.), (1., 0.))), ]; let iter = segments(dash(shape.path_elements(0.), 0., &dashes)); assert_eq!(iter.collect::>(), expansion); } #[test] fn dash_sequence_offset() { // Same as dash_sequence, but with a dash offset // of 3, which skips the first dash and cuts into // the first gap. let shape = Line::new((0.0, 0.0), (21.0, 0.0)); let dashes = [1., 5., 2., 5.]; let expansion = [ PathSeg::Line(Line::new((3., 0.), (5., 0.))), PathSeg::Line(Line::new((10., 0.), (11., 0.))), PathSeg::Line(Line::new((16., 0.), (18., 0.))), ]; let iter = segments(dash(shape.path_elements(0.), 3., &dashes)); assert_eq!(iter.collect::>(), expansion); } } kurbo-0.11.1/src/svg.rs000064400000000000000000000533161046102023000127720ustar 00000000000000// Copyright 2018 the Kurbo Authors // SPDX-License-Identifier: Apache-2.0 OR MIT //! SVG path representation. use alloc::vec::Vec; use core::f64::consts::PI; use core::fmt::{self, Display, Formatter}; // MSRV: Once our MSRV is 1.81, we can switch to `core::error` #[cfg(feature = "std")] use std::error::Error; #[cfg(feature = "std")] use std::io::{self, Write}; use crate::{Arc, BezPath, ParamCurve, PathEl, PathSeg, Point, Vec2}; #[cfg(not(feature = "std"))] use crate::common::FloatFuncs; // 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 = "schemars", derive(schemars::JsonSchema))] #[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). #[cfg(feature = "std")] 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. #[cfg(feature = "std")] pub fn write_to(&self, mut writer: W) -> io::Result<()> { for (i, el) in self.elements().iter().enumerate() { if i > 0 { write!(writer, " ")?; } 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 path.elements().is_empty() { return Err(SvgParseError::UninitializedPath); } 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)] #[non_exhaustive] pub enum SvgParseError { /// A number was expected. Wrong, /// The input string ended while still expecting input. UnexpectedEof, /// Encountered an unknown command letter. UnknownCommand(char), /// Encountered a command that precedes expected 'moveto' command. UninitializedPath, } 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}\""), SvgParseError::UninitializedPath => { write!(f, "Uninitialized path (missing moveto command)") } } } } #[cfg(feature = "std")] 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.is_ascii_lowercase() || c.is_ascii_uppercase() { return Some(c); } else if last_cmd != 0 && (c == b'-' || c == b'.' || c.is_ascii_digit()) { // 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.is_ascii_digit() { 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.is_ascii_digit() { return Err(SvgParseError::Wrong); } while let Some(c) = self.get_byte() { if !c.is_ascii_digit() { 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.is_ascii_lowercase() { 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 (sin_phi, cos_phi) = xr.sin_cos(); 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, PathEl, 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_parse_svg_uninitialized() { let path = BezPath::from_svg("L10 10 100 0 0 100"); assert!(path.is_err()); } #[test] #[allow(clippy::float_cmp)] fn test_parse_scientific_notation() { let path = BezPath::from_svg("M 0 0 L 1e-123 -4E+5").unwrap(); assert_eq!( path.elements(), &[ PathEl::MoveTo(Point { x: 0.0, y: 0.0 }), PathEl::LineTo(Point { x: 1e-123, y: -4E+5 }) ] ); } #[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,10 C20,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,10 C20,20 30,30 40,40 C30,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,10 C20,20 30,30 40,40 M50,50 C30,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 = 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 = 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.11.1/src/translate_scale.rs000064400000000000000000000230661046102023000153360ustar 00000000000000// Copyright 2019 the Kurbo Authors // SPDX-License-Identifier: Apache-2.0 OR MIT //! A transformation that includes both scale and translation. use core::ops::{Add, AddAssign, Mul, MulAssign, Sub, SubAssign}; use crate::{ Affine, Circle, CubicBez, Line, Point, QuadBez, Rect, RoundedRect, RoundedRectRadii, Vec2, }; /// A transformation consisting of a uniform scaling followed by a 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`] 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`]. #[derive(Clone, Copy, Debug)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct TranslateScale { /// The translation component of this transformation pub translation: Vec2, /// The scale component of this transformation pub 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 fn translate(translation: impl Into) -> TranslateScale { TranslateScale::new(translation.into(), 1.0) } /// Decompose transformation into translation and scale. #[deprecated(note = "use the struct fields directly")] #[inline] pub const fn as_tuple(self) -> (Vec2, f64) { (self.translation, self.scale) } /// Create a transform that scales about a point other than the origin. /// /// # Examples /// /// ``` /// # use kurbo::{Point, TranslateScale}; /// # fn assert_near(p0: Point, p1: Point) { /// # assert!((p1 - p0).hypot() < 1e-9, "{p0:?} != {p1:?}"); /// # } /// let center = Point::new(1., 1.); /// let ts = TranslateScale::from_scale_about(2., center); /// // Should keep the point (1., 1.) stationary /// assert_near(ts * center, center); /// // (2., 2.) -> (3., 3.) /// assert_near(ts * Point::new(2., 2.), Point::new(3., 3.)); /// ``` #[inline] pub fn from_scale_about(scale: f64, focus: impl Into) -> Self { // We need to create a transform that is equivalent to translating `focus` // to the origin, followed by a normal scale, followed by reversing the translation. // We need to find the (translation ∘ scale) that matches this. let focus = focus.into().to_vec2(); let translation = focus - focus * scale; Self::new(translation, 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. #[inline] pub fn inverse(self) -> TranslateScale { let scale_recip = self.scale.recip(); TranslateScale { translation: self.translation * -scale_recip, scale: scale_recip, } } /// Is this translate/scale [finite]? /// /// [finite]: f64::is_finite #[inline] pub fn is_finite(&self) -> bool { self.translation.is_finite() && self.scale.is_finite() } /// Is this translate/scale [NaN]? /// /// [NaN]: f64::is_nan #[inline] pub fn is_nan(&self) -> bool { self.translation.is_nan() || self.scale.is_nan() } } impl Default for TranslateScale { #[inline] fn default() -> TranslateScale { TranslateScale::new(Vec2::ZERO, 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 * other.radii()) } } impl Mul for TranslateScale { type Output = RoundedRectRadii; #[inline] fn mul(self, other: RoundedRectRadii) -> RoundedRectRadii { RoundedRectRadii::new( self.scale * other.top_left, self.scale * other.top_right, self.scale * other.bottom_right, self.scale * other.bottom_left, ) } } 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.11.1/src/vec2.rs000064400000000000000000000264761046102023000130410ustar 00000000000000// Copyright 2018 the Kurbo Authors // SPDX-License-Identifier: Apache-2.0 OR MIT //! A simple 2D vector. use core::fmt; use core::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign}; use crate::common::FloatExt; use crate::{Point, Size}; #[cfg(not(feature = "std"))] use crate::common::FloatFuncs; /// 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 = "schemars", derive(schemars::JsonSchema))] #[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. /// /// This is similar to `self.hypot2().sqrt()` but defers to the platform /// [`f64::hypot`] method, which in general will handle the case where /// `self.hypot2() > f64::MAX`. /// /// See [`Point::distance`] for the same operation on [`Point`]. /// /// # Examples /// /// ``` /// use kurbo::Vec2; /// let v = Vec2::new(3.0, 4.0); /// assert_eq!(v.hypot(), 5.0); /// ``` #[inline] pub fn hypot(self) -> f64 { self.x.hypot(self.y) } /// Magnitude of vector. /// /// This is an alias for [`Vec2::hypot`]. #[inline] pub fn length(self) -> f64 { self.hypot() } /// Magnitude squared of vector. /// /// See [`Point::distance_squared`] for the same operation on [`Point`]. /// /// # Examples /// /// ``` /// use kurbo::Vec2; /// let v = Vec2::new(3.0, 4.0); /// assert_eq!(v.hypot2(), 25.0); /// ``` #[inline] pub fn hypot2(self) -> f64 { self.dot(self) } /// Magnitude squared of vector. /// /// This is an alias for [`Vec2::hypot2`]. #[inline] pub fn length_squared(self) -> f64 { self.hypot2() } /// Find the angle in radians between this vector and the vector `Vec2 { x: 1.0, y: 0.0 }` /// in the positive `y` direction. /// /// 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) } /// Find the angle in radians between this vector and the vector `Vec2 { x: 1.0, y: 0.0 }` /// in the positive `y` direction. /// /// This is an alias for [`Vec2::atan2`]. #[inline] pub fn angle(self) -> f64 { self.atan2() } /// 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`]. /// /// [`Affine::rotate`]: crate::Affine::rotate #[inline] pub fn from_angle(th: f64) -> Vec2 { let (th_sin, th_cos) = th.sin_cos(); 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 magnitude is `0`. /// /// [magnitude]: Self::hypot #[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); /// ``` /// /// [rounded]: f64::round #[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); /// ``` /// /// [rounded up]: f64::ceil #[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); /// ``` /// /// [rounded down]: f64::floor #[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); /// ``` /// /// [rounded away]: FloatExt::expand #[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); /// ``` /// /// [rounded towards]: f64::trunc #[inline] pub fn trunc(self) -> Vec2 { Vec2::new(self.x.trunc(), self.y.trunc()) } /// Is this `Vec2` [finite]? /// /// [finite]: f64::is_finite #[inline] pub fn is_finite(self) -> bool { self.x.is_finite() && self.y.is_finite() } /// Is this `Vec2` [`NaN`]? /// /// [`NaN`]: f64::is_nan #[inline] pub fn is_nan(self) -> bool { self.x.is_nan() || self.y.is_nan() } /// Divides this `Vec2` by a scalar. /// /// Unlike the division by scalar operator, which multiplies by the /// reciprocal for performance, this performs the division /// per-component for consistent rounding behavior. pub(crate) fn div_exact(self, divisor: f64) -> Vec2 { Vec2 { x: self.x / divisor, y: self.y / divisor, } } } 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!("{v:.2}"); assert_eq!(s.as_str(), "𝐯=(1.23, 532.11)"); } }