Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 43 additions & 48 deletions kurbo/src/affine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -417,7 +417,7 @@ impl Affine {
// Frobenius norms, could be `0.5 (frob^2 + sqrt(frob^4 - 4 det^2))`. In terms of operations
// it's a wash: one fewer sqrt if the user actually wants the squared form, but it uses more
// muls. More importantly, that form has worse numeric conditioning.
self.svd().0.x
self.svd_singular_values().x
}

/// Compute the inverse transform.
Expand Down Expand Up @@ -476,8 +476,10 @@ impl Affine {
|| self.0[5].is_nan()
}

/// Compute the singular value decomposition of the linear transformation (ignoring the
/// translation).
/// Computes the singular values of the singular value decomposition of the linear
/// transformation (ignoring the translation).
///
/// Use [`Self::svd_post_scale_rotation`] to compute the rotation of the decomposition.
///
/// All non-degenerate linear transformations can be represented as
///
Expand All @@ -489,30 +491,11 @@ impl Affine {
/// 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 non-finite.
///
/// The first part of the returned tuple is the scaling, the second part is the angle of
/// rotation (in radians). The scaling along the x-axis is guaranteed to be greater than or
/// equal to the scaling along the y-axis.
//
// Note: though this does quite some computation, we are often interested only in specific
// components of the result. Hence this is marked `#[inline(always)]`, to give the compiler a
// good chance at eliminating dead code.
/// The returned vector contains the scaling (the diagonal of Σ). The scaling along the x-axis
/// is guaranteed to be greater than or equal to the scaling along the y-axis.
#[inline(always)]
pub(crate) fn svd(self) -> (Vec2, f64) {
pub(crate) fn svd_singular_values(self) -> Vec2 {
let [a, b, c, d, _, _] = self.0;
let a2 = a * a;
let b2 = b * b;
let c2 = c * c;
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);

// Given matrix A = [ a c ]
// [ b d ]
Expand Down Expand Up @@ -549,13 +532,26 @@ impl Affine {
// and similarly σ2 = 1/2 |S1 - S2|
let s1 = ((a + d).powi(2) + (b - c).powi(2)).sqrt();
let s2 = ((a - d).powi(2) + (b + c).powi(2)).sqrt();
(
Vec2 {
x: 0.5 * (s1 + s2),
y: 0.5 * (s1 - s2).abs(),
},
angle,
)
Vec2 {
x: 0.5 * (s1 + s2),
y: 0.5 * (s1 - s2).abs(),
}
}

/// Computes the post-scale rotation in radians of the singular value decomposition of the
/// linear transformation (ignoring the translation).
///
/// This is the rotation represented by the `U`-term of the decomposition `U Σ V^T` (see
/// [`Self::svd_singular_values`] for more information about this decomposition).
///
/// The `V^T` rotation (i.e., the "pre-scale" rotation) is not computed: we currently only
/// require rotations to construct an ellipse from an affine map on the unit circle. 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.
#[inline(always)]
pub(crate) fn svd_post_scale_rotation(self) -> f64 {
let [a, b, c, d, _, _] = self.0;
0.5 * (2.0 * (a * b + c * d)).atan2(a * a - b * b + c * c - d * d)
}

/// Returns the translation part of this affine map (`(self.0[4], self.0[5])`).
Expand Down Expand Up @@ -763,8 +759,10 @@ mod tests {
let a_no_translate = a.with_translation(Vec2::ZERO);

// translation should have no effect
let (scale, rotation) = a.svd();
let (scale_no_translate, rotation_no_translate) = a_no_translate.svd();
let scale = a.svd_singular_values();
let rotation = a.svd_post_scale_rotation();
let scale_no_translate = a_no_translate.svd_singular_values();
let rotation_no_translate = a_no_translate.svd_post_scale_rotation();
assert_near(scale.to_point(), scale_no_translate.to_point());
assert!((rotation - rotation_no_translate).abs() <= 1e-9);

Expand All @@ -777,7 +775,8 @@ mod tests {
// singular affine
let a = Affine::new([0., 0., 0., 0., 5., 6.]);
assert_eq!(a.determinant(), 0.);
let (scale, rotation) = a.svd();
let scale = a.svd_singular_values();
let rotation = a.svd_post_scale_rotation();
assert_eq!(scale, Vec2::new(0., 0.));
assert_eq!(rotation, 0.);
}
Expand All @@ -787,40 +786,36 @@ mod tests {
// Test a few known singular values.
let mat = |a, b, c, d| Affine::new([a, b, c, d, 0., 0.]);

let s = mat(1., 0., 0., 1.).svd().0;
let s = mat(1., 0., 0., 1.).svd_singular_values();
assert_near(s.to_point(), Point::new(1., 1.));

let s = mat(1., 0., 0., -1.).svd().0;
let s = mat(1., 0., 0., -1.).svd_singular_values();
assert_near(s.to_point(), Point::new(1., 1.));

let s = mat(1., 1., 1., 1.).svd().0;
assert_near(s.to_point(), Point::new(2., 0.));

let s = mat(1., 1., 1., 1.).svd().0;
let s = mat(1., 1., 1., 1.).svd_singular_values();
Comment on lines -796 to +795

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test was duplicated. Removed one, updated the other.

assert_near(s.to_point(), Point::new(2., 0.));

let s = mat(0., 0., 1., 0.).svd().0;
let s = mat(0., 0., 1., 0.).svd_singular_values();
assert_near(s.to_point(), Point::new(1., 0.));

// The singular values are the scaling of the affine map. So let's test that.
let s = Affine::scale_non_uniform(4., 8.)
.then_rotate_about(42_f64.to_radians(), (-2., 50.))
.svd()
.0;
.svd_singular_values();
assert_near(s.to_point(), Point::new(8., 4.));

// Correctly handles negative scaling (singular values are necessarily non-negative).
let s = Affine::scale_non_uniform(-20., 3.).svd().0;
let s = Affine::scale_non_uniform(-20., 3.).svd_singular_values();
assert_near(s.to_point(), Point::new(20., 3.));
let s = Affine::scale_non_uniform(-20., -3.).svd().0;
let s = Affine::scale_non_uniform(-20., -3.).svd_singular_values();
assert_near(s.to_point(), Point::new(20., 3.));
let s = Affine::scale_non_uniform(20., -3.).svd().0;
let s = Affine::scale_non_uniform(20., -3.).svd_singular_values();
assert_near(s.to_point(), Point::new(20., 3.));

// One more property: given a full-rank transform, the product of its singular values
// should be equal to its absolute determinant.
let m = mat(10., 9., -2.5, 3.3333);
let s = m.svd().0;
let s = m.svd_singular_values();
let prod = s.x * s.y;
let det = m.determinant().abs();
assert!(
Expand Down
20 changes: 12 additions & 8 deletions kurbo/src/ellipse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ impl Ellipse {
#[inline]
#[must_use]
pub fn with_radii(self, new_radii: Vec2) -> Ellipse {
let rotation = self.inner.svd().1;
let rotation = self.inner.svd_post_scale_rotation();
let translation = self.inner.translation();
Ellipse::private_new(translation, new_radii.x, new_radii.y, rotation)
}
Expand All @@ -91,7 +91,7 @@ impl Ellipse {
#[inline]
#[must_use]
pub fn with_rotation(self, rotation: f64) -> Ellipse {
let scale = self.inner.svd().0;
let scale = self.inner.svd_singular_values();
let translation = self.inner.translation();
Ellipse::private_new(translation, scale.x, scale.y, rotation)
}
Expand Down Expand Up @@ -125,23 +125,23 @@ impl Ellipse {
/// consider using [`Ellipse::major_radius`] or [`Ellipse::minor_radius`] instead.
#[inline]
pub fn radii(&self) -> Vec2 {
self.inner.svd().0
self.inner.svd_singular_values()
}

/// Returns the major radius of this ellipse.
///
/// This metric is also known as the semi-major axis.
#[inline]
pub fn major_radius(&self) -> f64 {
self.inner.svd().0.x
self.inner.svd_singular_values().x
}

/// Returns the minor radius of this ellipse.
///
/// This metric is also known as the semi-minor axis.
#[inline]
pub fn minor_radius(&self) -> f64 {
self.inner.svd().0.y
self.inner.svd_singular_values().y
}

/// The ellipse's rotation, in radians.
Expand All @@ -150,15 +150,18 @@ impl Ellipse {
/// an ellipse with the two radii on the x and y axes.
#[inline]
pub fn rotation(&self) -> f64 {
self.inner.svd().1
self.inner.svd_post_scale_rotation()
}

/// Returns the radii and the rotation of this ellipse.
///
/// Equivalent to `(self.radii(), self.rotation())` but more efficient.
#[inline]
pub fn radii_and_rotation(&self) -> (Vec2, f64) {
self.inner.svd()
(
self.inner.svd_singular_values(),
self.inner.svd_post_scale_rotation(),
)
}

/// Is this ellipse [finite]?
Expand Down Expand Up @@ -224,7 +227,8 @@ impl Shape for Ellipse {
type PathElementsIter<'iter> = iter::Chain<iter::Once<PathEl>, ArcAppendIter>;

fn path_elements(&self, tolerance: f64) -> Self::PathElementsIter<'_> {
let (radii, x_rotation) = self.inner.svd();
let radii = self.inner.svd_singular_values();
let x_rotation = self.inner.svd_post_scale_rotation();
Arc {
center: self.center(),
radii,
Expand Down
Loading