use super::Editor;
use crate::geometry::{PixelRect, Point, Rect};
use crate::model::{Item, ItemId, Step};
use crate::panic_free::Clamped;
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum AspectChoice {
Free,
Original,
Square,
FourThree,
ThreeTwo,
SixteenNine,
Custom(u32, u32),
}
impl AspectChoice {
pub const PRESETS: [AspectChoice; 6] = [
AspectChoice::Free,
AspectChoice::Original,
AspectChoice::Square,
AspectChoice::FourThree,
AspectChoice::ThreeTwo,
AspectChoice::SixteenNine,
];
pub fn label(self) -> String {
match self {
AspectChoice::Free => "Free".to_owned(),
AspectChoice::Original => "Original".to_owned(),
AspectChoice::Square => "1:1".to_owned(),
AspectChoice::FourThree => "4:3".to_owned(),
AspectChoice::ThreeTwo => "3:2".to_owned(),
AspectChoice::SixteenNine => "16:9".to_owned(),
AspectChoice::Custom(w, h) => format!("{w}:{h}"),
}
}
pub fn ratio(self, original: (u32, u32)) -> Option<f64> {
let r = |w: u32, h: u32| (h > 0).then(|| f64::from(w) / f64::from(h));
match self {
AspectChoice::Free => None,
AspectChoice::Original => r(original.0, original.1),
AspectChoice::Square => Some(1.0),
AspectChoice::FourThree => Some(4.0 / 3.0),
AspectChoice::ThreeTwo => Some(1.5),
AspectChoice::SixteenNine => Some(16.0 / 9.0),
AspectChoice::Custom(w, h) => r(w, h),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct CropTool {
pub item: ItemId,
pub rect: Rect,
pub limits: PixelRect,
pub aspect: AspectChoice,
pub source_size: (u32, u32),
}
impl CropTool {
fn new(item: &Item) -> Self {
let c = item.crop;
Self {
item: item.id,
rect: Rect::new(
f64::from(c.left),
f64::from(c.top),
f64::from(c.width),
f64::from(c.height),
),
limits: PixelRect::new(0, 0, item.source_width, item.source_height),
aspect: AspectChoice::Free,
source_size: (item.source_width, item.source_height),
}
}
pub fn ratio(&self) -> Option<f64> {
self.aspect.ratio(self.source_size)
}
fn limit_rect(&self) -> Rect {
let l = self.limits;
Rect::new(
f64::from(l.left),
f64::from(l.top),
f64::from(l.width),
f64::from(l.height),
)
}
pub fn pixel_rect(&self) -> PixelRect {
let lim = self.limit_rect();
let left = self.rect.x.round().clamped(lim.x, lim.right() - 1.0);
let top = self.rect.y.round().clamped(lim.y, lim.bottom() - 1.0);
let right = self.rect.right().round().clamped(left + 1.0, lim.right());
let bottom = self.rect.bottom().round().clamped(top + 1.0, lim.bottom());
PixelRect::new(
left as u32,
top as u32,
(right - left) as u32,
(bottom - top) as u32,
)
}
pub(super) fn translate(&mut self, dx: f64, dy: f64) {
let lim = self.limit_rect();
self.rect.x = (self.rect.x + dx).clamped(lim.x, lim.right() - self.rect.width);
self.rect.y = (self.rect.y + dy).clamped(lim.y, lim.bottom() - self.rect.height);
}
pub(super) fn set_edges(&mut self, left: f64, top: f64, right: f64, bottom: f64) {
let lim = self.limit_rect();
let left = left.clamped(lim.x, lim.right() - 1.0);
let top = top.clamped(lim.y, lim.bottom() - 1.0);
let right = right.clamped(left + 1.0, lim.right());
let bottom = bottom.clamped(top + 1.0, lim.bottom());
self.rect = Rect::new(left, top, right - left, bottom - top);
}
pub(super) fn apply_ratio(&mut self) {
let Some(ratio) = self.ratio() else { return };
let c = self.rect.center();
let (mut w, mut h) = (self.rect.width, self.rect.height);
if w / h > ratio {
w = h * ratio;
} else {
h = w / ratio;
}
let lim = self.limit_rect();
w = w.min(lim.width);
h = h.min(lim.height);
let x = (c.x - w / 2.0).clamped(lim.x, lim.right() - w);
let y = (c.y - h / 2.0).clamped(lim.y, lim.bottom() - h);
self.rect = Rect::new(x, y, w, h);
}
}
pub(super) struct ItemMapping {
item: Item,
}
impl ItemMapping {
pub(super) fn new(item: Item) -> Self {
Self { item }
}
fn scale(&self) -> (f64, f64) {
(
f64::from(self.item.width) / f64::from(self.item.crop.width),
f64::from(self.item.height) / f64::from(self.item.crop.height),
)
}
pub(super) fn source_to_local(&self, p: Point) -> Point {
let (sx, sy) = self.scale();
Point::new(
(p.x - f64::from(self.item.crop.left)) * sx,
(p.y - f64::from(self.item.crop.top)) * sy,
)
}
pub(super) fn source_to_canvas(&self, p: Point) -> Point {
self.item.to_canvas(self.source_to_local(p))
}
pub(super) fn canvas_to_source(&self, p: Point) -> Point {
let local = self.item.to_local(p);
let (sx, sy) = self.scale();
Point::new(
local.x / sx + f64::from(self.item.crop.left),
local.y / sy + f64::from(self.item.crop.top),
)
}
}
impl Editor {
pub(super) fn start_crop(&mut self, item: ItemId) -> bool {
let Some(item) = self.project.state().item(item).copied() else {
return false;
};
self.crop = Some(CropTool::new(&item));
self.selection = super::Selection::Item(item.id);
true
}
pub fn set_crop_rect(&mut self, rect: PixelRect) {
if let Some(crop) = &mut self.crop {
crop.set_edges(
f64::from(rect.left),
f64::from(rect.top),
f64::from(rect.right()),
f64::from(rect.bottom()),
);
}
}
pub fn set_crop_aspect(&mut self, aspect: AspectChoice) {
if let Some(crop) = &mut self.crop {
crop.aspect = aspect;
crop.apply_ratio();
}
}
pub(super) fn apply_crop(&mut self) -> bool {
let Some(tool) = self.crop.take() else {
return false;
};
let Some(item) = self.project.state().item(tool.item).copied() else {
return false;
};
let crop = tool.pixel_rect();
if crop == item.crop {
return true;
}
let recropped = with_crop(&item, crop);
self.record(Step::CropItem {
item: item.id,
crop,
x: recropped.x,
y: recropped.y,
width: recropped.width,
height: recropped.height,
})
}
}
pub(super) fn with_crop(item: &Item, crop: PixelRect) -> Item {
let mapping = ItemMapping::new(*item);
let (sx, sy) = mapping.scale();
let width = ((f64::from(crop.width) * sx).round() as u32).max(1);
let height = ((f64::from(crop.height) * sy).round() as u32).max(1);
let center = mapping.source_to_canvas(Point::new(
f64::from(crop.left) + f64::from(crop.width) / 2.0,
f64::from(crop.top) + f64::from(crop.height) / 2.0,
));
Item {
crop,
x: (center.x - f64::from(width) / 2.0).round() as i32,
y: (center.y - f64::from(height) / 2.0).round() as i32,
width,
height,
..*item
}
}