Skip to content

Commit 5c25129

Browse files
4adexKeavon
andauthored
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 <[email protected]>
1 parent 44ad6c9 commit 5c25129

File tree

3 files changed

+349
-36
lines changed

3 files changed

+349
-36
lines changed

editor/src/messages/tool/common_functionality/shape_editor.rs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -107,11 +107,7 @@ impl SelectedLayerState {
107107
}
108108

109109
pub fn selected_points_count(&self) -> usize {
110-
let count = self.selected_points.iter().fold(0, |acc, point| {
111-
let is_ignored = (point.as_handle().is_some() && self.ignore_handles) || (point.as_anchor().is_some() && self.ignore_anchors);
112-
acc + if is_ignored { 0 } else { 1 }
113-
});
114-
count
110+
self.selected_points.len()
115111
}
116112
}
117113

editor/src/messages/tool/common_functionality/utility_functions.rs

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use crate::messages::portfolio::document::utility_types::document_metadata::Laye
22
use crate::messages::prelude::*;
33
use crate::messages::tool::common_functionality::graph_modification_utils::get_text;
44
use crate::messages::tool::tool_messages::path_tool::PathOverlayMode;
5+
use bezier_rs::Bezier;
56
use glam::DVec2;
67
use graphene_core::renderer::Quad;
78
use graphene_core::text::{FontCache, load_face};
@@ -196,3 +197,99 @@ pub fn is_visible_point(
196197
}
197198
}
198199
}
200+
201+
/// Calculates similarity metric between new bezier curve and two old beziers by using sampled points.
202+
#[allow(clippy::too_many_arguments)]
203+
pub fn log_optimization(a: f64, b: f64, p1: DVec2, p3: DVec2, d1: DVec2, d2: DVec2, points1: &[DVec2], n: usize) -> f64 {
204+
let start_handle_length = a.exp();
205+
let end_handle_length = b.exp();
206+
207+
// Compute the handle positions of new bezier curve
208+
let c1 = p1 + d1 * start_handle_length;
209+
let c2 = p3 + d2 * end_handle_length;
210+
211+
let new_curve = Bezier::from_cubic_coordinates(p1.x, p1.y, c1.x, c1.y, c2.x, c2.y, p3.x, p3.y);
212+
213+
// Sample 2*n points from new curve and get the L2 metric between all of points
214+
let points = new_curve.compute_lookup_table(Some(2 * n), None).collect::<Vec<_>>();
215+
216+
let dist = points1.iter().zip(points.iter()).map(|(p1, p2)| (p1.x - p2.x).powi(2) + (p1.y - p2.y).powi(2)).sum::<f64>();
217+
218+
dist / (2 * n) as f64
219+
}
220+
221+
/// Calculates optimal handle lengths with adam optimization.
222+
#[allow(clippy::too_many_arguments)]
223+
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) {
224+
let h = 1e-6;
225+
let tol = 1e-6;
226+
let max_iter = 200;
227+
228+
let mut a = (5_f64).ln();
229+
let mut b = (5_f64).ln();
230+
231+
let mut m_a = 0.;
232+
let mut v_a = 0.;
233+
let mut m_b = 0.;
234+
let mut v_b = 0.;
235+
236+
let initial_alpha = 0.05;
237+
let decay_rate: f64 = 0.99;
238+
239+
let beta1 = 0.9;
240+
let beta2 = 0.999;
241+
let epsilon = 1e-8;
242+
243+
let n = 20;
244+
245+
let farther_segment = if farther_segment.start.distance(p1) >= f64::EPSILON {
246+
farther_segment.reverse()
247+
} else {
248+
farther_segment
249+
};
250+
251+
let other_segment = if other_segment.end.distance(p3) >= f64::EPSILON { other_segment.reverse() } else { other_segment };
252+
253+
// Now we sample points proportional to the lengths of the beziers
254+
let l1 = farther_segment.length(None);
255+
let l2 = other_segment.length(None);
256+
let ratio = l1 / (l1 + l2);
257+
let n_points1 = ((2 * n) as f64 * ratio).floor() as usize;
258+
let mut points1 = farther_segment.compute_lookup_table(Some(n_points1), None).collect::<Vec<_>>();
259+
let mut points2 = other_segment.compute_lookup_table(Some(n), None).collect::<Vec<_>>();
260+
points1.append(&mut points2);
261+
262+
let f = |a: f64, b: f64| -> f64 { log_optimization(a, b, p1, p3, d1, d2, &points1, n) };
263+
264+
for t in 1..=max_iter {
265+
let dfa = (f(a + h, b) - f(a - h, b)) / (2. * h);
266+
let dfb = (f(a, b + h) - f(a, b - h)) / (2. * h);
267+
268+
m_a = beta1 * m_a + (1. - beta1) * dfa;
269+
m_b = beta1 * m_b + (1. - beta1) * dfb;
270+
271+
v_a = beta2 * v_a + (1. - beta2) * dfa * dfa;
272+
v_b = beta2 * v_b + (1. - beta2) * dfb * dfb;
273+
274+
let m_a_hat = m_a / (1. - beta1.powi(t));
275+
let v_a_hat = v_a / (1. - beta2.powi(t));
276+
let m_b_hat = m_b / (1. - beta1.powi(t));
277+
let v_b_hat = v_b / (1. - beta2.powi(t));
278+
279+
let alpha_t = initial_alpha * decay_rate.powi(t);
280+
281+
// Update log-lengths
282+
a -= alpha_t * m_a_hat / (v_a_hat.sqrt() + epsilon);
283+
b -= alpha_t * m_b_hat / (v_b_hat.sqrt() + epsilon);
284+
285+
// Convergence check
286+
if dfa.abs() < tol && dfb.abs() < tol {
287+
break;
288+
}
289+
}
290+
291+
let len1 = a.exp().max(min_len1);
292+
let len2 = b.exp().max(min_len2);
293+
294+
(d1 * len1, d2 * len2)
295+
}

0 commit comments

Comments
 (0)