Skip to content

Add Path tool support for G/R/S rotation and scaling with a single selected handle #2180

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jan 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading