Skip to content

Commit

Permalink
Merge pull request #145 from pol-rivero/crop-improvements
Browse files Browse the repository at this point in the history
Crop improvements
  • Loading branch information
gabm authored Dec 30, 2024
2 parents da0616c + 0df085b commit 478113f
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 79 deletions.
22 changes: 13 additions & 9 deletions src/femtovg_area/imp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ use resource::resource;

use crate::{
configuration::Action,
math::Vec2D,
math::{rect_ensure_in_bounds, rect_round, Vec2D},
sketch_board::SketchBoardInput,
tools::{CropTool, Drawable, Tool},
APP_CONFIG,
Expand Down Expand Up @@ -279,19 +279,23 @@ impl FemtoVgAreaMut {
canvas: &mut femtovg::Canvas<femtovg::renderer::OpenGl>,
font: FontId,
) -> anyhow::Result<ImgVec<RGBA8>> {
let bounds = (
Vec2D::zero(),
Vec2D::new(
self.background_image.width() as f32,
self.background_image.height() as f32,
),
);
// get offset and size of the area in question
let (pos, size) = self
.crop_tool
.borrow()
.get_crop()
.and_then(|c| c.get_rectangle())
.unwrap_or((
Vec2D::zero(),
Vec2D::new(
self.background_image.width() as f32,
self.background_image.height() as f32,
),
));
.map(|c| c.get_rectangle())
.map(|rect| rect_ensure_in_bounds(rect, bounds))
.map(rect_round)
.filter(|(_, size)| !size.is_zero())
.unwrap_or(bounds);

// create render-target
let image_id = canvas.create_image_empty(
Expand Down
39 changes: 39 additions & 0 deletions src/math.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ impl Vec2D {
Vec2D::new(-a, -b)
}
}

pub fn is_zero(&self) -> bool {
self.x.abs() < f32::EPSILON && self.y.abs() < f32::EPSILON
}
}

impl Add for Vec2D {
Expand Down Expand Up @@ -169,3 +173,38 @@ pub fn rect_ensure_positive_size(pos: Vec2D, size: Vec2D) -> (Vec2D, Vec2D) {

(Vec2D::new(pos_x, pos_y), Vec2D::new(size_x, size_y))
}

pub fn rect_ensure_in_bounds(rect: (Vec2D, Vec2D), bounds: (Vec2D, Vec2D)) -> (Vec2D, Vec2D) {
let (mut pos, mut size) = rect;

if pos.x < bounds.0.x {
pos.x = bounds.0.x;
size.x -= bounds.0.x - pos.x;
}

if pos.y < bounds.0.y {
pos.y = bounds.0.y;
size.y -= bounds.0.y - pos.y;
}

if pos.x + size.x > bounds.1.x {
size.x = bounds.1.x - pos.x;
}

if pos.y + size.y > bounds.1.y {
size.y = bounds.1.y - pos.y;
}

(pos, size)
}

pub fn rect_round(rect: (Vec2D, Vec2D)) -> (Vec2D, Vec2D) {
let (mut pos, mut size) = rect;

pos.x = pos.x.round();
pos.y = pos.y.round();
size.x = size.x.round();
size.y = size.y.round();

(pos, size)
}
160 changes: 90 additions & 70 deletions src/tools/crop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use super::{Drawable, Tool, ToolUpdateResult};
#[derive(Debug, Clone)]
pub struct Crop {
pos: Vec2D,
size: Option<Vec2D>,
size: Vec2D,
active: bool,
}

Expand All @@ -27,6 +27,14 @@ impl Crop {
const HANDLE_RADIUS: f32 = 5.0;
const HANDLE_BORDER: f32 = 2.0;

fn new(pos: Vec2D) -> Self {
Self {
pos,
size: Vec2D::zero(),
active: true,
}
}

fn draw_single_handle(
canvas: &mut femtovg::Canvas<femtovg::renderer::OpenGl>,
center: Vec2D,
Expand All @@ -50,9 +58,46 @@ impl Crop {
canvas.stroke_path(&path, &border_paint);
}

pub fn get_rectangle(&self) -> Option<(Vec2D, Vec2D)> {
self.size
.map(|size| math::rect_ensure_positive_size(self.pos, size))
pub fn get_rectangle(&self) -> (Vec2D, Vec2D) {
math::rect_ensure_positive_size(self.pos, self.size)
}

fn get_handle_pos(crop_pos: Vec2D, crop_size: Vec2D, handle: CropHandle) -> Vec2D {
match handle {
CropHandle::TopLeftCorner => crop_pos,
CropHandle::TopEdge => crop_pos + Vec2D::new(crop_size.x / 2.0, 0.0),
CropHandle::TopRightCorner => crop_pos + Vec2D::new(crop_size.x, 0.0),
CropHandle::RightEdge => crop_pos + Vec2D::new(crop_size.x, crop_size.y / 2.0),
CropHandle::BottomRightCorner => crop_pos + Vec2D::new(crop_size.x, crop_size.y),
CropHandle::BottomEdge => crop_pos + Vec2D::new(crop_size.x / 2.0, crop_size.y),
CropHandle::BottomLeftCorner => crop_pos + Vec2D::new(0.0, crop_size.y),
CropHandle::LeftEdge => crop_pos + Vec2D::new(0.0, crop_size.y / 2.0),
}
}
fn get_closest_handle(&self, mouse_pos: Vec2D) -> (CropHandle, f32) {
let mut min_distance_squared = f32::MAX;
let mut closest_handle = CropHandle::TopLeftCorner;
for h in CropHandle::all() {
let handle_pos = Self::get_handle_pos(self.pos, self.size, h);
let distance_squared = (handle_pos - mouse_pos).norm2();
if distance_squared < min_distance_squared {
min_distance_squared = distance_squared;
closest_handle = h;
}
}
(closest_handle, min_distance_squared)
}
fn test_handle_hit(&self, mouse_pos: Vec2D, margin2: f32) -> Option<CropHandle> {
const HANDLE_SIZE: f32 = Crop::HANDLE_RADIUS + Crop::HANDLE_BORDER;
const HANDLE_SIZE2: f32 = HANDLE_SIZE * HANDLE_SIZE;
let allowed_distance2 = HANDLE_SIZE2 + margin2;

let (handle, distance2) = self.get_closest_handle(mouse_pos);
if distance2 < allowed_distance2 {
Some(handle)
} else {
None
}
}
}

Expand All @@ -62,11 +107,7 @@ impl Drawable for Crop {
canvas: &mut femtovg::Canvas<femtovg::renderer::OpenGl>,
_font: femtovg::FontId,
) -> Result<()> {
let size = match self.size {
Some(s) => s,
None => return Ok(()), // early exit if none
};

let size = self.size;
let scale = canvas.transform().average_scale();
let dimensions = Vec2D::new(
canvas.width() as f32 / scale,
Expand Down Expand Up @@ -156,55 +197,28 @@ impl CropHandle {
}

impl CropTool {
fn get_handle_pos(crop_pos: Vec2D, crop_size: Vec2D, handle: CropHandle) -> Vec2D {
match handle {
CropHandle::TopLeftCorner => crop_pos,
CropHandle::TopEdge => crop_pos + Vec2D::new(crop_size.x / 2.0, 0.0),
CropHandle::TopRightCorner => crop_pos + Vec2D::new(crop_size.x, 0.0),
CropHandle::RightEdge => crop_pos + Vec2D::new(crop_size.x, crop_size.y / 2.0),
CropHandle::BottomRightCorner => crop_pos + Vec2D::new(crop_size.x, crop_size.y),
CropHandle::BottomEdge => crop_pos + Vec2D::new(crop_size.x / 2.0, crop_size.y),
CropHandle::BottomLeftCorner => crop_pos + Vec2D::new(0.0, crop_size.y),
CropHandle::LeftEdge => crop_pos + Vec2D::new(0.0, crop_size.y / 2.0),
}
}
fn test_handle_hit(&self, mouse_pos: Vec2D) -> Option<(CropHandle, Vec2D, Vec2D)> {
let crop = self.crop.as_ref()?;

let crop_size = crop.size?;
let crop_pos = crop.pos;

const MAX_DISTANCE2: f32 = (Crop::HANDLE_BORDER + Crop::HANDLE_RADIUS)
* (Crop::HANDLE_RADIUS + Crop::HANDLE_BORDER);
const HANDLE_MARGIN_IN_2: f32 = 15.0 * 15.0;
const HANDLE_MARGIN_OUT: f32 = 40.0;

for h in CropHandle::all() {
if (Self::get_handle_pos(crop_pos, crop_size, h) - mouse_pos).norm2() < MAX_DISTANCE2 {
return Some((h, crop_pos, crop_size));
}
}
None
}

fn test_inside_crop(&self, mouse_pos: Vec2D) -> bool {
fn test_inside_crop(&self, mouse_pos: Vec2D, margin: f32) -> bool {
let crop = match &self.crop {
Some(c) => c,
None => return false,
};

let crop_size = match crop.size {
Some(s) => s,
None => return false,
};

let (mut min_x, mut max_x) = (crop.pos.x, crop.pos.x + crop_size.x);
let (mut min_x, mut max_x) = (crop.pos.x, crop.pos.x + crop.size.x);
if min_x > max_x {
(min_x, max_x) = (max_x, min_x);
}
min_x -= margin;
max_x += margin;

let (mut min_y, mut max_y) = (crop.pos.y, crop.pos.y + crop_size.y);
let (mut min_y, mut max_y) = (crop.pos.y, crop.pos.y + crop.size.y);
if min_y > max_y {
(min_y, max_y) = (max_y, min_y);
}
min_y -= margin;
max_y += margin;

min_x < mouse_pos.x && mouse_pos.x < max_x && min_y < mouse_pos.y && mouse_pos.y < max_y
}
Expand Down Expand Up @@ -249,34 +263,40 @@ impl CropTool {

// convert back and save
crop.pos = tl;
crop.size = Some(br - tl);
crop.size = br - tl;
}

fn begin_drag(&mut self, pos: Vec2D) -> ToolUpdateResult {
if let Some((handle, pos, size)) = self.test_handle_hit(pos) {
let top_left_start = pos;
let bottom_right_start = pos + size;
self.action = Some(CropToolAction::DragHandle(DragHandleState {
handle,
top_left_start,
bottom_right_start,
}));
} else {
// only start a new crop if none exists
match &self.crop {
None => {
self.crop = Some(Crop {
pos,
size: None,
active: true,
});
match &self.crop {
None => {
// No crop exists, create a new one
self.crop = Some(Crop::new(pos));
self.action = Some(CropToolAction::NewCrop);
}
Some(c) => {
if let Some(handle) = c.test_handle_hit(pos, CropTool::HANDLE_MARGIN_IN_2) {
// Crop exists and we are near a handle, drag it
self.action = Some(CropToolAction::DragHandle(DragHandleState {
handle,
top_left_start: c.pos,
bottom_right_start: c.pos + c.size,
}));
} else if self.test_inside_crop(pos, 0.0) {
// Crop exists and we are inside it, move it
self.action = Some(CropToolAction::Move(MoveState { start: c.pos }));
} else if self.test_inside_crop(pos, CropTool::HANDLE_MARGIN_OUT) {
// Crop exists and we are near the edge, drag from the closest handle
let (handle, _) = c.get_closest_handle(pos);
self.action = Some(CropToolAction::DragHandle(DragHandleState {
handle,
top_left_start: c.pos,
bottom_right_start: c.pos + c.size,
}));
} else {
// Crop exists, but we far outside from it, create a new one
self.crop = Some(Crop::new(pos));
self.action = Some(CropToolAction::NewCrop);
}
Some(c) => {
if self.test_inside_crop(pos) {
self.action = Some(CropToolAction::Move(MoveState { start: c.pos }));
}
}
}
}
ToolUpdateResult::Redraw
Expand All @@ -295,7 +315,7 @@ impl CropTool {

match action {
CropToolAction::NewCrop => {
crop.size = Some(direction);
crop.size = direction;
ToolUpdateResult::Redraw
}
CropToolAction::DragHandle(state) => {
Expand All @@ -322,7 +342,7 @@ impl CropTool {
// crop never returns "commit" because nothing gets
// committed to the drawables stack
CropToolAction::NewCrop => {
crop.size = Some(direction);
crop.size = direction;
self.action = None;
ToolUpdateResult::Redraw
}
Expand Down

0 comments on commit 478113f

Please sign in to comment.