Files
vectorcraft/crates/geom/src/projective.rs
T
dotCooCoo b5885e35fa Geometry: a projective map at a small scale maps a far point without overflowing
The power of two that brings a small map's entries up before a point is mapped is now reduced by the power of two of the point's larger coordinate, so no product of an entry and a coordinate reaches 1. A map that mapped a point before the scaling step does so again, and affine_at uses the same entries.
2026-11-02 08:24:29 -08:00

690 lines
34 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Projective (perspective) transforms of the plane: 3×3 homographies.
//!
//! The Perspective Grid maps each grid plane to the page with one, objects in perspective move by
//! conjugating plane-space affine maps with it, and type and symbol instances in perspective keep
//! one that projects their flat art.
use kurbo::{Affine, Point, Rect};
/// A 3×3 projective transform (row-major), acting on `(x, y, 1)`.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Homography(pub [[f64; 3]; 3]);
impl Homography {
pub const IDENTITY: Self = Self([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]);
/// From the images of (1,0,0), (0,1,0) and (0,0,1).
pub fn from_cols(c1: [f64; 3], c2: [f64; 3], c3: [f64; 3]) -> Self {
Self([[c1[0], c2[0], c3[0]], [c1[1], c2[1], c3[1]], [c1[2], c2[2], c3[2]]])
}
/// The affine map `a` as a homography.
pub fn from_affine(a: Affine) -> Self {
let [m0, m1, m2, m3, m4, m5] = a.as_coeffs();
Self([[m0, m2, m4], [m1, m3, m5], [0.0, 0.0, 1.0]])
}
/// From nine row-major numbers (as stored in documents); `None` unless all are finite and the
/// map is invertible. When the determinant is not finite, this is the same map divided by the
/// largest magnitude in its `w` row (so that row's largest entry is 1), and `None` when that
/// copy has an entry that is not finite or is not invertible.
pub fn from_array(v: [f64; 9]) -> Option<Self> {
if v.iter().any(|x| !x.is_finite()) {
return None;
}
let h = Self([[v[0], v[1], v[2]], [v[3], v[4], v[5]], [v[6], v[7], v[8]]]);
if h.det().is_finite() {
return h.is_invertible().then_some(h);
}
let k = h.0[2].iter().fold(0.0_f64, |k, v| k.max(v.abs()));
if k <= 0.0 || !h.is_invertible() {
return None;
}
let c = Self(h.0.map(|row| row.map(|v| v / k)));
(c.is_finite() && c.is_invertible()).then_some(c)
}
/// The nine row-major numbers.
pub fn to_array(&self) -> [f64; 9] {
let m = &self.0;
[m[0][0], m[0][1], m[0][2], m[1][0], m[1][1], m[1][2], m[2][0], m[2][1], m[2][2]]
}
/// Map a point; `None` when it lands on or beyond the horizon (w at most 1e-9 times the largest
/// magnitude in the map's `w` row, so the answer is the same at any scale of the map), or when
/// it does not map to a finite page point. The point is mapped through [`Self::upscaled`].
pub fn apply(&self, p: Point) -> Option<Point> {
let m = self.upscaled(p);
let x = m[0][0] * p.x + m[0][1] * p.y + m[0][2];
let y = m[1][0] * p.x + m[1][1] * p.y + m[1][2];
let w = m[2][0] * p.x + m[2][1] * p.y + m[2][2];
let k = m[2].iter().fold(0.0_f64, |k, v| k.max(v.abs()));
if w <= 1e-9 * k || !x.is_finite() || !y.is_finite() {
return None;
}
let q = Point::new(x / w, y / w);
q.is_finite().then_some(q)
}
/// The entries for mapping `p`, multiplied by a power of two of at least 1 (an exact step that
/// keeps a map at a tiny scale from losing precision to subnormal products). When every entry
/// is below 1/2 in magnitude, the power brings the largest to at least 1/2 and below 1, divided
/// by the power of two of `p`'s larger coordinate magnitude when that is 1 or more, so that no
/// product of an entry and a coordinate reaches 1. Otherwise the entries are unchanged.
fn upscaled(&self, p: Point) -> [[f64; 3]; 3] {
let largest = self.0.iter().flatten().fold(0.0_f64, |k, v| k.max(v.abs()));
let e_p = frexp(p.x.abs().max(p.y.abs())).1.max(0);
let e = (frexp(largest).1.min(0) + e_p).min(0);
self.0.map(|row| row.map(|v| ldexp(v, -e)))
}
pub fn det(&self) -> f64 {
let m = &self.0;
m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1]) - m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0])
+ m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0])
}
/// Whether the map is invertible: [`Self::has_significant_det`], or, when the determinant is
/// not finite, [`Self::inverse`] gives one.
fn is_invertible(&self) -> bool {
if self.det().is_finite() {
return self.has_significant_det();
}
self.inverse().is_some()
}
/// Whether every entry is finite and the determinant's magnitude is more than 1e-15 times that
/// of the largest of the six products it sums (a test that gives the same answer at any scale
/// of the map). Each product is formed as a mantissa product and an exponent sum, and all six
/// are divided by the power of two of the largest before they are summed, so the largest is at
/// least 1/8 and below 1 and none of them overflows.
fn has_significant_det(&self) -> bool {
if !self.is_finite() {
return false;
}
let terms = self.det_terms();
let Some(e) = terms.iter().filter(|(m, _)| *m != 0.0).map(|(_, e)| *e).max() else {
return false;
};
let terms = terms.map(|(m, x)| ldexp(m, x - e));
let largest = terms.iter().fold(0.0_f64, |k, v| k.max(v.abs()));
let d: f64 = terms.iter().sum();
d.abs() > 1e-15 * largest
}
/// The six signed products the determinant sums, each as a mantissa product and an exponent
/// sum.
fn det_terms(&self) -> [(f64, i32); 6] {
let f = self.0.map(|row| row.map(frexp));
// The columns each of the six products takes from rows 0, 1 and 2, and its sign.
let cols = [(0, 1, 2, 1.0), (0, 2, 1, -1.0), (1, 0, 2, -1.0), (1, 2, 0, 1.0), (2, 0, 1, 1.0), (2, 1, 0, -1.0)];
cols.map(|(a, b, c, sign)| {
let (a, b, c) = (f[0][a], f[1][b], f[2][c]);
(sign * a.0 * b.0 * c.0, a.1 + b.1 + c.1)
})
}
/// The same map with every entry divided by one power of two, chosen so that the largest of the
/// six products its determinant sums is at least 1/8 and below 4. `None` when every one of
/// those products is 0 or an entry is not finite.
fn scaled(&self) -> Option<Self> {
if !self.is_finite() {
return None;
}
let f = self.0.map(|row| row.map(frexp));
// The columns each of the six products takes from rows 0, 1 and 2.
let cols = [(0, 1, 2), (0, 2, 1), (1, 0, 2), (1, 2, 0), (2, 0, 1), (2, 1, 0)];
let e = cols
.iter()
.map(|&(a, b, c)| (f[0][a], f[1][b], f[2][c]))
.filter(|(a, b, c)| a.0 * b.0 * c.0 != 0.0)
.map(|(a, b, c)| a.1 + b.1 + c.1)
.max()?;
let e = e.div_euclid(3);
Some(Self(self.0.map(|row| row.map(|v| ldexp(v, -e)))))
}
/// Inverse; `None` when [`Self::has_significant_det`] counts the map as not invertible. The
/// inverse is formed from the map itself when its determinant is finite, and is kept when every
/// entry is finite and its own determinant is not 0. Otherwise it is the inverse of
/// [`Self::scaled`] (the exact inverse multiplied by one power of two, which is the same map),
/// and `None` when that one fails the same checks.
pub fn inverse(&self) -> Option<Self> {
if !self.has_significant_det() {
return None;
}
if self.det().is_finite()
&& let Some(inv) = self.adjugate_inverse()
{
return Some(inv);
}
self.scaled()?.adjugate_inverse()
}
/// The adjugate divided by the determinant; `None` when an entry is not finite, the
/// determinant is 0, or the result has an entry that is not finite or a determinant of 0 (its
/// six products, each held as a mantissa and a power of two, sum to 0, as when a row underflows
/// to 0). Every cofactor and the determinant are held as a mantissa and a power of two, so an
/// entry of the result is lost only when that entry itself is beyond the range of `f64`.
fn adjugate_inverse(&self) -> Option<Self> {
if !self.is_finite() {
return None;
}
let f = self.0.map(|row| row.map(frexp));
let mul = |a: (f64, i32), b: (f64, i32)| (a.0 * b.0, a.1 + b.1);
let c = |r0: usize, c0: usize, r1: usize, c1: usize| {
let (a, b) = (mul(f[r0][c0], f[r1][c1]), mul(f[r0][c1], f[r1][c0]));
split_sum(&[a, (-b.0, b.1)])
};
let neg = |(m, e): (f64, i32)| (-m, e);
let adj = [
[c(1, 1, 2, 2), neg(c(0, 1, 2, 2)), c(0, 1, 1, 2)],
[neg(c(1, 0, 2, 2)), c(0, 0, 2, 2), neg(c(0, 0, 1, 2))],
[c(1, 0, 2, 1), neg(c(0, 0, 2, 1)), c(0, 0, 1, 1)],
];
// The determinant expanded along the first row, as `det` sums it.
let (dm, de) = split_sum(&[mul(f[0][0], adj[0][0]), mul(f[0][1], adj[1][0]), mul(f[0][2], adj[2][0])]);
if dm == 0.0 {
return None;
}
let mut out = [[0.0; 3]; 3];
for (r, row) in adj.iter().enumerate() {
for (k, (m, e)) in row.iter().enumerate() {
out[r][k] = ldexp(m / dm, e - de);
}
}
let out = Self(out);
// H⁻¹·(x, y, 1) = (u, v, 1)/w_H, so points in front of the camera keep w > 0.
(out.is_finite() && split_sum(&out.det_terms()).0 != 0.0).then_some(out)
}
/// `self ∘ other`: `other` first.
pub fn then_after(&self, other: &Self) -> Self {
let (a, b) = (&self.0, &other.0);
let mut out = [[0.0; 3]; 3];
for (r, row) in out.iter_mut().enumerate() {
for (k, v) in row.iter_mut().enumerate() {
*v = a[r][0] * b[0][k] + a[r][1] * b[1][k] + a[r][2] * b[2][k];
}
}
Self(out)
}
/// [`Self::then_after`]; `None` when an entry of the product is not finite (an entry
/// overflows, or a factor has one that is not finite).
pub fn checked_then_after(&self, other: &Self) -> Option<Self> {
let c = self.then_after(other);
c.is_finite().then_some(c)
}
/// `self` seen through `a`: the map `a ∘ self ∘ a⁻¹` (what `self` becomes when everything it
/// maps from and to is moved by `a`). When the product overflows, every entry is formed with
/// its powers of two kept apart, and the product is returned divided by the largest magnitude
/// in its `w` row (the same map, with a `w` row whose largest entry is 1). `None` when `a`
/// isn't invertible, or when the product overflows and the result at that scale has an entry
/// that is not finite or is not invertible.
pub fn conjugated(&self, a: Affine) -> Option<Self> {
if a.determinant().abs() < 1e-15 {
return None;
}
let (to, from) = (Self::from_affine(a), Self::from_affine(a.inverse()));
let c = to.then_after(self).then_after(&from);
if c.is_finite() {
return Some(c);
}
// Each entry of the product is a sum of nine terms to[r][i]·self[i][j]·from[j][k]. Every
// factor is split into a mantissa and a power of two, so each term is a mantissa product
// and an exponent sum, and the whole product is scaled by one power of two at the end.
let split = |h: &Self| h.0.map(|row| row.map(frexp));
let (t, s, f) = (split(&to), split(self), split(&from));
let term = |r: usize, i: usize, j: usize, k: usize| {
let (a, b, c) = (t[r][i], s[i][j], f[j][k]);
(a.0 * b.0 * c.0, a.1 + b.1 + c.1)
};
let ijk = (0..27).map(|n| (n / 9, n / 3 % 3, n % 3));
let e = ijk.map(|(i, j, k)| term(2, i, j, k)).filter(|(m, _)| *m != 0.0).map(|(_, e)| e).max()?;
let mut c = [[0.0; 3]; 3];
for (r, row) in c.iter_mut().enumerate() {
for (k, v) in row.iter_mut().enumerate() {
*v = (0..9).map(|n| term(r, n / 3, n % 3, k)).map(|(m, x)| ldexp(m, x - e)).sum();
}
}
let k = c[2].iter().fold(0.0_f64, |k, v| k.max(v.abs()));
if k <= 0.0 {
return None;
}
let c = Self(c.map(|row| row.map(|v| v / k)));
(c.is_finite() && c.is_invertible()).then_some(c)
}
fn is_finite(&self) -> bool {
self.0.iter().flatten().all(|v| v.is_finite())
}
/// The affine map that matches `self` to first order at `p` (its derivative there), `None`
/// beyond the horizon or when the derivative is not finite.
pub fn affine_at(&self, p: Point) -> Option<Affine> {
let m = self.upscaled(p);
let w = m[2][0] * p.x + m[2][1] * p.y + m[2][2];
let q = self.apply(p)?;
// d(x/w) = (dx − (x/w)·dw) / w, and the same for y.
let j = |r: usize, c: usize| (m[r][c] - [q.x, q.y][r] * m[2][c]) / w;
let lin = Affine::new([j(0, 0), j(1, 0), j(0, 1), j(1, 1), 0.0, 0.0]);
let out = Affine::translate(q.to_vec2()) * lin * Affine::translate(-p.to_vec2());
out.is_finite().then_some(out)
}
/// The bounding box of the image of `r` (lines stay lines: its corners bound it), `None` when
/// part of it crosses the horizon.
pub fn map_rect_bbox(&self, r: Rect) -> Option<Rect> {
let pts = [Point::new(r.x0, r.y0), Point::new(r.x1, r.y0), Point::new(r.x1, r.y1), Point::new(r.x0, r.y1)];
let mut out: Option<Rect> = None;
for p in pts {
let q = self.apply(p)?;
out = Some(out.map_or(Rect::from_points(q, q), |b| b.union_pt(q)));
}
out
}
}
/// `v` as `m·2^e` with `0.5 ≤ |m| < 1`; 0, infinities and NaN come back as `(v, 0)`.
fn frexp(v: f64) -> (f64, i32) {
if v == 0.0 || !v.is_finite() {
return (v, 0);
}
// A subnormal is scaled into the normal range first.
let (v, bias) = if v.abs() < f64::MIN_POSITIVE { (v * pow2(64), -64) } else { (v, 0) };
let bits = v.to_bits();
let e = ((bits >> 52) & 0x7ff) as i32 - 1022;
(f64::from_bits((bits & !(0x7ff << 52)) | (1022 << 52)), e + bias)
}
/// The sum of `m·2^e` over `terms`, in order, as a mantissa `0.5 ≤ |m| < 1` (or 0) and a power of
/// two. Every term is first divided by the power of two of the largest nonzero one, so none of
/// them overflows.
fn split_sum(terms: &[(f64, i32)]) -> (f64, i32) {
let Some(e) = terms.iter().filter(|(m, _)| *m != 0.0).map(|(_, e)| *e).max() else {
return (0.0, 0);
};
let (m, x) = frexp(terms.iter().map(|&(m, x)| ldexp(m, x - e)).sum());
(m, e + x)
}
/// `m·2^e`, multiplied in steps of powers of two that are each within the normal range.
fn ldexp(mut m: f64, mut e: i32) -> f64 {
while e > 1023 && m.is_finite() {
m *= pow2(1023);
e -= 1023;
}
while e < -1022 && m != 0.0 {
m *= pow2(-1022);
e += 1022;
}
m * pow2(e.clamp(-1022, 1023))
}
/// `2^e` for `-1022 ≤ e ≤ 1023`.
fn pow2(e: i32) -> f64 {
f64::from_bits(((e + 1023) as u64) << 52)
}
#[cfg(test)]
mod tests {
use super::*;
fn h() -> Homography {
Homography([[1.2, 0.1, 5.0], [-0.2, 0.9, 3.0], [0.001, 0.002, 1.0]])
}
#[test]
fn compose_invert_and_round_trip() {
let a = h();
let inv = a.inverse().unwrap();
let id = a.then_after(&inv);
for (r, row) in id.0.iter().enumerate() {
for (k, v) in row.iter().enumerate() {
assert!((v - if r == k { 1.0 } else { 0.0 }).abs() < 1e-9);
}
}
let p = Point::new(30.0, -12.0);
assert!(inv.apply(a.apply(p).unwrap()).unwrap().distance(p) < 1e-9);
assert_eq!(Homography::from_array(a.to_array()), Some(a));
assert!(Homography::from_array([f64::NAN; 9]).is_none());
assert!(Homography::from_array([0.0; 9]).is_none());
}
#[test]
fn affine_and_conjugation() {
let t = Affine::translate((10.0, -4.0)) * Affine::scale(2.0);
let p = Point::new(3.0, 7.0);
assert!(Homography::from_affine(t).apply(p).unwrap().distance(t * p) < 1e-12);
// Conjugating: moving the input and the output by `t` keeps the picture moved by `t`.
let c = h().conjugated(t).unwrap();
assert!(c.apply(t * p).unwrap().distance(t * h().apply(p).unwrap()) < 1e-9);
assert!(h().conjugated(Affine::scale(0.0)).is_none());
}
#[test]
fn affine_at_is_the_derivative() {
let p = Point::new(12.0, -7.0);
let a = h().affine_at(p).unwrap();
assert!(a * p == h().apply(p).unwrap() || (a * p).distance(h().apply(p).unwrap()) < 1e-9);
let e = 1e-4;
for d in [kurbo::Vec2::new(e, 0.0), kurbo::Vec2::new(0.0, e)] {
let exact = h().apply(p + d).unwrap();
assert!((a * (p + d)).distance(exact) < 1e-7, "{d:?}");
}
}
#[test]
fn a_point_that_overflows_maps_to_nothing() {
// w = 1e-8 is in front of the horizon, but 1e305 / 1e-8 overflows.
let near = Homography([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1e-8]]);
assert_eq!(near.apply(Point::new(1e305, 0.0)), None);
assert_eq!(near.apply(Point::new(0.0, -1e305)), None);
assert_eq!(near.map_rect_bbox(Rect::new(0.0, 0.0, 1e305, 1.0)), None);
assert!(near.apply(Point::new(2.0, 3.0)).unwrap().distance(Point::new(2e8, 3e8)) < 1e-6);
// A NaN w is not caught by the horizon test.
let nan = Homography([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, f64::NAN]]);
assert_eq!(nan.apply(Point::new(1.0, 1.0)), None);
}
#[test]
fn an_inverse_that_overflows_is_none() {
let big = Homography([[1e300, 0.0, 0.0], [0.0, 1e300, 0.0], [0.0, 0.0, 1e-310]]);
assert_eq!(big.inverse(), None);
assert!(h().inverse().is_some());
}
#[test]
fn a_determinant_that_overflows_is_taken_at_a_smaller_scale() {
// (x, y) ↦ (x + 1, y) / (x + 2) with every entry multiplied by 1e200: the determinant is
// inf − inf, though the map is invertible.
let big = Homography([[1e200, 0.0, 1e200], [0.0, 1e200, 0.0], [1e200, 0.0, 2e200]]);
assert!(big.det().is_nan());
// Read back as the same map with a w row whose largest entry is 1.
let read = Homography::from_array(big.to_array()).unwrap();
assert_eq!(read, Homography([[0.5, 0.0, 0.5], [0.0, 0.5, 0.0], [0.5, 0.0, 1.0]]));
assert_eq!(Homography::from_array(read.to_array()), Some(read));
let inv = big.inverse().unwrap();
for p in [Point::new(3.0, -2.0), Point::new(-1.5, 4.0)] {
assert!(read.apply(p).unwrap().distance(big.apply(p).unwrap()) < 1e-12);
assert!(inv.apply(big.apply(p).unwrap()).unwrap().distance(p) < 1e-9);
}
// The identity with every entry 1e308: stored as it is, (2, 2) overflows to 2e308, but the
// map read back draws it at (2, 2).
let wide = Homography([[1e308, 0.0, 0.0], [0.0, 1e308, 0.0], [0.0, 0.0, 1e308]]);
assert_eq!(wide.apply(Point::new(2.0, 2.0)), None);
let read = Homography::from_array(wide.to_array()).unwrap();
assert_eq!(read.apply(Point::new(2.0, 2.0)), Some(Point::new(2.0, 2.0)));
// A large map whose w row is 1e-205 times its largest entry: read back at a w row of 1,
// its points are in front of the horizon and draw.
let low = Homography([[1e300, 0.0, 1e305], [0.0, 1e300, 0.0], [1e90, 0.0, 1e100]]);
assert!(low.det().is_nan());
let read = Homography::from_array(low.to_array()).unwrap();
assert_eq!(Homography::from_array(read.to_array()), Some(read));
let p = Point::new(1.0, 1.0);
let want = Point::new((1e200 + 1e205) / (1.0 + 1e-10), 1e200 / (1.0 + 1e-10));
assert!(read.apply(p).unwrap().distance(want) <= 1e-9 * want.to_vec2().hypot());
// A scale by 1e305 and a move by -3.9e307 with a w row of 1: the determinant is inf, and
// the inverse is a scale by 1e-305 and a move by 390.
let moved = Homography([[1e305, 0.0, -3.9e307], [0.0, 1e305, -3.2e307], [0.0, 0.0, 1.0]]);
assert!(moved.det().is_infinite());
assert_eq!(Homography::from_array(moved.to_array()), Some(moved));
let inv = moved.inverse().unwrap();
for p in [Point::new(400.0, 330.0), Point::new(500.0, 400.0)] {
assert!(inv.apply(moved.apply(p).unwrap()).unwrap().distance(p) < 1e-9);
}
// A scale by 1e500: the determinant is inf, but scaled so that its products are near 1
// the w row is 0, and no point maps to a finite page point except the origin.
let flat = Homography([[1e300, 0.0, 0.0], [0.0, 1e300, 0.0], [0.0, 0.0, 1e-200]]);
assert!(flat.det().is_infinite());
assert_eq!(Homography::from_array(flat.to_array()), None);
assert_eq!(flat.inverse(), None);
// Two equal rows with large entries.
let twice = Homography([[1e200, 1e200, 0.0], [1e200, 1e200, 0.0], [0.0, 0.0, 1e200]]);
assert_eq!(Homography::from_array(twice.to_array()), None);
assert_eq!(twice.inverse(), None);
// (x, y) ↦ 1e200·(x + 1, y) / (x + 2) seen through a scale by 1e10: the product
// overflows, and the determinant of the result at a w row of 1 is inf − inf.
let far = Homography([[1e300, 0.0, 1e300], [0.0, 1e300, 0.0], [1e100, 0.0, 2e100]]);
let t = Affine::scale(1e10);
let c = far.conjugated(t).unwrap();
assert!(c.det().is_nan());
assert_eq!(Homography::from_array(c.to_array()), Some(c));
for p in [Point::new(3.0, -2.0), Point::new(0.5, 7.0)] {
let want = t * far.apply(p).unwrap();
assert!(c.apply(t * p).unwrap().distance(want) <= 1e-9 * want.to_vec2().hypot(), "{c:?}");
}
// A determinant that does not overflow is judged against the size of the map's entries:
// the identity with every entry 1e-6 is the identity, and two equal rows are not invertible.
let small = [1e-6, 0.0, 0.0, 0.0, 1e-6, 0.0, 0.0, 0.0, 1e-6];
let q = Homography::from_array(small).unwrap().apply(Point::new(2.0, 3.0)).unwrap();
assert!(q.distance(Point::new(2.0, 3.0)) < 1e-12);
assert!(Homography::from_array([1e-6, 2e-6, 0.0, 1e-6, 2e-6, 0.0, 0.0, 0.0, 1e-6]).is_none());
assert!(Homography::from_array([f64::NAN; 9]).is_none());
assert!(Homography::from_array([0.0; 9]).is_none());
}
#[test]
fn the_checks_do_not_depend_on_the_scale_of_the_map() {
let times = |h: Homography, f: f64| Homography(h.0.map(|row| row.map(|v| v * f)));
// The same map with its w row's largest entry 1.
let unit = |h: Homography| {
let k = h.0[2].iter().fold(0.0_f64, |k, v| k.max(v.abs()));
Homography(h.0.map(|row| row.map(|v| v / k)))
};
// The horizon of h() is 0.001x + 0.002y + 1 = 0: two points in front of it, one with w of
// about 5e-10 (too close to it to draw), and two beyond it.
let pts =
[Point::new(30.0, -12.0), Point::new(-999.99, 0.0), Point::new(-999.9999995, 0.0), Point::new(-2000.0, 0.0), Point::new(0.0, -600.0)];
let at_unit: Vec<_> = pts.iter().map(|&p| h().apply(p)).collect();
assert!(at_unit[0].is_some() && at_unit[1].is_some());
assert!(at_unit[2..].iter().all(Option::is_none));
let inv = h().inverse().unwrap();
// Two equal rows (up to a factor of 2), with entries that are exact in binary.
let flat = Homography([[1.0, 2.0, 3.0], [2.0, 4.0, 6.0], [0.25, 0.5, 1.0]]);
assert!(!flat.is_invertible());
assert_eq!(flat.inverse(), None);
for f in [1e-200, 1e-12, 1e12, 1e200] {
let s = times(h(), f);
for (&p, want) in pts.iter().zip(&at_unit) {
match (s.apply(p), want) {
(Some(q), Some(w)) => assert!(q.distance(*w) <= 1e-6 * w.to_vec2().hypot(), "{f} {p:?}"),
(None, None) => {}
(got, _) => panic!("{f} {p:?}: {got:?}"),
}
}
assert!(s.is_invertible(), "{f}");
let si = unit(s.inverse().unwrap_or_else(|| panic!("{f}")));
for (a, b) in si.to_array().iter().zip(unit(inv).to_array()) {
assert!((a - b).abs() <= 1e-9 * b.abs().max(1.0), "{f}: {si:?}");
}
let sf = times(flat, f);
assert!(!sf.is_invertible(), "{f}");
assert_eq!(sf.inverse(), None, "{f}");
}
// Entries of very different sizes: the determinant is about 1e-7 and every entry of the
// inverse is finite, though one power of two that brings the products near 1 takes the
// first entry past the largest finite number.
let uneven = Homography([[1e307, 0.0, 0.0], [0.0, 1e-157, 0.0], [0.0, 0.0, 1e-157]]);
assert!(uneven.det().is_finite());
assert_eq!(Homography::from_array(uneven.to_array()), Some(uneven));
let inv = uneven.inverse().unwrap();
for p in [Point::new(1e-300, 2.0), Point::new(-3e-299, -0.5)] {
let q = uneven.apply(p).unwrap();
let back = inv.apply(q).unwrap();
assert!((back.x - p.x).abs() <= 1e-9 * p.x.abs() && (back.y - p.y).abs() <= 1e-9 * p.y.abs(), "{p:?} {back:?}");
}
// A normal determinant (1e-200) whose cofactor 1e-200 · 1e-200 is below the smallest
// positive f64: the inverse still has a w row that is not 0.
let tiny = Homography([[1e-200, 0.0, 0.0], [0.0, 1e-200, 0.0], [0.0, 0.0, 1e200]]);
assert_eq!(Homography::from_array(tiny.to_array()), Some(tiny));
let inv = tiny.inverse().unwrap();
for p in [Point::new(1e300, -2e299), Point::new(-4e299, 5e300)] {
let q = tiny.apply(p).unwrap();
let back = inv.apply(q).unwrap();
assert!((back.x - p.x).abs() <= 1e-9 * p.x.abs() && (back.y - p.y).abs() <= 1e-9 * p.y.abs(), "{p:?} {back:?}");
}
// A normal determinant (1e-300) whose cofactor 1e-300 · 1e-300 is below the smallest
// positive f64, and where one power of two that brings the products near 1 takes 1e300 past
// the largest finite number: the inverse is still diag(1e300, 1e-300, 1e300).
let spread = Homography([[1e-300, 0.0, 0.0], [0.0, 1e300, 0.0], [0.0, 0.0, 1e-300]]);
assert_eq!(Homography::from_array(spread.to_array()), Some(spread));
let inv = spread.inverse().unwrap();
let want = [1e300, 0.0, 0.0, 0.0, 1e-300, 0.0, 0.0, 0.0, 1e300];
for (v, w) in inv.to_array().iter().zip(want) {
assert!((v - w).abs() <= 1e-12 * w.abs(), "{inv:?}");
}
}
#[test]
fn an_invertible_map_has_an_inverse_at_any_scale() {
let times = |h: Homography, f: f64| Homography(h.0.map(|row| row.map(|v| v * f)));
// Determinant 1e-17 against a largest product of 1e-9 (and 1e-8 at 1e3 times the map): its
// inverse is finite, though that inverse's own determinant is near 1e-16 of its largest
// product.
let skew = Homography([[-1000.0, -1e-8, -1e-5], [0.0, 1e-4, -1e7], [1e-8, 0.0, 1e-8]]);
let exact = [1e5, 10.0, 1e16 + 1e8, -1e16, -1e12 + 1e4, -1e27, -1e5, -10.0, -1e16];
for f in [1.0, 1e3] {
let s = times(skew, f);
assert_eq!(Homography::from_array(s.to_array()), Some(s), "{f}");
let inv = s.inverse().unwrap_or_else(|| panic!("{f}"));
for (v, w) in inv.to_array().iter().zip(exact) {
assert!((v * f - w).abs() <= 1e-9 * w.abs(), "{f}: {inv:?}");
}
// The map times its inverse is the identity, to within the rounding of the products
// each entry sums (they cancel by up to 1e23 in the second row).
let id = s.then_after(&inv);
for (r, row) in id.0.iter().enumerate() {
for (k, v) in row.iter().enumerate() {
let size: f64 = (0..3).map(|i| (s.0[r][i] * inv.0[i][k]).abs()).sum();
let want = if r == k { 1.0 } else { 0.0 };
assert!((v - want).abs() <= 1e-12 * size, "{f}: {id:?}");
}
}
}
}
#[test]
fn a_map_at_a_tiny_scale_maps_points_as_at_unit_scale() {
for f in [5e-324, 1e-300] {
let h = Homography(Homography::IDENTITY.0.map(|row| row.map(|v| v * f)));
assert_eq!(Homography::from_array(h.to_array()), Some(h), "{f}");
for p in [Point::new(0.5, 0.5), Point::new(1e-20, 1e-20)] {
let q = h.apply(p).unwrap_or_else(|| panic!("{f}: {p:?}"));
assert!((q.x - p.x).abs() <= 1e-12 * p.x && (q.y - p.y).abs() <= 1e-12 * p.y, "{f}: {p:?} -> {q:?}");
}
}
// A perspective map whose entries are exact at 2^-1060 times their size, so the scaled
// copy holds the same map.
let unit = Homography([[1.0, 0.0, 2.0], [0.0, 1.0, 3.0], [0.5, 0.25, 1.0]]);
let tiny = Homography(unit.0.map(|row| row.map(|v| v * pow2(-1022) * pow2(-38))));
let p = Point::new(0.3, 0.7);
let (a, b) = (tiny.affine_at(p).unwrap(), unit.affine_at(p).unwrap());
for (u, v) in a.as_coeffs().iter().zip(b.as_coeffs()) {
assert!((u - v).abs() <= 1e-12 * v.abs().max(1.0), "{a:?} {b:?}");
}
}
#[test]
fn a_map_at_a_small_scale_maps_a_far_point() {
// Every product of an entry and a coordinate is finite, but the entries brought to 1/2 or
// more would overflow against this point.
let h = Homography([[1e-6, 1e-6, 0.0], [0.0, 1e-6, 0.0], [1e-6, 0.0, 1e-6]]);
let p = Point::new(1.75e308, 1.75e308);
let q = h.apply(p).unwrap_or_else(|| panic!("{p:?}"));
assert!((q.x - 2.0).abs() <= 1e-12 && (q.y - 1.0).abs() <= 1e-12, "{q:?}");
assert!(h.affine_at(p).is_some());
}
#[test]
fn a_product_that_overflows_is_none() {
// Each factor is finite and invertible, but the product's first entry is 1e400.
let big = Homography([[1e200, 0.0, 0.0], [0.0, 1e200, 0.0], [0.0, 0.0, 1e200]]);
assert!(Homography::from_array(big.to_array()).is_some());
assert_eq!(big.checked_then_after(&big), None);
let nan = Homography([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, f64::NAN]]);
assert_eq!(h().checked_then_after(&nan), None);
assert_eq!(h().checked_then_after(&Homography::IDENTITY), Some(h()));
let inv = h().inverse().unwrap();
assert_eq!(h().checked_then_after(&inv), Some(h().then_after(&inv)));
}
#[test]
fn a_derivative_that_overflows_is_none() {
// The point maps to 1e300, but the derivative there is 1e302 / 1e-8.
let steep = Homography([[1e302, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1e-8]]);
let p = Point::new(1e-10, 0.0);
assert!(steep.apply(p).is_some());
assert_eq!(steep.affine_at(p), None);
}
#[test]
fn a_conjugate_that_overflows_is_scaled_down() {
// A move by 10, scaled by 1e307: seen through a scale by 10 it moves by 100, but the
// product's corner entry is 1e309.
let big = Homography([[1e307, 0.0, 1e308], [0.0, 1e307, 0.0], [0.0, 0.0, 1e307]]);
let t = Affine::scale(10.0);
let c = big.conjugated(t).unwrap();
assert!(c.is_finite());
assert_eq!(c.0[2], [0.0, 0.0, 1.0]);
assert_eq!(Homography::from_array(c.to_array()), Some(c));
for p in [Point::new(3.0, 7.0), Point::new(-4.0, 1.5)] {
assert!(c.apply(t * p).unwrap().distance(t * big.apply(p).unwrap()) < 1e-9);
assert!(c.apply(p).unwrap().distance(p + kurbo::Vec2::new(100.0, 0.0)) < 1e-9);
}
// A move by 1 scaled by 1e307, seen through a scale by 1e7: a move by 1e7, whose
// determinant is 1 at a w row of 1 but 1e-21 at a largest entry of 1.
let one = Homography([[1e307, 0.0, 1e307], [0.0, 1e307, 0.0], [0.0, 0.0, 1e307]]);
assert!(Homography::from_array(one.to_array()).is_some());
let c = one.conjugated(Affine::scale(1e7)).unwrap();
assert_eq!(Homography::from_array(c.to_array()), Some(c));
let p = Point::new(3.0, -2.0);
assert!(c.apply(p).unwrap().distance(p + kurbo::Vec2::new(1e7, 0.0)) < 1e-6);
// A scale by 1e10 seen through a scale by 1e10: still a scale by 1e10, which every point
// reaches only when w is not left at 1e-10.
let wide = Homography([[1e300, 0.0, 0.0], [0.0, 1e300, 0.0], [0.0, 0.0, 1e290]]);
let c = wide.conjugated(Affine::scale(1e10)).unwrap();
assert_eq!(Homography::from_array(c.to_array()), Some(c));
for p in [Point::new(1.0, 2.0), Point::new(-5.0, 0.5)] {
assert!(c.apply(p).unwrap().distance(Point::new(p.x * 1e10, p.y * 1e10)) < 1e-3);
}
// A scale by 2 seen through a move by 1e308: p goes to 2p - 1e308, though the product's
// corner entry is 2 * -1e308 + 1e308 and the moves scaled to a largest entry of 1 leave
// their w rows at 1e-308.
let two = Homography([[2.0, 0.0, 0.0], [0.0, 2.0, 0.0], [0.0, 0.0, 1.0]]);
let c = two.conjugated(Affine::translate((1e308, 0.0))).unwrap();
let want = [2.0, 0.0, -1e308, 0.0, 2.0, 0.0, 0.0, 0.0, 1.0];
for (v, w) in c.to_array().iter().zip(want) {
assert!((v - w).abs() <= 1e-9 * w.abs().max(1.0), "{c:?}");
}
assert_eq!(Homography::from_array(c.to_array()), Some(c));
// A scale by (1e160, 1e-160) seen through the same scale is itself, though the product's
// first entry is 1e320 and a scale to a largest entry of 1 takes its second to 1e-480.
let thin = Homography([[1e160, 0.0, 0.0], [0.0, 1e-160, 0.0], [0.0, 0.0, 1.0]]);
assert!(Homography::from_array(thin.to_array()).is_some());
let c = thin.conjugated(Affine::scale_non_uniform(1e160, 1e-160)).unwrap();
for (v, w) in c.to_array().iter().zip(thin.to_array()) {
assert!((v - w).abs() <= 1e-9 * w.abs(), "{c:?}");
}
assert_eq!(Homography::from_array(c.to_array()), Some(c));
// A move by 1e200 seen through a scale by 1e150 sends every point to about (1e350, 0),
// which no invertible map of finite numbers draws.
let far = Homography([[1.0, 0.0, 1e200], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]);
assert_eq!(far.conjugated(Affine::scale(1e150)), None);
assert!(far.conjugated(Affine::scale(2.0)).is_some());
// A product that does not overflow is kept as it is.
assert_eq!(far.conjugated(Affine::scale(2.0)).unwrap().0[0][2], 2e200);
}
#[test]
fn rect_bbox_is_the_corners_box() {
let b = h().map_rect_bbox(Rect::new(0.0, 0.0, 10.0, 20.0)).unwrap();
for p in [Point::new(0.0, 0.0), Point::new(10.0, 20.0), Point::new(10.0, 0.0), Point::new(5.0, 10.0)] {
let q = h().apply(p).unwrap();
assert!(b.inflate(1e-9, 1e-9).contains(q));
}
}
}