diff --git a/editor/src/messages/tool/common_functionality/shape_editor.rs b/editor/src/messages/tool/common_functionality/shape_editor.rs index b32b1fa7..0fdbc07e 100644 --- a/editor/src/messages/tool/common_functionality/shape_editor.rs +++ b/editor/src/messages/tool/common_functionality/shape_editor.rs @@ -212,6 +212,114 @@ impl ShapeEditor { } } + /// The opposing handle lengths. + pub fn opposing_handle_lengths(&self, document: &Document) -> HashMap, HashMap> { + self.selected_layers() + .iter() + .filter_map(|path| document.layer(path).ok().map(|layer| (path, layer))) + .filter_map(|(path, shape)| shape.as_subpath().map(|subpath| (path, subpath))) + .map(|(path, shape)| { + let opposing_handle_lengths = shape + .manipulator_groups() + .enumerate() + .filter_map(|(id, manipulator_group)| { + // We will keep track of the opposing handle length when: + // i) Both handles exist and exactly one is selected. + // ii) The anchor is not selected. + // iii) We have to mirror the angle between handles. + + if !manipulator_group.editor_state.mirror_angle_between_handles { + return None; + } + + let mut selected_handles = manipulator_group.selected_handles(); + let handle = selected_handles.next()?; + + // Check that handle is the only selected handle. + if selected_handles.next().is_none() { + let opposing_handle_position = manipulator_group.opposing_handle(handle)?.position; + let anchor = manipulator_group.points[ManipulatorType::Anchor].as_ref()?; + if !anchor.is_selected() { + let opposing_handle_length = opposing_handle_position.distance(anchor.position); + Some((*id, opposing_handle_length)) + } else { + None + } + } else { + None + } + }) + .collect::>(); + (path.clone(), opposing_handle_lengths) + }) + .collect::>() + } + + /// Reset the opposing handle lengths. + pub fn reset_opposing_handle_lengths(&self, document: &Document, opposing_handle_lengths: &HashMap, HashMap>, responses: &mut VecDeque) { + self.selected_layers() + .iter() + .filter_map(|path| document.layer(path).ok().map(|layer| (path, layer))) + .filter_map(|(path, shape)| shape.as_subpath().map(|subpath| (path, subpath))) + .filter_map(|(path, shape)| opposing_handle_lengths.get(path).map(|layer_opposing_handle_lengths| (path, shape, layer_opposing_handle_lengths))) + .flat_map(|(path, shape, layer_opposing_handle_lengths)| { + shape + .manipulator_groups() + .enumerate() + .map(move |(id, manipulator_group)| (path, layer_opposing_handle_lengths, id, manipulator_group)) + }) + .for_each(|(path, layer_opposing_handle_lengths, id, manipulator_group)| { + if !manipulator_group.editor_state.mirror_angle_between_handles { + return; + } + + let opposing_handle_length = if let Some(length) = layer_opposing_handle_lengths.get(id) { + length + } else { + return; + }; + + let mut selected_handles = manipulator_group.selected_handles(); + let handle = if let Some(handle) = selected_handles.next() { + handle + } else { + return; + }; + + // Check that handle is the only selected handle. + if selected_handles.next().is_none() { + let opposing_handle = if let Some(opposing_handle) = manipulator_group.opposing_handle(handle) { + opposing_handle + } else { + return; + }; + + let anchor = if let Some(anchor) = manipulator_group.points[ManipulatorType::Anchor].as_ref() { + anchor + } else { + return; + }; + if anchor.is_selected() { + return; + } + + if let Some(offset) = (opposing_handle.position - anchor.position).try_normalize() { + let new_opposing_handle_position = anchor.position + offset * (*opposing_handle_length); + assert!(new_opposing_handle_position.is_finite(), "Opposing handle not finite!"); + responses.push_back( + Operation::MoveManipulatorPoint { + layer_path: path.clone(), + id: *id, + manipulator_type: opposing_handle.manipulator_type, + position: new_opposing_handle_position.into(), + } + .into(), + ); + } + } + }); + } + /// Dissolve the selected points. pub fn delete_selected_points(&self, responses: &mut VecDeque) { responses.push_back(DocumentMessage::DeleteSelectedManipulatorPoints.into()); diff --git a/editor/src/messages/tool/tool_messages/path_tool.rs b/editor/src/messages/tool/tool_messages/path_tool.rs index 74bc5a0d..021548e5 100644 --- a/editor/src/messages/tool/tool_messages/path_tool.rs +++ b/editor/src/messages/tool/tool_messages/path_tool.rs @@ -9,6 +9,7 @@ use crate::messages::tool::common_functionality::snapping::SnapManager; use crate::messages::tool::utility_types::{EventToMessageMap, Fsm, HintData, HintGroup, HintInfo, ToolActionHandlerData, ToolMetadata, ToolTransition, ToolType}; use document_legacy::intersection::Quad; +use document_legacy::LayerId; use graphene_std::vector::consts::ManipulatorType; use glam::DVec2; @@ -112,6 +113,7 @@ struct PathToolData { drag_start_pos: DVec2, previous_mouse_position: DVec2, alt_debounce: bool, + opposing_handle_lengths: Option, HashMap>>, } impl Fsm for PathToolFsmState { @@ -142,6 +144,7 @@ impl Fsm for PathToolFsmState { tool_data.overlay_renderer.render_subpath_overlays(&document.document_legacy, layer_path.to_vec(), responses); } + tool_data.opposing_handle_lengths = None; // This can happen in any state (which is why we return self) self } @@ -158,6 +161,8 @@ impl Fsm for PathToolFsmState { (_, PathToolMessage::DragStart { add_to_selection }) => { let shift_pressed = input.keyboard.get(add_to_selection as usize); + tool_data.opposing_handle_lengths = None; + // Select the first point within the threshold (in pixels) if let Some(mut selected_points) = tool_data .shape_editor @@ -240,6 +245,7 @@ impl Fsm for PathToolFsmState { tool_data.alt_debounce = alt_pressed; // Only on alt down if alt_pressed { + tool_data.opposing_handle_lengths = None; tool_data.shape_editor.toggle_handle_mirroring_on_selected(true, responses); } } @@ -247,6 +253,17 @@ impl Fsm for PathToolFsmState { // Determine when shift state changes let shift_pressed = input.keyboard.get(shift_mirror_distance as usize); + if shift_pressed { + if tool_data.opposing_handle_lengths.is_none() { + tool_data.opposing_handle_lengths = Some(tool_data.shape_editor.opposing_handle_lengths(&document.document_legacy)); + } + } else { + if let Some(opposing_handle_lengths) = &tool_data.opposing_handle_lengths { + tool_data.shape_editor.reset_opposing_handle_lengths(&document.document_legacy, opposing_handle_lengths, responses); + tool_data.opposing_handle_lengths = None; + } + } + // Move the selected points by the mouse position let snapped_position = tool_data.snap_manager.snap_position(responses, document, input.mouse.position); tool_data diff --git a/node-graph/gcore/src/vector/manipulator_group.rs b/node-graph/gcore/src/vector/manipulator_group.rs index b20a6668..1c2baddf 100644 --- a/node-graph/gcore/src/vector/manipulator_group.rs +++ b/node-graph/gcore/src/vector/manipulator_group.rs @@ -146,6 +146,7 @@ impl ManipulatorGroup { // If the anchor isn't selected, but both handles are, drag only handles if self.both_handles_selected() { + self.editor_state.mirror_angle_between_handles = false; for point in self.selected_handles_mut() { move_point(point, delta); }