diff --git a/editor/src/messages/tool/common_functionality/shape_editor.rs b/editor/src/messages/tool/common_functionality/shape_editor.rs index e363f3db48..db61db5221 100644 --- a/editor/src/messages/tool/common_functionality/shape_editor.rs +++ b/editor/src/messages/tool/common_functionality/shape_editor.rs @@ -729,7 +729,9 @@ impl ShapeState { let length = transform.transform_vector2(unselected_position - anchor).length(); let position = transform.inverse().transform_vector2(direction * length); let modification_type = unselected_handle.set_relative_position(position); - responses.add(GraphOperationMessage::Vector { layer, modification_type }); + if (anchor - selected_position).length() > 1e-6 { + responses.add(GraphOperationMessage::Vector { layer, modification_type }); + } } // If both handles are selected, average the angles of the handles else { @@ -775,6 +777,7 @@ impl ShapeState { in_viewport_space: bool, was_alt_dragging: bool, opposite_handle_position: Option, + skip_opposite_handle: bool, responses: &mut VecDeque, ) { for (&layer, state) in &self.selected_shape_state { @@ -816,6 +819,11 @@ impl ShapeState { responses.add(GraphOperationMessage::Vector { layer, modification_type }); let Some(other) = vector_data.other_colinear_handle(handle) else { continue }; + + if skip_opposite_handle { + continue; + } + if state.is_selected(other.to_manipulator_point()) { // If two colinear handles are being dragged at the same time but not the anchor, it is necessary to break the colinear state. let handles = [handle, other]; diff --git a/editor/src/messages/tool/common_functionality/utility_functions.rs b/editor/src/messages/tool/common_functionality/utility_functions.rs index 9e0b2700b4..47800a26b7 100644 --- a/editor/src/messages/tool/common_functionality/utility_functions.rs +++ b/editor/src/messages/tool/common_functionality/utility_functions.rs @@ -4,7 +4,7 @@ use crate::messages::tool::common_functionality::graph_modification_utils::get_t use glam::DVec2; use graphene_core::renderer::Quad; use graphene_core::text::{FontCache, load_face}; -use graphene_std::vector::PointId; +use graphene_std::vector::{ManipulatorPointId, PointId, SegmentId, VectorData}; /// Determines if a path should be extended. Goal in viewport space. Returns the path and if it is extending from the start, if applicable. pub fn should_extend( @@ -66,3 +66,30 @@ pub fn text_bounding_box(layer: LayerNodeIdentifier, document: &DocumentMessageH Quad::from_box([DVec2::ZERO, far]) } + +pub fn calculate_segment_angle(anchor: PointId, segment: SegmentId, vector_data: &VectorData, pen_tool: bool) -> Option { + let is_start = |point: PointId, segment: SegmentId| vector_data.segment_start_from_id(segment) == Some(point); + let anchor_position = vector_data.point_domain.position_from_id(anchor)?; + let end_handle = ManipulatorPointId::EndHandle(segment).get_position(vector_data); + let start_handle = ManipulatorPointId::PrimaryHandle(segment).get_position(vector_data); + + let start_point = if is_start(anchor, segment) { + vector_data.segment_end_from_id(segment).and_then(|id| vector_data.point_domain.position_from_id(id)) + } else { + vector_data.segment_start_from_id(segment).and_then(|id| vector_data.point_domain.position_from_id(id)) + }; + + let required_handle = if is_start(anchor, segment) { + start_handle + .filter(|&handle| pen_tool && handle != anchor_position) + .or(end_handle.filter(|&handle| Some(handle) != start_point)) + .or(start_point) + } else { + end_handle + .filter(|&handle| pen_tool && handle != anchor_position) + .or(start_handle.filter(|&handle| Some(handle) != start_point)) + .or(start_point) + }; + + required_handle.map(|handle| -(handle - anchor_position).angle_to(DVec2::X)) +} diff --git a/editor/src/messages/tool/tool_messages/path_tool.rs b/editor/src/messages/tool/tool_messages/path_tool.rs index 537f3fd091..3954bf96f6 100644 --- a/editor/src/messages/tool/tool_messages/path_tool.rs +++ b/editor/src/messages/tool/tool_messages/path_tool.rs @@ -15,6 +15,7 @@ use crate::messages::tool::common_functionality::shape_editor::{ ClosestSegment, ManipulatorAngle, OpposingHandleLengths, SelectedPointsInfo, SelectionChange, SelectionShape, SelectionShapeType, ShapeState, }; use crate::messages::tool::common_functionality::snapping::{SnapCache, SnapCandidatePoint, SnapConstraint, SnapData, SnapManager}; +use crate::messages::tool::common_functionality::utility_functions::calculate_segment_angle; use graphene_core::renderer::Quad; use graphene_core::vector::{ManipulatorPointId, PointId, VectorModificationType}; use graphene_std::vector::{HandleId, NoHashBuilder, SegmentId, VectorData}; @@ -380,6 +381,8 @@ struct PathToolData { snapping_axis: Option, alt_clicked_on_anchor: bool, alt_dragging_from_anchor: bool, + angle_locked: bool, + temporary_colinear_handles: bool, } impl PathToolData { @@ -727,11 +730,38 @@ impl PathToolData { Some((handle_position_document, anchor_position_document, handle_id)) } - fn calculate_handle_angle(&mut self, handle_vector: DVec2, handle_id: ManipulatorPointId, lock_angle: bool, snap_angle: bool) -> f64 { + #[allow(clippy::too_many_arguments)] + fn calculate_handle_angle( + &mut self, + shape_editor: &mut ShapeState, + document: &DocumentMessageHandler, + responses: &mut VecDeque, + relative_vector: DVec2, + handle_vector: DVec2, + handle_id: ManipulatorPointId, + lock_angle: bool, + snap_angle: bool, + ) -> f64 { let current_angle = -handle_vector.angle_to(DVec2::X); + if let Some(vector_data) = shape_editor + .selected_shape_state + .iter() + .next() + .and_then(|(layer, _)| document.network_interface.compute_modified_vector(*layer)) + { + if relative_vector.length() < 25. && lock_angle && !self.angle_locked { + if let Some(angle) = calculate_lock_angle(self, shape_editor, responses, document, &vector_data, handle_id) { + self.angle = angle; + return angle; + } + } + } + // When the angle is locked we use the old angle + if self.current_selected_handle_id == Some(handle_id) && lock_angle { + self.angle_locked = true; return self.angle; } @@ -785,7 +815,7 @@ impl PathToolData { let drag_start = self.drag_start_pos; let opposite_delta = drag_start - current_mouse; - shape_editor.move_selected_points(None, document, opposite_delta, false, true, false, None, responses); + shape_editor.move_selected_points(None, document, opposite_delta, false, true, false, None, false, responses); // Calculate the projected delta and shift the points along that delta let delta = current_mouse - drag_start; @@ -797,7 +827,7 @@ impl PathToolData { _ => DVec2::new(delta.x, 0.), }; - shape_editor.move_selected_points(None, document, projected_delta, false, true, false, None, responses); + shape_editor.move_selected_points(None, document, projected_delta, false, true, false, None, false, responses); } fn stop_snap_along_axis(&mut self, shape_editor: &mut ShapeState, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque) { @@ -813,12 +843,12 @@ impl PathToolData { _ => DVec2::new(opposite_delta.x, 0.), }; - shape_editor.move_selected_points(None, document, opposite_projected_delta, false, true, false, None, responses); + shape_editor.move_selected_points(None, document, opposite_projected_delta, false, true, false, None, false, responses); // Calculate what actually would have been the original delta for the point, and apply that let delta = current_mouse - drag_start; - shape_editor.move_selected_points(None, document, delta, false, true, false, None, responses); + shape_editor.move_selected_points(None, document, delta, false, true, false, None, false, responses); self.snapping_axis = None; } @@ -872,7 +902,7 @@ impl PathToolData { let snapped_delta = if let Some((handle_pos, anchor_pos, handle_id)) = self.try_get_selected_handle_and_anchor(shape_editor, document) { let cursor_pos = handle_pos + raw_delta; - let handle_angle = self.calculate_handle_angle(cursor_pos - anchor_pos, handle_id, lock_angle, snap_angle); + let handle_angle = self.calculate_handle_angle(shape_editor, document, responses, handle_pos - anchor_pos, cursor_pos - anchor_pos, handle_id, lock_angle, snap_angle); let constrained_direction = DVec2::new(handle_angle.cos(), handle_angle.sin()); let projected_length = (cursor_pos - anchor_pos).dot(constrained_direction); @@ -931,7 +961,14 @@ impl PathToolData { self.alt_dragging_from_anchor = false; self.alt_clicked_on_anchor = false; } - shape_editor.move_selected_points(handle_lengths, document, snapped_delta, equidistant, true, was_alt_dragging, opposite, responses); + + let mut skip_opposite = false; + if self.temporary_colinear_handles && !lock_angle { + shape_editor.disable_colinear_handles_state_on_selected(&document.network_interface, responses); + self.temporary_colinear_handles = false; + skip_opposite = true; + } + shape_editor.move_selected_points(handle_lengths, document, snapped_delta, equidistant, true, was_alt_dragging, opposite, skip_opposite, responses); self.previous_mouse_position += document_to_viewport.inverse().transform_vector2(snapped_delta); } else { let Some(axis) = self.snapping_axis else { return }; @@ -940,7 +977,7 @@ impl PathToolData { Axis::Y => DVec2::new(0., unsnapped_delta.y), _ => DVec2::new(unsnapped_delta.x, 0.), }; - shape_editor.move_selected_points(handle_lengths, document, projected_delta, equidistant, true, false, opposite, responses); + shape_editor.move_selected_points(handle_lengths, document, projected_delta, equidistant, true, false, opposite, false, responses); self.previous_mouse_position += document_to_viewport.inverse().transform_vector2(unsnapped_delta); } @@ -1238,6 +1275,10 @@ impl Fsm for PathToolFsmState { let lock_angle_state = input.keyboard.get(lock_angle as usize); let snap_angle_state = input.keyboard.get(snap_angle as usize); + if !lock_angle_state { + tool_data.angle_locked = false; + } + if !tool_data.update_colinear(equidistant_state, toggle_colinear_state, tool_action_data.shape_editor, tool_action_data.document, responses) { tool_data.drag( equidistant_state, @@ -1412,6 +1453,10 @@ impl Fsm for PathToolFsmState { } } + if tool_data.temporary_colinear_handles { + tool_data.temporary_colinear_handles = false; + } + if tool_data.handle_drag_toggle && drag_occurred { shape_editor.deselect_all_points(); shape_editor.select_points_by_manipulator_id(&tool_data.saved_points_before_handle_drag); @@ -1511,6 +1556,7 @@ impl Fsm for PathToolFsmState { false, false, tool_data.opposite_handle_position, + false, responses, ); @@ -1749,3 +1795,52 @@ fn get_selection_status(network_interface: &NodeNetworkInterface, shape_state: & SelectionStatus::None } + +fn calculate_lock_angle( + tool_data: &mut PathToolData, + shape_state: &mut ShapeState, + responses: &mut VecDeque, + document: &DocumentMessageHandler, + vector_data: &VectorData, + handle_id: ManipulatorPointId, +) -> Option { + let anchor = handle_id.get_anchor(vector_data)?; + let anchor_position = vector_data.point_domain.position_from_id(anchor); + let current_segment = handle_id.get_segment(); + let points_connected = vector_data.connected_count(anchor); + + let (anchor_position, segment) = anchor_position.zip(current_segment)?; + if points_connected == 1 { + calculate_segment_angle(anchor, segment, vector_data, false) + } else { + let opposite_handle = handle_id + .get_handle_pair(vector_data) + .iter() + .flatten() + .find(|&h| h.to_manipulator_point() != handle_id) + .copied() + .map(|h| h.to_manipulator_point()); + let opposite_handle_position = opposite_handle.and_then(|h| h.get_position(vector_data)).filter(|pos| (pos - anchor_position).length() > 1e-6); + + if let Some(opposite_pos) = opposite_handle_position { + if !vector_data.colinear_manipulators.iter().flatten().map(|h| h.to_manipulator_point()).any(|h| h == handle_id) { + shape_state.convert_selected_manipulators_to_colinear_handles(responses, document); + tool_data.temporary_colinear_handles = true; + } + Some(-(opposite_pos - anchor_position).angle_to(DVec2::X)) + } else { + let angle_1 = vector_data + .adjacent_segment(&handle_id) + .and_then(|(_, adjacent_segment)| calculate_segment_angle(anchor, adjacent_segment, vector_data, false)); + + let angle_2 = calculate_segment_angle(anchor, segment, vector_data, false); + + match (angle_1, angle_2) { + (Some(angle_1), Some(angle_2)) => Some((angle_1 + angle_2) / 2.0), + (Some(angle_1), None) => Some(angle_1), + (None, Some(angle_2)) => Some(angle_2), + (None, None) => None, + } + } + } +} diff --git a/editor/src/messages/tool/tool_messages/pen_tool.rs b/editor/src/messages/tool/tool_messages/pen_tool.rs index 0f9e7cc0c2..a7e4eebed3 100644 --- a/editor/src/messages/tool/tool_messages/pen_tool.rs +++ b/editor/src/messages/tool/tool_messages/pen_tool.rs @@ -10,7 +10,7 @@ use crate::messages::tool::common_functionality::color_selector::{ToolColorOptio use crate::messages::tool::common_functionality::graph_modification_utils::{self, merge_layers}; use crate::messages::tool::common_functionality::shape_editor::ShapeState; use crate::messages::tool::common_functionality::snapping::{SnapCache, SnapCandidatePoint, SnapConstraint, SnapData, SnapManager, SnapTypeConfiguration}; -use crate::messages::tool::common_functionality::utility_functions::{closest_point, should_extend}; +use crate::messages::tool::common_functionality::utility_functions::{calculate_segment_angle, closest_point, should_extend}; use bezier_rs::{Bezier, BezierHandles}; use graph_craft::document::NodeId; use graphene_core::Color; @@ -1295,31 +1295,8 @@ impl PenToolData { match (self.handle_type, self.path_closed) { (TargetHandle::FuturePreviewOutHandle, _) | (TargetHandle::PreviewInHandle, true) => { - let is_start = |point: PointId, segment: SegmentId| vector_data.segment_start_from_id(segment) == Some(point); - - let end_handle = ManipulatorPointId::EndHandle(segment).get_position(vector_data); - let start_handle = ManipulatorPointId::PrimaryHandle(segment).get_position(vector_data); - - let start_point = if is_start(anchor, segment) { - vector_data.segment_end_from_id(segment).and_then(|id| vector_data.point_domain.position_from_id(id)) - } else { - vector_data.segment_start_from_id(segment).and_then(|id| vector_data.point_domain.position_from_id(id)) - }; - - let required_handle = if is_start(anchor, segment) { - start_handle - .filter(|&handle| handle != anchor_position) - .or(end_handle.filter(|&handle| Some(handle) != start_point)) - .or(start_point) - } else { - end_handle - .filter(|&handle| handle != anchor_position) - .or(start_handle.filter(|&handle| Some(handle) != start_point)) - .or(start_point) - }; - - if let Some(required_handle) = required_handle { - self.angle = -(required_handle - anchor_position).angle_to(DVec2::X); + if let Some(required_handle) = calculate_segment_angle(anchor, segment, vector_data, true) { + self.angle = required_handle; self.handle_mode = HandleMode::ColinearEquidistant; } } @@ -1332,8 +1309,6 @@ impl PenToolData { self.handle_mode = HandleMode::ColinearEquidistant; } } - - // Closure to check if a point is the start or end of a segment } fn add_point_layer_position(&mut self, document: &DocumentMessageHandler, responses: &mut VecDeque, layer: LayerNodeIdentifier, viewport: DVec2) { diff --git a/node-graph/gcore/src/vector/vector_data.rs b/node-graph/gcore/src/vector/vector_data.rs index 7d78242144..aba89db824 100644 --- a/node-graph/gcore/src/vector/vector_data.rs +++ b/node-graph/gcore/src/vector/vector_data.rs @@ -561,6 +561,13 @@ impl ManipulatorPointId { _ => None, } } + + pub fn get_segment(self) -> Option { + match self { + ManipulatorPointId::PrimaryHandle(segment) | ManipulatorPointId::EndHandle(segment) => Some(segment), + _ => None, + } + } } /// The type of handle found on a bézier curve.