feat: crop history and fitting in the editor

This commit is contained in:
elwrcl committed 2026-10-03 00:05:09 +03:00
1 parent 04f51402b3
commit 8fab01bd35
4 files changed
+215 -45

No files matched your search

+73 -15
View File
@@ -1,20 +1,36 @@
//! Fitting the image into the canvas: image pixels ↔ egui points.
//! Fitting the image (or its crop) into the canvas: image pixels ↔ egui
//! points.
use crate::editor::shape::P;
use crate::frame::PixelRect;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Fit {
/// Points per image pixel.
pub scale: f32,
/// Where the image's top-left corner is, in points.
/// Where `origin` is drawn, in points.
pub offset: P,
/// The image pixel at the top-left of what is shown (the crop's corner).
pub origin: P,
}
impl Fit {
/// Fits `image` into `area` (x, y, w, h in points), centred, never
/// above 100 % (one image pixel per physical pixel).
/// Fits the whole image; see `with_crop`.
#[cfg(test)]
pub fn new(image: (u32, u32), area: (f32, f32, f32, f32), pixels_per_point: f32) -> Fit {
let (iw, ih) = (image.0 as f32, image.1 as f32);
let whole = PixelRect {
x: 0,
y: 0,
width: image.0,
height: image.1,
};
Fit::with_crop(whole, area, pixels_per_point)
}
/// Fits `crop` into `area` (x, y, w, h in points), centred, never above
/// 100 % (one image pixel per physical pixel).
pub fn with_crop(crop: PixelRect, area: (f32, f32, f32, f32), pixels_per_point: f32) -> Fit {
let (iw, ih) = (crop.width as f32, crop.height as f32);
let (x, y, w, h) = area;
let scale = (w / iw).min(h / ih).min(1.0 / pixels_per_point);
// Centred, then onto a physical pixel boundary so that at 100 % each
@@ -26,29 +42,30 @@ impl Fit {
snap(x + (w - iw * scale) / 2.0),
snap(y + (h - ih * scale) / 2.0),
),
origin: (crop.x as f32, crop.y as f32),
}
}
pub fn to_screen(self, p: P) -> P {
(
self.offset.0 + p.0 * self.scale,
self.offset.1 + p.1 * self.scale,
self.offset.0 + (p.0 - self.origin.0) * self.scale,
self.offset.1 + (p.1 - self.origin.1) * self.scale,
)
}
pub fn to_image(self, p: P) -> P {
(
(p.0 - self.offset.0) / self.scale,
(p.1 - self.offset.1) / self.scale,
(p.0 - self.offset.0) / self.scale + self.origin.0,
(p.1 - self.offset.1) / self.scale + self.origin.1,
)
}
}
/// `p` moved onto the image's area.
pub fn clamp(p: P, image: (u32, u32)) -> P {
/// `p` moved into `rect`.
pub fn clamp(p: P, rect: PixelRect) -> P {
(
p.0.clamp(0.0, image.0 as f32),
p.1.clamp(0.0, image.1 as f32),
p.0.clamp(rect.x as f32, (rect.x + rect.width) as f32),
p.1.clamp(rect.y as f32, (rect.y + rect.height) as f32),
)
}
@@ -103,7 +120,48 @@ mod tests {
#[test]
fn clamp_keeps_points_on_the_image() {
assert_eq!(clamp((-5.0, 50.0), (100, 80)), (0.0, 50.0));
assert_eq!(clamp((150.0, 90.0), (100, 80)), (100.0, 80.0));
let r = PixelRect {
x: 0,
y: 0,
width: 100,
height: 80,
};
assert_eq!(clamp((-5.0, 50.0), r), (0.0, 50.0));
assert_eq!(clamp((150.0, 90.0), r), (100.0, 80.0));
}
#[test]
fn a_crop_fills_the_area() {
let crop = PixelRect {
x: 100,
y: 50,
width: 200,
height: 100,
};
let f = Fit::with_crop(crop, (0.0, 0.0, 400.0, 400.0), 1.0);
assert_eq!(f.scale, 1.0);
assert_eq!(
f.to_screen((100.0, 50.0)),
(100.0, 150.0),
"the crop's corner is centred"
);
}
#[test]
fn round_trip_with_a_crop() {
let crop = PixelRect {
x: 640,
y: 360,
width: 640,
height: 360,
};
let f = Fit::with_crop(crop, (0.0, 30.0, 500.0, 400.0), 1.0);
for p in [(640.0, 360.0), (1279.0, 719.0), (800.5, 400.25)] {
let q = f.to_image(f.to_screen(p));
assert!(
(q.0 - p.0).abs() < 1e-3 && (q.1 - p.1).abs() < 1e-3,
"{p:?} {q:?}"
);
}
}
}