From 072bcfeaf128a81e9550e216bb87e53331297f58 Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Thu, 15 Oct 2026 00:31:45 +0200 Subject: [PATCH] Fix crop.rotateAspect turning a zero-size crop into a NaN rect Found while testing crates/engine/src/cmd/develop.rs:676 (crop.rotateAspect); also crates/geom/src/lib.rs:180 (Rect::fit_aspect) Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Sonnet 5.5 --- crates/engine/src/cmd/develop.rs | 5 +++-- crates/engine/src/tests.rs | 15 +++++++++++++++ crates/geom/src/lib.rs | 2 +- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/crates/engine/src/cmd/develop.rs b/crates/engine/src/cmd/develop.rs index 6f064b260..15c40ccb6 100644 --- a/crates/engine/src/cmd/develop.rs +++ b/crates/engine/src/cmd/develop.rs @@ -678,10 +678,11 @@ pub fn specs() -> Vec { let (w, h) = crop_dims(s, id); edit(s, "crop.rotateAspect", "Rotate Crop", |d| { let r = d.crop.geometry.rect_px(w, h); - let a = r.height() / r.width(); + // a degenerate crop has no aspect to rotate: fall back to the image's own + let a = Some(r.height() / r.width()).filter(|a| a.is_finite() && *a > 0.0); d.crop.aspect = d.crop.aspect.map(|(x, y)| (y, x)); let angle = d.crop.geometry.angle; - d.crop.geometry = crop_fit_angle(w, h, angle, Some(a)); + d.crop.geometry = crop_fit_angle(w, h, angle, a); Ok(()) }) }), diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 4e5e73850..d52c7245a 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -424,6 +424,21 @@ fn degenerate_crops_render_with_spots_and_masks() { } } +/// `crop.rotateAspect` on a crop of no size took its aspect from 0/0 = NaN and stored a NaN crop +/// rectangle; it now falls back to the image's own aspect, a finite crop inside the image. +#[test] +fn rotate_aspect_of_degenerate_crop_stays_finite() { + for rect in [[0.5, 0.5, 0.5, 0.5], [0.2, 0.2, 0.2, 0.8], [0.2, 0.2, 0.8, 0.2]] { + let mut s = demo(); + s.execute("crop.set", &json!({"rect": rect})).unwrap(); + s.execute("crop.rotateAspect", &json!({})).unwrap(); + let r = active_dev(&s).crop.geometry.rect; + assert!([r.x0, r.y0, r.x1, r.y1].iter().all(|v| v.is_finite()), "{rect:?}: {r:?}"); + assert!(r.width() > 0.0 && r.height() > 0.0, "{rect:?}: {r:?}"); + assert!(r.x0 >= 0.0 && r.y0 >= 0.0 && r.x1 <= 1.0 && r.y1 <= 1.0, "{rect:?}: {r:?}"); + } +} + /// `mask.move` with a delta near `i64::MAX` overflowed `index + delta` (a panic wherever overflow /// checks are on); extreme deltas and indices now move the mask to the bottom or the top. #[test] diff --git a/crates/geom/src/lib.rs b/crates/geom/src/lib.rs index 4b8f623e2..14eaaf065 100644 --- a/crates/geom/src/lib.rs +++ b/crates/geom/src/lib.rs @@ -178,7 +178,7 @@ impl Rect { } /// Largest rectangle of aspect `a` (w/h) centred in `self`. pub fn fit_aspect(&self, a: f64) -> Rect { - if a <= 0.0 || self.is_empty() { + if !(a.is_finite() && a > 0.0) || self.is_empty() { return *self; } let (w, h) = (self.width(), self.height());