Skip to content

Commit

Permalink
Add Path tool support for G/R/S rotation and scaling with a single se…
Browse files Browse the repository at this point in the history
…lected handle (#2180)

* grab_scale_path and backspace for pen

* minor improvements and fixes

* code-review changes

* Avoid more nesting, and other code cleanup

---------

Co-authored-by: Keavon Chambers <[email protected]>
  • Loading branch information
0SlowPoke0 and Keavon authored Jan 15, 2025
1 parent 9ad6c31 commit 2e4fb95
Show file tree
Hide file tree
Showing 6 changed files with 119 additions and 31 deletions.
2 changes: 2 additions & 0 deletions editor/src/messages/input_mapper/input_mappings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,8 @@ pub fn input_mappings() -> Mapping {
entry!(KeyDown(MouseRight); action_dispatch=PenToolMessage::Confirm),
entry!(KeyDown(Escape); action_dispatch=PenToolMessage::Confirm),
entry!(KeyDown(Enter); action_dispatch=PenToolMessage::Confirm),
entry!(KeyDown(Delete); action_dispatch=PenToolMessage::RemovePreviousHandle),
entry!(KeyDown(Backspace); action_dispatch=PenToolMessage::RemovePreviousHandle),
//
// FreehandToolMessage
entry!(PointerMove; action_dispatch=FreehandToolMessage::PointerMove),
Expand Down
13 changes: 9 additions & 4 deletions editor/src/messages/tool/tool_message_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,8 @@ impl MessageHandler<ToolMessage, ToolMessageData<'_>> for ToolMessageHandler {
self.tool_is_active = true;

// Send the old and new tools a transition to their FSM Abort states
let mut send_abort_to_tool = |tool_type, update_hints_and_cursor: bool| {
if let Some(tool) = tool_data.tools.get_mut(&tool_type) {
let mut send_abort_to_tool = |old_tool: ToolType, new_tool: ToolType, update_hints_and_cursor: bool| {
if let Some(tool) = tool_data.tools.get_mut(&new_tool) {
let mut data = ToolActionHandlerData {
document,
document_id,
Expand All @@ -101,9 +101,14 @@ impl MessageHandler<ToolMessage, ToolMessageData<'_>> for ToolMessageHandler {
tool.process_message(ToolMessage::UpdateCursor, responses, &mut data);
}
}

if matches!(old_tool, ToolType::Path | ToolType::Select) {
responses.add(TransformLayerMessage::CancelTransformOperation);
}
};
send_abort_to_tool(tool_type, true);
send_abort_to_tool(old_tool, false);

send_abort_to_tool(old_tool, tool_type, true);
send_abort_to_tool(old_tool, old_tool, false);

// Unsubscribe old tool from the broadcaster
tool_data.tools.get(&tool_type).unwrap().deactivate(responses);
Expand Down
14 changes: 1 addition & 13 deletions editor/src/messages/tool/tool_messages/path_tool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -648,19 +648,7 @@ impl Fsm for PathToolFsmState {
self
}
(Self::InsertPoint, PathToolMessage::Escape | PathToolMessage::Delete | PathToolMessage::RightClick) => tool_data.end_insertion(shape_editor, responses, InsertEndKind::Abort),
(Self::InsertPoint, PathToolMessage::GRS { key: propagate }) => {
// MAYBE: use `InputMapperMessage::KeyDown(..)` instead
match propagate {
// TODO: Don't use `Key::G` directly, instead take it as a variable from the input mappings list like in all other places
Key::KeyG => responses.add(TransformLayerMessage::BeginGrab),
// TODO: Don't use `Key::R` directly, instead take it as a variable from the input mappings list like in all other places
Key::KeyR => responses.add(TransformLayerMessage::BeginRotate),
// TODO: Don't use `Key::S` directly, instead take it as a variable from the input mappings list like in all other places
Key::KeyS => responses.add(TransformLayerMessage::BeginScale),
_ => warn!("Unexpected GRS key"),
}
tool_data.end_insertion(shape_editor, responses, InsertEndKind::Abort)
}
(Self::InsertPoint, PathToolMessage::GRS { key: _ }) => PathToolFsmState::InsertPoint,
// Mouse down
(
_,
Expand Down
11 changes: 11 additions & 0 deletions editor/src/messages/tool/tool_messages/pen_tool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ pub enum PenToolMessage {
Undo,
UpdateOptions(PenOptionsUpdate),
RecalculateLatestPointsPosition,
RemovePreviousHandle,
}

#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
Expand Down Expand Up @@ -175,6 +176,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for PenTool
PointerMove,
Confirm,
Abort,
RemovePreviousHandle,
),
}
}
Expand Down Expand Up @@ -685,6 +687,15 @@ impl Fsm for PenToolFsmState {
PenToolFsmState::PlacingAnchor
}
}
(PenToolFsmState::PlacingAnchor, PenToolMessage::RemovePreviousHandle) => {
if let Some(last_point) = tool_data.latest_points.last_mut() {
last_point.handle_start = last_point.pos;
responses.add(OverlaysMessage::Draw);
} else {
log::warn!("No latest point available to modify handle_start.");
}
self
}
(PenToolFsmState::DraggingHandle, PenToolMessage::DragStop) => tool_data
.finish_placing_handle(SnapData::new(document, input), transform, responses)
.unwrap_or(PenToolFsmState::PlacingAnchor),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ type TransformData<'a> = (&'a DocumentMessageHandler, &'a InputPreprocessorMessa
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;

// TODO: Add support for transforming layer not in the document network
let selected_layers = document
Expand Down Expand Up @@ -75,10 +76,18 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
let viewspace = document.metadata().transform_to_viewport(selected_layers[0]);

let mut point_count: usize = 0;
let get_location = |point: &ManipulatorPointId| point.get_position(&vector_data).map(|position| viewspace.transform_point2(position));
let get_location = |point: &&ManipulatorPointId| point.get_position(&vector_data).map(|position| viewspace.transform_point2(position));
let points = shape_editor.selected_points();

*selected.pivot = points.filter_map(get_location).inspect(|_| point_count += 1).sum::<DVec2>() / point_count as f64;
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 {
*selected.pivot = selected.mean_average_of_pivots();
Expand All @@ -104,12 +113,13 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
responses.add(NodeGraphMessage::RunDocumentGraph);
}
TransformLayerMessage::BeginGrab => {
if let TransformOperation::Grabbing(_) = self.transform_operation {
return;
}
if (!using_path_tool && !using_select_tool)
|| (using_path_tool && shape_editor.selected_points().next().is_none())
|| selected_layers.is_empty()
|| matches!(self.transform_operation, TransformOperation::Grabbing(_))
{
selected.original_transforms.clear();

// Don't allow grab with no selected layers
if selected_layers.is_empty() {
return;
}

Expand All @@ -120,13 +130,42 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
selected.original_transforms.clear();
}
TransformLayerMessage::BeginRotate => {
if let TransformOperation::Rotating(_) = self.transform_operation {
let selected_points: Vec<&ManipulatorPointId> = shape_editor.selected_points().collect();

if (!using_path_tool && !using_select_tool)
|| (using_path_tool && selected_points.is_empty())
|| selected_layers.is_empty()
|| matches!(self.transform_operation, TransformOperation::Rotating(_))
{
selected.original_transforms.clear();
return;
}

// Don't allow rotate with no selected layers
if selected_layers.is_empty() {
let Some(vector_data) = selected_layers.first().and_then(|&layer| document.network_interface.compute_modified_vector(layer)) else {
selected.original_transforms.clear();
return;
};

if let [point] = selected_points.as_slice() {
if matches!(point, ManipulatorPointId::Anchor(_)) {
if let Some([handle1, handle2]) = point.get_handle_pair(&vector_data) {
let handle1_length = handle1.length(&vector_data);
let handle2_length = handle2.length(&vector_data);

if (handle1_length == 0. && handle2_length == 0.) || (handle1_length == f64::MAX && handle2_length == f64::MAX) {
return;
}
}
} else {
// TODO: Fix handle snap to anchor issue, see <https://discord.com/channels/731730685944922173/1217752903209713715>

let handle_length = point.as_handle().map(|handle| handle.length(&vector_data));

if handle_length == Some(0.) {
selected.original_transforms.clear();
return;
}
}
}

begin_operation(self.transform_operation, &mut self.typing, &mut self.mouse_position, &mut self.start_mouse);
Expand All @@ -136,13 +175,41 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
selected.original_transforms.clear();
}
TransformLayerMessage::BeginScale => {
if let TransformOperation::Scaling(_) = self.transform_operation {
let selected_points: Vec<&ManipulatorPointId> = shape_editor.selected_points().collect();

if (using_path_tool && selected_points.is_empty())
|| (!using_path_tool && !using_select_tool)
|| selected_layers.is_empty()
|| matches!(self.transform_operation, TransformOperation::Scaling(_))
{
selected.original_transforms.clear();
return;
}

// Don't allow scale with no selected layers
if selected_layers.is_empty() {
let Some(vector_data) = selected_layers.first().and_then(|&layer| document.network_interface.compute_modified_vector(layer)) else {
selected.original_transforms.clear();
return;
};

if let [point] = selected_points.as_slice() {
if matches!(point, ManipulatorPointId::Anchor(_)) {
if let Some([handle1, handle2]) = point.get_handle_pair(&vector_data) {
let handle1_length = handle1.length(&vector_data);
let handle2_length = handle2.length(&vector_data);

if (handle1_length == 0. && handle2_length == 0.) || (handle1_length == f64::MAX && handle2_length == f64::MAX) {
selected.original_transforms.clear();
return;
}
}
} else {
let handle_length = point.as_handle().map(|handle| handle.length(&vector_data));

if handle_length == Some(0.) {
selected.original_transforms.clear();
return;
}
}
}

begin_operation(self.transform_operation, &mut self.typing, &mut self.mouse_position, &mut self.start_mouse);
Expand Down Expand Up @@ -215,6 +282,7 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
}
};
}

self.mouse_position = input.mouse.position;
}
TransformLayerMessage::SelectionChanged => {
Expand Down
14 changes: 14 additions & 0 deletions node-graph/gcore/src/vector/vector_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,13 @@ impl ManipulatorPointId {
}
}

pub fn get_anchor_position(&self, vector_data: &VectorData) -> Option<DVec2> {
match self {
ManipulatorPointId::EndHandle(_) | ManipulatorPointId::PrimaryHandle(_) => self.get_anchor(vector_data).and_then(|id| vector_data.point_domain.position_from_id(id)),
_ => self.get_position(vector_data),
}
}

/// Attempt to get a pair of handles. For an anchor this is the first two handles connected. For a handle it is self and the first opposing handle.
#[must_use]
pub fn get_handle_pair(self, vector_data: &VectorData) -> Option<[HandleId; 2]> {
Expand Down Expand Up @@ -396,6 +403,13 @@ impl HandleId {
}
}

/// Calculate the magnitude of the handle from the anchor.
pub fn length(self, vector_data: &VectorData) -> f64 {
let anchor_position = self.to_manipulator_point().get_anchor_position(vector_data).unwrap();
let handle_position = self.to_manipulator_point().get_position(vector_data);
handle_position.map(|pos| (pos - anchor_position).length()).unwrap_or(f64::MAX)
}

/// Set the handle's position relative to the anchor which is the start anchor for the primary handle and end anchor for the end handle.
#[must_use]
pub fn set_relative_position(self, relative_position: DVec2) -> VectorModificationType {
Expand Down

0 comments on commit 2e4fb95

Please sign in to comment.