drafftink_core/
selection.rs

1//! Selection and manipulation handle system.
2
3use crate::shapes::{Shape, ShapeId, ShapeTrait};
4use kurbo::{Point, Rect};
5use serde::{Deserialize, Serialize};
6
7/// Handle size in screen pixels.
8pub const HANDLE_SIZE: f64 = 8.0;
9/// Handle hit tolerance in screen pixels.
10pub const HANDLE_HIT_TOLERANCE: f64 = 12.0;
11
12/// Type of selection handle.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
14pub enum HandleKind {
15    /// Endpoint handle for lines/arrows (index 0 = start, 1 = end).
16    Endpoint(usize),
17    /// Corner handle for rectangles/ellipses.
18    Corner(Corner),
19    /// Edge midpoint handle for rectangles/ellipses (for resizing).
20    Edge(Edge),
21}
22
23/// Corner positions.
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
25pub enum Corner {
26    TopLeft,
27    TopRight,
28    BottomLeft,
29    BottomRight,
30}
31
32/// Edge positions.
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
34pub enum Edge {
35    Top,
36    Right,
37    Bottom,
38    Left,
39}
40
41/// A selection handle with its position and type.
42#[derive(Debug, Clone, Copy)]
43pub struct Handle {
44    /// Position in world coordinates.
45    pub position: Point,
46    /// Handle type.
47    pub kind: HandleKind,
48}
49
50impl Handle {
51    /// Create a new handle.
52    pub fn new(position: Point, kind: HandleKind) -> Self {
53        Self { position, kind }
54    }
55
56    /// Check if a point (in world coordinates) hits this handle.
57    /// `tolerance` should be adjusted for camera zoom.
58    pub fn hit_test(&self, point: Point, tolerance: f64) -> bool {
59        let dx = point.x - self.position.x;
60        let dy = point.y - self.position.y;
61        let dist_sq = dx * dx + dy * dy;
62        dist_sq <= tolerance * tolerance
63    }
64}
65
66/// Get the selection handles for a shape.
67pub fn get_handles(shape: &Shape) -> Vec<Handle> {
68    match shape {
69        Shape::Line(line) => vec![
70            Handle::new(line.start, HandleKind::Endpoint(0)),
71            Handle::new(line.end, HandleKind::Endpoint(1)),
72        ],
73        Shape::Arrow(arrow) => vec![
74            Handle::new(arrow.start, HandleKind::Endpoint(0)),
75            Handle::new(arrow.end, HandleKind::Endpoint(1)),
76        ],
77        Shape::Rectangle(_) | Shape::Ellipse(_) => {
78            let bounds = shape.bounds();
79            corner_handles(bounds)
80        }
81        Shape::Freehand(_) => {
82            // Freehand uses bounding box corners
83            let bounds = shape.bounds();
84            corner_handles(bounds)
85        }
86        Shape::Text(_) => {
87            // Text uses bounding box corners
88            let bounds = shape.bounds();
89            corner_handles(bounds)
90        }
91        Shape::Group(_) => {
92            // Groups use bounding box corners (resize not supported, only move)
93            let bounds = shape.bounds();
94            corner_handles(bounds)
95        }
96        Shape::Image(_) => {
97            // Images use bounding box corners for resizing
98            let bounds = shape.bounds();
99            corner_handles(bounds)
100        }
101    }
102}
103
104/// Generate corner handles for a bounding rectangle.
105fn corner_handles(bounds: Rect) -> Vec<Handle> {
106    vec![
107        Handle::new(
108            Point::new(bounds.x0, bounds.y0),
109            HandleKind::Corner(Corner::TopLeft),
110        ),
111        Handle::new(
112            Point::new(bounds.x1, bounds.y0),
113            HandleKind::Corner(Corner::TopRight),
114        ),
115        Handle::new(
116            Point::new(bounds.x0, bounds.y1),
117            HandleKind::Corner(Corner::BottomLeft),
118        ),
119        Handle::new(
120            Point::new(bounds.x1, bounds.y1),
121            HandleKind::Corner(Corner::BottomRight),
122        ),
123    ]
124}
125
126/// Find which handle (if any) is hit at the given point.
127/// Returns the handle kind if hit.
128pub fn hit_test_handles(shape: &Shape, point: Point, tolerance: f64) -> Option<HandleKind> {
129    let handles = get_handles(shape);
130    for handle in handles {
131        if handle.hit_test(point, tolerance) {
132            return Some(handle.kind);
133        }
134    }
135    None
136}
137
138/// State of an active manipulation operation (single shape with handle).
139#[derive(Debug, Clone)]
140pub struct ManipulationState {
141    /// The shape being manipulated.
142    pub shape_id: ShapeId,
143    /// The handle being dragged (None = moving the whole shape).
144    pub handle: Option<HandleKind>,
145    /// Starting point of the drag.
146    pub start_point: Point,
147    /// Current point of the drag.
148    pub current_point: Point,
149    /// Original shape state for preview/undo.
150    pub original_shape: Shape,
151}
152
153/// State for moving multiple shapes at once.
154#[derive(Debug, Clone)]
155pub struct MultiMoveState {
156    /// Starting point of the drag.
157    pub start_point: Point,
158    /// Current point of the drag.
159    pub current_point: Point,
160    /// Original shapes state for preview/undo (shape_id -> original shape).
161    pub original_shapes: std::collections::HashMap<ShapeId, Shape>,
162    /// Whether this is an alt-drag duplicate operation.
163    pub is_duplicate: bool,
164    /// IDs of duplicated shapes (only set if is_duplicate is true).
165    pub duplicated_ids: Vec<ShapeId>,
166}
167
168impl ManipulationState {
169    /// Create a new manipulation state.
170    pub fn new(shape_id: ShapeId, handle: Option<HandleKind>, start_point: Point, original_shape: Shape) -> Self {
171        Self {
172            shape_id,
173            handle,
174            start_point,
175            current_point: start_point,
176            original_shape,
177        }
178    }
179
180    /// Get the drag delta.
181    pub fn delta(&self) -> kurbo::Vec2 {
182        kurbo::Vec2::new(
183            self.current_point.x - self.start_point.x,
184            self.current_point.y - self.start_point.y,
185        )
186    }
187}
188
189impl MultiMoveState {
190    /// Create a new multi-move state.
191    pub fn new(start_point: Point, original_shapes: std::collections::HashMap<ShapeId, Shape>) -> Self {
192        Self {
193            start_point,
194            current_point: start_point,
195            original_shapes,
196            is_duplicate: false,
197            duplicated_ids: Vec::new(),
198        }
199    }
200    
201    /// Create a new multi-move state for duplication (Alt+drag).
202    pub fn new_duplicate(start_point: Point, original_shapes: std::collections::HashMap<ShapeId, Shape>) -> Self {
203        Self {
204            start_point,
205            current_point: start_point,
206            original_shapes,
207            is_duplicate: true,
208            duplicated_ids: Vec::new(),
209        }
210    }
211
212    /// Get the drag delta.
213    pub fn delta(&self) -> kurbo::Vec2 {
214        kurbo::Vec2::new(
215            self.current_point.x - self.start_point.x,
216            self.current_point.y - self.start_point.y,
217        )
218    }
219    
220    /// Get the shape IDs being moved.
221    pub fn shape_ids(&self) -> Vec<ShapeId> {
222        self.original_shapes.keys().copied().collect()
223    }
224}
225
226/// Get the position of the manipulation target (handle or shape center).
227/// This is used for snapping - we snap the target position, not the cursor.
228pub fn get_manipulation_target_position(shape: &Shape, handle: Option<HandleKind>) -> Point {
229    match handle {
230        None => {
231            // Moving entire shape - use top-left of bounding box as reference
232            let bounds = shape.bounds();
233            Point::new(bounds.x0, bounds.y0)
234        }
235        Some(HandleKind::Endpoint(idx)) => {
236            match shape {
237                Shape::Line(line) => {
238                    if idx == 0 { line.start } else { line.end }
239                }
240                Shape::Arrow(arrow) => {
241                    if idx == 0 { arrow.start } else { arrow.end }
242                }
243                _ => shape.bounds().center(),
244            }
245        }
246        Some(HandleKind::Corner(corner)) => {
247            let bounds = shape.bounds();
248            match corner {
249                Corner::TopLeft => Point::new(bounds.x0, bounds.y0),
250                Corner::TopRight => Point::new(bounds.x1, bounds.y0),
251                Corner::BottomLeft => Point::new(bounds.x0, bounds.y1),
252                Corner::BottomRight => Point::new(bounds.x1, bounds.y1),
253            }
254        }
255        Some(HandleKind::Edge(edge)) => {
256            let bounds = shape.bounds();
257            match edge {
258                Edge::Top => Point::new(bounds.center().x, bounds.y0),
259                Edge::Right => Point::new(bounds.x1, bounds.center().y),
260                Edge::Bottom => Point::new(bounds.center().x, bounds.y1),
261                Edge::Left => Point::new(bounds.x0, bounds.center().y),
262            }
263        }
264    }
265}
266
267/// Apply a handle manipulation to a shape.
268/// Returns the modified shape.
269pub fn apply_manipulation(shape: &Shape, handle: Option<HandleKind>, delta: kurbo::Vec2) -> Shape {
270    let mut shape = shape.clone();
271    
272    match handle {
273        None => {
274            // Move the entire shape
275            let translation = kurbo::Affine::translate(delta);
276            shape.transform(translation);
277        }
278        Some(HandleKind::Endpoint(idx)) => {
279            // Move an endpoint (for lines/arrows)
280            match &mut shape {
281                Shape::Line(line) => {
282                    if idx == 0 {
283                        line.start.x += delta.x;
284                        line.start.y += delta.y;
285                    } else {
286                        line.end.x += delta.x;
287                        line.end.y += delta.y;
288                    }
289                }
290                Shape::Arrow(arrow) => {
291                    if idx == 0 {
292                        arrow.start.x += delta.x;
293                        arrow.start.y += delta.y;
294                    } else {
295                        arrow.end.x += delta.x;
296                        arrow.end.y += delta.y;
297                    }
298                }
299                _ => {}
300            }
301        }
302        Some(HandleKind::Corner(corner)) => {
303            // Resize from a corner
304            match &mut shape {
305                Shape::Rectangle(rect) => {
306                    apply_corner_resize_rect(rect, corner, delta);
307                }
308                Shape::Ellipse(ellipse) => {
309                    apply_corner_resize_ellipse(ellipse, corner, delta);
310                }
311                _ => {}
312            }
313        }
314        Some(HandleKind::Edge(_)) => {
315            // Edge resize not implemented yet
316        }
317    }
318    
319    shape
320}
321
322/// Apply corner resize to a rectangle.
323fn apply_corner_resize_rect(rect: &mut crate::shapes::Rectangle, corner: Corner, delta: kurbo::Vec2) {
324    let bounds = rect.bounds();
325    let (new_x0, new_y0, new_x1, new_y1) = match corner {
326        Corner::TopLeft => (bounds.x0 + delta.x, bounds.y0 + delta.y, bounds.x1, bounds.y1),
327        Corner::TopRight => (bounds.x0, bounds.y0 + delta.y, bounds.x1 + delta.x, bounds.y1),
328        Corner::BottomLeft => (bounds.x0 + delta.x, bounds.y0, bounds.x1, bounds.y1 + delta.y),
329        Corner::BottomRight => (bounds.x0, bounds.y0, bounds.x1 + delta.x, bounds.y1 + delta.y),
330    };
331    
332    // Ensure min size and handle flipping
333    let (x0, x1) = if new_x0 < new_x1 { (new_x0, new_x1) } else { (new_x1, new_x0) };
334    let (y0, y1) = if new_y0 < new_y1 { (new_y0, new_y1) } else { (new_y1, new_y0) };
335    
336    rect.position = Point::new(x0, y0);
337    rect.width = (x1 - x0).max(1.0);
338    rect.height = (y1 - y0).max(1.0);
339}
340
341/// Apply corner resize to an ellipse.
342fn apply_corner_resize_ellipse(ellipse: &mut crate::shapes::Ellipse, corner: Corner, delta: kurbo::Vec2) {
343    let bounds = ellipse.bounds();
344    let (new_x0, new_y0, new_x1, new_y1) = match corner {
345        Corner::TopLeft => (bounds.x0 + delta.x, bounds.y0 + delta.y, bounds.x1, bounds.y1),
346        Corner::TopRight => (bounds.x0, bounds.y0 + delta.y, bounds.x1 + delta.x, bounds.y1),
347        Corner::BottomLeft => (bounds.x0 + delta.x, bounds.y0, bounds.x1, bounds.y1 + delta.y),
348        Corner::BottomRight => (bounds.x0, bounds.y0, bounds.x1 + delta.x, bounds.y1 + delta.y),
349    };
350    
351    // Ensure min size and handle flipping
352    let (x0, x1) = if new_x0 < new_x1 { (new_x0, new_x1) } else { (new_x1, new_x0) };
353    let (y0, y1) = if new_y0 < new_y1 { (new_y0, new_y1) } else { (new_y1, new_y0) };
354    
355    let width = (x1 - x0).max(1.0);
356    let height = (y1 - y0).max(1.0);
357    
358    ellipse.center = Point::new(x0 + width / 2.0, y0 + height / 2.0);
359    ellipse.radius_x = width / 2.0;
360    ellipse.radius_y = height / 2.0;
361}
362
363#[cfg(test)]
364mod tests {
365    use super::*;
366    use crate::shapes::{Line, Rectangle};
367
368    #[test]
369    fn test_line_handles() {
370        let line = Line::new(Point::new(0.0, 0.0), Point::new(100.0, 100.0));
371        let handles = get_handles(&Shape::Line(line));
372        
373        assert_eq!(handles.len(), 2);
374        assert!(matches!(handles[0].kind, HandleKind::Endpoint(0)));
375        assert!(matches!(handles[1].kind, HandleKind::Endpoint(1)));
376    }
377
378    #[test]
379    fn test_rectangle_handles() {
380        let rect = Rectangle::new(Point::new(0.0, 0.0), 100.0, 50.0);
381        let handles = get_handles(&Shape::Rectangle(rect));
382        
383        assert_eq!(handles.len(), 4);
384        assert!(matches!(handles[0].kind, HandleKind::Corner(Corner::TopLeft)));
385    }
386
387    #[test]
388    fn test_handle_hit_test() {
389        let handle = Handle::new(Point::new(50.0, 50.0), HandleKind::Endpoint(0));
390        
391        assert!(handle.hit_test(Point::new(50.0, 50.0), 10.0));
392        assert!(handle.hit_test(Point::new(55.0, 55.0), 10.0));
393        assert!(!handle.hit_test(Point::new(70.0, 70.0), 10.0));
394    }
395
396    #[test]
397    fn test_apply_endpoint_manipulation() {
398        let line = Line::new(Point::new(0.0, 0.0), Point::new(100.0, 100.0));
399        let shape = Shape::Line(line);
400        
401        let result = apply_manipulation(&shape, Some(HandleKind::Endpoint(1)), kurbo::Vec2::new(10.0, 20.0));
402        
403        if let Shape::Line(line) = result {
404            assert!((line.end.x - 110.0).abs() < f64::EPSILON);
405            assert!((line.end.y - 120.0).abs() < f64::EPSILON);
406        } else {
407            panic!("Expected Line shape");
408        }
409    }
410
411    #[test]
412    fn test_apply_corner_manipulation() {
413        let rect = Rectangle::new(Point::new(0.0, 0.0), 100.0, 100.0);
414        let shape = Shape::Rectangle(rect);
415        
416        let result = apply_manipulation(
417            &shape, 
418            Some(HandleKind::Corner(Corner::BottomRight)), 
419            kurbo::Vec2::new(50.0, 50.0)
420        );
421        
422        if let Shape::Rectangle(rect) = result {
423            assert!((rect.width - 150.0).abs() < f64::EPSILON);
424            assert!((rect.height - 150.0).abs() < f64::EPSILON);
425        } else {
426            panic!("Expected Rectangle shape");
427        }
428    }
429}