From d7bb94dddc6364faa3302e4468e1fe66c7f08fc1 Mon Sep 17 00:00:00 2001 From: Raph Levien Date: Wed, 15 Jan 2025 12:45:00 -0800 Subject: [PATCH] Fix documentation of cross product; add convenience Vec2 methods (#409) I managed to get this wrong when I was trying to nail down the sign conventions, and it's been bothering me since I noticed it. The PR adds a couple of simple convenience methods I find myself using quite a bit as I implement stroke expansion. Note that a lot of methods can become const when we bump the MSRV, but that should happen separately. --- CHANGELOG.md | 6 +++++ src/vec2.rs | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f133ff48..32fa8234 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,12 +20,17 @@ This release has an [MSRV][] of 1.65. - `Stroke` is now `PartialEq`, `StrokeOpts` is now `Clone`, `Copy`, `Debug`, `Eq`, `PartialEq`. ([#379][] by [@waywardmonkeys][]) - Implement `Sum` for `Vec2`. ([#399][] by [@Philipp-M][]) - Add triangle shape. ([#350][] by [@juliapaci][]) +- Add `Vec2::turn_90` and `Vec2::rotate_scale` methods ([#409] by [@raphlinus][]) ### Changed - Reduce number of operations in `Triangle::circumscribed_circle`. ([#390][] by [@tomcur][]) - Numerically approximate ellipse perimeter. ([#383] by [@tomcur][]) +### Fixed + +- Fix documentation of cross product. ([#409] by [@raphlinus][]) + ## [0.11.1][] (2024-09-12) This release has an [MSRV][] of 1.65. @@ -95,6 +100,7 @@ Note: A changelog was not kept for or before this release [#388]: https://github.com/linebender/kurbo/pull/388 [#390]: https://github.com/linebender/kurbo/pull/390 [#399]: https://github.com/linebender/kurbo/pull/399 +[#409]: https://github.com/linebender/kurbo/pull/409 [Unreleased]: https://github.com/linebender/kurbo/compare/v0.11.1...HEAD [0.11.0]: https://github.com/linebender/kurbo/releases/tag/v0.11.0 diff --git a/src/vec2.rs b/src/vec2.rs index 1012ffb1..20ae12f6 100644 --- a/src/vec2.rs +++ b/src/vec2.rs @@ -63,7 +63,13 @@ impl Vec2 { /// Cross product of two vectors. /// - /// This is signed so that `(0, 1) × (1, 0) = 1`. + /// This is signed so that `(1, 0) × (0, 1) = 1`. + /// + /// The following relations hold: + /// + /// `u.cross(v) = -v.cross(u)` + /// + /// `v.cross(v) = 0.0` #[inline] pub fn cross(self, other: Vec2) -> f64 { self.x * other.y - self.y * other.x @@ -314,6 +320,32 @@ impl Vec2 { y: self.y / divisor, } } + + /// Turn by 90 degrees. + /// + /// The rotation is clockwise in a Y-down coordinate system. The following relations hold: + /// + /// `u.dot(v) = u.cross(v.turn_90())` + /// + /// `u.cross(v) = u.turn_90().dot(v)` + #[inline] + pub fn turn_90(self) -> Vec2 { + Vec2::new(-self.y, self.x) + } + + /// Combine two vectors interpreted as rotation and scaling. + /// + /// Interpret both vectors as a rotation and a scale, and combine + /// their effects. by adding the angles and multiplying the magnitudes. + /// This operation is equivalent to multiplication when the vectors + /// are interpreted as complex numbers. It is commutative. + #[inline] + pub fn rotate_scale(self, rhs: Vec2) -> Vec2 { + Vec2::new( + self.x * rhs.x - self.y * rhs.y, + self.x * rhs.y + self.y * rhs.x, + ) + } } impl From<(f64, f64)> for Vec2 { @@ -472,6 +504,8 @@ impl From> for Vec2 { #[cfg(test)] mod tests { + use core::f64::consts::FRAC_PI_2; + use super::*; #[test] fn display() { @@ -479,4 +513,31 @@ mod tests { let s = format!("{v:.2}"); assert_eq!(s.as_str(), "𝐯=(1.23, 532.11)"); } + + #[test] + fn cross_sign() { + let v = Vec2::new(1., 0.).cross(Vec2::new(0., 1.)); + assert_eq!(v, 1.); + } + + #[test] + fn turn_90() { + let u = Vec2::new(0.1, 0.2); + let turned = u.turn_90(); + // This should be exactly equal by IEEE rules, might fail + // in fastmath conditions. + assert_eq!(u.length(), turned.length()); + const EPSILON: f64 = 1e-12; + assert!((u.angle() + FRAC_PI_2 - turned.angle()).abs() < EPSILON); + } + + #[test] + fn rotate_scale() { + let u = Vec2::new(0.1, 0.2); + let v = Vec2::new(0.3, -0.4); + let uv = u.rotate_scale(v); + const EPSILON: f64 = 1e-12; + assert!((u.length() * v.length() - uv.length()).abs() < EPSILON); + assert!((u.angle() + v.angle() - uv.angle()).abs() < EPSILON); + } }