Add G/R/S to the Pen tool to control the outgoing segment handle (#2211)

* more_refactoring_solve_conflict

* overlays-target-fix

* Code review

* select-broken-fix

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
0SlowPoke0 2025-01-26 17:44:51 +05:30 committed by GitHub
parent 96c57605b7
commit 9e2bda36b0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 235 additions and 49 deletions

View File

@ -260,6 +260,9 @@ pub fn input_mappings() -> Mapping {
entry!(KeyDown(Enter); action_dispatch=PenToolMessage::Confirm),
entry!(KeyDown(Delete); action_dispatch=PenToolMessage::RemovePreviousHandle),
entry!(KeyDown(Backspace); action_dispatch=PenToolMessage::RemovePreviousHandle),
entry!(KeyDown(KeyG); action_dispatch=PenToolMessage::GRS { grab: KeyG, rotate: KeyR, scale: KeyS }),
entry!(KeyDown(KeyR); action_dispatch=PenToolMessage::GRS { grab: KeyG, rotate: KeyR, scale: KeyS }),
entry!(KeyDown(KeyS); action_dispatch=PenToolMessage::GRS { grab: KeyG, rotate: KeyR, scale: KeyS }),
//
// FreehandToolMessage
entry!(PointerMove; action_dispatch=FreehandToolMessage::PointerMove),

View File

@ -384,6 +384,8 @@ pub struct Selected<'a> {
pub pivot: &'a mut DVec2,
pub shape_editor: Option<&'a ShapeState>,
pub tool_type: &'a ToolType,
// Only for the Pen tool
pub pen_handle: Option<&'a mut DVec2>,
}
impl<'a> Selected<'a> {
@ -396,6 +398,7 @@ impl<'a> Selected<'a> {
network_interface: &'a NodeNetworkInterface,
shape_editor: Option<&'a ShapeState>,
tool_type: &'a ToolType,
pen_handle: Option<&'a mut DVec2>,
) -> Self {
// If user is using the Select tool then use the original layer transforms
if (*tool_type == ToolType::Select) && (*original_transforms == OriginalTransforms::Path(HashMap::new())) {
@ -412,6 +415,7 @@ impl<'a> Selected<'a> {
pivot,
shape_editor,
tool_type,
pen_handle,
}
}
@ -502,6 +506,13 @@ impl<'a> Selected<'a> {
}
}
pub fn apply_transform_pen(&mut self, transformation: DAffine2) {
if let Some(pen_handle) = &self.pen_handle {
let final_position = transformation.transform_point2(**pen_handle);
self.responses.add(PenToolMessage::FinalPosition { final_position });
}
}
pub fn apply_transformation(&mut self, transformation: DAffine2) {
if !self.selected.is_empty() {
// TODO: Cache the result of `shallowest_unique_layers` to avoid this heavy computation every frame of movement, see https://github.com/GraphiteEditor/Graphite/pull/481
@ -523,7 +534,10 @@ impl<'a> Selected<'a> {
pub fn update_transforms(&mut self, delta: DAffine2) {
let pivot = DAffine2::from_translation(*self.pivot);
let transformation = pivot * delta * pivot.inverse();
self.apply_transformation(transformation);
match self.tool_type {
ToolType::Pen => self.apply_transform_pen(transformation),
_ => self.apply_transformation(transformation),
}
}
pub fn revert_operation(&mut self) {

View File

@ -63,6 +63,8 @@ pub enum PenToolMessage {
UpdateOptions(PenOptionsUpdate),
RecalculateLatestPointsPosition,
RemovePreviousHandle,
GRS { grab: Key, rotate: Key, scale: Key },
FinalPosition { final_position: DVec2 },
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
@ -71,6 +73,7 @@ enum PenToolFsmState {
Ready,
DraggingHandle,
PlacingAnchor,
GRSHandle,
}
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
@ -162,13 +165,14 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for PenTool
fn actions(&self) -> ActionList {
match self.fsm_state {
PenToolFsmState::Ready => actions!(PenToolMessageDiscriminant;
PenToolFsmState::Ready | PenToolFsmState::GRSHandle => actions!(PenToolMessageDiscriminant;
Undo,
DragStart,
DragStop,
Confirm,
Abort,
PointerMove,
FinalPosition
),
PenToolFsmState::DraggingHandle | PenToolFsmState::PlacingAnchor => actions!(PenToolMessageDiscriminant;
DragStart,
@ -177,6 +181,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for PenTool
Confirm,
Abort,
RemovePreviousHandle,
GRS,
),
}
}
@ -223,6 +228,8 @@ struct PenToolData {
modifiers: ModifierState,
buffering_merged_vector: bool,
before_grs_pos: DVec2,
}
impl PenToolData {
fn latest_point(&self) -> Option<&LastPoint> {
@ -563,6 +570,72 @@ impl Fsm for PenToolFsmState {
let ToolMessage::Pen(event) = event else { return self };
match (self, event) {
(PenToolFsmState::PlacingAnchor | PenToolFsmState::GRSHandle, PenToolMessage::GRS { grab, rotate, scale }) => {
let Some(latest) = tool_data.latest_point_mut() else { return PenToolFsmState::PlacingAnchor };
let Some(layer) = layer else { return PenToolFsmState::PlacingAnchor };
if latest.handle_start != latest.pos {
let viewport = document.metadata().transform_to_viewport(layer);
let last_point = viewport.transform_point2(latest.pos);
let handle = viewport.transform_point2(latest.handle_start);
if input.keyboard.key(grab) {
responses.add(TransformLayerMessage::BeginGrabPen { last_point, handle });
} else if input.keyboard.key(rotate) {
responses.add(TransformLayerMessage::BeginRotatePen { last_point, handle });
} else if input.keyboard.key(scale) {
responses.add(TransformLayerMessage::BeginScalePen { last_point, handle });
}
tool_data.before_grs_pos = latest.handle_start;
}
PenToolFsmState::GRSHandle
}
(PenToolFsmState::GRSHandle, PenToolMessage::FinalPosition { final_position: final_pos }) => {
let Some(layer) = layer else { return PenToolFsmState::GRSHandle };
if let Some(latest_pt) = tool_data.latest_point_mut() {
let layer_space_to_viewport = document.metadata().transform_to_viewport(layer);
let final_pos = layer_space_to_viewport.inverse().transform_point2(final_pos);
latest_pt.handle_start = final_pos;
}
responses.add(OverlaysMessage::Draw);
PenToolFsmState::GRSHandle
}
(PenToolFsmState::GRSHandle, PenToolMessage::Confirm) => {
tool_data.next_point = input.mouse.position;
tool_data.next_handle_start = input.mouse.position;
responses.add(OverlaysMessage::Draw);
responses.add(PenToolMessage::PointerMove {
snap_angle: Key::Control,
break_handle: Key::Alt,
lock_angle: Key::Shift,
});
PenToolFsmState::PlacingAnchor
}
(PenToolFsmState::GRSHandle, PenToolMessage::Abort) => {
tool_data.next_point = input.mouse.position;
tool_data.next_handle_start = input.mouse.position;
let previous = tool_data.before_grs_pos;
if let Some(latest) = tool_data.latest_point_mut() {
latest.handle_start = previous;
}
responses.add(OverlaysMessage::Draw);
responses.add(PenToolMessage::PointerMove {
snap_angle: Key::Control,
break_handle: Key::Alt,
lock_angle: Key::Shift,
});
PenToolFsmState::PlacingAnchor
}
(_, PenToolMessage::SelectionChanged) => {
responses.add(OverlaysMessage::Draw);
self
@ -814,7 +887,7 @@ impl Fsm for PenToolFsmState {
fn update_hints(&self, responses: &mut VecDeque<Message>) {
let hint_data = match self {
PenToolFsmState::Ready => HintData(vec![HintGroup(vec![
PenToolFsmState::Ready | PenToolFsmState::GRSHandle => HintData(vec![HintGroup(vec![
HintInfo::mouse(MouseMotion::Lmb, "Draw Path"),
// TODO: Only show this if a single layer is selected and it's of a valid type (e.g. a vector path but not raster or artboard)
HintInfo::keys([Key::Shift], "Append to Selected Layer").prepend_plus(),

View File

@ -632,6 +632,7 @@ impl Fsm for SelectToolFsmState {
&document.network_interface,
None,
&ToolType::Select,
None
);
bounds.center_of_transformation = selected.mean_average_of_pivots();
}
@ -660,6 +661,7 @@ impl Fsm for SelectToolFsmState {
&document.network_interface,
None,
&ToolType::Select,
None
);
bounds.center_of_transformation = selected.mean_average_of_pivots();
@ -785,7 +787,16 @@ impl Fsm for SelectToolFsmState {
}
});
let selected = &tool_data.layers_dragging;
let mut selected = Selected::new(&mut bounds.original_transforms, &mut pivot, selected, responses, &document.network_interface, None, &ToolType::Select);
let mut selected = Selected::new(
&mut bounds.original_transforms,
&mut pivot,
selected,
responses,
&document.network_interface,
None,
&ToolType::Select,
None,
);
selected.apply_transformation(bounds.original_bound_transform * transformation * bounds.original_bound_transform.inverse());
@ -833,6 +844,7 @@ impl Fsm for SelectToolFsmState {
&document.network_interface,
None,
&ToolType::Select,
None,
);
selected.update_transforms(delta);

View File

@ -2,6 +2,8 @@ use crate::messages::input_mapper::utility_types::input_keyboard::Key;
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
use crate::messages::prelude::*;
use glam::DVec2;
#[impl_message(Message, ToolMessage, TransformLayer)]
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
pub enum TransformLayerMessage {
@ -10,6 +12,9 @@ pub enum TransformLayerMessage {
BeginGrab,
BeginRotate,
BeginScale,
BeginGrabPen { last_point: DVec2, handle: DVec2 },
BeginRotatePen { last_point: DVec2, handle: DVec2 },
BeginScalePen { last_point: DVec2, handle: DVec2 },
CancelTransformOperation,
ConstrainX,
ConstrainY,

View File

@ -4,10 +4,12 @@ use crate::messages::portfolio::document::overlays::utility_types::{OverlayProvi
use crate::messages::portfolio::document::utility_types::transformation::{Axis, OriginalTransforms, Selected, TransformOperation, Typing};
use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::shape_editor::ShapeState;
use crate::messages::tool::tool_messages::tool_prelude::Key;
use crate::messages::tool::utility_types::{ToolData, ToolType};
use graphene_core::renderer::Quad;
use graphene_core::vector::ManipulatorPointId;
use graphene_std::vector::VectorData;
use glam::{DAffine2, DVec2};
use std::f64::consts::TAU;
@ -29,6 +31,12 @@ pub struct TransformLayerMessageHandler {
original_transforms: OriginalTransforms,
pivot: DVec2,
grab_target: DVec2,
// Pen tool (outgoing handle GRS manipulation)
handle: DVec2,
last_point: DVec2,
grs_pen_handle: bool,
}
impl TransformLayerMessageHandler {
@ -41,11 +49,37 @@ impl TransformLayerMessageHandler {
}
}
fn calculate_pivot(selected_points: &Vec<&ManipulatorPointId>, vector_data: &VectorData, viewspace: DAffine2, get_location: impl Fn(&ManipulatorPointId) -> Option<DVec2>) -> Option<(DVec2, DVec2)> {
let [point] = selected_points.as_slice() else {
// Handle the case where there are multiple points
let mut point_count = 0;
let average_position = selected_points.iter().filter_map(|p| get_location(p)).inspect(|_| point_count += 1).sum::<DVec2>() / point_count as f64;
return Some((average_position, average_position));
};
match point {
ManipulatorPointId::PrimaryHandle(_) | ManipulatorPointId::EndHandle(_) => {
// Get the anchor position and transform it to the pivot
let pivot_pos = point.get_anchor_position(vector_data).map(|anchor_position| viewspace.transform_point2(anchor_position))?;
let target = viewspace.transform_point2(point.get_position(vector_data)?);
Some((pivot_pos, target))
}
_ => {
// Calculate the average position of all selected points
let mut point_count = 0;
let average_position = selected_points.iter().filter_map(|p| get_location(p)).inspect(|_| point_count += 1).sum::<DVec2>() / point_count as f64;
Some((average_position, average_position))
}
}
}
type TransformData<'a> = (&'a DocumentMessageHandler, &'a InputPreprocessorMessageHandler, &'a ToolData, &'a mut ShapeState);
impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayerMessageHandler {
fn process_message(&mut self, message: TransformLayerMessage, responses: &mut VecDeque<Message>, (document, input, tool_data, shape_editor): TransformData) {
let using_path_tool = tool_data.active_tool_type == ToolType::Path;
let using_select_tool = tool_data.active_tool_type == ToolType::Select;
let using_pen_tool = tool_data.active_tool_type == ToolType::Pen;
// TODO: Add support for transforming layer not in the document network
let selected_layers = document
@ -64,6 +98,7 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
&document.network_interface,
Some(shape_editor),
&tool_data.active_tool_type,
Some(&mut self.handle),
);
let mut begin_operation = |operation: TransformOperation, typing: &mut Typing, mouse_position: &mut DVec2, start_mouse: &mut DVec2| {
@ -72,27 +107,32 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
typing.clear();
}
if using_path_tool {
if let Some(vector_data) = selected_layers.first().and_then(|&layer| document.network_interface.compute_modified_vector(layer)) {
*selected.original_transforms = OriginalTransforms::default();
let viewspace = document.metadata().transform_to_viewport(selected_layers[0]);
if using_pen_tool {
selected.responses.add(PenToolMessage::GRS {
grab: Key::KeyG,
rotate: Key::KeyR,
scale: Key::KeyS,
});
return;
}
let mut point_count: usize = 0;
let get_location = |point: &&ManipulatorPointId| point.get_position(&vector_data).map(|position| viewspace.transform_point2(position));
let points = shape_editor.selected_points();
let selected_points: Vec<&ManipulatorPointId> = points.collect();
if let [point] = selected_points.as_slice() {
if let ManipulatorPointId::PrimaryHandle(_) | ManipulatorPointId::EndHandle(_) = point {
let anchor_position = point.get_anchor_position(&vector_data).unwrap();
*selected.pivot = viewspace.transform_point2(anchor_position);
} else {
*selected.pivot = selected_points.iter().filter_map(get_location).inspect(|_| point_count += 1).sum::<DVec2>() / point_count as f64;
}
}
}
} else {
if !using_path_tool {
*selected.pivot = selected.mean_average_of_pivots();
self.grab_target = selected.mean_average_of_pivots();
} else if let Some(vector_data) = selected_layers.first().and_then(|&layer| document.network_interface.compute_modified_vector(layer)) {
*selected.original_transforms = OriginalTransforms::default();
let viewspace = document.metadata().transform_to_viewport(selected_layers[0]);
let selected_points = shape_editor.selected_points().collect::<Vec<_>>();
let get_location = |point: &&ManipulatorPointId| point.get_position(&vector_data).map(|position| viewspace.transform_point2(position));
if let Some((new_pivot, grab_target)) = calculate_pivot(&selected_points, &vector_data, viewspace, |point: &ManipulatorPointId| get_location(&point)) {
*selected.pivot = new_pivot;
self.grab_target = grab_target;
} else {
log::warn!("Failed to calculate pivot.");
}
}
*mouse_position = input.mouse.position;
@ -106,18 +146,53 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
match message {
TransformLayerMessage::ApplyTransformOperation => {
selected.original_transforms.clear();
self.typing.clear();
self.transform_operation = TransformOperation::None;
if using_pen_tool {
self.last_point = DVec2::ZERO;
self.grs_pen_handle = false;
selected.pen_handle = None;
selected.responses.add(PenToolMessage::Confirm);
} else {
responses.add(DocumentMessage::EndTransaction);
responses.add(ToolMessage::UpdateHints);
responses.add(NodeGraphMessage::RunDocumentGraph);
}
responses.add(OverlaysMessage::RemoveProvider(TRANSFORM_GRS_OVERLAY_PROVIDER));
}
TransformLayerMessage::BeginGrabPen { last_point, handle } | TransformLayerMessage::BeginRotatePen { last_point, handle } | TransformLayerMessage::BeginScalePen { last_point, handle } => {
self.typing.clear();
self.last_point = last_point;
self.handle = handle;
self.grs_pen_handle = true;
self.mouse_position = input.mouse.position;
self.start_mouse = input.mouse.position;
let top_left = DVec2::new(last_point.x, handle.y);
let bottom_right = DVec2::new(handle.x, last_point.y);
self.local = false;
self.fixed_bbox = Quad::from_box([top_left, bottom_right]);
self.grab_target = handle;
self.pivot = last_point;
self.handle = handle;
// Operation-specific logic
self.transform_operation = match message {
TransformLayerMessage::BeginGrabPen { .. } => TransformOperation::Grabbing(Default::default()),
TransformLayerMessage::BeginRotatePen { .. } => TransformOperation::Rotating(Default::default()),
TransformLayerMessage::BeginScalePen { .. } => TransformOperation::Scaling(Default::default()),
_ => unreachable!(), // Safe because the match arms are exhaustive
};
responses.add(OverlaysMessage::AddProvider(TRANSFORM_GRS_OVERLAY_PROVIDER));
}
TransformLayerMessage::BeginGrab => {
if (!using_path_tool && !using_select_tool)
if (!using_path_tool && !using_select_tool && !using_pen_tool)
|| (using_path_tool && shape_editor.selected_points().next().is_none())
|| selected_layers.is_empty()
|| matches!(self.transform_operation, TransformOperation::Grabbing(_))
@ -140,7 +215,7 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
TransformLayerMessage::BeginRotate => {
let selected_points: Vec<&ManipulatorPointId> = shape_editor.selected_points().collect();
if (!using_path_tool && !using_select_tool)
if (!using_path_tool && !using_select_tool && !using_pen_tool)
|| (using_path_tool && selected_points.is_empty())
|| selected_layers.is_empty()
|| matches!(self.transform_operation, TransformOperation::Rotating(_))
@ -191,7 +266,7 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
let selected_points: Vec<&ManipulatorPointId> = shape_editor.selected_points().collect();
if (using_path_tool && selected_points.is_empty())
|| (!using_path_tool && !using_select_tool)
|| (!using_path_tool && !using_select_tool && !using_pen_tool)
|| selected_layers.is_empty()
|| matches!(self.transform_operation, TransformOperation::Scaling(_))
{
@ -237,15 +312,23 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
responses.add(OverlaysMessage::AddProvider(TRANSFORM_GRS_OVERLAY_PROVIDER));
}
TransformLayerMessage::CancelTransformOperation => {
selected.revert_operation();
selected.original_transforms.clear();
if using_pen_tool {
self.typing.clear();
self.last_point = DVec2::ZERO;
self.transform_operation = TransformOperation::None;
self.handle = DVec2::ZERO;
responses.add(PenToolMessage::Abort);
} else {
selected.revert_operation();
selected.original_transforms.clear();
self.typing.clear();
self.transform_operation = TransformOperation::None;
responses.add(DocumentMessage::AbortTransaction);
responses.add(ToolMessage::UpdateHints);
}
responses.add(OverlaysMessage::RemoveProvider(TRANSFORM_GRS_OVERLAY_PROVIDER));
}
@ -298,7 +381,7 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
TransformOperation::Grabbing(translation) => {
let translation = document_to_viewport.inverse().transform_vector2(translation.to_dvec(document_to_viewport));
let vec_to_end = self.mouse_position - self.start_mouse;
let quad = Quad::from_box([self.pivot, self.pivot + vec_to_end]).0;
let quad = Quad::from_box([self.grab_target, self.grab_target + vec_to_end]).0;
let e1 = (self.fixed_bbox.0[1] - self.fixed_bbox.0[0]).normalize();
if matches!(axis_constraint, Axis::Both | Axis::X) {
@ -374,7 +457,8 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
TransformOperation::Rotating(rotation) => {
let angle = rotation.to_f64(self.snap);
let quad = self.fixed_bbox.0;
let offset_angle = (quad[1] - quad[0]).to_angle();
let offset_angle = if self.grs_pen_handle { self.handle - self.last_point } else { quad[1] - quad[0] };
let offset_angle = offset_angle.to_angle();
let width = viewport_box.max_element();
let radius = self.start_mouse.distance(self.pivot);
let arc_radius = ANGLE_MEASURE_RADIUS_FACTOR * width;
@ -436,18 +520,13 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
(current_frame_dist - previous_frame_dist) / start_transform_dist
};
let region_negate = (self.start_mouse - *selected.pivot).dot(self.mouse_position - *selected.pivot) < 0.;
let change = if self.slow { change / SLOWING_DIVISOR } else { change };
let change = change * scale.dragged_factor.signum();
self.transform_operation = TransformOperation::Scaling(scale.increment_amount(change));
if region_negate {
let tmp_operation = TransformOperation::Scaling(scale.negate());
tmp_operation.apply_transform_operation(&mut selected, self.snap, self.local, self.fixed_bbox, document_to_viewport);
} else {
self.transform_operation
.apply_transform_operation(&mut selected, self.snap, self.local, self.fixed_bbox, document_to_viewport);
}
}
};
}
@ -470,7 +549,7 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
}
TransformLayerMessage::TypeNegate => {
if self.typing.digits.is_empty() {
self.transform_operation.negate(&mut selected, self.snap, self.local, self.fixed_bbox, document_to_viewport)
self.transform_operation.negate(&mut selected, self.snap, self.local, self.fixed_bbox, document_to_viewport);
}
self.transform_operation
.grs_typed(self.typing.type_negate(), &mut selected, self.snap, self.local, self.fixed_bbox, document_to_viewport)