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:
Paul Kupper 2022-02-10 01:22:57 +01:00 committed by Keavon Chambers
parent 45edeb2a2b
commit f8e72be492
17 changed files with 409 additions and 29 deletions

View File

@ -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;

View File

@ -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},

View File

@ -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::*;

View File

@ -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,

View File

@ -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),

View File

@ -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()),
}

View File

@ -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 {

View File

@ -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()),
}

View File

@ -11,4 +11,5 @@ pub mod rectangle;
pub mod select;
pub mod shape;
pub mod shared;
pub mod spline;
pub mod text;

View File

@ -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))

View File

@ -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()),
}

View File

@ -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()),
}

View File

@ -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()
}

View File

@ -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')" />

View File

@ -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()) {

View File

@ -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,
}
}
}

View File

@ -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,