Implement the Spline Tool (#512)
* Add Spline Tool * Adapt to changes from master * Apply review feedback * Fixes Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
parent
45edeb2a2b
commit
f8e72be492
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
|
|
|
|||
|
|
@ -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::*;
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,4 +11,5 @@ pub mod rectangle;
|
|||
pub mod select;
|
||||
pub mod shape;
|
||||
pub mod shared;
|
||||
pub mod spline;
|
||||
pub mod text;
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<ToolMessage, ToolActionHandlerData<'a>> for Spline {
|
||||
fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
|
||||
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<DVec2>,
|
||||
next_point: DVec2,
|
||||
weight: u32,
|
||||
path: Option<Vec<LayerId>>,
|
||||
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<Message>,
|
||||
) -> 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<Message>) {
|
||||
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<Message>) {
|
||||
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()
|
||||
}
|
||||
|
|
@ -46,7 +46,7 @@
|
|||
<ShelfItemInput icon="VectorPathTool" title="Path Tool (A)" :active="activeTool === 'Path'" :action="() => selectTool('Path')" />
|
||||
<ShelfItemInput icon="VectorPenTool" title="Pen Tool (P)" :active="activeTool === 'Pen'" :action="() => selectTool('Pen')" />
|
||||
<ShelfItemInput icon="VectorFreehandTool" title="Freehand Tool (N)" :active="activeTool === 'Freehand'" :action="() => selectTool('Freehand')" />
|
||||
<ShelfItemInput icon="VectorSplineTool" title="Spline Tool" :active="activeTool === 'Spline'" :action="() => (dialog.comingSoon(), false) && selectTool('Spline')" />
|
||||
<ShelfItemInput icon="VectorSplineTool" title="Spline Tool" :active="activeTool === 'Spline'" :action="() => selectTool('Spline')" />
|
||||
<ShelfItemInput icon="VectorLineTool" title="Line Tool (L)" :active="activeTool === 'Line'" :action="() => selectTool('Line')" />
|
||||
<ShelfItemInput icon="VectorRectangleTool" title="Rectangle Tool (M)" :active="activeTool === 'Rectangle'" :action="() => selectTool('Rectangle')" />
|
||||
<ShelfItemInput icon="VectorEllipseTool" title="Ellipse Tool (E)" :active="activeTool === 'Ellipse'" :action="() => selectTool('Ellipse')" />
|
||||
|
|
|
|||
|
|
@ -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<glam::DVec2> = 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<LayerId>, responses: &mut Vec<DocumentResponse>) {
|
||||
for (id, layer) in folder.layer_ids.iter().zip(folder.layers()) {
|
||||
|
|
|
|||
|
|
@ -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<impl Into<glam::DVec2>>, 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,6 +68,13 @@ pub enum Operation {
|
|||
points: Vec<(f64, f64)>,
|
||||
style: style::PathStyle,
|
||||
},
|
||||
AddSpline {
|
||||
path: Vec<LayerId>,
|
||||
transform: [f64; 6],
|
||||
insert_index: isize,
|
||||
points: Vec<(f64, f64)>,
|
||||
style: style::PathStyle,
|
||||
},
|
||||
AddNgon {
|
||||
path: Vec<LayerId>,
|
||||
insert_index: isize,
|
||||
|
|
|
|||
Loading…
Reference in New Issue