Skip to content

Add Pen Tool #536

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Feb 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion editor/src/viewport_tools/tool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ pub fn standard_tool_message(tool: ToolType, message_type: StandardToolMessageTy
ToolType::BlurSharpen => None, // Some(BlurSharpenMessage::DocumentIsDirty.into()),
ToolType::Relight => None, // Some(RelightMessage::DocumentIsDirty.into()),
ToolType::Path => Some(PathMessage::DocumentIsDirty.into()),
ToolType::Pen => None, // Some(PenMessage::DocumentIsDirty.into()),
ToolType::Pen => Some(PenMessage::DocumentIsDirty.into()),
ToolType::Freehand => None, // Some(FreehandMessage::DocumentIsDirty.into()),
ToolType::Spline => None, // Some(SplineMessage::DocumentIsDirty.into()),
ToolType::Line => None, // Some(LineMessage::DocumentIsDirty.into()),
Expand Down
5 changes: 4 additions & 1 deletion editor/src/viewport_tools/tools/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ impl Default for PathToolFsmState {
struct PathToolData {
shape_editor: ShapeEditor,
snap_handler: SnapHandler,

drag_start_pos: DVec2,
alt_debounce: bool,
shift_debounce: bool,
}
Expand Down Expand Up @@ -151,6 +153,7 @@ impl Fsm for PathToolFsmState {
.map(|point| point.position)
.collect();
data.snap_handler.add_snap_points(document, snap_points);
data.drag_start_pos = input.mouse.position;
Dragging
}
// We didn't find a point nearby, so consider selecting the nearest shape instead
Expand Down Expand Up @@ -207,7 +210,7 @@ impl Fsm for PathToolFsmState {

// Move the selected points by the mouse position
let snapped_position = data.snap_handler.snap_position(responses, input.viewport_bounds.size(), document, input.mouse.position);
data.shape_editor.move_selected_points(snapped_position, responses);
data.shape_editor.move_selected_points(snapped_position - data.drag_start_pos, true, responses);
Dragging
}
// Mouse up
Expand Down
164 changes: 121 additions & 43 deletions editor/src/viewport_tools/tools/pen.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
use crate::consts::DRAG_THRESHOLD;
use crate::document::DocumentMessageHandler;
use crate::frontend::utility_types::MouseCursorIcon;
use crate::input::keyboard::{Key, MouseMotion};
Expand All @@ -8,11 +7,14 @@ 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 crate::viewport_tools::vector_editor::shape_editor::ShapeEditor;
use crate::viewport_tools::vector_editor::vector_shape::VectorShape;

use graphene::layers::style;
use graphene::Operation;

use glam::{DAffine2, DVec2};
use kurbo::{PathEl, Point};
use serde::{Deserialize, Serialize};

#[derive(Default)]
Expand All @@ -38,6 +40,8 @@ impl Default for PenOptions {
pub enum PenMessage {
// Standard messages
#[remain::unsorted]
DocumentIsDirty,
#[remain::unsorted]
Abort,

// Tool-specific messages
Expand Down Expand Up @@ -111,7 +115,7 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Pen {

match self.fsm_state {
Ready => actions!(PenMessageDiscriminant; Undo, DragStart, DragStop, Confirm, Abort),
Drawing => actions!(PenMessageDiscriminant; DragStop, PointerMove, Confirm, Abort),
Drawing => actions!(PenMessageDiscriminant; DragStart, DragStop, PointerMove, Confirm, Abort),
}
}
}
Expand All @@ -123,11 +127,12 @@ impl Default for PenToolFsmState {
}
#[derive(Clone, Debug, Default)]
struct PenToolData {
points: Vec<DVec2>,
next_point: DVec2,
weight: u32,
path: Option<Vec<LayerId>>,
curve_shape: VectorShape,
bez_path: Vec<PathEl>,
snap_handler: SnapHandler,
shape_editor: ShapeEditor,
}

impl Fsm for PenToolFsmState {
Expand All @@ -151,67 +156,92 @@ impl Fsm for PenToolFsmState {

if let ToolMessage::Pen(event) = event {
match (self, event) {
(_, DocumentIsDirty) => {
data.shape_editor.update_shapes(document, responses);
self
}
(Ready, DragStart) => {
responses.push_back(DocumentMessage::StartTransaction.into());
responses.push_back(DocumentMessage::DeselectAllLayers.into());
data.path = Some(document.get_path_for_new_layer());

// Create a new layer and prep snap system
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;

// Get the position and set properties
let start_position = transform.inverse().transform_point2(snapped_position);
data.weight = tool_options.line_weight;

responses.push_back(add_polyline(data, tool_data, true));
// Create the initial shape with a bez_path (only contains a moveto initially)
if let Some(layer_path) = &data.path {
data.bez_path = start_bez_path(start_position);
responses.push_back(
Operation::AddShape {
path: layer_path.clone(),
transform: transform.to_cols_array(),
insert_index: -1,
bez_path: data.bez_path.clone().into_iter().collect(),
style: style::PathStyle::new(Some(style::Stroke::new(tool_data.primary_color, data.weight as f32)), None),
closed: false,
}
.into(),
);
}

add_to_curve(data, input, transform, document, responses);
Drawing
}
(Drawing, DragStart) => {
add_to_curve(data, input, transform, document, responses);
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);
// Deselect everything (this means we are no longer dragging the handle)
data.shape_editor.deselect_all(responses);

if let Some(last_pos) = data.points.last() {
if last_pos.distance(pos) > DRAG_THRESHOLD {
data.points.push(pos);
data.next_point = pos;
}
// Reselect the last point
if let Some(last_anchor) = data.shape_editor.select_last_anchor() {
last_anchor.select_point(0, true, responses);
}

responses.push_back(remove_preview(data));
responses.push_back(add_polyline(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_polyline(data, tool_data, true));
//data.shape_editor.update_shapes(document, responses);
data.shape_editor.move_selected_points(snapped_position, false, responses);

Drawing
}
(Drawing, Confirm) | (Drawing, Abort) => {
if data.points.len() >= 2 {
// Add a curve to the path
if let Some(layer_path) = &data.path {
remove_curve_from_end(&mut data.bez_path);
responses.push_back(apply_bez_path(layer_path.clone(), data.bez_path.clone(), transform));
}

// Cleanup, we are either canceling or finished drawing
if data.bez_path.len() >= 2 {
responses.push_back(DocumentMessage::DeselectAllLayers.into());
responses.push_back(remove_preview(data));
responses.push_back(add_polyline(data, tool_data, false));
responses.push_back(DocumentMessage::CommitTransaction.into());
} else {
responses.push_back(DocumentMessage::AbortTransaction.into());
}

data.shape_editor.remove_overlays(responses);
data.shape_editor.clear_shapes_to_modify();

data.path = None;
data.points.clear();
data.snap_handler.cleanup(responses);

Ready
}
(_, Abort) => {
data.shape_editor.remove_overlays(responses);
data.shape_editor.clear_shapes_to_modify();
Ready
}
_ => self,
}
} else {
Expand Down Expand Up @@ -251,22 +281,70 @@ impl Fsm for PenToolFsmState {
}
}

fn remove_preview(data: &PenToolData) -> Message {
Operation::DeleteLayer { path: data.path.clone().unwrap() }.into()
}
// Add to the curve and select the second anchor of the last point and the newly added anchor point
fn add_to_curve(data: &mut PenToolData, input: &InputPreprocessorMessageHandler, transform: DAffine2, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
// We need to make sure we have the most up-to-date bez_path
// Would like to remove this hack eventually
if !data.shape_editor.shapes_to_modify.is_empty() {
// Hacky way of saving the curve changes
data.bez_path = data.shape_editor.shapes_to_modify[0].bez_path.elements().to_vec();
}

// Setup our position params
let snapped_position = data.snap_handler.snap_position(responses, input.viewport_bounds.size(), document, input.mouse.position);
let position = transform.inverse().transform_point2(snapped_position);

// Add a curve to the path
if let Some(layer_path) = &data.path {
add_curve_to_end(position, &mut data.bez_path);
responses.push_back(apply_bez_path(layer_path.clone(), data.bez_path.clone(), transform));

// Clear previous overlays
data.shape_editor.remove_overlays(responses);

// Create a new shape from the updated bez_path
let bez_path = data.bez_path.clone().into_iter().collect();
data.curve_shape = VectorShape::new(layer_path.to_vec(), transform, &bez_path, false, responses);
data.shape_editor.set_shapes_to_modify(vec![data.curve_shape.clone()]);

// Select the second to last segment's handle
data.shape_editor.set_shape_selected(0);
let handle_element = data.shape_editor.select_nth_anchor(0, -2);
handle_element.select_point(2, true, responses);

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))
// Select the last segment's anchor point
if let Some(last_anchor) = data.shape_editor.select_last_anchor() {
last_anchor.select_point(0, true, responses);
}
data.shape_editor.set_selected_mirror_options(true, true);
}
}

// Create the initial moveto for the bez_path
fn start_bez_path(start_position: DVec2) -> Vec<PathEl> {
vec![PathEl::MoveTo(Point {
x: start_position.x,
y: start_position.y,
})]
}

// Add a curve to the bez_path
fn add_curve_to_end(point: DVec2, bez_path: &mut Vec<PathEl>) {
let point = Point { x: point.x, y: point.y };
bez_path.push(PathEl::CurveTo(point, point, point));
}

// Add a curve to the bez_path
fn remove_curve_from_end(bez_path: &mut Vec<PathEl>) {
bez_path.pop();
}

Operation::AddPolyline {
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),
// Apply the bez_path to the shape in the viewport
fn apply_bez_path(layer_path: Vec<LayerId>, bez_path: Vec<PathEl>, transform: DAffine2) -> Message {
Operation::SetShapePathInViewport {
path: layer_path,
bez_path: bez_path.into_iter().collect(),
transform: transform.to_cols_array(),
}
.into()
}
Loading