diff --git a/client/web/wasm/src/wrappers.rs b/client/web/wasm/src/wrappers.rs index d6430fd2a..00d5ef5f4 100644 --- a/client/web/wasm/src/wrappers.rs +++ b/client/web/wasm/src/wrappers.rs @@ -109,6 +109,7 @@ pub fn translate_key(name: &str) -> events::Key { "9" => Key9, "Enter" => KeyEnter, "Shift" => KeyShift, + "Control" => KeyControl, "Alt" => KeyAlt, _ => UnknownKey, } diff --git a/core/document/src/document.rs b/core/document/src/document.rs index 4a128d26f..e9ca769e1 100644 --- a/core/document/src/document.rs +++ b/core/document/src/document.rs @@ -247,7 +247,7 @@ impl Document { sides, style, } => { - let s = Shape::new((x0, y0), (x0 - x1, y0 - y1), sides, style); + let s = Shape::new((x0, y0), (x1, y1), sides, style); self.add_layer(&path, Layer::new(LayerDataTypes::Shape(s)), insert_index)?; Some(vec![DocumentResponse::DocumentChanged]) diff --git a/core/document/src/layers/shape.rs b/core/document/src/layers/shape.rs index 488278103..0c56aa89b 100644 --- a/core/document/src/layers/shape.rs +++ b/core/document/src/layers/shape.rs @@ -7,14 +7,16 @@ use std::fmt::Write; #[derive(Debug, Clone, Copy, PartialEq)] pub struct Shape { + bounding_rect: kurbo::Rect, shape: shape_points::ShapePoints, style: style::PathStyle, } impl Shape { - pub fn new(center: impl Into, extent: impl Into, sides: u8, style: style::PathStyle) -> Shape { + pub fn new(p0: impl Into, p1: impl Into, sides: u8, style: style::PathStyle) -> Shape { Shape { - shape: shape_points::ShapePoints::new(center, extent, sides), + bounding_rect: kurbo::Rect::from_points(p0, p1), + shape: shape_points::ShapePoints::new(kurbo::Point::new(0.5, 0.5), kurbo::Vec2::new(0.5, 0.0), sides), style, } } @@ -22,6 +24,15 @@ impl Shape { impl LayerData for Shape { fn render(&mut self, svg: &mut String) { - let _ = write!(svg, r#""#, self.shape, self.style.render(),); + let _ = write!( + svg, + r#""#, + self.shape, + self.bounding_rect.origin().x, + self.bounding_rect.origin().y, + self.bounding_rect.width(), + self.bounding_rect.height(), + self.style.render(), + ); } } diff --git a/core/editor/src/dispatcher/events.rs b/core/editor/src/dispatcher/events.rs index 8fde796f4..a29a67196 100644 --- a/core/editor/src/dispatcher/events.rs +++ b/core/editor/src/dispatcher/events.rs @@ -185,6 +185,7 @@ pub enum Key { Key8, Key9, KeyShift, + KeyControl, KeyAlt, } diff --git a/core/editor/src/tools/ellipse.rs b/core/editor/src/tools/ellipse.rs index 52fcd0e3a..bb4a2ca3f 100644 --- a/core/editor/src/tools/ellipse.rs +++ b/core/editor/src/tools/ellipse.rs @@ -74,8 +74,10 @@ impl Fsm for EllipseToolFsmState { data.drag_current = mouse_state.position; operations.push(Operation::ClearWorkingFolder); - operations.push(make_operation(data, tool_data)); - operations.push(Operation::CommitTransaction); + if data.drag_start != data.drag_current { + operations.push(make_operation(data, tool_data)); + operations.push(Operation::CommitTransaction); + } EllipseToolFsmState::Ready } diff --git a/core/editor/src/tools/line.rs b/core/editor/src/tools/line.rs index e6842c91b..f9beca692 100644 --- a/core/editor/src/tools/line.rs +++ b/core/editor/src/tools/line.rs @@ -7,6 +7,8 @@ use document_core::Operation; use super::DocumentToolData; +use std::f64::consts::PI; + #[derive(Default)] pub struct Line { fsm_state: LineToolFsmState, @@ -37,6 +39,11 @@ impl Default for LineToolFsmState { #[derive(Clone, Debug, Default)] struct LineToolData { drag_start: ViewportPosition, + drag_current: ViewportPosition, + angle: f64, + snap_angle: bool, + lock_angle: bool, + center_around_cursor: bool, } impl Fsm for LineToolFsmState { @@ -46,52 +53,143 @@ impl Fsm for LineToolFsmState { match (self, event) { (LineToolFsmState::Ready, Event::LmbDown(mouse_state)) => { data.drag_start = mouse_state.position; + data.drag_current = mouse_state.position; + operations.push(Operation::MountWorkingFolder { path: vec![] }); + LineToolFsmState::LmbDown } (LineToolFsmState::Ready, Event::KeyDown(Key::KeyZ)) => { if let Some(id) = document.root.list_layers().last() { operations.push(Operation::DeleteLayer { path: vec![*id] }) } + LineToolFsmState::Ready } (LineToolFsmState::LmbDown, Event::MouseMove(mouse_state)) => { + data.drag_current = *mouse_state; + operations.push(Operation::ClearWorkingFolder); - let start = data.drag_start; - let end = mouse_state; - operations.push(Operation::AddLine { - path: vec![], - insert_index: -1, - x0: start.x as f64, - y0: start.y as f64, - x1: end.x as f64, - y1: end.y as f64, - style: style::PathStyle::new(Some(style::Stroke::new(tool_data.primary_color, 5.)), None), - }); + operations.push(make_operation(data, tool_data)); LineToolFsmState::LmbDown } (LineToolFsmState::LmbDown, Event::LmbUp(mouse_state)) => { - let distance = data.drag_start.distance(&mouse_state.position); - log::info!("draw Line with distance: {:.2}", distance); + data.drag_current = mouse_state.position; + operations.push(Operation::ClearWorkingFolder); - let start = data.drag_start; - let end = mouse_state.position; - operations.push(Operation::AddLine { - path: vec![], - insert_index: -1, - x0: start.x as f64, - y0: start.y as f64, - x1: end.x as f64, - y1: end.y as f64, - style: style::PathStyle::new(Some(style::Stroke::new(tool_data.primary_color, 5.)), None), - }); - operations.push(Operation::CommitTransaction); + if data.drag_start != data.drag_current { + operations.push(make_operation(data, tool_data)); + operations.push(Operation::CommitTransaction); + } LineToolFsmState::Ready } + (state, Event::KeyDown(Key::KeyShift)) => { + data.snap_angle = true; + + if state == LineToolFsmState::LmbDown { + operations.push(Operation::ClearWorkingFolder); + operations.push(make_operation(data, tool_data)); + } + + self + } + + (state, Event::KeyUp(Key::KeyShift)) => { + data.snap_angle = false; + + if state == LineToolFsmState::LmbDown { + operations.push(Operation::ClearWorkingFolder); + operations.push(make_operation(data, tool_data)); + } + + self + } + + (state, Event::KeyDown(Key::KeyControl)) => { + data.lock_angle = true; + + if state == LineToolFsmState::LmbDown { + operations.push(Operation::ClearWorkingFolder); + operations.push(make_operation(data, tool_data)); + } + + self + } + + (state, Event::KeyUp(Key::KeyControl)) => { + data.lock_angle = false; + + if state == LineToolFsmState::LmbDown { + operations.push(Operation::ClearWorkingFolder); + operations.push(make_operation(data, tool_data)); + } + + self + } + + (state, Event::KeyDown(Key::KeyAlt)) => { + data.center_around_cursor = true; + + if state == LineToolFsmState::LmbDown { + operations.push(Operation::ClearWorkingFolder); + operations.push(make_operation(data, tool_data)); + } + + self + } + + (state, Event::KeyUp(Key::KeyAlt)) => { + data.center_around_cursor = false; + + if state == LineToolFsmState::LmbDown { + operations.push(Operation::ClearWorkingFolder); + operations.push(make_operation(data, tool_data)); + } + + self + } + _ => self, } } } + +fn make_operation(data: &mut LineToolData, tool_data: &DocumentToolData) -> Operation { + 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; + + let (dx, dy) = (x1 - x0, y1 - y0); + let mut angle = f64::atan2(dx, dy); + + if data.lock_angle { + angle = data.angle + }; + + if data.snap_angle { + let snap_resolution = 12.0; + angle = (angle * snap_resolution / PI).round() / snap_resolution * PI; + } + + data.angle = angle; + + let (dir_x, dir_y) = (f64::sin(angle), f64::cos(angle)); + let projected_length = dx * dir_x + dy * dir_y; + let (x1, y1) = (x0 + dir_x * projected_length, y0 + dir_y * projected_length); + + let (x0, y0) = if data.center_around_cursor { (x0 - (x1 - x0), y0 - (y1 - y0)) } else { (x0, y0) }; + + Operation::AddLine { + path: vec![], + insert_index: -1, + x0, + y0, + x1, + y1, + style: style::PathStyle::new(Some(style::Stroke::new(tool_data.primary_color, 5.)), None), + } +} diff --git a/core/editor/src/tools/rectangle.rs b/core/editor/src/tools/rectangle.rs index f1c177606..3bc4e2612 100644 --- a/core/editor/src/tools/rectangle.rs +++ b/core/editor/src/tools/rectangle.rs @@ -37,6 +37,9 @@ impl Default for RectangleToolFsmState { #[derive(Clone, Debug, Default)] struct RectangleToolData { drag_start: ViewportPosition, + drag_current: ViewportPosition, + constrain_to_square: bool, + center_around_cursor: bool, } impl Fsm for RectangleToolFsmState { @@ -46,6 +49,7 @@ impl Fsm for RectangleToolFsmState { match (self, event) { (RectangleToolFsmState::Ready, Event::LmbDown(mouse_state)) => { data.drag_start = mouse_state.position; + data.drag_current = mouse_state.position; operations.push(Operation::MountWorkingFolder { path: vec![] }); RectangleToolFsmState::LmbDown } @@ -56,42 +60,100 @@ impl Fsm for RectangleToolFsmState { RectangleToolFsmState::Ready } (RectangleToolFsmState::LmbDown, Event::MouseMove(mouse_state)) => { + data.drag_current = *mouse_state; + operations.push(Operation::ClearWorkingFolder); - let start = data.drag_start; - let end = mouse_state; - operations.push(Operation::AddRect { - path: vec![], - insert_index: -1, - x0: start.x as f64, - y0: start.y as f64, - x1: end.x as f64, - y1: end.y as f64, - style: style::PathStyle::new(None, Some(style::Fill::new(tool_data.primary_color))), - }); + operations.push(make_operation(data, tool_data)); RectangleToolFsmState::LmbDown } (RectangleToolFsmState::LmbDown, Event::LmbUp(mouse_state)) => { - let r = data.drag_start.distance(&mouse_state.position); - log::info!("draw rectangle with radius: {:.2}", r); + data.drag_current = mouse_state.position; + operations.push(Operation::ClearWorkingFolder); - let start = data.drag_start; - let end = mouse_state.position; - operations.push(Operation::AddRect { - path: vec![], - insert_index: -1, - x0: start.x as f64, - y0: start.y as f64, - x1: end.x as f64, - y1: end.y as f64, - style: style::PathStyle::new(None, Some(style::Fill::new(tool_data.primary_color))), - }); - operations.push(Operation::CommitTransaction); + if data.drag_start != data.drag_current { + operations.push(make_operation(data, tool_data)); + operations.push(Operation::CommitTransaction); + } RectangleToolFsmState::Ready } + (state, Event::KeyDown(Key::KeyShift)) => { + data.constrain_to_square = true; + + if state == RectangleToolFsmState::LmbDown { + operations.push(Operation::ClearWorkingFolder); + operations.push(make_operation(data, tool_data)); + } + + self + } + + (state, Event::KeyUp(Key::KeyShift)) => { + data.constrain_to_square = false; + + if state == RectangleToolFsmState::LmbDown { + operations.push(Operation::ClearWorkingFolder); + operations.push(make_operation(data, tool_data)); + } + + self + } + + (state, Event::KeyDown(Key::KeyAlt)) => { + data.center_around_cursor = true; + + if state == RectangleToolFsmState::LmbDown { + operations.push(Operation::ClearWorkingFolder); + operations.push(make_operation(data, tool_data)); + } + + self + } + + (state, Event::KeyUp(Key::KeyAlt)) => { + data.center_around_cursor = false; + + if state == RectangleToolFsmState::LmbDown { + operations.push(Operation::ClearWorkingFolder); + operations.push(make_operation(data, tool_data)); + } + + self + } + _ => self, } } } + +fn make_operation(data: &RectangleToolData, tool_data: &DocumentToolData) -> Operation { + 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; + + let (x0, y0, x1, y1) = if data.constrain_to_square { + let (x_dir, y_dir) = ((x1 - x0).signum(), (y1 - y0).signum()); + let max_dist = f64::max((x1 - x0).abs(), (y1 - y0).abs()); + if data.center_around_cursor { + (x0 - max_dist * x_dir, y0 - max_dist * y_dir, x0 + max_dist * x_dir, y0 + max_dist * y_dir) + } else { + (x0, y0, x0 + max_dist * x_dir, y0 + max_dist * y_dir) + } + } else { + let (x0, y0) = if data.center_around_cursor { (x0 - 2.0 * (x1 - x0), y0 - 2.0 * (y1 - y0)) } else { (x0, y0) }; + (x0, y0, x1, y1) + }; + + Operation::AddRect { + path: vec![], + insert_index: -1, + x0, + y0, + x1, + y1, + style: style::PathStyle::new(None, Some(style::Fill::new(tool_data.primary_color))), + } +} diff --git a/core/editor/src/tools/shape.rs b/core/editor/src/tools/shape.rs index a4759046f..dddfb09ec 100644 --- a/core/editor/src/tools/shape.rs +++ b/core/editor/src/tools/shape.rs @@ -37,6 +37,9 @@ impl Default for ShapeToolFsmState { #[derive(Clone, Debug, Default)] struct ShapeToolData { drag_start: ViewportPosition, + drag_current: ViewportPosition, + constrain_to_square: bool, + center_around_cursor: bool, sides: u8, } @@ -47,6 +50,10 @@ impl Fsm for ShapeToolFsmState { match (self, event) { (ShapeToolFsmState::Ready, Event::LmbDown(mouse_state)) => { data.drag_start = mouse_state.position; + data.drag_current = mouse_state.position; + + data.sides = 6; + operations.push(Operation::MountWorkingFolder { path: vec![] }); ShapeToolFsmState::LmbDown } @@ -57,48 +64,99 @@ impl Fsm for ShapeToolFsmState { ShapeToolFsmState::Ready } (ShapeToolFsmState::LmbDown, Event::MouseMove(mouse_state)) => { + data.drag_current = *mouse_state; operations.push(Operation::ClearWorkingFolder); - let start = data.drag_start; - let end = mouse_state; - operations.push(Operation::AddShape { - path: vec![], - insert_index: -1, - x0: start.x as f64, - y0: start.y as f64, - x1: end.x as f64, - y1: end.y as f64, - sides: 6, - style: style::PathStyle::new(None, Some(style::Fill::new(tool_data.primary_color))), - }); + operations.push(make_operation(data, tool_data)); ShapeToolFsmState::LmbDown } (ShapeToolFsmState::LmbDown, Event::LmbUp(mouse_state)) => { - let r = data.drag_start.distance(&mouse_state.position); - log::info!("Draw Shape with radius: {:.2}", r); - - let start = data.drag_start; - let end = mouse_state.position; - // TODO: Set the sides value and use it for the operation. - // let sides = data.sides; - let sides = 6; + data.drag_current = mouse_state.position; operations.push(Operation::ClearWorkingFolder); - operations.push(Operation::AddShape { - path: vec![], - insert_index: -1, - x0: start.x as f64, - y0: start.y as f64, - x1: end.x as f64, - y1: end.y as f64, - sides, - style: style::PathStyle::new(None, Some(style::Fill::new(tool_data.primary_color))), - }); - operations.push(Operation::CommitTransaction); + if data.drag_start != data.drag_current { + operations.push(make_operation(data, tool_data)); + operations.push(Operation::CommitTransaction); + } ShapeToolFsmState::Ready } + (state, Event::KeyDown(Key::KeyShift)) => { + data.constrain_to_square = true; + + if state == ShapeToolFsmState::LmbDown { + operations.push(Operation::ClearWorkingFolder); + operations.push(make_operation(data, tool_data)); + } + + self + } + + (state, Event::KeyUp(Key::KeyShift)) => { + data.constrain_to_square = false; + + if state == ShapeToolFsmState::LmbDown { + operations.push(Operation::ClearWorkingFolder); + operations.push(make_operation(data, tool_data)); + } + + self + } + + (state, Event::KeyDown(Key::KeyAlt)) => { + data.center_around_cursor = true; + + if state == ShapeToolFsmState::LmbDown { + operations.push(Operation::ClearWorkingFolder); + operations.push(make_operation(data, tool_data)); + } + + self + } + + (state, Event::KeyUp(Key::KeyAlt)) => { + data.center_around_cursor = false; + + if state == ShapeToolFsmState::LmbDown { + operations.push(Operation::ClearWorkingFolder); + operations.push(make_operation(data, tool_data)); + } + + self + } + _ => self, } } } + +fn make_operation(data: &ShapeToolData, tool_data: &DocumentToolData) -> Operation { + 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; + + let (x0, y0, x1, y1) = if data.constrain_to_square { + let (x_dir, y_dir) = ((x1 - x0).signum(), (y1 - y0).signum()); + let max_dist = f64::max((x1 - x0).abs(), (y1 - y0).abs()); + if data.center_around_cursor { + (x0 - max_dist * x_dir, y0 - max_dist * y_dir, x0 + max_dist * x_dir, y0 + max_dist * y_dir) + } else { + (x0, y0, x0 + max_dist * x_dir, y0 + max_dist * y_dir) + } + } else { + let (x0, y0) = if data.center_around_cursor { (x0 - 2.0 * (x1 - x0), y0 - 2.0 * (y1 - y0)) } else { (x0, y0) }; + (x0, y0, x1, y1) + }; + + Operation::AddShape { + path: vec![], + insert_index: -1, + x0, + y0, + x1, + y1, + sides: data.sides, + style: style::PathStyle::new(None, Some(style::Fill::new(tool_data.primary_color))), + } +}