mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-16 23:08:05 +08:00
Add point insertion to the Path tool (#754)
* Messaging cleanup * Add bezier iter * Add splitting * Use bezier_rs bounding box * Cleanup * Fix comments * Fix typo * Code review tweaks Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
@@ -139,6 +139,7 @@ pub fn default_mapping() -> Mapping {
|
||||
entry!(KeyDown(Delete); action_dispatch=PathToolMessage::Delete),
|
||||
entry!(KeyDown(Backspace); action_dispatch=PathToolMessage::Delete),
|
||||
entry!(KeyUp(Lmb); action_dispatch=PathToolMessage::DragStop),
|
||||
entry!(DoubleClick; action_dispatch=PathToolMessage::InsertPoint),
|
||||
//
|
||||
// PenToolMessage
|
||||
entry!(PointerMove; refresh_keys=[Shift, Control], action_dispatch=PenToolMessage::PointerMove { snap_angle: Control, break_handle: Shift }),
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::messages::prelude::*;
|
||||
use graphene::layers::vector::consts::ManipulatorType;
|
||||
use graphene::layers::vector::manipulator_group::ManipulatorGroup;
|
||||
use graphene::layers::vector::manipulator_point::ManipulatorPoint;
|
||||
use graphene::layers::vector::subpath::Subpath;
|
||||
use graphene::layers::vector::subpath::{BezierId, Subpath};
|
||||
use graphene::{LayerId, Operation};
|
||||
|
||||
use glam::DVec2;
|
||||
@@ -259,6 +259,69 @@ impl ShapeEditor {
|
||||
result
|
||||
}
|
||||
|
||||
/// Find the `t` value along the path segment we have clicked upon, together with that segment ID.
|
||||
///
|
||||
/// Returns a tuple of [`BezierId`] and `t` as an f64.
|
||||
fn closest_segment(&self, document: &Document, layer_path: &[LayerId], position: glam::DVec2, tolerance: f64) -> Option<(BezierId, f64)> {
|
||||
let transform = document.generate_transform_relative_to_viewport(layer_path).ok()?;
|
||||
let layer_pos = transform.inverse().transform_point2(position);
|
||||
let projection_options = bezier_rs::ProjectionOptions { lut_size: 5, ..Default::default() };
|
||||
|
||||
let mut result: Option<(BezierId, f64)> = None;
|
||||
let mut closest_distance_squared: f64 = tolerance * tolerance;
|
||||
|
||||
for bezier_id in document.layer(layer_path).ok()?.as_subpath()?.bezier_iter() {
|
||||
let bezier = bezier_id.internal;
|
||||
let t = bezier.project(layer_pos, projection_options);
|
||||
let layerspace = bezier.evaluate(t);
|
||||
|
||||
let screenspace = transform.transform_point2(layerspace);
|
||||
let distance_squared = screenspace.distance_squared(position);
|
||||
|
||||
if distance_squared < closest_distance_squared {
|
||||
closest_distance_squared = distance_squared;
|
||||
result = Some((bezier_id, t));
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Handles the splitting of a curve to insert new points (which can be activated by double clicking on a curve with the Path tool).
|
||||
pub fn split(&self, document: &Document, position: glam::DVec2, tolerance: f64, responses: &mut VecDeque<Message>) {
|
||||
for layer_path in &self.selected_layers {
|
||||
if let Some((bezier_id, t)) = self.closest_segment(document, layer_path, position, tolerance) {
|
||||
let [first, second] = bezier_id.internal.split(t);
|
||||
|
||||
// Adjust the first manipulator group's out handle
|
||||
let out_handle = Operation::SetManipulatorPoints {
|
||||
layer_path: layer_path.clone(),
|
||||
id: bezier_id.start,
|
||||
manipulator_type: ManipulatorType::OutHandle,
|
||||
position: first.handle_start().map(|p| p.into()),
|
||||
};
|
||||
|
||||
// Insert a new manipulator group between the existing ones
|
||||
let insert = Operation::InsertManipulatorGroup {
|
||||
layer_path: layer_path.clone(),
|
||||
manipulator_group: ManipulatorGroup::new_with_handles(first.end(), first.handle_end(), second.handle_start()),
|
||||
after_id: bezier_id.end,
|
||||
};
|
||||
|
||||
// Adjust the last manipulator group's in handle
|
||||
let in_handle = Operation::SetManipulatorPoints {
|
||||
layer_path: layer_path.clone(),
|
||||
id: bezier_id.end,
|
||||
manipulator_type: ManipulatorType::InHandle,
|
||||
position: second.handle_end().map(|p| p.into()),
|
||||
};
|
||||
|
||||
responses.extend([out_handle.into(), insert.into(), in_handle.into()]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn shape<'a>(&'a self, document: &'a Document, layer_id: &[u64]) -> Option<&'a Subpath> {
|
||||
document.layer(layer_id).ok()?.as_subpath()
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::consts::SELECTION_THRESHOLD;
|
||||
use crate::consts::{SELECTION_THRESHOLD, SELECTION_TOLERANCE};
|
||||
use crate::messages::frontend::utility_types::MouseCursorIcon;
|
||||
use crate::messages::input_mapper::utility_types::input_keyboard::{Key, KeysGroup, MouseMotion};
|
||||
use crate::messages::layout::utility_types::layout_widget::PropertyHolder;
|
||||
@@ -39,6 +39,7 @@ pub enum PathToolMessage {
|
||||
add_to_selection: Key,
|
||||
},
|
||||
DragStop,
|
||||
InsertPoint,
|
||||
PointerMove {
|
||||
alt_mirror_angle: Key,
|
||||
shift_mirror_distance: Key,
|
||||
@@ -86,10 +87,12 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for PathTool {
|
||||
|
||||
match self.fsm_state {
|
||||
Ready => actions!(PathToolMessageDiscriminant;
|
||||
InsertPoint,
|
||||
DragStart,
|
||||
Delete,
|
||||
),
|
||||
Dragging => actions!(PathToolMessageDiscriminant;
|
||||
InsertPoint,
|
||||
DragStop,
|
||||
PointerMove,
|
||||
Delete,
|
||||
@@ -144,11 +147,8 @@ impl Fsm for PathToolFsmState {
|
||||
responses: &mut VecDeque<Message>,
|
||||
) -> Self {
|
||||
if let ToolMessage::Path(event) = event {
|
||||
use PathToolFsmState::*;
|
||||
use PathToolMessage::*;
|
||||
|
||||
match (self, event) {
|
||||
(_, SelectionChanged) => {
|
||||
(_, PathToolMessage::SelectionChanged) => {
|
||||
// Set the previously selected layers to invisible
|
||||
for layer_path in document.all_layers() {
|
||||
tool_data.overlay_renderer.layer_overlay_visibility(&document.graphene_document, layer_path.to_vec(), false, responses);
|
||||
@@ -165,7 +165,7 @@ impl Fsm for PathToolFsmState {
|
||||
// This can happen in any state (which is why we return self)
|
||||
self
|
||||
}
|
||||
(_, DocumentIsDirty) => {
|
||||
(_, PathToolMessage::DocumentIsDirty) => {
|
||||
// When the document has moved / needs to be redraw, re-render the overlays
|
||||
// TODO the overlay system should probably receive this message instead of the tool
|
||||
for layer_path in document.selected_visible_layers() {
|
||||
@@ -175,7 +175,7 @@ impl Fsm for PathToolFsmState {
|
||||
self
|
||||
}
|
||||
// Mouse down
|
||||
(_, DragStart { add_to_selection }) => {
|
||||
(_, PathToolMessage::DragStart { add_to_selection }) => {
|
||||
let toggle_add_to_selection = input.keyboard.get(add_to_selection as usize);
|
||||
|
||||
// Select the first point within the threshold (in pixels)
|
||||
@@ -204,7 +204,7 @@ impl Fsm for PathToolFsmState {
|
||||
tool_data.snap_manager.add_all_document_handles(document, &include_handles, &[], &new_selected);
|
||||
|
||||
tool_data.drag_start_pos = input.mouse.position;
|
||||
Dragging
|
||||
PathToolFsmState::Dragging
|
||||
}
|
||||
// We didn't find a point nearby, so consider selecting the nearest shape instead
|
||||
else {
|
||||
@@ -230,13 +230,13 @@ impl Fsm for PathToolFsmState {
|
||||
responses.push_back(DocumentMessage::DeselectAllLayers.into());
|
||||
}
|
||||
}
|
||||
Ready
|
||||
PathToolFsmState::Ready
|
||||
}
|
||||
}
|
||||
// Dragging
|
||||
(
|
||||
Dragging,
|
||||
PointerMove {
|
||||
PathToolFsmState::Dragging,
|
||||
PathToolMessage::PointerMove {
|
||||
alt_mirror_angle,
|
||||
shift_mirror_distance,
|
||||
},
|
||||
@@ -262,34 +262,39 @@ impl Fsm for PathToolFsmState {
|
||||
let snapped_position = tool_data.snap_manager.snap_position(responses, document, input.mouse.position);
|
||||
tool_data.shape_editor.move_selected_points(snapped_position - tool_data.drag_start_pos, snapped_position, responses);
|
||||
tool_data.drag_start_pos = snapped_position;
|
||||
Dragging
|
||||
PathToolFsmState::Dragging
|
||||
}
|
||||
// Mouse up
|
||||
(_, DragStop) => {
|
||||
(_, PathToolMessage::DragStop) => {
|
||||
tool_data.snap_manager.cleanup(responses);
|
||||
Ready
|
||||
PathToolFsmState::Ready
|
||||
}
|
||||
// Delete key
|
||||
(_, Delete) => {
|
||||
(_, PathToolMessage::Delete) => {
|
||||
// Delete the selected points and clean up overlays
|
||||
responses.push_back(DocumentMessage::StartTransaction.into());
|
||||
tool_data.shape_editor.delete_selected_points(responses);
|
||||
responses.push_back(SelectionChanged.into());
|
||||
responses.push_back(PathToolMessage::SelectionChanged.into());
|
||||
for layer_path in document.all_layers() {
|
||||
tool_data.overlay_renderer.clear_subpath_overlays(&document.graphene_document, layer_path.to_vec(), responses);
|
||||
}
|
||||
Ready
|
||||
PathToolFsmState::Ready
|
||||
}
|
||||
(_, Abort) => {
|
||||
(_, PathToolMessage::InsertPoint) => {
|
||||
tool_data.shape_editor.split(&document.graphene_document, input.mouse.position, SELECTION_TOLERANCE, responses);
|
||||
|
||||
self
|
||||
}
|
||||
(_, PathToolMessage::Abort) => {
|
||||
// TODO Tell overlay manager to remove the overlays
|
||||
for layer_path in document.all_layers() {
|
||||
tool_data.overlay_renderer.clear_subpath_overlays(&document.graphene_document, layer_path.to_vec(), responses);
|
||||
}
|
||||
Ready
|
||||
PathToolFsmState::Ready
|
||||
}
|
||||
(
|
||||
_,
|
||||
PointerMove {
|
||||
PathToolMessage::PointerMove {
|
||||
alt_mirror_angle: _,
|
||||
shift_mirror_distance: _,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user