Break handle of path being drawn by placing anchor on previous anchor with Pen tool (#814)

* Break path by placing anchor on previous one

* Fix offset on click without dragging

* Fix bug where ids reassigned from saved document

* Fix stuck overlay

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
0HyperCube 2022-10-24 23:11:04 +01:00 committed by Keavon Chambers
parent 0553cc5100
commit cee1add3a4
6 changed files with 95 additions and 17 deletions

View file

@ -1 +1 @@
{"graphene_document":{"root":{"visible":true,"name":null,"data":{"Folder":{"next_assignment_id":10689566813179949075,"layer_ids":[10689566813179949074],"layers":[{"visible":true,"name":"Folder 1","data":{"Folder":{"next_assignment_id":17222868332373661780,"layer_ids":[17222868332373661779],"layers":[{"visible":true,"name":"Shape 1","data":{"Shape":{"shape":{"elements":[{"points":[{"position":[0.5,1.0],"manipulator_type":"Anchor"},{"position":[0.7761415,1.0],"manipulator_type":"InHandle"},{"position":[0.22385850000000002,1.0],"manipulator_type":"OutHandle"}]},{"points":[{"position":[0.0,0.5],"manipulator_type":"Anchor"},{"position":[0.0,0.7761415],"manipulator_type":"InHandle"},{"position":[0.0,0.22385850000000002],"manipulator_type":"OutHandle"}]},{"points":[{"position":[0.5,0.0],"manipulator_type":"Anchor"},{"position":[0.22385850000000002,0.0],"manipulator_type":"InHandle"},{"position":[0.7761415,0.0],"manipulator_type":"OutHandle"}]},{"points":[{"position":[1.0,0.5],"manipulator_type":"Anchor"},{"position":[1.0,0.22385850000000002],"manipulator_type":"InHandle"},{"position":[1.0,0.7761415],"manipulator_type":"OutHandle"}]},{"points":[null,null,null]}],"element_ids":[1,2,3,4,5]},"style":{"stroke":null,"fill":{"Solid":{"red":0.0,"green":0.0,"blue":0.0,"alpha":1.0}}},"render_index":1}},"transform":{"matrix2":[379.0,0.0,-0.0,239.0],"translation":[-479.3046875,-99.5]},"pivot":[0.5,0.5],"blend_mode":"Normal","opacity":1.0}]}},"transform":{"matrix2":[1.0,0.0,0.0,1.0],"translation":[0.0,0.0]},"pivot":[0.5,0.5],"blend_mode":"Normal","opacity":1.0}]}},"transform":{"matrix2":[1.0,0.0,0.0,1.0],"translation":[1060.3046875,373.5]},"pivot":[0.5,0.5],"blend_mode":"Normal","opacity":1.0}},"saved_document_identifier":6520881531418194372,"name":"Untitled Document","version":"0.0.13","document_mode":"DesignMode","view_mode":"Normal","snapping_enabled":true,"overlays_visible":true,"layer_metadata":[[[],{"selected":false,"expanded":true}],[[10689566813179949074],{"selected":false,"expanded":true}],[[10689566813179949074,17222868332373661779],{"selected":true,"expanded":false}]],"layer_range_selection_reference":[10689566813179949074,17222868332373661779],"navigation_handler":{"pan":[0.0,0.0],"panning":false,"snap_tilt":false,"snap_tilt_released":false,"tilt":0.0,"tilting":false,"zoom":1.0,"zooming":false,"snap_zoom":false,"mouse_position":[0.0,0.0]},"artboard_message_handler":{"artboards_graphene_document":{"root":{"visible":true,"name":null,"data":{"Folder":{"next_assignment_id":0,"layer_ids":[],"layers":[]}},"transform":{"matrix2":[1.0,0.0,0.0,1.0],"translation":[1060.3046875,373.5]},"pivot":[0.5,0.5],"blend_mode":"Normal","opacity":1.0}},"artboard_ids":[]},"properties_panel_message_handler":{"active_selection":[[10689566813179949074,17222868332373661779],"Artwork"]}}
{"graphene_document":{"root":{"visible":true,"name":null,"data":{"Folder":{"next_assignment_id":10689566813179949075,"layer_ids":[10689566813179949074],"layers":[{"visible":true,"name":"Folder 1","data":{"Folder":{"next_assignment_id":17222868332373661780,"layer_ids":[17222868332373661779],"layers":[{"visible":true,"name":"Shape 1","data":{"Shape":{"shape":{"elements":[{"points":[{"position":[0.5,1.0],"manipulator_type":"Anchor"},{"position":[0.7761415,1.0],"manipulator_type":"InHandle"},{"position":[0.22385850000000002,1.0],"manipulator_type":"OutHandle"}]},{"points":[{"position":[0.0,0.5],"manipulator_type":"Anchor"},{"position":[0.0,0.7761415],"manipulator_type":"InHandle"},{"position":[0.0,0.22385850000000002],"manipulator_type":"OutHandle"}]},{"points":[{"position":[0.5,0.0],"manipulator_type":"Anchor"},{"position":[0.22385850000000002,0.0],"manipulator_type":"InHandle"},{"position":[0.7761415,0.0],"manipulator_type":"OutHandle"}]},{"points":[{"position":[1.0,0.5],"manipulator_type":"Anchor"},{"position":[1.0,0.22385850000000002],"manipulator_type":"InHandle"},{"position":[1.0,0.7761415],"manipulator_type":"OutHandle"}]},{"points":[null,null,null]}],"element_ids":[1,2,3,4,5],"next_id":6},"style":{"stroke":null,"fill":{"Solid":{"red":0.0,"green":0.0,"blue":0.0,"alpha":1.0}}},"render_index":1}},"transform":{"matrix2":[379.0,0.0,-0.0,239.0],"translation":[-479.3046875,-99.5]},"pivot":[0.5,0.5],"blend_mode":"Normal","opacity":1.0}]}},"transform":{"matrix2":[1.0,0.0,0.0,1.0],"translation":[0.0,0.0]},"pivot":[0.5,0.5],"blend_mode":"Normal","opacity":1.0}]}},"transform":{"matrix2":[1.0,0.0,0.0,1.0],"translation":[1060.3046875,373.5]},"pivot":[0.5,0.5],"blend_mode":"Normal","opacity":1.0}},"saved_document_identifier":6520881531418194372,"name":"Untitled Document","version":"0.0.14","document_mode":"DesignMode","view_mode":"Normal","snapping_enabled":true,"overlays_visible":true,"layer_metadata":[[[],{"selected":false,"expanded":true}],[[10689566813179949074],{"selected":false,"expanded":true}],[[10689566813179949074,17222868332373661779],{"selected":true,"expanded":false}]],"layer_range_selection_reference":[10689566813179949074,17222868332373661779],"navigation_handler":{"pan":[0.0,0.0],"panning":false,"snap_tilt":false,"snap_tilt_released":false,"tilt":0.0,"tilting":false,"zoom":1.0,"zooming":false,"snap_zoom":false,"mouse_position":[0.0,0.0]},"artboard_message_handler":{"artboards_graphene_document":{"root":{"visible":true,"name":null,"data":{"Folder":{"next_assignment_id":0,"layer_ids":[],"layers":[]}},"transform":{"matrix2":[1.0,0.0,0.0,1.0],"translation":[1060.3046875,373.5]},"pivot":[0.5,0.5],"blend_mode":"Normal","opacity":1.0}},"artboard_ids":[]},"properties_panel_message_handler":{"active_selection":[[10689566813179949074,17222868332373661779],"Artwork"]}}

View file

@ -74,7 +74,7 @@ pub const DEFAULT_FONT_FAMILY: &str = "Merriweather";
pub const DEFAULT_FONT_STYLE: &str = "Normal (400)";
// Document
pub const GRAPHITE_DOCUMENT_VERSION: &str = "0.0.13"; // Remember to save a simple document and replace the test file `graphite-test-document.graphite`
pub const GRAPHITE_DOCUMENT_VERSION: &str = "0.0.14"; // Remember to save a simple document and replace the test file `graphite-test-document.graphite`
pub const DEFAULT_DOCUMENT_NAME: &str = "Untitled Document";
pub const FILE_SAVE_SUFFIX: &str = ".graphite";

View file

@ -31,7 +31,7 @@ pub struct ShapeEditor {
// TODO Consider keeping a list of selected manipulators to minimize traversals of the layers
impl ShapeEditor {
/// Select the first point within the selection threshold.
/// Returns the points if found, None otherwise.
/// Returns a tuple of the points if found and the offset, or None otherwise.
pub fn select_point(
&self,
document: &Document,
@ -39,7 +39,7 @@ impl ShapeEditor {
select_threshold: f64,
add_to_selection: bool,
responses: &mut VecDeque<Message>,
) -> Option<Vec<(&[LayerId], u64, ManipulatorType)>> {
) -> Option<(Vec<(&[LayerId], u64, ManipulatorType)>, DVec2)> {
if self.selected_layers.is_empty() {
return None;
}
@ -98,12 +98,15 @@ impl ShapeEditor {
}
.into(),
);
// Snap the selected point to the cursor
if let Ok(viewspace) = document.generate_transform_relative_to_viewport(shape_layer_path) {
self.move_selected_points(mouse_position - viewspace.transform_point2(point_position), responses)
}
return Some(points);
// Offset to snap the selected point to the cursor
let offset = if let Ok(viewspace) = document.generate_transform_relative_to_viewport(shape_layer_path) {
mouse_position - viewspace.transform_point2(point_position)
} else {
DVec2::ZERO
};
return Some((points, offset));
} else {
responses.push_back(
Operation::DeselectManipulatorPoints {

View file

@ -179,9 +179,10 @@ impl Fsm for PathToolFsmState {
let toggle_add_to_selection = input.keyboard.get(add_to_selection as usize);
// Select the first point within the threshold (in pixels)
if let Some(mut new_selected) = tool_data
.shape_editor
.select_point(&document.graphene_document, input.mouse.position, SELECTION_THRESHOLD, toggle_add_to_selection, responses)
if let Some((mut new_selected, offset)) =
tool_data
.shape_editor
.select_point(&document.graphene_document, input.mouse.position, SELECTION_THRESHOLD, toggle_add_to_selection, responses)
{
responses.push_back(DocumentMessage::StartTransaction.into());
@ -203,7 +204,7 @@ impl Fsm for PathToolFsmState {
let include_handles = tool_data.shape_editor.selected_layers_ref();
tool_data.snap_manager.add_all_document_handles(document, &include_handles, &[], &new_selected);
tool_data.drag_start_pos = input.mouse.position;
tool_data.drag_start_pos = input.mouse.position - offset;
PathToolFsmState::Dragging
}
// We didn't find a point nearby, so consider selecting the nearest shape instead

View file

@ -279,7 +279,67 @@ impl Fsm for PenToolFsmState {
// Enter the dragging handle state while the mouse is held down, allowing the user to move the mouse and position the handle
PenToolFsmState::DraggingHandle
}
(PenToolFsmState::PlacingAnchor, PenToolMessage::DragStart) => PenToolFsmState::DraggingHandle,
(PenToolFsmState::PlacingAnchor, PenToolMessage::DragStart) => {
// If you place the anchor on top of the previous anchor then you break the mirror
let mut check_break = || {
// Get subpath
let layer_path = tool_data.path.as_ref()?;
let subpath = document.graphene_document.layer(layer_path).ok().and_then(|layer| layer.as_subpath())?;
// Get the last manipulator group and the one previous to that
let mut manipulator_groups = subpath.manipulator_groups().enumerate();
let (&last_id, last_manipulator_group) = if tool_data.from_start { manipulator_groups.next()? } else { manipulator_groups.next_back()? };
let previous = if tool_data.from_start { manipulator_groups.next() } else { manipulator_groups.next_back() };
// Get correct handle types
let outwards_handle = if tool_data.from_start { ManipulatorType::InHandle } else { ManipulatorType::OutHandle };
// Get manipulator points
let last_anchor = last_manipulator_group.points[ManipulatorType::Anchor].as_ref()?;
if let Some((previous_id, previous_anchor)) = previous
.as_ref()
.and_then(|(&id, manipulator_group)| manipulator_group.points[ManipulatorType::Anchor].as_ref().map(|x| (id, x)))
{
// Break the control
if transform.transform_point2(last_anchor.position).distance_squared(transform.transform_point2(previous_anchor.position)) < crate::consts::SNAP_POINT_TOLERANCE.powi(2) {
// Remove the point that has just been placed
let op = Operation::RemoveManipulatorGroup {
layer_path: layer_path.clone(),
id: last_id,
};
responses.push_back(op.into());
// Move the in handle of the previous anchor to on top of the previous position
let op = Operation::MoveManipulatorPoint {
layer_path: layer_path.clone(),
id: previous_id,
manipulator_type: outwards_handle,
position: previous_anchor.position.into(),
};
responses.push_back(op.into());
// Stop the handles on the last point from mirroring
let op = Operation::SetManipulatorHandleMirroring {
layer_path: layer_path.clone(),
id: previous_id,
mirror_distance: false,
mirror_angle: false,
};
responses.push_back(op.into());
// The overlay system cannot detect deleted points so we must just delete all the overlays
for layer_path in document.all_layers() {
tool_data.overlay_renderer.clear_subpath_overlays(&document.graphene_document, layer_path.to_vec(), responses);
}
tool_data.should_mirror = false;
}
}
None
};
check_break().unwrap_or(PenToolFsmState::DraggingHandle)
}
(PenToolFsmState::DraggingHandle, PenToolMessage::DragStop) => {
let mut process = || {
// Get subpath
@ -391,8 +451,9 @@ impl Fsm for PenToolFsmState {
};
responses.push_back(msg.into());
let should_mirror = !input.keyboard.get(break_handle as usize) && tool_data.should_mirror;
// Mirror handle of last segment
if !input.keyboard.get(break_handle as usize) && tool_data.should_mirror {
if should_mirror {
// Could also be written as `last_anchor.position * 2 - pos` but this way avoids overflow/underflow better
let pos = last_anchor.position - (pos - last_anchor.position);
@ -405,6 +466,15 @@ impl Fsm for PenToolFsmState {
responses.push_back(msg.into());
}
// Update the mirror status of the currently modifying point
let op = Operation::SetManipulatorHandleMirroring {
layer_path: layer_path.clone(),
id: last_id,
mirror_distance: should_mirror,
mirror_angle: should_mirror,
};
responses.push_back(op.into());
Some(())
};
if process().is_none() {
@ -440,7 +510,12 @@ impl Fsm for PenToolFsmState {
}
if let Some(relative) = previous.as_ref().and_then(|(_, manipulator_group)| manipulator_group.points[ManipulatorType::Anchor].as_ref()) {
pos = compute_snapped_angle(input, snap_angle, pos, relative.position);
// Snap to the previously placed point (to show break control)
if mouse.distance_squared(transform.transform_point2(relative.position)) < crate::consts::SNAP_POINT_TOLERANCE.powi(2) {
pos = relative.position;
} else {
pos = compute_snapped_angle(input, snap_angle, pos, relative.position);
}
}
for manipulator_type in [ManipulatorType::Anchor, ManipulatorType::InHandle, ManipulatorType::OutHandle] {

View file

@ -21,7 +21,6 @@ pub struct IdBackedVec<T> {
/// The IDs of the [Elements] contained within this
element_ids: Vec<ElementId>,
/// The ID that will be assigned to the next element that is added to this
#[serde(skip)]
next_id: ElementId,
}