diff --git a/crates/ui-egui/src/panels/crop_overlay.rs b/crates/ui-egui/src/panels/crop_overlay.rs index 37fe76a5e..763c4b9ab 100644 --- a/crates/ui-egui/src/panels/crop_overlay.rs +++ b/crates/ui-egui/src/panels/crop_overlay.rs @@ -45,12 +45,22 @@ pub fn paths(kind: CropOverlay, orient: u8, w: f32, h: f32) -> Vec Vec> { + // A `PHI × 1` rectangle less a `1 × 1` square is `1 × 1/PHI`, the same shape again. + const PHI: f32 = std::f32::consts::GOLDEN_RATIO; // work landscape; transpose back for portrait crops let portrait = h > w; - let (rw, rh) = if portrait { (h, w) } else { (w, h) }; + let (rw, rh) = (PHI, 1.0); let (mut x, mut y, mut cw, mut ch) = (0.0f32, 0.0f32, rw, rh); let mut curve = Vec::new(); let mut squares = Vec::new(); @@ -128,6 +138,27 @@ mod tests { assert_eq!(paths(CropOverlay::Triangle, 1, w, h)[0], vec![(1.0, 0.0), (0.0, 1.0)]); } + /// The spiral is the same complete guide at every crop shape. Carving the squares off the + /// crop itself made it depend on the crop: a square kept one arc of ten, 3:2 kept three and + /// 16:9 four, so changing the crop truncated the guide (#2393). + #[test] + fn the_spiral_is_complete_at_every_aspect_ratio() { + let shapes = [(1.0f32, 1.0f32), (1.618, 1.0), (1.0, 1.618), (3.0, 2.0), (2.0, 3.0), (16.0, 9.0), (9.0, 16.0), (4.0, 5.0)]; + let (squares, points) = { + let p = paths(CropOverlay::Spiral, 0, 1.618, 1.0); + (p.len() - 1, p.last().unwrap().len()) + }; + assert!(squares >= 8, "the golden rectangle itself gives a long spiral: {squares}"); + for (w, h) in shapes { + for o in 0..4 { + let p = paths(CropOverlay::Spiral, o, w, h); + assert_eq!(p.len() - 1, squares, "{w}x{h} orient {o}: {} squares, not {squares}", p.len() - 1); + assert_eq!(p.last().unwrap().len(), points, "{w}x{h} orient {o}: a shorter curve"); + assert!(in_unit(&p), "{w}x{h} orient {o}"); + } + } + } + #[test] fn spiral_is_one_continuous_curve() { for (w, h) in [(1.618, 1.0), (1.0, 1.618), (3.0, 2.0)] {