Add anchor sliding along adjacent segments in the Path tool (#2682)

* Improved comments

* Add point sliding with approximate t value

* Add similarity calculation

* Numerical approach to fit the curve

* Reliable point sliding for cubic segments

* Fix formatting and clean comments

* Fix cubic with one handle logic

* Cancel on right click and escape

* Two parameter optimization

* Esc/ Right click cancellation

* Code review

* Fix dynamic hints

* Revert selected_points_counts and fix comments

* Code review

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
Adesh Gupta
2025-06-19 18:59:22 -07:00
committed by Keavon Chambers
co-authored by Keavon Chambers
parent eda858b6e2
commit 781271782c
3 changed files with 349 additions and 36 deletions
@@ -107,11 +107,7 @@ impl SelectedLayerState {
}
pub fn selected_points_count(&self) -> usize {
let count = self.selected_points.iter().fold(0, |acc, point| {
let is_ignored = (point.as_handle().is_some() && self.ignore_handles) || (point.as_anchor().is_some() && self.ignore_anchors);
acc + if is_ignored { 0 } else { 1 }
});
count
self.selected_points.len()
}
}
@@ -2,6 +2,7 @@ use crate::messages::portfolio::document::utility_types::document_metadata::Laye
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};
@@ -196,3 +197,99 @@ pub fn is_visible_point(
}
}
}
/// Calculates similarity metric between new bezier curve and two old beziers by using sampled points.
#[allow(clippy::too_many_arguments)]
pub fn log_optimization(a: f64, b: f64, p1: DVec2, p3: DVec2, d1: DVec2, d2: DVec2, points1: &[DVec2], n: usize) -> f64 {
let start_handle_length = a.exp();
let end_handle_length = b.exp();
// Compute the handle positions of new bezier curve
let c1 = p1 + d1 * start_handle_length;
let c2 = 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);
// Sample 2*n points from new curve and get the L2 metric between all of points
let points = new_curve.compute_lookup_table(Some(2 * n), None).collect::<Vec<_>>();
let dist = points1.iter().zip(points.iter()).map(|(p1, p2)| (p1.x - p2.x).powi(2) + (p1.y - p2.y).powi(2)).sum::<f64>();
dist / (2 * n) as f64
}
/// Calculates optimal handle lengths with adam optimization.
#[allow(clippy::too_many_arguments)]
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_f64).ln();
let mut b = (5_f64).ln();
let mut m_a = 0.;
let mut v_a = 0.;
let mut m_b = 0.;
let mut v_b = 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::<Vec<_>>();
let mut points2 = other_segment.compute_lookup_table(Some(n), None).collect::<Vec<_>>();
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. * h);
let dfb = (f(a, b + h) - f(a, b - h)) / (2. * h);
m_a = beta1 * m_a + (1. - beta1) * dfa;
m_b = beta1 * m_b + (1. - beta1) * dfb;
v_a = beta2 * v_a + (1. - beta2) * dfa * dfa;
v_b = beta2 * v_b + (1. - beta2) * dfb * dfb;
let m_a_hat = m_a / (1. - beta1.powi(t));
let v_a_hat = v_a / (1. - beta2.powi(t));
let m_b_hat = m_b / (1. - beta1.powi(t));
let v_b_hat = v_b / (1. - 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)
}