diff --git a/core/document/src/document.rs b/core/document/src/document.rs index 8307cf5dbe..260a0fc86f 100644 --- a/core/document/src/document.rs +++ b/core/document/src/document.rs @@ -1,4 +1,4 @@ -use glam::DAffine2; +use glam::{DAffine2, DVec2}; use crate::{ layers::{self, style::PathStyle, Folder, Layer, LayerDataTypes, Line, PolyLine, Rect, Shape}, @@ -64,6 +64,19 @@ impl Document { svg } + /// Checks whether each layer under `path` intersects with the provided `quad` and adds all intersection layers as paths to `intersections`. + pub fn intersects_quad(&self, quad: [DVec2; 4], path: &mut Vec, intersections: &mut Vec>) { + self.document_folder(path).unwrap().intersects_quad(quad, path, intersections); + return; + } + + /// Checks whether each layer under the root path intersects with the provided `quad` and returns the paths to all intersecting layers. + pub fn intersects_quad_root(&self, quad: [DVec2; 4]) -> Vec> { + let mut intersections = Vec::new(); + self.intersects_quad(quad, &mut vec![], &mut intersections); + intersections + } + fn is_mounted(&self, mount_path: &[LayerId], path: &[LayerId]) -> bool { path.starts_with(mount_path) && self.work_mounted } diff --git a/core/document/src/intersection.rs b/core/document/src/intersection.rs new file mode 100644 index 0000000000..9e749e07ff --- /dev/null +++ b/core/document/src/intersection.rs @@ -0,0 +1,57 @@ +use glam::DVec2; +use kurbo::{BezPath, Line, PathSeg, Point, Shape, Vec2}; + +fn to_point(vec: DVec2) -> Point { + Point::new(vec.x, vec.y) +} + +pub fn intersect_quad_bez_path(quad: [DVec2; 4], shape: &BezPath, closed: bool) -> bool { + let lines = vec![ + Line::new(to_point(quad[0]), to_point(quad[1])), + Line::new(to_point(quad[1]), to_point(quad[2])), + Line::new(to_point(quad[2]), to_point(quad[3])), + Line::new(to_point(quad[3]), to_point(quad[0])), + ]; + // check if outlines intersect + for path_segment in shape.segments() { + for line in &lines { + if !path_segment.intersect_line(*line).is_empty() { + return true; + } + } + } + // check if selection is entirely within the shape + if closed && shape.contains(to_point(quad[0])) { + return true; + } + // check if shape is entirely within the selection + if let Some(shape_point) = get_arbitrary_point_on_path(shape) { + let mut pos = 0; + let mut neg = 0; + for line in lines { + if line.p0 == shape_point { + return true; + }; + let line_vec = Vec2::new(line.p1.x - line.p0.x, line.p1.y - line.p0.y); + let point_vec = Vec2::new(line.p1.x - shape_point.x, line.p1.y - shape_point.y); + let cross = line_vec.cross(point_vec); + if cross > 0.0 { + pos += 1; + } else if cross < 0.0 { + neg += 1; + } + if pos > 0 && neg > 0 { + return false; + } + } + } + true +} + +pub fn get_arbitrary_point_on_path(path: &BezPath) -> Option { + path.segments().next().map(|seg| match seg { + PathSeg::Line(line) => line.p0, + PathSeg::Quad(quad) => quad.p0, + PathSeg::Cubic(cubic) => cubic.p0, + }) +} diff --git a/core/document/src/layers/ellipse.rs b/core/document/src/layers/ellipse.rs index 65e8b6ea87..d884cab41b 100644 --- a/core/document/src/layers/ellipse.rs +++ b/core/document/src/layers/ellipse.rs @@ -1,5 +1,10 @@ +use glam::DAffine2; +use glam::DVec2; use kurbo::Shape; +use crate::intersection::intersect_quad_bez_path; +use crate::LayerId; + use super::style; use super::LayerData; @@ -16,10 +21,17 @@ impl Ellipse { } impl LayerData for Ellipse { - fn to_kurbo_path(&mut self, transform: glam::DAffine2, _style: style::PathStyle) -> kurbo::BezPath { - kurbo::Ellipse::from_affine(kurbo::Affine::new(transform.to_cols_array())).to_path(0.1) + fn to_kurbo_path(&self, transform: glam::DAffine2, _style: style::PathStyle) -> kurbo::BezPath { + kurbo::Ellipse::from_affine(kurbo::Affine::new(transform.to_cols_array())).to_path(0.01) } + fn render(&mut self, svg: &mut String, transform: glam::DAffine2, style: style::PathStyle) { let _ = write!(svg, r#""#, self.to_kurbo_path(transform, style).to_svg(), style.render()); } + + fn intersects_quad(&self, quad: [DVec2; 4], path: &mut Vec, intersections: &mut Vec>, style: style::PathStyle) { + if intersect_quad_bez_path(quad, &self.to_kurbo_path(DAffine2::IDENTITY, style), true) { + intersections.push(path.clone()); + } + } } diff --git a/core/document/src/layers/folder.rs b/core/document/src/layers/folder.rs index dae9344c86..983482eafd 100644 --- a/core/document/src/layers/folder.rs +++ b/core/document/src/layers/folder.rs @@ -1,3 +1,5 @@ +use glam::DVec2; + use crate::{DocumentError, LayerId}; use super::{style, Layer, LayerData, LayerDataTypes}; @@ -13,6 +15,10 @@ pub struct Folder { } impl LayerData for Folder { + fn to_kurbo_path(&self, _: glam::DAffine2, _: style::PathStyle) -> kurbo::BezPath { + unimplemented!() + } + fn render(&mut self, svg: &mut String, transform: glam::DAffine2, _style: style::PathStyle) { let _ = writeln!(svg, r#""); } - fn to_kurbo_path(&mut self, _: glam::DAffine2, _: style::PathStyle) -> kurbo::BezPath { - unimplemented!() + fn intersects_quad(&self, quad: [DVec2; 4], path: &mut Vec, intersections: &mut Vec>, _style: style::PathStyle) { + for (layer, layer_id) in self.layers().iter().zip(&self.layer_ids) { + path.push(*layer_id); + layer.intersects_quad(quad, path, intersections); + path.pop(); + } } } diff --git a/core/document/src/layers/line.rs b/core/document/src/layers/line.rs index 8b36a2e2cb..d8502c7f8e 100644 --- a/core/document/src/layers/line.rs +++ b/core/document/src/layers/line.rs @@ -1,6 +1,10 @@ +use glam::DAffine2; use glam::DVec2; use kurbo::Point; +use crate::intersection::intersect_quad_bez_path; +use crate::LayerId; + use super::style; use super::LayerData; @@ -17,7 +21,7 @@ impl Line { } impl LayerData for Line { - fn to_kurbo_path(&mut self, transform: glam::DAffine2, _style: style::PathStyle) -> kurbo::BezPath { + fn to_kurbo_path(&self, transform: glam::DAffine2, _style: style::PathStyle) -> kurbo::BezPath { fn new_point(a: DVec2) -> Point { Point::new(a.x, a.y) } @@ -26,10 +30,17 @@ impl LayerData for Line { path.line_to(new_point(transform.transform_point2(DVec2::ONE))); path } + fn render(&mut self, svg: &mut String, transform: glam::DAffine2, style: style::PathStyle) { let [x1, y1] = transform.translation.to_array(); let [x2, y2] = transform.transform_point2(DVec2::ONE).to_array(); let _ = write!(svg, r#""#, x1, y1, x2, y2, style.render(),); } + + fn intersects_quad(&self, quad: [DVec2; 4], path: &mut Vec, intersections: &mut Vec>, style: style::PathStyle) { + if intersect_quad_bez_path(quad, &self.to_kurbo_path(DAffine2::IDENTITY, style), false) { + intersections.push(path.clone()); + } + } } diff --git a/core/document/src/layers/mod.rs b/core/document/src/layers/mod.rs index 48a8ebbdcd..dca61b3237 100644 --- a/core/document/src/layers/mod.rs +++ b/core/document/src/layers/mod.rs @@ -19,12 +19,16 @@ pub use shape::Shape; pub mod folder; use crate::DocumentError; +use crate::LayerId; pub use folder::Folder; use serde::{Deserialize, Serialize}; +pub const SELECTION_TOLERANCE: f64 = 5.0; + pub trait LayerData { fn render(&mut self, svg: &mut String, transform: glam::DAffine2, style: style::PathStyle); - fn to_kurbo_path(&mut self, transform: glam::DAffine2, style: style::PathStyle) -> BezPath; + fn to_kurbo_path(&self, transform: glam::DAffine2, style: style::PathStyle) -> BezPath; + fn intersects_quad(&self, quad: [DVec2; 4], path: &mut Vec, intersections: &mut Vec>, style: style::PathStyle); } #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] @@ -51,6 +55,15 @@ macro_rules! call_kurbo_path { } }; } + +macro_rules! call_intersects_quad { + ($self:ident.intersects_quad($quad:ident, $path:ident, $intersections:ident, $style:ident) { $($variant:ident),* }) => { + match $self { + $(Self::$variant(x) => x.intersects_quad($quad, $path, $intersections, $style)),* + } + }; +} + impl LayerDataTypes { pub fn render(&mut self, svg: &mut String, transform: glam::DAffine2, style: style::PathStyle) { call_render! { @@ -64,7 +77,7 @@ impl LayerDataTypes { } } } - pub fn to_kurbo_path(&mut self, transform: glam::DAffine2, style: style::PathStyle) -> BezPath { + pub fn to_kurbo_path(&self, transform: glam::DAffine2, style: style::PathStyle) -> BezPath { call_kurbo_path! { self.to_kurbo_path(transform, style) { Folder, @@ -76,6 +89,19 @@ impl LayerDataTypes { } } } + + pub fn intersects_quad(&self, quad: [DVec2; 4], path: &mut Vec, intersections: &mut Vec>, style: style::PathStyle) { + call_intersects_quad! { + self.intersects_quad(quad, path, intersections, style) { + Folder, + Ellipse, + Rect, + Line, + PolyLine, + Shape + } + } + } } #[derive(Serialize, Deserialize)] @@ -122,6 +148,20 @@ impl Layer { self.cache.as_str() } + pub fn intersects_quad(&self, quad: [DVec2; 4], path: &mut Vec, intersections: &mut Vec>) { + let inv_transform = self.transform.inverse(); + let transformed_quad = [ + inv_transform.transform_point2(quad[0]), + inv_transform.transform_point2(quad[1]), + inv_transform.transform_point2(quad[2]), + inv_transform.transform_point2(quad[3]), + ]; + if !self.visible { + return; + } + self.data.intersects_quad(transformed_quad, path, intersections, self.style) + } + pub fn render_on(&mut self, svg: &mut String) { *svg += self.render(); } @@ -129,12 +169,14 @@ impl Layer { pub fn to_kurbo_path(&mut self) -> BezPath { self.data.to_kurbo_path(self.transform, self.style) } + pub fn as_folder_mut(&mut self) -> Result<&mut Folder, DocumentError> { match &mut self.data { LayerDataTypes::Folder(f) => Ok(f), _ => Err(DocumentError::NotAFolder), } } + pub fn as_folder(&self) -> Result<&Folder, DocumentError> { match &self.data { LayerDataTypes::Folder(f) => Ok(&f), diff --git a/core/document/src/layers/polyline.rs b/core/document/src/layers/polyline.rs index c2558fae13..068b7585ab 100644 --- a/core/document/src/layers/polyline.rs +++ b/core/document/src/layers/polyline.rs @@ -1,3 +1,5 @@ +use crate::{intersection::intersect_quad_bez_path, LayerId}; +use glam::{DAffine2, DVec2}; use serde::{Deserialize, Serialize}; use std::fmt::Write; @@ -17,7 +19,7 @@ impl PolyLine { } impl LayerData for PolyLine { - fn to_kurbo_path(&mut self, transform: glam::DAffine2, _style: style::PathStyle) -> kurbo::BezPath { + fn to_kurbo_path(&self, transform: glam::DAffine2, _style: style::PathStyle) -> kurbo::BezPath { let mut path = kurbo::BezPath::new(); self.points .iter() @@ -27,6 +29,7 @@ impl LayerData for PolyLine { .for_each(|(i, p)| if i == 0 { path.move_to(p) } else { path.line_to(p) }); path } + fn render(&mut self, svg: &mut String, transform: glam::DAffine2, style: style::PathStyle) { if self.points.is_empty() { return; @@ -40,6 +43,12 @@ impl LayerData for PolyLine { } let _ = write!(svg, r#""{} />"#, style.render()); } + + fn intersects_quad(&self, quad: [DVec2; 4], path: &mut Vec, intersections: &mut Vec>, style: style::PathStyle) { + if intersect_quad_bez_path(quad, &self.to_kurbo_path(DAffine2::IDENTITY, style), false) { + intersections.push(path.clone()); + } + } } #[cfg(test)] diff --git a/core/document/src/layers/rect.rs b/core/document/src/layers/rect.rs index 0cd3e7f946..22c0befeb0 100644 --- a/core/document/src/layers/rect.rs +++ b/core/document/src/layers/rect.rs @@ -1,6 +1,10 @@ +use glam::DAffine2; use glam::DVec2; use kurbo::Point; +use crate::intersection::intersect_quad_bez_path; +use crate::LayerId; + use super::style; use super::LayerData; @@ -17,7 +21,7 @@ impl Rect { } impl LayerData for Rect { - fn to_kurbo_path(&mut self, transform: glam::DAffine2, _style: style::PathStyle) -> kurbo::BezPath { + fn to_kurbo_path(&self, transform: glam::DAffine2, _style: style::PathStyle) -> kurbo::BezPath { fn new_point(a: DVec2) -> Point { Point::new(a.x, a.y) } @@ -32,4 +36,10 @@ impl LayerData for Rect { fn render(&mut self, svg: &mut String, transform: glam::DAffine2, style: style::PathStyle) { let _ = write!(svg, r#""#, self.to_kurbo_path(transform, style).to_svg(), style.render()); } + + fn intersects_quad(&self, quad: [DVec2; 4], path: &mut Vec, intersections: &mut Vec>, style: style::PathStyle) { + if intersect_quad_bez_path(quad, &self.to_kurbo_path(DAffine2::IDENTITY, style), true) { + intersections.push(path.clone()); + } + } } diff --git a/core/document/src/layers/shape.rs b/core/document/src/layers/shape.rs index 6ff9a71c6e..b81257fe60 100644 --- a/core/document/src/layers/shape.rs +++ b/core/document/src/layers/shape.rs @@ -1,3 +1,8 @@ +use glam::DAffine2; +use glam::DVec2; + +use crate::intersection::intersect_quad_bez_path; +use crate::LayerId; use kurbo::BezPath; use kurbo::Vec2; @@ -20,7 +25,7 @@ impl Shape { } impl LayerData for Shape { - fn to_kurbo_path(&mut self, transform: glam::DAffine2, _style: style::PathStyle) -> BezPath { + fn to_kurbo_path(&self, transform: glam::DAffine2, _style: style::PathStyle) -> BezPath { fn unit_rotation(theta: f64) -> Vec2 { Vec2::new(-theta.sin(), theta.cos()) } @@ -66,4 +71,10 @@ impl LayerData for Shape { fn render(&mut self, svg: &mut String, transform: glam::DAffine2, style: style::PathStyle) { let _ = write!(svg, r#""#, self.to_kurbo_path(transform, style).to_svg(), style.render()); } + + fn intersects_quad(&self, quad: [DVec2; 4], path: &mut Vec, intersections: &mut Vec>, style: style::PathStyle) { + if intersect_quad_bez_path(quad, &self.to_kurbo_path(DAffine2::IDENTITY, style), true) { + intersections.push(path.clone()); + } + } } diff --git a/core/document/src/lib.rs b/core/document/src/lib.rs index d933bf42b1..9e419d6a32 100644 --- a/core/document/src/lib.rs +++ b/core/document/src/lib.rs @@ -1,5 +1,6 @@ pub mod color; pub mod document; +pub mod intersection; pub mod layers; pub mod operation; pub mod response; diff --git a/core/editor/src/document/document_message_handler.rs b/core/editor/src/document/document_message_handler.rs index a9d2e8872a..106a506c9b 100644 --- a/core/editor/src/document/document_message_handler.rs +++ b/core/editor/src/document/document_message_handler.rs @@ -281,6 +281,8 @@ impl MessageHandler for DocumentMessageHand for path in paths { responses.extend(self.select_layer(&path)); } + // TODO: Correctly update layer panel in clear_selection instead of here + responses.extend(self.handle_folder_changed(Vec::new())); } Undo => { // this is a temporary fix and will be addressed by #123 diff --git a/core/editor/src/input/input_mapper.rs b/core/editor/src/input/input_mapper.rs index 797e2d1b3f..d8fb62afb1 100644 --- a/core/editor/src/input/input_mapper.rs +++ b/core/editor/src/input/input_mapper.rs @@ -104,6 +104,12 @@ impl Default for Mapping { fn default() -> Self { let (up, down, pointer_move) = mapping![ entry! {action=DocumentMessage::PasteLayers, key_down=KeyV, modifiers=[KeyControl]}, + // Select + entry! {action=SelectMessage::MouseMove, message=InputMapperMessage::PointerMove}, + entry! {action=SelectMessage::DragStart, key_down=Lmb}, + entry! {action=SelectMessage::DragStop, key_up=Lmb}, + entry! {action=SelectMessage::Abort, key_down=Rmb}, + entry! {action=SelectMessage::Abort, key_down=KeyEscape}, // Rectangle entry! {action=RectangleMessage::Center, key_down=KeyAlt}, entry! {action=RectangleMessage::UnCenter, key_up=KeyAlt}, diff --git a/core/editor/src/tool/tools/select.rs b/core/editor/src/tool/tools/select.rs index 1df1166c72..832824c8cd 100644 --- a/core/editor/src/tool/tools/select.rs +++ b/core/editor/src/tool/tools/select.rs @@ -1,4 +1,11 @@ -use crate::input::InputPreprocessor; +use document_core::color::Color; +use document_core::layers::style::Fill; +use document_core::layers::style::Stroke; +use document_core::layers::{style, SELECTION_TOLERANCE}; +use document_core::Operation; +use glam::{DAffine2, DVec2}; + +use crate::input::{mouse::ViewportPosition, InputPreprocessor}; use crate::tool::{DocumentToolData, Fsm, ToolActionHandlerData}; use crate::{message_prelude::*, SvgDocument}; @@ -11,19 +18,29 @@ pub struct Select { #[impl_message(Message, ToolMessage, Select)] #[derive(PartialEq, Clone, Debug)] pub enum SelectMessage { + DragStart, + DragStop, MouseMove, + Abort, } impl<'a> MessageHandler> for Select { fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque) { self.fsm_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, data.2, responses); } - advertise_actions!(); + fn actions(&self) -> ActionList { + use SelectToolFsmState::*; + match self.fsm_state { + Ready => actions!(SelectMessageDiscriminant; DragStart), + Dragging => actions!(SelectMessageDiscriminant; DragStop, MouseMove, Abort), + } + } } #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum SelectToolFsmState { Ready, + Dragging, } impl Default for SelectToolFsmState { @@ -32,29 +49,96 @@ impl Default for SelectToolFsmState { } } -#[derive(Default)] -struct SelectToolData; +#[derive(Clone, Debug, Default)] +struct SelectToolData { + drag_start: ViewportPosition, + drag_current: ViewportPosition, +} impl Fsm for SelectToolFsmState { type ToolData = SelectToolData; - fn transition( - self, - event: ToolMessage, - _document: &SvgDocument, - _tool_data: &DocumentToolData, - _data: &mut Self::ToolData, - _input: &InputPreprocessor, - _responses: &mut VecDeque, - ) -> Self { + fn transition(self, event: ToolMessage, document: &SvgDocument, tool_data: &DocumentToolData, data: &mut Self::ToolData, input: &InputPreprocessor, responses: &mut VecDeque) -> Self { + let transform = document.root.transform; use SelectMessage::*; use SelectToolFsmState::*; if let ToolMessage::Select(event) = event { match (self, event) { - (Ready, MouseMove) => self, + (Ready, DragStart) => { + data.drag_start = input.mouse.position; + data.drag_current = input.mouse.position; + responses.push_back(Operation::MountWorkingFolder { path: vec![] }.into()); + Dragging + } + (Dragging, MouseMove) => { + data.drag_current = input.mouse.position; + + responses.push_back(Operation::ClearWorkingFolder.into()); + responses.push_back(make_operation(data, tool_data, transform)); + + Dragging + } + (Dragging, DragStop) => { + data.drag_current = input.mouse.position; + + responses.push_back(Operation::ClearWorkingFolder.into()); + + let (point_1, point_2) = if data.drag_start == data.drag_current { + let (x, y) = (data.drag_current.x as f64, data.drag_current.y as f64); + ( + DVec2::new(x - SELECTION_TOLERANCE, y - SELECTION_TOLERANCE), + DVec2::new(x + SELECTION_TOLERANCE, y + SELECTION_TOLERANCE), + ) + } else { + ( + DVec2::new(data.drag_start.x as f64, data.drag_start.y as f64), + DVec2::new(data.drag_current.x as f64, data.drag_current.y as f64), + ) + }; + + let quad = [ + DVec2::new(point_1.x, point_1.y), + DVec2::new(point_2.x, point_1.y), + DVec2::new(point_2.x, point_2.y), + DVec2::new(point_1.x, point_2.y), + ]; + + if data.drag_start == data.drag_current { + if let Some(intersection) = document.intersects_quad_root(quad).last() { + responses.push_back(DocumentMessage::SelectLayers(vec![intersection.clone()]).into()); + } else { + responses.push_back(DocumentMessage::SelectLayers(vec![]).into()); + } + } else { + responses.push_back(DocumentMessage::SelectLayers(document.intersects_quad_root(quad)).into()); + } + + Ready + } + (Dragging, Abort) => { + responses.push_back(Operation::DiscardWorkingFolder.into()); + + Ready + } + _ => self, } } else { self } } } + +fn make_operation(data: &SelectToolData, _tool_data: &DocumentToolData, transform: DAffine2) -> Message { + let x0 = data.drag_start.x as f64; + let y0 = data.drag_start.y as f64; + let x1 = data.drag_current.x as f64; + let y1 = data.drag_current.y as f64; + + Operation::AddRect { + path: vec![], + insert_index: -1, + transform: (transform.inverse() * glam::DAffine2::from_scale_angle_translation(DVec2::new(x1 - x0, y1 - y0), 0., DVec2::new(x0, y0))).to_cols_array(), + style: style::PathStyle::new(Some(Stroke::new(Color::from_rgb8(0x31, 0x94, 0xD6), 2.0)), Some(Fill::none())), + } + .into() +}