use crate::shapes::{Shape, ShapeId, ShapeTrait};
use kurbo::{Point, Rect};
use serde::{Deserialize, Serialize};
pub const HANDLE_SIZE: f64 = 8.0;
pub const HANDLE_HIT_TOLERANCE: f64 = 12.0;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum HandleKind {
Endpoint(usize),
Corner(Corner),
Edge(Edge),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Corner {
TopLeft,
TopRight,
BottomLeft,
BottomRight,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Edge {
Top,
Right,
Bottom,
Left,
}
#[derive(Debug, Clone, Copy)]
pub struct Handle {
pub position: Point,
pub kind: HandleKind,
}
impl Handle {
pub fn new(position: Point, kind: HandleKind) -> Self {
Self { position, kind }
}
pub fn hit_test(&self, point: Point, tolerance: f64) -> bool {
let dx = point.x - self.position.x;
let dy = point.y - self.position.y;
let dist_sq = dx * dx + dy * dy;
dist_sq <= tolerance * tolerance
}
}
pub fn get_handles(shape: &Shape) -> Vec<Handle> {
match shape {
Shape::Line(line) => vec![
Handle::new(line.start, HandleKind::Endpoint(0)),
Handle::new(line.end, HandleKind::Endpoint(1)),
],
Shape::Arrow(arrow) => vec![
Handle::new(arrow.start, HandleKind::Endpoint(0)),
Handle::new(arrow.end, HandleKind::Endpoint(1)),
],
Shape::Rectangle(_) | Shape::Ellipse(_) => {
let bounds = shape.bounds();
corner_handles(bounds)
}
Shape::Freehand(_) => {
let bounds = shape.bounds();
corner_handles(bounds)
}
Shape::Text(_) => {
let bounds = shape.bounds();
corner_handles(bounds)
}
Shape::Group(_) => {
let bounds = shape.bounds();
corner_handles(bounds)
}
Shape::Image(_) => {
let bounds = shape.bounds();
corner_handles(bounds)
}
}
}
fn corner_handles(bounds: Rect) -> Vec<Handle> {
vec![
Handle::new(
Point::new(bounds.x0, bounds.y0),
HandleKind::Corner(Corner::TopLeft),
),
Handle::new(
Point::new(bounds.x1, bounds.y0),
HandleKind::Corner(Corner::TopRight),
),
Handle::new(
Point::new(bounds.x0, bounds.y1),
HandleKind::Corner(Corner::BottomLeft),
),
Handle::new(
Point::new(bounds.x1, bounds.y1),
HandleKind::Corner(Corner::BottomRight),
),
]
}
pub fn hit_test_handles(shape: &Shape, point: Point, tolerance: f64) -> Option<HandleKind> {
let handles = get_handles(shape);
for handle in handles {
if handle.hit_test(point, tolerance) {
return Some(handle.kind);
}
}
None
}
#[derive(Debug, Clone)]
pub struct ManipulationState {
pub shape_id: ShapeId,
pub handle: Option<HandleKind>,
pub start_point: Point,
pub current_point: Point,
pub original_shape: Shape,
}
#[derive(Debug, Clone)]
pub struct MultiMoveState {
pub start_point: Point,
pub current_point: Point,
pub original_shapes: std::collections::HashMap<ShapeId, Shape>,
pub is_duplicate: bool,
pub duplicated_ids: Vec<ShapeId>,
}
impl ManipulationState {
pub fn new(shape_id: ShapeId, handle: Option<HandleKind>, start_point: Point, original_shape: Shape) -> Self {
Self {
shape_id,
handle,
start_point,
current_point: start_point,
original_shape,
}
}
pub fn delta(&self) -> kurbo::Vec2 {
kurbo::Vec2::new(
self.current_point.x - self.start_point.x,
self.current_point.y - self.start_point.y,
)
}
}
impl MultiMoveState {
pub fn new(start_point: Point, original_shapes: std::collections::HashMap<ShapeId, Shape>) -> Self {
Self {
start_point,
current_point: start_point,
original_shapes,
is_duplicate: false,
duplicated_ids: Vec::new(),
}
}
pub fn new_duplicate(start_point: Point, original_shapes: std::collections::HashMap<ShapeId, Shape>) -> Self {
Self {
start_point,
current_point: start_point,
original_shapes,
is_duplicate: true,
duplicated_ids: Vec::new(),
}
}
pub fn delta(&self) -> kurbo::Vec2 {
kurbo::Vec2::new(
self.current_point.x - self.start_point.x,
self.current_point.y - self.start_point.y,
)
}
pub fn shape_ids(&self) -> Vec<ShapeId> {
self.original_shapes.keys().copied().collect()
}
}
pub fn get_manipulation_target_position(shape: &Shape, handle: Option<HandleKind>) -> Point {
match handle {
None => {
let bounds = shape.bounds();
Point::new(bounds.x0, bounds.y0)
}
Some(HandleKind::Endpoint(idx)) => {
match shape {
Shape::Line(line) => {
if idx == 0 { line.start } else { line.end }
}
Shape::Arrow(arrow) => {
if idx == 0 { arrow.start } else { arrow.end }
}
_ => shape.bounds().center(),
}
}
Some(HandleKind::Corner(corner)) => {
let bounds = shape.bounds();
match corner {
Corner::TopLeft => Point::new(bounds.x0, bounds.y0),
Corner::TopRight => Point::new(bounds.x1, bounds.y0),
Corner::BottomLeft => Point::new(bounds.x0, bounds.y1),
Corner::BottomRight => Point::new(bounds.x1, bounds.y1),
}
}
Some(HandleKind::Edge(edge)) => {
let bounds = shape.bounds();
match edge {
Edge::Top => Point::new(bounds.center().x, bounds.y0),
Edge::Right => Point::new(bounds.x1, bounds.center().y),
Edge::Bottom => Point::new(bounds.center().x, bounds.y1),
Edge::Left => Point::new(bounds.x0, bounds.center().y),
}
}
}
}
pub fn apply_manipulation(shape: &Shape, handle: Option<HandleKind>, delta: kurbo::Vec2) -> Shape {
let mut shape = shape.clone();
match handle {
None => {
let translation = kurbo::Affine::translate(delta);
shape.transform(translation);
}
Some(HandleKind::Endpoint(idx)) => {
match &mut shape {
Shape::Line(line) => {
if idx == 0 {
line.start.x += delta.x;
line.start.y += delta.y;
} else {
line.end.x += delta.x;
line.end.y += delta.y;
}
}
Shape::Arrow(arrow) => {
if idx == 0 {
arrow.start.x += delta.x;
arrow.start.y += delta.y;
} else {
arrow.end.x += delta.x;
arrow.end.y += delta.y;
}
}
_ => {}
}
}
Some(HandleKind::Corner(corner)) => {
match &mut shape {
Shape::Rectangle(rect) => {
apply_corner_resize_rect(rect, corner, delta);
}
Shape::Ellipse(ellipse) => {
apply_corner_resize_ellipse(ellipse, corner, delta);
}
_ => {}
}
}
Some(HandleKind::Edge(_)) => {
}
}
shape
}
fn apply_corner_resize_rect(rect: &mut crate::shapes::Rectangle, corner: Corner, delta: kurbo::Vec2) {
let bounds = rect.bounds();
let (new_x0, new_y0, new_x1, new_y1) = match corner {
Corner::TopLeft => (bounds.x0 + delta.x, bounds.y0 + delta.y, bounds.x1, bounds.y1),
Corner::TopRight => (bounds.x0, bounds.y0 + delta.y, bounds.x1 + delta.x, bounds.y1),
Corner::BottomLeft => (bounds.x0 + delta.x, bounds.y0, bounds.x1, bounds.y1 + delta.y),
Corner::BottomRight => (bounds.x0, bounds.y0, bounds.x1 + delta.x, bounds.y1 + delta.y),
};
let (x0, x1) = if new_x0 < new_x1 { (new_x0, new_x1) } else { (new_x1, new_x0) };
let (y0, y1) = if new_y0 < new_y1 { (new_y0, new_y1) } else { (new_y1, new_y0) };
rect.position = Point::new(x0, y0);
rect.width = (x1 - x0).max(1.0);
rect.height = (y1 - y0).max(1.0);
}
fn apply_corner_resize_ellipse(ellipse: &mut crate::shapes::Ellipse, corner: Corner, delta: kurbo::Vec2) {
let bounds = ellipse.bounds();
let (new_x0, new_y0, new_x1, new_y1) = match corner {
Corner::TopLeft => (bounds.x0 + delta.x, bounds.y0 + delta.y, bounds.x1, bounds.y1),
Corner::TopRight => (bounds.x0, bounds.y0 + delta.y, bounds.x1 + delta.x, bounds.y1),
Corner::BottomLeft => (bounds.x0 + delta.x, bounds.y0, bounds.x1, bounds.y1 + delta.y),
Corner::BottomRight => (bounds.x0, bounds.y0, bounds.x1 + delta.x, bounds.y1 + delta.y),
};
let (x0, x1) = if new_x0 < new_x1 { (new_x0, new_x1) } else { (new_x1, new_x0) };
let (y0, y1) = if new_y0 < new_y1 { (new_y0, new_y1) } else { (new_y1, new_y0) };
let width = (x1 - x0).max(1.0);
let height = (y1 - y0).max(1.0);
ellipse.center = Point::new(x0 + width / 2.0, y0 + height / 2.0);
ellipse.radius_x = width / 2.0;
ellipse.radius_y = height / 2.0;
}
#[cfg(test)]
mod tests {
use super::*;
use crate::shapes::{Line, Rectangle};
#[test]
fn test_line_handles() {
let line = Line::new(Point::new(0.0, 0.0), Point::new(100.0, 100.0));
let handles = get_handles(&Shape::Line(line));
assert_eq!(handles.len(), 2);
assert!(matches!(handles[0].kind, HandleKind::Endpoint(0)));
assert!(matches!(handles[1].kind, HandleKind::Endpoint(1)));
}
#[test]
fn test_rectangle_handles() {
let rect = Rectangle::new(Point::new(0.0, 0.0), 100.0, 50.0);
let handles = get_handles(&Shape::Rectangle(rect));
assert_eq!(handles.len(), 4);
assert!(matches!(handles[0].kind, HandleKind::Corner(Corner::TopLeft)));
}
#[test]
fn test_handle_hit_test() {
let handle = Handle::new(Point::new(50.0, 50.0), HandleKind::Endpoint(0));
assert!(handle.hit_test(Point::new(50.0, 50.0), 10.0));
assert!(handle.hit_test(Point::new(55.0, 55.0), 10.0));
assert!(!handle.hit_test(Point::new(70.0, 70.0), 10.0));
}
#[test]
fn test_apply_endpoint_manipulation() {
let line = Line::new(Point::new(0.0, 0.0), Point::new(100.0, 100.0));
let shape = Shape::Line(line);
let result = apply_manipulation(&shape, Some(HandleKind::Endpoint(1)), kurbo::Vec2::new(10.0, 20.0));
if let Shape::Line(line) = result {
assert!((line.end.x - 110.0).abs() < f64::EPSILON);
assert!((line.end.y - 120.0).abs() < f64::EPSILON);
} else {
panic!("Expected Line shape");
}
}
#[test]
fn test_apply_corner_manipulation() {
let rect = Rectangle::new(Point::new(0.0, 0.0), 100.0, 100.0);
let shape = Shape::Rectangle(rect);
let result = apply_manipulation(
&shape,
Some(HandleKind::Corner(Corner::BottomRight)),
kurbo::Vec2::new(50.0, 50.0)
);
if let Shape::Rectangle(rect) = result {
assert!((rect.width - 150.0).abs() < f64::EPSILON);
assert!((rect.height - 150.0).abs() < f64::EPSILON);
} else {
panic!("Expected Rectangle shape");
}
}
}