From 80399cfec8e4cef943847bebe8e6861d7351fa14 Mon Sep 17 00:00:00 2001 From: Richard Dodd Date: Thu, 8 Jun 2023 10:29:30 +0100 Subject: [PATCH 01/10] Implement reflection over a line -> affine transformation Implementation using Householder matrix, which is numerically robust IIUC. Sometimes other operations can be made more robust by using reflections. --- src/affine.rs | 85 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 84 insertions(+), 1 deletion(-) diff --git a/src/affine.rs b/src/affine.rs index 47c7748b..f752eba2 100644 --- a/src/affine.rs +++ b/src/affine.rs @@ -112,6 +112,50 @@ impl 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)` @@ -432,13 +476,19 @@ impl From> for Affine { #[cfg(test)] mod tests { - use crate::{Affine, Point}; + 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); @@ -480,4 +530,37 @@ mod tests { 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.)); + } } From 18cc02eb6bb8bdb7203d0226afb9225cc238f277 Mon Sep 17 00:00:00 2001 From: Bruce Mitchener Date: Fri, 7 Jul 2023 23:42:02 +0700 Subject: [PATCH 02/10] ci: Update to actions/checkout@v3. --- .github/workflows/ci.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1fb74f2b..93472c20 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest name: cargo fmt steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: install stable toolchain uses: actions-rs/toolchain@v1 @@ -37,7 +37,7 @@ jobs: os: [macOS-latest, windows-2019, ubuntu-latest] name: cargo clippy+test steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: install stable toolchain uses: actions-rs/toolchain@v1 @@ -76,7 +76,7 @@ jobs: name: cargo clippy+test (wasm32) steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: install stable toolchain uses: actions-rs/toolchain@v1 @@ -107,7 +107,7 @@ jobs: os: [macOS-latest, windows-2019, ubuntu-latest] name: cargo test nightly steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: install nightly toolchain uses: actions-rs/toolchain@v1 @@ -129,7 +129,7 @@ jobs: matrix: os: [macOS-latest, windows-2019, ubuntu-latest] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: install stable toolchain uses: actions-rs/toolchain@v1 From b5f780eb40fc2bbea627aca8c15dc72e707521de Mon Sep 17 00:00:00 2001 From: Bruce Mitchener Date: Fri, 7 Jul 2023 23:42:02 +0700 Subject: [PATCH 03/10] ci: actions-rs/toolchain to dtolnay/rust-toolchain `actions-rs` actions have been unmaintained for years and produce warnings when used as they trigger deprecation notices about functionality that GitHub will be removing. --- .github/workflows/ci.yml | 30 ++++++++++-------------------- 1 file changed, 10 insertions(+), 20 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 93472c20..7971b62f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,12 +14,10 @@ jobs: - uses: actions/checkout@v3 - name: install stable toolchain - uses: actions-rs/toolchain@v1 + uses: dtolnay/rust-toolchain@master with: - toolchain: stable - profile: minimal + toolchain: "stable" components: rustfmt - override: true - name: install rustfmt run: rustup component add rustfmt @@ -40,12 +38,10 @@ jobs: - uses: actions/checkout@v3 - name: install stable toolchain - uses: actions-rs/toolchain@v1 + uses: dtolnay/rust-toolchain@master with: - toolchain: stable + toolchain: "stable" components: clippy - profile: minimal - override: true - run: rustup target add thumbv7m-none-eabi @@ -79,13 +75,11 @@ jobs: - uses: actions/checkout@v3 - name: install stable toolchain - uses: actions-rs/toolchain@v1 + uses: dtolnay/rust-toolchain@master with: - toolchain: stable + toolchain: "stable" target: wasm32-unknown-unknown components: clippy - profile: minimal - override: true - name: cargo clippy uses: actions-rs/cargo@v1 @@ -110,11 +104,9 @@ jobs: - uses: actions/checkout@v3 - name: install nightly toolchain - uses: actions-rs/toolchain@v1 + uses: dtolnay/rust-toolchain@master with: - toolchain: nightly - profile: minimal - override: true + toolchain: "nightly" - name: cargo test uses: actions-rs/cargo@v1 @@ -132,11 +124,9 @@ jobs: - uses: actions/checkout@v3 - name: install stable toolchain - uses: actions-rs/toolchain@v1 + uses: dtolnay/rust-toolchain@master with: - toolchain: stable - profile: minimal - override: true + toolchain: "stable" - name: cargo doc uses: actions-rs/cargo@v1 From cf63413eb1553677eb471b0e9bc2c6d8d9007594 Mon Sep 17 00:00:00 2001 From: Bruce Mitchener Date: Fri, 7 Jul 2023 23:42:03 +0700 Subject: [PATCH 04/10] ci: Remove explicit install of rustfmt. This is already handled in the toolchain install prior to this step. --- .github/workflows/ci.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7971b62f..1305d04d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,9 +19,6 @@ jobs: toolchain: "stable" components: rustfmt - - name: install rustfmt - run: rustup component add rustfmt - - name: cargo fmt uses: actions-rs/cargo@v1 with: From 0de15bdb9b639582d43d70b91e14428a9d350692 Mon Sep 17 00:00:00 2001 From: Bruce Mitchener Date: Fri, 7 Jul 2023 23:42:03 +0700 Subject: [PATCH 05/10] ci: Run cargo directly, no actions-rs/cargo. `actions-rs` actions have been unmaintained for years and produce warnings when used as they trigger deprecation notices about functionality that GitHub will be removing. --- .github/workflows/ci.yml | 35 +++++++---------------------------- 1 file changed, 7 insertions(+), 28 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1305d04d..eeba953e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,10 +20,7 @@ jobs: components: rustfmt - name: cargo fmt - uses: actions-rs/cargo@v1 - with: - command: fmt - args: --all -- --check + run: cargo fmt --all -- --check test-stable: runs-on: ${{ matrix.os }} @@ -43,16 +40,10 @@ jobs: - run: rustup target add thumbv7m-none-eabi - name: cargo clippy - uses: actions-rs/cargo@v1 - with: - command: clippy - args: --all-features --all-targets -- -D warnings + run: cargo clippy --all-features --all-targets -- -D warnings - name: cargo test - uses: actions-rs/cargo@v1 - with: - command: test - args: --all-features + run: cargo test --all-features - name: Build with no default features # Use no-std target to ensure we don't link to std. @@ -79,17 +70,11 @@ jobs: components: clippy - name: cargo clippy - uses: actions-rs/cargo@v1 - with: - command: clippy - args: --all-features --all-targets --target wasm32-unknown-unknown -- -D warnings + run: cargo clippy --all-features --all-targets --target wasm32-unknown-unknown -- -D warnings # TODO: Find a way to make tests work. Until then the tests are merely compiled. - name: cargo test compile - uses: actions-rs/cargo@v1 - with: - command: test - args: --all-features --no-run --target wasm32-unknown-unknown + run: cargo test --all-features --no-run --target wasm32-unknown-unknown test-nightly: runs-on: ${{ matrix.os }} @@ -106,10 +91,7 @@ jobs: toolchain: "nightly" - name: cargo test - uses: actions-rs/cargo@v1 - with: - command: test - args: --all-features + run: cargo test --all-features check-docs: name: Docs @@ -126,7 +108,4 @@ jobs: toolchain: "stable" - name: cargo doc - uses: actions-rs/cargo@v1 - with: - command: doc - args: --all-features --document-private-items + run: cargo doc --all-features --document-private-items From acea3e87df349293ee1f831a55087e4a972fb98b Mon Sep 17 00:00:00 2001 From: Bruce Mitchener Date: Sun, 9 Jul 2023 22:02:04 +0700 Subject: [PATCH 06/10] ci/Cargo.toml: Specify and test MSRV. The MSRV was specified in the README.md, so we can also specify it in the standard location and validate it via CI on Linux. --- .github/workflows/ci.yml | 14 ++++++++++++++ Cargo.toml | 1 + 2 files changed, 15 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eeba953e..7862b969 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -76,6 +76,20 @@ jobs: - name: cargo test compile run: cargo test --all-features --no-run --target wasm32-unknown-unknown + test-msrv: + runs-on: ubuntu-latest + name: cargo test msrv + steps: + - uses: actions/checkout@v3 + + - name: install msrv toolchain + uses: dtolnay/rust-toolchain@master + with: + toolchain: "1.65" + + - name: cargo test + run: cargo test --all-features + test-nightly: runs-on: ${{ matrix.os }} strategy: diff --git a/Cargo.toml b/Cargo.toml index ba8ba2fc..0ee6911c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ version = "0.9.5" authors = ["Raph Levien "] license = "MIT/Apache-2.0" edition = "2021" +rust-version = "1.65" # When updating this, also update the README.md and CI. keywords = ["graphics", "curve", "curves", "bezier", "geometry"] repository = "https://github.com/linebender/kurbo" description = "A 2D curves library" From c1bb90ba331601fb0cfec4f77c6069c5467f3ddb Mon Sep 17 00:00:00 2001 From: Bruce Mitchener Date: Mon, 10 Jul 2023 14:55:58 +0700 Subject: [PATCH 07/10] docs: Small formatting improvements. --- src/bezpath.rs | 2 +- src/common.rs | 2 +- src/cubicbez.rs | 2 +- src/fit.rs | 2 +- src/offset.rs | 2 +- src/rounded_rect_radii.rs | 4 ++-- src/svg.rs | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/bezpath.rs b/src/bezpath.rs index ab2f03cd..19d3c082 100644 --- a/src/bezpath.rs +++ b/src/bezpath.rs @@ -1361,7 +1361,7 @@ impl Shape for PathSeg { /// The area under the curve. /// - /// We could just return 0, but this seems more useful. + /// We could just return `0`, but this seems more useful. fn area(&self) -> f64 { self.signed_area() } diff --git a/src/common.rs b/src/common.rs index 6f475f9b..697d274d 100644 --- a/src/common.rs +++ b/src/common.rs @@ -302,7 +302,7 @@ fn solve_quartic_inner(a: f64, b: f64, c: f64, d: f64, rescale: bool) -> Option< /// Factor a quartic into two quadratics. /// -/// Attempt to factor a quartic equation into two quadratic equations. Returns None either if there +/// 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. /// diff --git a/src/cubicbez.rs b/src/cubicbez.rs index 2100db6c..cd295ab3 100644 --- a/src/cubicbez.rs +++ b/src/cubicbez.rs @@ -161,7 +161,7 @@ impl CubicBez { /// 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. + /// 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); diff --git a/src/fit.rs b/src/fit.rs index e090e6ce..30a6c573 100644 --- a/src/fit.rs +++ b/src/fit.rs @@ -281,7 +281,7 @@ impl CurveDist { /// Evaluate distance to a cubic approximation. /// - /// If distance exceeds stated accuracy, can return None. Note that + /// If distance exceeds stated accuracy, can return `None`. Note that /// `acc2` is the square of the target. /// /// Returns the squre of the error, which is intended to be a good diff --git a/src/offset.rs b/src/offset.rs index e6728142..29fd8166 100644 --- a/src/offset.rs +++ b/src/offset.rs @@ -67,7 +67,7 @@ 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`] instead to deal with a wider range of inputs. /// /// [`new_regularized`]: Self::new_regularized pub fn new(c: CubicBez, d: f64) -> Self { diff --git a/src/rounded_rect_radii.rs b/src/rounded_rect_radii.rs index 11ab402b..fd43a000 100644 --- a/src/rounded_rect_radii.rs +++ b/src/rounded_rect_radii.rs @@ -74,7 +74,7 @@ impl RoundedRectRadii { ) } - /// Returns true if all radius values are finite. + /// Returns `true` if all radius values are finite. pub fn is_finite(&self) -> bool { self.top_left.is_finite() && self.top_right.is_finite() @@ -82,7 +82,7 @@ impl RoundedRectRadii { && self.bottom_left.is_finite() } - /// Returns true if any corner radius value is NaN. + /// 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() diff --git a/src/svg.rs b/src/svg.rs index 67c036fb..3b380fa8 100644 --- a/src/svg.rs +++ b/src/svg.rs @@ -33,7 +33,7 @@ pub struct SvgArc { } impl BezPath { - /// Create a BezPath with segments corresponding to the sequence of + /// 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(); From b22ba21c63cf455dbe22a75df7151caefbe7b501 Mon Sep 17 00:00:00 2001 From: Kaur Kuut Date: Thu, 13 Jul 2023 19:47:31 +0300 Subject: [PATCH 08/10] Rename master branch to main in CI. (#297) --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7862b969..b80495fc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,7 +3,7 @@ name: CI on: push: branches: - - master + - main pull_request: jobs: From 588b18429df00bcbdba9980055d0d47dca132680 Mon Sep 17 00:00:00 2001 From: Bruce Mitchener Date: Wed, 19 Jul 2023 12:09:08 +0700 Subject: [PATCH 09/10] Fix minor typos. --- src/fit.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/fit.rs b/src/fit.rs index 30a6c573..3baa0f72 100644 --- a/src/fit.rs +++ b/src/fit.rs @@ -71,7 +71,7 @@ pub trait ParamCurveFit { /// 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 evaulated with + /// with Green's theorem, in terms of samples evaluated with /// [`sample_pt_deriv`]. /// /// [`sample_pt_deriv`]: ParamCurveFit::sample_pt_deriv @@ -284,7 +284,7 @@ impl CurveDist { /// If distance exceeds stated accuracy, can return `None`. Note that /// `acc2` is the square of the target. /// - /// Returns the squre of the error, which is intended to be a good + /// 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; From 81848332f9a8b05670f0649c58b767a5c26e562a Mon Sep 17 00:00:00 2001 From: Bruce Mitchener Date: Sun, 23 Jul 2023 11:11:42 +0700 Subject: [PATCH 10/10] Use SPDX 2.1 license expression. This improves the usage of automated tooling which consumes the license information. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 0ee6911c..f1114d25 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "kurbo" version = "0.9.5" authors = ["Raph Levien "] -license = "MIT/Apache-2.0" +license = "MIT OR Apache-2.0" edition = "2021" rust-version = "1.65" # When updating this, also update the README.md and CI. keywords = ["graphics", "curve", "curves", "bezier", "geometry"]