diff --git a/editor/src/messages/tool/common_functionality/utility_functions.rs b/editor/src/messages/tool/common_functionality/utility_functions.rs index 61b300a183..1c6a508e2e 100644 --- a/editor/src/messages/tool/common_functionality/utility_functions.rs +++ b/editor/src/messages/tool/common_functionality/utility_functions.rs @@ -1,7 +1,10 @@ +use core::f64; + use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::prelude::*; use crate::messages::tool::common_functionality::graph_modification_utils::get_text; use crate::messages::tool::tool_messages::path_tool::PathOverlayMode; +use bezier_rs::Bezier; use glam::DVec2; use graphene_core::renderer::Quad; use graphene_core::text::{FontCache, load_face}; @@ -137,3 +140,118 @@ pub fn is_visible_point( } } } + +/// Calculates similarity metric between new bezier curve and two old beziers by using sampled points +pub fn log_optimization(a: f64, b: f64, p1: DVec2, p3: DVec2, d1: DVec2, d2: DVec2, points1: &Vec, n: usize) -> f64 { + let start_handle_length = a.exp(); + let end_handle_length = b.exp(); + + // Calculate the similairty somehow using the new bezier curve + let c1: DVec2 = p1 + d1 * start_handle_length; + let c2: DVec2 = p3 + d2 * end_handle_length; + + let new_curve = Bezier::from_cubic_coordinates(p1.x, p1.y, c1.x, c1.y, c2.x, c2.y, p3.x, p3.y); + + let points = new_curve.compute_lookup_table(Some(2 * n), None).collect::>(); + + let dist = points1.iter().zip(points.iter()).map(|(p1, p2)| (p1.x - p2.x).powi(2) + (p1.y - p2.y).powi(2)).sum::(); + let dist = dist / (2. * (n as f64)); + + dist +} + +/// Calculates the handles lengths for a bezier curve with fixed handle directions and passing through a given point p2 with parameter t +pub fn calculate_curve_for_given_t(t: f64, p1: DVec2, p2: DVec2, p3: DVec2, d1: DVec2, d2: DVec2) -> (f64, f64) { + let a = 3. * (1. - t).powi(2) * t; + let b = 3. * (1. - t) * t.powi(2); + + let rx = p2.x - ((1. - t).powi(3) + 3. * (1. - t).powi(2) * t) * p1.x - (3. * (1. - t) * t.powi(2) + t.powi(3)) * p3.x; + let ry = p2.y - ((1. - t).powi(3) + 3. * (1. - t).powi(2) * t) * p1.y - (3. * (1. - t) * t.powi(2) + t.powi(3)) * p3.y; + + let cross_product = d1.x * d2.y - d1.y * d2.x; + let det = a * b * cross_product; + + let start_handle_length = (rx * b * d2.y - ry * b * d2.x) / det; + let end_handle_length = (ry * a * d1.x - rx * a * d1.y) / det; + + (start_handle_length, end_handle_length) +} + +// Calculate optimal handle lengths with adam optimization +pub fn find_two_param_best_approximate(p1: DVec2, p3: DVec2, d1: DVec2, d2: DVec2, min_len1: f64, min_len2: f64, farther_segment: Bezier, other_segment: Bezier) -> (DVec2, DVec2) { + let h = 1e-6; + let tol = 1e-6; + let max_iter = 200; + + let mut a = (5.0f64).ln(); + let mut b = (5.0f64).ln(); + + let mut m_a = 0.0; + let mut v_a = 0.0; + let mut m_b = 0.0; + let mut v_b = 0.0; + + let initial_alpha = 0.05; + let decay_rate: f64 = 0.99; + + let beta1 = 0.9; + let beta2 = 0.999; + let epsilon = 1e-8; + + let n = 20; + + let farther_segment = if !(farther_segment.start.distance(p1) < f64::EPSILON) { + farther_segment.reverse() + } else { + farther_segment + }; + + let other_segment = if !(other_segment.end.distance(p3) < f64::EPSILON) { + other_segment.reverse() + } else { + other_segment + }; + + //Now we sample points proportional to the lengths of the beziers + let l1 = farther_segment.length(None); + let l2 = other_segment.length(None); + let ratio = l1 / (l1 + l2); + let n_points1 = ((2 * n) as f64 * ratio).floor() as usize; + let mut points1 = farther_segment.compute_lookup_table(Some(n_points1), None).collect::>(); + let mut points2 = other_segment.compute_lookup_table(Some(n), None).collect::>(); + points1.append(&mut points2); + + let f = |a: f64, b: f64| -> f64 { log_optimization(a, b, p1, p3, d1, d2, &points1, n) }; + + for t in 1..=max_iter { + let dfa = (f(a + h, b) - f(a - h, b)) / (2.0 * h); + let dfb = (f(a, b + h) - f(a, b - h)) / (2.0 * h); + + m_a = beta1 * m_a + (1.0 - beta1) * dfa; + m_b = beta1 * m_b + (1.0 - beta1) * dfb; + + v_a = beta2 * v_a + (1.0 - beta2) * dfa * dfa; + v_b = beta2 * v_b + (1.0 - beta2) * dfb * dfb; + + let m_a_hat = m_a / (1.0 - beta1.powi(t)); + let v_a_hat = v_a / (1.0 - beta2.powi(t)); + let m_b_hat = m_b / (1.0 - beta1.powi(t)); + let v_b_hat = v_b / (1.0 - beta2.powi(t)); + + let alpha_t = initial_alpha * decay_rate.powi(t); + + // Update log-lengths + a -= alpha_t * m_a_hat / (v_a_hat.sqrt() + epsilon); + b -= alpha_t * m_b_hat / (v_b_hat.sqrt() + epsilon); + + // Convergence check + if dfa.abs() < tol && dfb.abs() < tol { + break; + } + } + + let len1 = a.exp().max(min_len1); + let len2 = b.exp().max(min_len2); + + (d1 * len1, d2 * len2) +} diff --git a/editor/src/messages/tool/tool_messages/path_tool.rs b/editor/src/messages/tool/tool_messages/path_tool.rs index 1dfd7f837d..39707a9742 100644 --- a/editor/src/messages/tool/tool_messages/path_tool.rs +++ b/editor/src/messages/tool/tool_messages/path_tool.rs @@ -15,7 +15,8 @@ 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 crate::messages::tool::common_functionality::utility_functions::{calculate_segment_angle, find_two_param_best_approximate}; +use bezier_rs::{Bezier, TValue}; use graphene_core::renderer::Quad; use graphene_core::vector::{ManipulatorPointId, PointId, VectorModificationType}; use graphene_std::vector::{HandleId, NoHashBuilder, SegmentId, VectorData}; @@ -306,6 +307,10 @@ impl<'a> MessageHandler> for PathToo Escape, RightClick, ), + PathToolFsmState::SlidingPoint => actions!(PathToolMessageDiscriminant; + PointerMove, + DragStop, + ), } } } @@ -334,6 +339,20 @@ pub enum PointSelectState { Anchor, } +#[derive(Clone, Copy)] +pub struct SlidingSegmentData { + segment_id: SegmentId, + bezier: Bezier, + start: PointId, +} + +#[derive(Clone, Copy)] +pub struct SlidingPointInfo { + anchor: PointId, + layer: LayerNodeIdentifier, + connected_segments: [SlidingSegmentData; 2], +} + #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] enum PathToolFsmState { #[default] @@ -342,6 +361,7 @@ enum PathToolFsmState { Drawing { selection_shape: SelectionShapeType, }, + SlidingPoint, } #[derive(Default)] @@ -381,6 +401,7 @@ struct PathToolData { temporary_colinear_handles: bool, frontier_handles_info: Option>>, adjacent_anchor_offset: Option, + sliding_point_info: Option, } impl PathToolData { @@ -897,12 +918,151 @@ impl PathToolData { tangent_vector.try_normalize() } + fn start_sliding_point(&mut self, shape_editor: &mut ShapeState, document: &DocumentMessageHandler) -> bool { + let single_anchor_selected = shape_editor.selected_points().count() == 1 && shape_editor.selected_points().any(|point| matches!(point, ManipulatorPointId::Anchor(_))); + + if single_anchor_selected { + let Some(anchor) = shape_editor.selected_points().next() else { return false }; + let Some(layer) = document.network_interface.selected_nodes().selected_layers(document.metadata()).next() else { + return false; + }; + let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else { + return false; + }; + + // Check that the handles of anchor point are also colinear + if !vector_data.colinear(*anchor) { + return false; + }; + + let Some(pointid) = anchor.as_anchor() else { return false }; + + let mut segments_vec = vec![]; + for (segment, bezier, start, end) in vector_data.segment_bezier_iter() { + if start == pointid || end == pointid { + segments_vec.push(SlidingSegmentData { segment_id: segment, bezier, start }); + } + } + + if segments_vec.iter().count() != 2 { + warn!("expected exactly two connected segments"); + return false; + } + + let connected_segments = [segments_vec[0], segments_vec[1]]; + + self.sliding_point_info = Some(SlidingPointInfo { + anchor: pointid, + layer, + connected_segments, + }); + return true; + } + false + } + + fn slide_point(&mut self, target_position: DVec2, responses: &mut VecDeque, network_interface: &NodeNetworkInterface, shape_editor: &ShapeState) { + let Some(sliding_point_info) = self.sliding_point_info else { return }; + let anchor = sliding_point_info.anchor; + // let initial_position = sliding_point_info.initial_position; + let layer = sliding_point_info.layer; + + let Some(vector_data) = network_interface.compute_modified_vector(layer) else { return }; + let transform = network_interface.document_metadata().transform_to_viewport(layer); + let layer_pos = transform.inverse().transform_point2(target_position); + + let segments = sliding_point_info.connected_segments; + + let t1 = segments[0].bezier.project(layer_pos); + let position1 = segments[0].bezier.evaluate(TValue::Parametric(t1)); + + let t2 = segments[1].bezier.project(layer_pos); + let position2 = segments[1].bezier.evaluate(TValue::Parametric(t2)); + + let (closer_segment, farther_segment, t_value, new_position) = if position2.distance(layer_pos) < position1.distance(layer_pos) { + (segments[1], segments[0], t2, position2) + } else { + (segments[0], segments[1], t1, position1) + }; + + // Move the anchor to the new position + let Some(current_position) = ManipulatorPointId::Anchor(anchor).get_position(&vector_data) else { + return; + }; + let delta = new_position - current_position; + + shape_editor.move_anchor(anchor, &vector_data, delta, layer, None, responses); + + // Make a split at the t_value + let [first, second] = closer_segment.bezier.split(TValue::Parametric(t_value)); + let closer_segment_other_point = if anchor == closer_segment.start { closer_segment.bezier.end } else { closer_segment.bezier.start }; + + let (split_segment, other_segment) = if first.start == closer_segment_other_point { (first, second) } else { (second, first) }; + + // Primary handle maps to primary handle and secondary maps to secondary + let closer_primary_handle = HandleId::primary(closer_segment.segment_id); + let Some(handle_position) = split_segment.handle_start() else { return }; + let relative_position1 = handle_position - split_segment.start; + let modification_type = closer_primary_handle.set_relative_position(relative_position1); + responses.add(GraphOperationMessage::Vector { layer, modification_type }); + + let closer_secondary_handle = HandleId::end(closer_segment.segment_id); + let Some(handle_position) = split_segment.handle_end() else { return }; + let relative_position2 = handle_position - split_segment.end; + let modification_type = closer_secondary_handle.set_relative_position(relative_position2); + responses.add(GraphOperationMessage::Vector { layer, modification_type }); + + let end_handle_direction = if anchor == closer_segment.start { -1. * relative_position1 } else { -1. * relative_position2 }; + + let (farther_other_point, start_handle, end_handle, start_handle_pos) = if anchor == farther_segment.start { + ( + farther_segment.bezier.end, + HandleId::end(farther_segment.segment_id), + HandleId::primary(farther_segment.segment_id), + farther_segment.bezier.handle_end(), + ) + } else { + ( + farther_segment.bezier.start, + HandleId::primary(farther_segment.segment_id), + HandleId::end(farther_segment.segment_id), + farther_segment.bezier.handle_start(), + ) + }; + let Some(start_handle_position) = start_handle_pos else { return }; + let start_handle_direction = start_handle_position - farther_other_point; + + // Get normalized direction vectors, if cubic handle is zero then we consider corresponding tangent + let d1 = start_handle_direction.try_normalize().unwrap_or({ + if anchor == farther_segment.start { + -1. * farther_segment.bezier.tangent(TValue::Parametric(0.99)) + } else { + farther_segment.bezier.tangent(TValue::Parametric(0.01)) + } + }); + + let d2 = end_handle_direction.try_normalize().unwrap_or_default(); + + let min_len1 = start_handle_direction.length() * 0.4; + let min_len2 = end_handle_direction.length() * 0.4; + + let (relative_pos1, relative_pos2) = find_two_param_best_approximate(farther_other_point, new_position, d1, d2, min_len1, min_len2, farther_segment.bezier, other_segment); + + // Now set those handles to these handle lengths keeping the directions d1, d2 + let modification_type = start_handle.set_relative_position(relative_pos1); + responses.add(GraphOperationMessage::Vector { layer, modification_type }); + + let modification_type = end_handle.set_relative_position(relative_pos2); + responses.add(GraphOperationMessage::Vector { layer, modification_type }); + } + #[allow(clippy::too_many_arguments)] fn drag( &mut self, equidistant: bool, lock_angle: bool, snap_angle: bool, + snap_axis: bool, shape_editor: &mut ShapeState, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, @@ -915,9 +1075,10 @@ impl PathToolData { .selected_points() .any(|point| matches!(point, ManipulatorPointId::EndHandle(_) | ManipulatorPointId::PrimaryHandle(_))); - if snap_angle && self.snapping_axis.is_none() && !single_handle_selected { + // This is where it starts snapping along axis + if snap_axis && self.snapping_axis.is_none() && !single_handle_selected { self.start_snap_along_axis(shape_editor, document, input, responses); - } else if !snap_angle && self.snapping_axis.is_some() { + } else if !snap_axis && self.snapping_axis.is_some() { self.stop_snap_along_axis(shape_editor, document, input, responses); } @@ -1027,7 +1188,8 @@ impl PathToolData { self.previous_mouse_position += document_to_viewport.inverse().transform_vector2(unsnapped_delta); } - if snap_angle && self.snapping_axis.is_some() { + // Constantly checking and changing the snapping axis based on current mouse position + if snap_axis && self.snapping_axis.is_some() { let Some(current_axis) = self.snapping_axis else { return }; let total_delta = self.drag_start_pos - input.mouse.position; @@ -1195,6 +1357,7 @@ impl Fsm for PathToolFsmState { } } } + Self::SlidingPoint => {} } responses.add(PathToolMessage::SelectedPointUpdated); @@ -1336,10 +1499,17 @@ impl Fsm for PathToolFsmState { } if !tool_data.update_colinear(equidistant_state, toggle_colinear_state, tool_action_data.shape_editor, tool_action_data.document, responses) { + if snap_angle_state && lock_angle_state { + if tool_data.start_sliding_point(tool_action_data.shape_editor, &tool_action_data.document) { + return PathToolFsmState::SlidingPoint; + } + } + tool_data.drag( equidistant_state, lock_angle_state, snap_angle_state, + snap_angle_state, tool_action_data.shape_editor, tool_action_data.document, input, @@ -1372,6 +1542,10 @@ impl Fsm for PathToolFsmState { PathToolFsmState::Dragging(tool_data.dragging_state) } + (PathToolFsmState::SlidingPoint, PathToolMessage::PointerMove { .. }) => { + tool_data.slide_point(input.mouse.position, responses, &document.network_interface, &shape_editor); + PathToolFsmState::SlidingPoint + } (PathToolFsmState::Ready, PathToolMessage::PointerMove { delete_segment, .. }) => { tool_data.delete_segment_pressed = input.keyboard.get(delete_segment as usize); @@ -1524,6 +1698,12 @@ impl Fsm for PathToolFsmState { tool_data.snap_manager.cleanup(responses); PathToolFsmState::Ready } + (PathToolFsmState::SlidingPoint, PathToolMessage::Escape | PathToolMessage::RightClick) => { + tool_data.sliding_point_info = None; + responses.add(DocumentMessage::AbortTransaction); + tool_data.snap_manager.cleanup(responses); + PathToolFsmState::Ready + } // Mouse up (PathToolFsmState::Drawing { selection_shape }, PathToolMessage::DragStop { extend_selection, shrink_selection }) => { let extend_selection = input.keyboard.get(extend_selection as usize); @@ -1570,7 +1750,7 @@ impl Fsm for PathToolFsmState { (_, PathToolMessage::DragStop { extend_selection, .. }) => { let extend_selection = input.keyboard.get(extend_selection as usize); let drag_occurred = tool_data.drag_start_pos.distance(input.mouse.position) > DRAG_THRESHOLD; - // TODO: Here we want only visible points to be considered + let nearest_point = shape_editor.find_nearest_visible_point_indices( &document.network_interface, input.mouse.position, @@ -1633,9 +1813,8 @@ impl Fsm for PathToolFsmState { shape_editor.deselect_all_points(); } - if tool_data.snapping_axis.is_some() { - tool_data.snapping_axis = None; - } + tool_data.snapping_axis = None; + tool_data.sliding_point_info = None; responses.add(DocumentMessage::EndTransaction); responses.add(PathToolMessage::SelectedPointUpdated); @@ -1845,6 +2024,7 @@ impl Fsm for PathToolFsmState { HintInfo::keys([Key::Alt], "Subtract").prepend_plus(), ]), ]), + PathToolFsmState::SlidingPoint { .. } => HintData(vec![HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()])]), }; responses.add(FrontendMessage::UpdateInputHints { hint_data });