diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1fb74f2b..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: @@ -11,24 +11,16 @@ 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 + uses: dtolnay/rust-toolchain@master with: - toolchain: stable - profile: minimal + toolchain: "stable" components: rustfmt - override: true - - - name: install rustfmt - run: rustup component add rustfmt - name: cargo fmt - uses: actions-rs/cargo@v1 - with: - command: fmt - args: --all -- --check + run: cargo fmt --all -- --check test-stable: runs-on: ${{ matrix.os }} @@ -37,29 +29,21 @@ 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 + uses: dtolnay/rust-toolchain@master with: - toolchain: stable + toolchain: "stable" components: clippy - profile: minimal - override: true - 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. @@ -76,29 +60,35 @@ jobs: name: cargo clippy+test (wasm32) steps: - - uses: actions/checkout@v2 + - 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 - 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 + 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: - command: test - args: --all-features --no-run --target wasm32-unknown-unknown + toolchain: "1.65" + + - name: cargo test + run: cargo test --all-features test-nightly: runs-on: ${{ matrix.os }} @@ -107,20 +97,15 @@ 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 + uses: dtolnay/rust-toolchain@master with: - toolchain: nightly - profile: minimal - override: true + 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 @@ -129,17 +114,12 @@ 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 + uses: dtolnay/rust-toolchain@master with: - toolchain: stable - profile: minimal - override: true + 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 diff --git a/Cargo.toml b/Cargo.toml index ba8ba2fc..f1114d25 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,8 +2,9 @@ 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"] repository = "https://github.com/linebender/kurbo" description = "A 2D curves library" 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.)); + } } diff --git a/src/bezpath.rs b/src/bezpath.rs index 26cab53a..1843f0a4 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, _tolerance: f64) -> 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 36b9799c..aac2af20 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..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 @@ -281,10 +281,10 @@ 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 + /// 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; 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 df747c22..ded69f0f 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();