Skip to content

Commit 58157d9

Browse files
otdaviesFlorent Collin
authored and
Florent Collin
committed
Pen tool fixes (GraphiteEditor#563)
Resolves 3 known bugs with the pen tool. * Fixed crash pointed out by @caleb-ad * Fixed issue with final path segment losing handle data * Replace curves with lines when under a drag threshold, improves usability. * Readability improvements, improved comments
1 parent 392af9a commit 58157d9

File tree

2 files changed

+71
-31
lines changed

2 files changed

+71
-31
lines changed

editor/src/consts.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ pub const BOUNDS_ROTATE_THRESHOLD: f64 = 20.;
3838
pub const VECTOR_MANIPULATOR_ANCHOR_MARKER_SIZE: f64 = 5.;
3939
pub const SELECTION_THRESHOLD: f64 = 10.;
4040

41+
// Pen tool
42+
pub const CREATE_CURVE_THRESHOLD: f64 = 5.;
43+
4144
// Line tool
4245
pub const LINE_ROTATE_SNAP_ANGLE: f64 = 15.;
4346

editor/src/viewport_tools/tools/pen_tool.rs

Lines changed: 68 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::consts::CREATE_CURVE_THRESHOLD;
12
use crate::document::DocumentMessageHandler;
23
use crate::frontend::utility_types::MouseCursorIcon;
34
use crate::input::keyboard::{Key, MouseMotion};
@@ -7,6 +8,7 @@ use crate::message_prelude::*;
78
use crate::misc::{HintData, HintGroup, HintInfo, KeysGroup};
89
use crate::viewport_tools::snapping::SnapHandler;
910
use crate::viewport_tools::tool::{DocumentToolData, Fsm, ToolActionHandlerData};
11+
use crate::viewport_tools::vector_editor::constants::ControlPointType;
1012
use crate::viewport_tools::vector_editor::shape_editor::ShapeEditor;
1113
use crate::viewport_tools::vector_editor::vector_shape::VectorShape;
1214

@@ -133,6 +135,7 @@ struct PenToolData {
133135
bez_path: Vec<PathEl>,
134136
snap_handler: SnapHandler,
135137
shape_editor: ShapeEditor,
138+
drag_start_position: DVec2,
136139
}
137140

138141
impl Fsm for PenToolFsmState {
@@ -173,7 +176,7 @@ impl Fsm for PenToolFsmState {
173176
let start_position = transform.inverse().transform_point2(snapped_position);
174177
data.weight = tool_options.line_weight;
175178

176-
// Create the initial shape with a bez_path (only contains a moveto initially)
179+
// Create the initial shape with a `bez_path` (only contains a moveto initially)
177180
if let Some(layer_path) = &data.path {
178181
data.bez_path = start_bez_path(start_position);
179182
responses.push_back(
@@ -193,36 +196,49 @@ impl Fsm for PenToolFsmState {
193196
Drawing
194197
}
195198
(Drawing, DragStart) => {
199+
data.drag_start_position = input.mouse.position;
196200
add_to_curve(data, input, transform, document, responses);
197201
Drawing
198202
}
199203
(Drawing, DragStop) => {
200204
// Deselect everything (this means we are no longer dragging the handle)
201205
data.shape_editor.deselect_all(responses);
202206

207+
// If the drag does not exceed the threshold, then replace the curve with a line
208+
if data.drag_start_position.distance(input.mouse.position) < CREATE_CURVE_THRESHOLD {
209+
// Modify the second to last element (as we have an unplaced element tracing to the cursor as the last element)
210+
let replace_index = data.bez_path.len() - 2;
211+
let line_from_curve = convert_curve_to_line(data.bez_path[replace_index]);
212+
replace_path_element(data, transform, replace_index, line_from_curve, responses);
213+
}
214+
203215
// Reselect the last point
204216
if let Some(last_anchor) = data.shape_editor.select_last_anchor() {
205-
last_anchor.select_point(0, true, responses);
217+
last_anchor.select_point(ControlPointType::Anchor as usize, true, responses);
206218
}
207219

220+
// Move the newly selected points to the cursor
221+
let snapped_position = data.snap_handler.snap_position(responses, input.viewport_bounds.size(), document, input.mouse.position);
222+
data.shape_editor.move_selected_points(snapped_position, false, responses);
223+
208224
Drawing
209225
}
210226
(Drawing, PointerMove) => {
227+
// Move selected points
211228
let snapped_position = data.snap_handler.snap_position(responses, input.viewport_bounds.size(), document, input.mouse.position);
212-
//data.shape_editor.update_shapes(document, responses);
213229
data.shape_editor.move_selected_points(snapped_position, false, responses);
214230

215231
Drawing
216232
}
217233
(Drawing, Confirm) | (Drawing, Abort) => {
218-
// Add a curve to the path
219-
if let Some(layer_path) = &data.path {
220-
remove_curve_from_end(&mut data.bez_path);
221-
responses.push_back(apply_bez_path(layer_path.clone(), data.bez_path.clone(), transform));
222-
}
223-
224234
// Cleanup, we are either canceling or finished drawing
225235
if data.bez_path.len() >= 2 {
236+
// Remove the last segment
237+
remove_from_curve(data);
238+
if let Some(layer_path) = &data.path {
239+
responses.push_back(apply_bez_path(layer_path.clone(), data.bez_path.clone(), transform));
240+
}
241+
226242
responses.push_back(DocumentMessage::DeselectAllLayers.into());
227243
responses.push_back(DocumentMessage::CommitTransaction.into());
228244
} else {
@@ -281,65 +297,86 @@ impl Fsm for PenToolFsmState {
281297
}
282298
}
283299

284-
// Add to the curve and select the second anchor of the last point and the newly added anchor point
300+
/// Add to the curve and select the second anchor of the last point and the newly added anchor point
285301
fn add_to_curve(data: &mut PenToolData, input: &InputPreprocessorMessageHandler, transform: DAffine2, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
286-
// We need to make sure we have the most up-to-date bez_path
287-
// Would like to remove this hack eventually
288-
if !data.shape_editor.shapes_to_modify.is_empty() {
289-
// Hacky way of saving the curve changes
290-
data.bez_path = data.shape_editor.shapes_to_modify[0].bez_path.elements().to_vec();
291-
}
302+
// Refresh data's representation of the path
303+
update_path_representation(data);
292304

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

297309
// Add a curve to the path
298310
if let Some(layer_path) = &data.path {
299-
add_curve_to_end(position, &mut data.bez_path);
311+
// Push curve onto path
312+
let point = Point { x: position.x, y: position.y };
313+
data.bez_path.push(PathEl::CurveTo(point, point, point));
314+
300315
responses.push_back(apply_bez_path(layer_path.clone(), data.bez_path.clone(), transform));
301316

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

305-
// Create a new shape from the updated bez_path
320+
// Create a new `shape` from the updated `bez_path`
306321
let bez_path = data.bez_path.clone().into_iter().collect();
307322
data.curve_shape = VectorShape::new(layer_path.to_vec(), transform, &bez_path, false, responses);
308323
data.shape_editor.set_shapes_to_modify(vec![data.curve_shape.clone()]);
309324

310-
// Select the second to last segment's handle
325+
// Select the second to last `PathEl`'s handle
311326
data.shape_editor.set_shape_selected(0);
312327
let handle_element = data.shape_editor.select_nth_anchor(0, -2);
313-
handle_element.select_point(2, true, responses);
328+
handle_element.select_point(ControlPointType::Handle2 as usize, true, responses);
314329

315-
// Select the last segment's anchor point
330+
// Select the last `PathEl`'s anchor point
316331
if let Some(last_anchor) = data.shape_editor.select_last_anchor() {
317-
last_anchor.select_point(0, true, responses);
332+
last_anchor.select_point(ControlPointType::Anchor as usize, true, responses);
318333
}
319334
data.shape_editor.set_selected_mirror_options(true, true);
320335
}
321336
}
322337

323-
// Create the initial moveto for the bez_path
338+
/// Replace a `PathEl` with another inside of `bez_path` by index
339+
fn replace_path_element(data: &mut PenToolData, transform: DAffine2, replace_index: usize, replacement: PathEl, responses: &mut VecDeque<Message>) {
340+
data.bez_path[replace_index] = replacement;
341+
if let Some(layer_path) = &data.path {
342+
responses.push_back(apply_bez_path(layer_path.clone(), data.bez_path.clone(), transform));
343+
}
344+
}
345+
346+
/// Remove a curve from the end of the `bez_path`
347+
fn remove_from_curve(data: &mut PenToolData) {
348+
// Refresh data's representation of the path
349+
update_path_representation(data);
350+
data.bez_path.pop();
351+
}
352+
353+
/// Create the initial moveto for the `bez_path`
324354
fn start_bez_path(start_position: DVec2) -> Vec<PathEl> {
325355
vec![PathEl::MoveTo(Point {
326356
x: start_position.x,
327357
y: start_position.y,
328358
})]
329359
}
330360

331-
// Add a curve to the bez_path
332-
fn add_curve_to_end(point: DVec2, bez_path: &mut Vec<PathEl>) {
333-
let point = Point { x: point.x, y: point.y };
334-
bez_path.push(PathEl::CurveTo(point, point, point));
361+
/// Convert curve `PathEl` into a line `PathEl`
362+
fn convert_curve_to_line(curve: PathEl) -> PathEl {
363+
match curve {
364+
PathEl::CurveTo(_, _, p) => PathEl::LineTo(p),
365+
_ => PathEl::MoveTo(Point::ZERO),
366+
}
335367
}
336368

337-
// Add a curve to the bez_path
338-
fn remove_curve_from_end(bez_path: &mut Vec<PathEl>) {
339-
bez_path.pop();
369+
/// Update data's version of `bez_path` to match `ShapeEditor`'s version
370+
fn update_path_representation(data: &mut PenToolData) {
371+
// TODO Update ShapeEditor to provide similar functionality
372+
// We need to make sure we have the most up-to-date bez_path
373+
if !data.shape_editor.shapes_to_modify.is_empty() {
374+
// Hacky way of saving the curve changes
375+
data.bez_path = data.shape_editor.shapes_to_modify[0].bez_path.elements().to_vec();
376+
}
340377
}
341378

342-
// Apply the bez_path to the shape in the viewport
379+
/// Apply the `bez_path` to the `shape` in the viewport
343380
fn apply_bez_path(layer_path: Vec<LayerId>, bez_path: Vec<PathEl>, transform: DAffine2) -> Message {
344381
Operation::SetShapePathInViewport {
345382
path: layer_path,

0 commit comments

Comments
 (0)