mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-01 05:37:23 -06:00
Perspective: a projection that a transform overflows is scaled down or removed
When moving type or a symbol instance in perspective makes the conjugated projection overflow, Homography::conjugated now forms the product from factors scaled to a largest entry of 1 and returns it at that scale, which maps every point to the same place. When even the scaled result is not invertible, the transform removes the projection and the art is drawn flat, instead of keeping the old projection while the flat art moves.
This commit is contained in:
@@ -339,6 +339,35 @@ fn transform_group_recurses() {
|
||||
assert_eq!(child.appearance.stroke_width(), 2.0);
|
||||
}
|
||||
|
||||
/// A symbol instance in perspective keeps its picture moved by a transform, and loses its
|
||||
/// projection when no invertible map of finite numbers draws the moved picture.
|
||||
#[test]
|
||||
fn a_projection_follows_the_transform_or_is_removed() {
|
||||
use vectorcraft_geom::{Homography, Point};
|
||||
let instance = |h: Homography| {
|
||||
let mut n = Node::new(NodeId(1), NodeKind::SymbolInstance { symbol: "S".into(), xf: Affine::IDENTITY });
|
||||
n.extras_mut().perspective =
|
||||
Some(Box::new(crate::PerspectiveAttachment { projection: Some(h.to_array()), ..crate::PerspectiveAttachment::new("left", 0.0) }));
|
||||
n
|
||||
};
|
||||
let projection = |n: &Node| n.extras().perspective.as_deref().and_then(|p| p.projection);
|
||||
// A move by 10 scaled by 1e307: moved by a scale by 10, the product overflows unless scaled down.
|
||||
let big = Homography([[1e307, 0.0, 1e308], [0.0, 1e307, 0.0], [0.0, 0.0, 1e307]]);
|
||||
let mut n = instance(big);
|
||||
n.transform(Affine::scale(10.0), false);
|
||||
let p = Point::new(3.0, 7.0);
|
||||
assert!(n.projection().unwrap().apply(p).unwrap().distance(Point::new(103.0, 7.0)) < 1e-9);
|
||||
// A move by 1e200 moved by a scale by 1e150 sends every point to about (1e350, 0).
|
||||
let far = Homography([[1.0, 0.0, 1e200], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]);
|
||||
let mut n = instance(far);
|
||||
n.transform(Affine::scale(1e150), false);
|
||||
assert_eq!(projection(&n), None);
|
||||
// A transform that is not invertible leaves the projection as it is.
|
||||
let mut n = instance(far);
|
||||
n.transform(Affine::scale(0.0), false);
|
||||
assert_eq!(projection(&n), Some(far.to_array()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unplaced_gradients_pin_only_when_a_refit_would_differ() {
|
||||
use vectorcraft_color::{Gradient, GradientGeom, GradientKind, GradientPaint, Paint};
|
||||
|
||||
@@ -58,12 +58,14 @@ impl Node {
|
||||
}
|
||||
}
|
||||
|
||||
/// Keep a projection drawing the same picture moved by `a` (the flat art moves by `a` too).
|
||||
/// Keep a projection drawing the same picture moved by `a` (the flat art moves by `a` too). An
|
||||
/// invertible `a` that the projection cannot follow removes it, and the art is drawn flat.
|
||||
pub(crate) fn transform_projection(&mut self, a: vectorcraft_geom::Affine) {
|
||||
if let Some(p) = self.extras_if_any().and_then(|e| e.perspective.as_deref_mut())
|
||||
&& let Some(h) = p.homography().and_then(|h| h.conjugated(a))
|
||||
&& let Some(h) = p.homography()
|
||||
&& a.determinant().abs() >= 1e-15
|
||||
{
|
||||
p.projection = Some(h.to_array());
|
||||
p.projection = h.conjugated(a).map(|h| h.to_array());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,14 +100,31 @@ impl Homography {
|
||||
}
|
||||
|
||||
/// `self` seen through `a`: the map `a ∘ self ∘ a⁻¹` (what `self` becomes when everything it
|
||||
/// maps from and to is moved by `a`). `None` when `a` isn't invertible or the result has an
|
||||
/// entry that is not finite.
|
||||
/// maps from and to is moved by `a`). When the product overflows, it is formed from factors
|
||||
/// scaled to a largest entry of 1 and returned at that scale (the same map). `None` when `a`
|
||||
/// isn't invertible, or when the product overflows and the scaled result is not invertible.
|
||||
pub fn conjugated(&self, a: Affine) -> Option<Self> {
|
||||
if a.determinant().abs() < 1e-15 {
|
||||
return None;
|
||||
}
|
||||
let c = Self::from_affine(a).then_after(self).then_after(&Self::from_affine(a.inverse()));
|
||||
c.0.iter().flatten().all(|v| v.is_finite()).then_some(c)
|
||||
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);
|
||||
}
|
||||
let c = to.normalized()?.then_after(&self.normalized()?).then_after(&from.normalized()?).normalized()?;
|
||||
(c.det().abs() > 1e-15).then_some(c)
|
||||
}
|
||||
|
||||
fn is_finite(&self) -> bool {
|
||||
self.0.iter().flatten().all(|v| v.is_finite())
|
||||
}
|
||||
|
||||
/// `self` divided by its largest entry magnitude, a positive scale that maps every point to
|
||||
/// the same place; `None` when an entry is not finite or every entry is 0.
|
||||
fn normalized(&self) -> Option<Self> {
|
||||
let k = self.0.iter().flatten().fold(0.0_f64, |k, v| k.max(v.abs()));
|
||||
(self.is_finite() && k > 0.0).then(|| Self(self.0.map(|row| row.map(|v| v / k))))
|
||||
}
|
||||
|
||||
/// The affine map that matches `self` to first order at `p` (its derivative there), `None`
|
||||
@@ -214,10 +231,25 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_conjugate_that_overflows_is_none() {
|
||||
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.0.iter().flatten().all(|v| v.is_finite() && v.abs() <= 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 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]
|
||||
|
||||
Reference in New Issue
Block a user