From f8e72be49204392f4b9b0449cb6bb46d654389db Mon Sep 17 00:00:00 2001 From: Paul Kupper <11900073+pkupper@users.noreply.github.com> Date: Thu, 10 Feb 2022 01:22:57 +0100 Subject: [PATCH] Implement the Spline Tool (#512) * Add Spline Tool * Adapt to changes from master * Apply review feedback * Fixes Co-authored-by: Keavon Chambers --- editor/src/consts.rs | 2 + editor/src/input/input_mapper.rs | 7 + editor/src/lib.rs | 1 + editor/src/viewport_tools/tool.rs | 6 +- editor/src/viewport_tools/tool_message.rs | 6 +- editor/src/viewport_tools/tools/ellipse.rs | 4 +- editor/src/viewport_tools/tools/freehand.rs | 8 +- editor/src/viewport_tools/tools/line.rs | 5 +- editor/src/viewport_tools/tools/mod.rs | 1 + editor/src/viewport_tools/tools/pen.rs | 20 +- editor/src/viewport_tools/tools/rectangle.rs | 4 +- editor/src/viewport_tools/tools/shape.rs | 4 +- editor/src/viewport_tools/tools/spline.rs | 272 +++++++++++++++++++ frontend/src/components/panels/Document.vue | 2 +- graphene/src/document.rs | 11 + graphene/src/layers/simple_shape.rs | 78 ++++++ graphene/src/operation.rs | 7 + 17 files changed, 409 insertions(+), 29 deletions(-) create mode 100644 editor/src/viewport_tools/tools/spline.rs diff --git a/editor/src/consts.rs b/editor/src/consts.rs index 36f4a8cc..e3b88147 100644 --- a/editor/src/consts.rs +++ b/editor/src/consts.rs @@ -19,6 +19,8 @@ pub const SNAP_TOLERANCE: f64 = 3.; pub const SNAP_OVERLAY_FADE_DISTANCE: f64 = 20.; pub const SNAP_OVERLAY_UNSNAPPED_OPACITY: f64 = 0.4; +pub const DRAG_THRESHOLD: f64 = 1.; + // Transforming layer pub const ROTATE_SNAP_ANGLE: f64 = 15.; pub const SCALE_SNAP_INTERVAL: f64 = 0.1; diff --git a/editor/src/input/input_mapper.rs b/editor/src/input/input_mapper.rs index 86bb85f2..c4819b8e 100644 --- a/editor/src/input/input_mapper.rs +++ b/editor/src/input/input_mapper.rs @@ -109,6 +109,13 @@ impl Default for Mapping { entry! {action=FreehandMessage::PointerMove, message=InputMapperMessage::PointerMove}, entry! {action=FreehandMessage::DragStart, key_down=Lmb}, entry! {action=FreehandMessage::DragStop, key_up=Lmb}, + // Spline + entry! {action=SplineMessage::PointerMove, message=InputMapperMessage::PointerMove}, + entry! {action=SplineMessage::DragStart, key_down=Lmb}, + entry! {action=SplineMessage::DragStop, key_up=Lmb}, + entry! {action=SplineMessage::Confirm, key_down=Rmb}, + entry! {action=SplineMessage::Confirm, key_down=KeyEscape}, + entry! {action=SplineMessage::Confirm, key_down=KeyEnter}, // Fill entry! {action=FillMessage::LeftMouseDown, key_down=Lmb}, entry! {action=FillMessage::RightMouseDown, key_down=Rmb}, diff --git a/editor/src/lib.rs b/editor/src/lib.rs index 5e8d5317..5af3452c 100644 --- a/editor/src/lib.rs +++ b/editor/src/lib.rs @@ -83,6 +83,7 @@ pub mod message_prelude { pub use crate::viewport_tools::tools::rectangle::{RectangleMessage, RectangleMessageDiscriminant}; pub use crate::viewport_tools::tools::select::{SelectMessage, SelectMessageDiscriminant}; pub use crate::viewport_tools::tools::shape::{ShapeMessage, ShapeMessageDiscriminant}; + pub use crate::viewport_tools::tools::spline::{SplineMessage, SplineMessageDiscriminant}; pub use crate::viewport_tools::tools::text::{TextMessage, TextMessageDiscriminant}; pub use graphite_proc_macros::*; diff --git a/editor/src/viewport_tools/tool.rs b/editor/src/viewport_tools/tool.rs index 2d5ce62c..ca3b446c 100644 --- a/editor/src/viewport_tools/tool.rs +++ b/editor/src/viewport_tools/tool.rs @@ -93,7 +93,7 @@ impl Default for ToolFsmState { Path => path::Path, Pen => pen::Pen, Freehand => freehand::Freehand, - // Spline => spline::Spline, + Spline => spline::Spline, Line => line::Line, Rectangle => rectangle::Rectangle, Ellipse => ellipse::Ellipse, @@ -225,7 +225,7 @@ pub fn standard_tool_message(tool: ToolType, message_type: StandardToolMessageTy ToolType::Path => Some(PathMessage::Abort.into()), ToolType::Pen => Some(PenMessage::Abort.into()), ToolType::Freehand => Some(FreehandMessage::Abort.into()), - // ToolType::Spline => Some(SplineMessage::Abort.into()), + ToolType::Spline => Some(SplineMessage::Abort.into()), ToolType::Line => Some(LineMessage::Abort.into()), ToolType::Rectangle => Some(RectangleMessage::Abort.into()), ToolType::Ellipse => Some(EllipseMessage::Abort.into()), @@ -259,7 +259,7 @@ pub fn message_to_tool_type(message: &ToolMessage) -> ToolType { Path(_) => ToolType::Path, Pen(_) => ToolType::Pen, Freehand(_) => ToolType::Freehand, - // Spline(_) => ToolType::Spline, + Spline(_) => ToolType::Spline, Line(_) => ToolType::Line, Rectangle(_) => ToolType::Rectangle, Ellipse(_) => ToolType::Ellipse, diff --git a/editor/src/viewport_tools/tool_message.rs b/editor/src/viewport_tools/tool_message.rs index 6f79ffdc..a3c89bb8 100644 --- a/editor/src/viewport_tools/tool_message.rs +++ b/editor/src/viewport_tools/tool_message.rs @@ -61,9 +61,9 @@ pub enum ToolMessage { #[remain::unsorted] #[child] Freehand(FreehandMessage), - // #[remain::unsorted] - // #[child] - // Spline(SplineMessage), + #[remain::unsorted] + #[child] + Spline(SplineMessage), #[remain::unsorted] #[child] Line(LineMessage), diff --git a/editor/src/viewport_tools/tools/ellipse.rs b/editor/src/viewport_tools/tools/ellipse.rs index 312e3e99..8f6b61b3 100644 --- a/editor/src/viewport_tools/tools/ellipse.rs +++ b/editor/src/viewport_tools/tools/ellipse.rs @@ -1,4 +1,5 @@ use super::shared::resize::Resize; +use crate::consts::DRAG_THRESHOLD; use crate::document::DocumentMessageHandler; use crate::frontend::utility_types::MouseCursorIcon; use crate::input::keyboard::{Key, MouseMotion}; @@ -134,8 +135,7 @@ impl Fsm for EllipseToolFsmState { state } (Drawing, DragStop) => { - // TODO: introduce comparison threshold when operating with canvas coordinates (https://github.com/GraphiteEditor/Graphite/issues/100) - match shape_data.drag_start == input.mouse.position { + match shape_data.drag_start.distance(input.mouse.position) <= DRAG_THRESHOLD { true => responses.push_back(DocumentMessage::AbortTransaction.into()), false => responses.push_back(DocumentMessage::CommitTransaction.into()), } diff --git a/editor/src/viewport_tools/tools/freehand.rs b/editor/src/viewport_tools/tools/freehand.rs index 2a0c12a7..5c5d1a14 100644 --- a/editor/src/viewport_tools/tools/freehand.rs +++ b/editor/src/viewport_tools/tools/freehand.rs @@ -156,7 +156,7 @@ impl Fsm for FreehandToolFsmState { data.weight = tool_options.line_weight; - responses.push_back(make_operation(data, tool_data)); + responses.push_back(add_polyline(data, tool_data)); Drawing } @@ -168,7 +168,7 @@ impl Fsm for FreehandToolFsmState { } responses.push_back(remove_preview(data)); - responses.push_back(make_operation(data, tool_data)); + responses.push_back(add_polyline(data, tool_data)); Drawing } @@ -176,7 +176,7 @@ impl Fsm for FreehandToolFsmState { if data.points.len() >= 2 { responses.push_back(DocumentMessage::DeselectAllLayers.into()); responses.push_back(remove_preview(data)); - responses.push_back(make_operation(data, tool_data)); + responses.push_back(add_polyline(data, tool_data)); responses.push_back(DocumentMessage::CommitTransaction.into()); } else { responses.push_back(DocumentMessage::AbortTransaction.into()); @@ -217,7 +217,7 @@ fn remove_preview(data: &FreehandToolData) -> Message { Operation::DeleteLayer { path: data.path.clone().unwrap() }.into() } -fn make_operation(data: &FreehandToolData, tool_data: &DocumentToolData) -> Message { +fn add_polyline(data: &FreehandToolData, tool_data: &DocumentToolData) -> Message { let points: Vec<(f64, f64)> = data.points.iter().map(|p| (p.x, p.y)).collect(); Operation::AddPolyline { diff --git a/editor/src/viewport_tools/tools/line.rs b/editor/src/viewport_tools/tools/line.rs index 31325640..5be190fd 100644 --- a/editor/src/viewport_tools/tools/line.rs +++ b/editor/src/viewport_tools/tools/line.rs @@ -1,4 +1,4 @@ -use crate::consts::LINE_ROTATE_SNAP_ANGLE; +use crate::consts::{DRAG_THRESHOLD, LINE_ROTATE_SNAP_ANGLE}; use crate::document::DocumentMessageHandler; use crate::frontend::utility_types::MouseCursorIcon; use crate::input::keyboard::{Key, MouseMotion}; @@ -188,8 +188,7 @@ impl Fsm for LineToolFsmState { data.drag_current = data.snap_handler.snap_position(responses, input.viewport_bounds.size(), document, input.mouse.position); data.snap_handler.cleanup(responses); - // TODO: introduce comparison threshold when operating with canvas coordinates (https://github.com/GraphiteEditor/Graphite/issues/100) - match data.drag_start == input.mouse.position { + match data.drag_start.distance(input.mouse.position) <= DRAG_THRESHOLD { true => responses.push_back(DocumentMessage::AbortTransaction.into()), false => responses.push_back(DocumentMessage::CommitTransaction.into()), } diff --git a/editor/src/viewport_tools/tools/mod.rs b/editor/src/viewport_tools/tools/mod.rs index c6c1334f..2238ff05 100644 --- a/editor/src/viewport_tools/tools/mod.rs +++ b/editor/src/viewport_tools/tools/mod.rs @@ -11,4 +11,5 @@ pub mod rectangle; pub mod select; pub mod shape; pub mod shared; +pub mod spline; pub mod text; diff --git a/editor/src/viewport_tools/tools/pen.rs b/editor/src/viewport_tools/tools/pen.rs index 13456384..96ba1a95 100644 --- a/editor/src/viewport_tools/tools/pen.rs +++ b/editor/src/viewport_tools/tools/pen.rs @@ -1,3 +1,4 @@ +use crate::consts::DRAG_THRESHOLD; use crate::document::DocumentMessageHandler; use crate::frontend::utility_types::MouseCursorIcon; use crate::input::keyboard::{Key, MouseMotion}; @@ -165,7 +166,7 @@ impl Fsm for PenToolFsmState { data.weight = tool_options.line_weight; - responses.push_back(make_operation(data, tool_data, true)); + responses.push_back(add_polyline(data, tool_data, true)); Drawing } @@ -173,14 +174,15 @@ impl Fsm for PenToolFsmState { let snapped_position = data.snap_handler.snap_position(responses, input.viewport_bounds.size(), document, input.mouse.position); let pos = transform.inverse().transform_point2(snapped_position); - // TODO: introduce comparison threshold when operating with canvas coordinates (https://github.com/GraphiteEditor/Graphite/issues/100) - if data.points.last() != Some(&pos) { - data.points.push(pos); - data.next_point = pos; + if let Some(last_pos) = data.points.last() { + if last_pos.distance(pos) > DRAG_THRESHOLD { + data.points.push(pos); + data.next_point = pos; + } } responses.push_back(remove_preview(data)); - responses.push_back(make_operation(data, tool_data, true)); + responses.push_back(add_polyline(data, tool_data, true)); Drawing } @@ -190,7 +192,7 @@ impl Fsm for PenToolFsmState { data.next_point = pos; responses.push_back(remove_preview(data)); - responses.push_back(make_operation(data, tool_data, true)); + responses.push_back(add_polyline(data, tool_data, true)); Drawing } @@ -198,7 +200,7 @@ impl Fsm for PenToolFsmState { if data.points.len() >= 2 { responses.push_back(DocumentMessage::DeselectAllLayers.into()); responses.push_back(remove_preview(data)); - responses.push_back(make_operation(data, tool_data, false)); + responses.push_back(add_polyline(data, tool_data, false)); responses.push_back(DocumentMessage::CommitTransaction.into()); } else { responses.push_back(DocumentMessage::AbortTransaction.into()); @@ -253,7 +255,7 @@ fn remove_preview(data: &PenToolData) -> Message { Operation::DeleteLayer { path: data.path.clone().unwrap() }.into() } -fn make_operation(data: &PenToolData, tool_data: &DocumentToolData, show_preview: bool) -> Message { +fn add_polyline(data: &PenToolData, tool_data: &DocumentToolData, show_preview: bool) -> Message { let mut points: Vec<(f64, f64)> = data.points.iter().map(|p| (p.x, p.y)).collect(); if show_preview { points.push((data.next_point.x, data.next_point.y)) diff --git a/editor/src/viewport_tools/tools/rectangle.rs b/editor/src/viewport_tools/tools/rectangle.rs index 23751b56..9544becf 100644 --- a/editor/src/viewport_tools/tools/rectangle.rs +++ b/editor/src/viewport_tools/tools/rectangle.rs @@ -1,4 +1,5 @@ use super::shared::resize::Resize; +use crate::consts::DRAG_THRESHOLD; use crate::document::DocumentMessageHandler; use crate::frontend::utility_types::MouseCursorIcon; use crate::input::keyboard::{Key, MouseMotion}; @@ -133,8 +134,7 @@ impl Fsm for RectangleToolFsmState { state } (Drawing, DragStop) => { - // TODO: introduce comparison threshold when operating with canvas coordinates (https://github.com/GraphiteEditor/Graphite/issues/100) - match shape_data.drag_start == input.mouse.position { + match shape_data.drag_start.distance(input.mouse.position) <= DRAG_THRESHOLD { true => responses.push_back(DocumentMessage::AbortTransaction.into()), false => responses.push_back(DocumentMessage::CommitTransaction.into()), } diff --git a/editor/src/viewport_tools/tools/shape.rs b/editor/src/viewport_tools/tools/shape.rs index b6c01680..1d413e66 100644 --- a/editor/src/viewport_tools/tools/shape.rs +++ b/editor/src/viewport_tools/tools/shape.rs @@ -1,4 +1,5 @@ use super::shared::resize::Resize; +use crate::consts::DRAG_THRESHOLD; use crate::document::DocumentMessageHandler; use crate::frontend::utility_types::MouseCursorIcon; use crate::input::keyboard::{Key, MouseMotion}; @@ -176,8 +177,7 @@ impl Fsm for ShapeToolFsmState { state } (Drawing, DragStop) => { - // TODO: introduce comparison threshold when operating with canvas coordinates (https://github.com/GraphiteEditor/Graphite/issues/100) - match shape_data.drag_start == input.mouse.position { + match shape_data.drag_start.distance(input.mouse.position) <= DRAG_THRESHOLD { true => responses.push_back(DocumentMessage::AbortTransaction.into()), false => responses.push_back(DocumentMessage::CommitTransaction.into()), } diff --git a/editor/src/viewport_tools/tools/spline.rs b/editor/src/viewport_tools/tools/spline.rs new file mode 100644 index 00000000..6b82abcd --- /dev/null +++ b/editor/src/viewport_tools/tools/spline.rs @@ -0,0 +1,272 @@ +use crate::consts::DRAG_THRESHOLD; +use crate::document::DocumentMessageHandler; +use crate::frontend::utility_types::MouseCursorIcon; +use crate::input::keyboard::{Key, MouseMotion}; +use crate::input::InputPreprocessorMessageHandler; +use crate::layout::widgets::{LayoutRow, NumberInput, PropertyHolder, Widget, WidgetCallback, WidgetHolder, WidgetLayout}; +use crate::message_prelude::*; +use crate::misc::{HintData, HintGroup, HintInfo, KeysGroup}; +use crate::viewport_tools::snapping::SnapHandler; +use crate::viewport_tools::tool::{DocumentToolData, Fsm, ToolActionHandlerData}; + +use graphene::layers::style; +use graphene::Operation; + +use glam::{DAffine2, DVec2}; +use serde::{Deserialize, Serialize}; + +#[derive(Default)] +pub struct Spline { + fsm_state: SplineToolFsmState, + data: SplineToolData, + options: SplineOptions, +} + +pub struct SplineOptions { + line_weight: u32, +} + +impl Default for SplineOptions { + fn default() -> Self { + Self { line_weight: 5 } + } +} + +#[remain::sorted] +#[impl_message(Message, ToolMessage, Spline)] +#[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)] +pub enum SplineMessage { + // Standard messages + #[remain::unsorted] + Abort, + + // Tool-specific messages + Confirm, + DragStart, + DragStop, + PointerMove, + Undo, + UpdateOptions(SplineOptionsUpdate), +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum SplineToolFsmState { + Ready, + Drawing, +} + +#[remain::sorted] +#[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)] +pub enum SplineOptionsUpdate { + LineWeight(u32), +} + +impl PropertyHolder for Spline { + fn properties(&self) -> WidgetLayout { + WidgetLayout::new(vec![LayoutRow::Row { + name: "".into(), + widgets: vec![WidgetHolder::new(Widget::NumberInput(NumberInput { + unit: " px".into(), + label: "Weight".into(), + value: self.options.line_weight as f64, + is_integer: true, + min: Some(0.), + on_update: WidgetCallback::new(|number_input| SplineMessage::UpdateOptions(SplineOptionsUpdate::LineWeight(number_input.value as u32)).into()), + ..NumberInput::default() + }))], + }]) + } +} + +impl<'a> MessageHandler> for Spline { + fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque) { + if action == ToolMessage::UpdateHints { + self.fsm_state.update_hints(responses); + return; + } + + if action == ToolMessage::UpdateCursor { + self.fsm_state.update_cursor(responses); + return; + } + + if let ToolMessage::Spline(SplineMessage::UpdateOptions(action)) = action { + match action { + SplineOptionsUpdate::LineWeight(line_weight) => self.options.line_weight = line_weight, + } + return; + } + + let new_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, &self.options, data.2, responses); + + if self.fsm_state != new_state { + self.fsm_state = new_state; + self.fsm_state.update_hints(responses); + self.fsm_state.update_cursor(responses); + } + } + + fn actions(&self) -> ActionList { + use SplineToolFsmState::*; + + match self.fsm_state { + Ready => actions!(SplineMessageDiscriminant; Undo, DragStart, DragStop, Confirm, Abort), + Drawing => actions!(SplineMessageDiscriminant; DragStop, PointerMove, Confirm, Abort), + } + } +} + +impl Default for SplineToolFsmState { + fn default() -> Self { + SplineToolFsmState::Ready + } +} +#[derive(Clone, Debug, Default)] +struct SplineToolData { + points: Vec, + next_point: DVec2, + weight: u32, + path: Option>, + snap_handler: SnapHandler, +} + +impl Fsm for SplineToolFsmState { + type ToolData = SplineToolData; + type ToolOptions = SplineOptions; + + fn transition( + self, + event: ToolMessage, + document: &DocumentMessageHandler, + tool_data: &DocumentToolData, + data: &mut Self::ToolData, + tool_options: &Self::ToolOptions, + input: &InputPreprocessorMessageHandler, + responses: &mut VecDeque, + ) -> Self { + use SplineMessage::*; + use SplineToolFsmState::*; + + let transform = document.graphene_document.root.transform; + + if let ToolMessage::Spline(event) = event { + match (self, event) { + (Ready, DragStart) => { + responses.push_back(DocumentMessage::StartTransaction.into()); + responses.push_back(DocumentMessage::DeselectAllLayers.into()); + data.path = Some(document.get_path_for_new_layer()); + + data.snap_handler.start_snap(document, document.bounding_boxes(None, None), true, true); + let snapped_position = data.snap_handler.snap_position(responses, input.viewport_bounds.size(), document, input.mouse.position); + + let pos = transform.inverse().transform_point2(snapped_position); + + data.points.push(pos); + data.next_point = pos; + + data.weight = tool_options.line_weight; + + responses.push_back(add_spline(data, tool_data, true)); + + Drawing + } + (Drawing, DragStop) => { + let snapped_position = data.snap_handler.snap_position(responses, input.viewport_bounds.size(), document, input.mouse.position); + let pos = transform.inverse().transform_point2(snapped_position); + + if let Some(last_pos) = data.points.last() { + if last_pos.distance(pos) > DRAG_THRESHOLD { + data.points.push(pos); + data.next_point = pos; + } + } + + responses.push_back(remove_preview(data)); + responses.push_back(add_spline(data, tool_data, true)); + + Drawing + } + (Drawing, PointerMove) => { + let snapped_position = data.snap_handler.snap_position(responses, input.viewport_bounds.size(), document, input.mouse.position); + let pos = transform.inverse().transform_point2(snapped_position); + data.next_point = pos; + + responses.push_back(remove_preview(data)); + responses.push_back(add_spline(data, tool_data, true)); + + Drawing + } + (Drawing, Confirm) | (Drawing, Abort) => { + if data.points.len() >= 2 { + responses.push_back(DocumentMessage::DeselectAllLayers.into()); + responses.push_back(remove_preview(data)); + responses.push_back(add_spline(data, tool_data, false)); + responses.push_back(DocumentMessage::CommitTransaction.into()); + } else { + responses.push_back(DocumentMessage::AbortTransaction.into()); + } + + data.path = None; + data.points.clear(); + data.snap_handler.cleanup(responses); + + Ready + } + _ => self, + } + } else { + self + } + } + + fn update_hints(&self, responses: &mut VecDeque) { + let hint_data = match self { + SplineToolFsmState::Ready => HintData(vec![HintGroup(vec![HintInfo { + key_groups: vec![], + mouse: Some(MouseMotion::Lmb), + label: String::from("Draw Spline"), + plus: false, + }])]), + SplineToolFsmState::Drawing => HintData(vec![ + HintGroup(vec![HintInfo { + key_groups: vec![], + mouse: Some(MouseMotion::Lmb), + label: String::from("Extend Spline"), + plus: false, + }]), + HintGroup(vec![HintInfo { + key_groups: vec![KeysGroup(vec![Key::KeyEnter])], + mouse: None, + label: String::from("End Spline"), + plus: false, + }]), + ]), + }; + + responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into()); + } + + fn update_cursor(&self, responses: &mut VecDeque) { + responses.push_back(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }.into()); + } +} + +fn remove_preview(data: &SplineToolData) -> Message { + Operation::DeleteLayer { path: data.path.clone().unwrap() }.into() +} + +fn add_spline(data: &SplineToolData, tool_data: &DocumentToolData, show_preview: bool) -> Message { + let mut points: Vec<(f64, f64)> = data.points.iter().map(|p| (p.x, p.y)).collect(); + if show_preview { + points.push((data.next_point.x, data.next_point.y)) + } + + Operation::AddSpline { + path: data.path.clone().unwrap(), + insert_index: -1, + transform: DAffine2::IDENTITY.to_cols_array(), + points, + style: style::PathStyle::new(Some(style::Stroke::new(tool_data.primary_color, data.weight as f32)), None), + } + .into() +} diff --git a/frontend/src/components/panels/Document.vue b/frontend/src/components/panels/Document.vue index e2e7cb14..c4ef737b 100644 --- a/frontend/src/components/panels/Document.vue +++ b/frontend/src/components/panels/Document.vue @@ -46,7 +46,7 @@ - + diff --git a/graphene/src/document.rs b/graphene/src/document.rs index afde8f9b..0e418030 100644 --- a/graphene/src/document.rs +++ b/graphene/src/document.rs @@ -519,6 +519,17 @@ impl Document { self.set_layer(path, Layer::new(LayerDataType::Shape(Shape::poly_line(points, *style)), *transform), *insert_index)?; Some([vec![DocumentChanged, CreatedLayer { path: path.clone() }], update_thumbnails_upstream(path)].concat()) } + Operation::AddSpline { + path, + insert_index, + points, + transform, + style, + } => { + let points: Vec = points.iter().map(|&it| it.into()).collect(); + self.set_layer(path, Layer::new(LayerDataType::Shape(Shape::spline(points, *style)), *transform), *insert_index)?; + Some([vec![DocumentChanged, CreatedLayer { path: path.clone() }], update_thumbnails_upstream(path)].concat()) + } Operation::DeleteLayer { path } => { fn aggregate_deletions(folder: &Folder, path: &mut Vec, responses: &mut Vec) { for (id, layer) in folder.layer_ids.iter().zip(folder.layers()) { diff --git a/graphene/src/layers/simple_shape.rs b/graphene/src/layers/simple_shape.rs index 1889e375..0d3e52be 100644 --- a/graphene/src/layers/simple_shape.rs +++ b/graphene/src/layers/simple_shape.rs @@ -155,4 +155,82 @@ impl Shape { closed: false, } } + + /// Creates a smooth bezier spline that passes through all given points. + /// The algorithm used in this implementation is described here: https://www.particleincell.com/2012/bezier-splines/ + pub fn spline(points: Vec>, style: PathStyle) -> Self { + let mut path = kurbo::BezPath::new(); + + // Creating a bezier spline is only necessary for 3 or more points. + // For 2 given points a line segment is created instead. + if points.len() > 2 { + let points: Vec<_> = points.into_iter().map(|v| v.into()).map(|v: DVec2| kurbo::Vec2 { x: v.x, y: v.y }).collect(); + + // Number of bezier segments + let n = points.len() - 1; + + // Control points for each bezier segment + let mut p1 = vec![kurbo::Vec2::ZERO; n]; + let mut p2 = vec![kurbo::Vec2::ZERO; n]; + + // Tri-diagonal matrix coefficients a, b and c (see https://en.wikipedia.org/wiki/Tridiagonal_matrix_algorithm) + let mut a = vec![1.0; n]; + a[0] = 0.0; + a[n - 1] = 2.0; + + let mut b = vec![4.0; n]; + b[0] = 2.0; + b[n - 1] = 7.0; + + let mut c = vec![1.0; n]; + c[n - 1] = 0.0; + + let mut r: Vec<_> = (0..n).map(|i| 4.0 * points[i] + 2.0 * points[i + 1]).collect(); + r[0] = points[0] + (2.0 * points[1]); + r[n - 1] = 8.0 * points[n - 1] + points[n]; + + // Solve with Thomas algorithm (see https://en.wikipedia.org/wiki/Tridiagonal_matrix_algorithm) + for i in 1..n { + let m = a[i] / b[i - 1]; + // TODO: Fix Clippy warning which makes the borrow checker angry + b[i] = b[i] - m * c[i - 1]; + r[i] = r[i] - m * r[i - 1]; + } + + // Determine first control point for each segment + p1[n - 1] = r[n - 1] / b[n - 1]; + for i in (0..n - 1).rev() { + p1[i] = (r[i] - c[i] * p1[i + 1]) / b[i]; + } + + // Determine second control point per segment from first + for i in 0..n - 1 { + p2[i] = 2.0 * points[i + 1] - p1[i + 1]; + } + p2[n - 1] = 0.5 * (points[n] + p1[n - 1]); + + // Create bezier path from given points and computed control points + points.into_iter().enumerate().for_each(|(i, p)| { + if i == 0 { + path.move_to(p.to_point()) + } else { + path.curve_to(p1[i - 1].to_point(), p2[i - 1].to_point(), p.to_point()) + } + }); + } else { + points + .into_iter() + .map(|v| v.into()) + .map(|v: DVec2| kurbo::Point { x: v.x, y: v.y }) + .enumerate() + .for_each(|(i, p)| if i == 0 { path.move_to(p) } else { path.line_to(p) }); + } + + Self { + path, + style, + render_index: 0, + closed: false, + } + } } diff --git a/graphene/src/operation.rs b/graphene/src/operation.rs index 68fcac0c..fcdf9fa3 100644 --- a/graphene/src/operation.rs +++ b/graphene/src/operation.rs @@ -68,6 +68,13 @@ pub enum Operation { points: Vec<(f64, f64)>, style: style::PathStyle, }, + AddSpline { + path: Vec, + transform: [f64; 6], + insert_index: isize, + points: Vec<(f64, f64)>, + style: style::PathStyle, + }, AddNgon { path: Vec, insert_index: isize,