mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 14:18:04 +08:00
Layer and grid snapping systems (#1521)
* Grid overlays * Rectangle tool basic snapping * Fix bezier demos * Fix bézier crate tests * Constrained snapping for circle & shape tool * Line tool snapping * Pen tool snapping * Path tool snapping * Snapping whilst dragging layers (not constrained) * Constrained drag * Resize snapping * Normal and tangent * Cleanup * Grid snapping * Grid snapping * Fix imports * Fix bug in artboard tool * Fix hang on 0 size grid spacing * Fix NaN when scaling * Polishing --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
@@ -130,9 +130,9 @@ impl Bezier {
|
||||
|
||||
/// Returns two lists of `t`-values representing the local extrema of the `x` and `y` parametric curves respectively.
|
||||
/// The local extrema are defined to be points at which the derivative of the curve is equal to zero.
|
||||
fn unrestricted_local_extrema(&self) -> [Vec<f64>; 2] {
|
||||
fn unrestricted_local_extrema(&self) -> [[Option<f64>; 3]; 2] {
|
||||
match self.handles {
|
||||
BezierHandles::Linear => [Vec::new(), Vec::new()],
|
||||
BezierHandles::Linear => [[None; 3]; 2],
|
||||
BezierHandles::Quadratic { handle } => {
|
||||
let a = handle - self.start;
|
||||
let b = self.end - handle;
|
||||
@@ -156,13 +156,8 @@ impl Bezier {
|
||||
/// Returns two lists of `t`-values representing the local extrema of the `x` and `y` parametric curves respectively.
|
||||
/// The list of `t`-values returned are filtered such that they fall within the range `[0, 1]`.
|
||||
/// <iframe frameBorder="0" width="100%" height="300px" src="https://graphite.rs/libraries/bezier-rs#bezier/local-extrema/solo" title="Local Extrema Demo"></iframe>
|
||||
pub fn local_extrema(&self) -> [Vec<f64>; 2] {
|
||||
self.unrestricted_local_extrema()
|
||||
.into_iter()
|
||||
.map(|t_values| t_values.into_iter().filter(|&t| t > 0. && t < 1.).collect::<Vec<f64>>())
|
||||
.collect::<Vec<Vec<f64>>>()
|
||||
.try_into()
|
||||
.unwrap()
|
||||
pub fn local_extrema(&self) -> [impl Iterator<Item = f64>; 2] {
|
||||
self.unrestricted_local_extrema().map(|t_values| t_values.into_iter().flatten().filter(|&t| t > 0. && t < 1.))
|
||||
}
|
||||
|
||||
/// Return the min and max corners that represent the bounding box of the curve.
|
||||
@@ -223,17 +218,18 @@ impl Bezier {
|
||||
}
|
||||
}
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter(|&t| utils::f64_approximately_in_range(t, 0., 1., MAX_ABSOLUTE_DIFFERENCE))
|
||||
}
|
||||
|
||||
// TODO: Use an `impl Iterator` return type instead of a `Vec`
|
||||
/// Returns list of `t`-values representing the inflection points of the curve.
|
||||
/// The inflection points are defined to be points at which the second derivative of the curve is equal to zero.
|
||||
pub fn unrestricted_inflections(&self) -> Vec<f64> {
|
||||
pub fn unrestricted_inflections(&self) -> impl Iterator<Item = f64> {
|
||||
match self.handles {
|
||||
// There exists no inflection points for linear and quadratic beziers.
|
||||
BezierHandles::Linear => Vec::new(),
|
||||
BezierHandles::Quadratic { .. } => Vec::new(),
|
||||
BezierHandles::Linear => [None; 3],
|
||||
BezierHandles::Quadratic { .. } => [None; 3],
|
||||
BezierHandles::Cubic { .. } => {
|
||||
// Axis align the curve.
|
||||
let translated_bezier = self.translate(-self.start);
|
||||
@@ -257,6 +253,8 @@ impl Bezier {
|
||||
}
|
||||
}
|
||||
}
|
||||
.into_iter()
|
||||
.flatten()
|
||||
}
|
||||
|
||||
/// Returns list of parametric `t`-values representing the inflection points of the curve.
|
||||
@@ -491,7 +489,7 @@ impl Bezier {
|
||||
|
||||
let discriminant = b * b - 4. * a * c;
|
||||
let two_times_a = 2. * a;
|
||||
for t in solve_quadratic(discriminant, two_times_a, b, c) {
|
||||
for t in solve_quadratic(discriminant, two_times_a, b, c).into_iter().flatten() {
|
||||
if (0.0..=1.).contains(&t) {
|
||||
let x = self.evaluate(TValue::Parametric(t)).x;
|
||||
if target_point.x >= x {
|
||||
@@ -514,7 +512,7 @@ impl Bezier {
|
||||
let b = 3. * (p2.y - 2. * p1.y + self.start.y);
|
||||
let c = 3. * (p1.y - self.start.y);
|
||||
let d = self.start.y - target_point.y;
|
||||
for t in solve_cubic(a, b, c, d) {
|
||||
for t in solve_cubic(a, b, c, d).into_iter().flatten() {
|
||||
if (0.0..=1.).contains(&t) {
|
||||
let x = self.evaluate(TValue::Parametric(t)).x;
|
||||
if target_point.x >= x {
|
||||
@@ -716,8 +714,8 @@ mod tests {
|
||||
// Linear bezier cannot have extrema
|
||||
let line = Bezier::from_linear_dvec2(DVec2::new(10., 10.), DVec2::new(50., 50.));
|
||||
let [x_extrema, y_extrema] = line.local_extrema();
|
||||
assert!(x_extrema.is_empty());
|
||||
assert!(y_extrema.is_empty());
|
||||
assert_eq!(y_extrema.count(), 0);
|
||||
assert_eq!(x_extrema.count(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -725,26 +723,26 @@ mod tests {
|
||||
// Test with no x-extrema, no y-extrema
|
||||
let bezier1 = Bezier::from_quadratic_coordinates(40., 35., 149., 54., 155., 170.);
|
||||
let [x_extrema1, y_extrema1] = bezier1.local_extrema();
|
||||
assert!(x_extrema1.is_empty());
|
||||
assert!(y_extrema1.is_empty());
|
||||
assert_eq!(x_extrema1.count(), 0);
|
||||
assert_eq!(y_extrema1.count(), 0);
|
||||
|
||||
// Test with 1 x-extrema, no y-extrema
|
||||
let bezier2 = Bezier::from_quadratic_coordinates(45., 30., 170., 90., 45., 150.);
|
||||
let [x_extrema2, y_extrema2] = bezier2.local_extrema();
|
||||
assert_eq!(x_extrema2.len(), 1);
|
||||
assert!(y_extrema2.is_empty());
|
||||
assert_eq!(x_extrema2.count(), 1);
|
||||
assert_eq!(y_extrema2.count(), 0);
|
||||
|
||||
// Test with no x-extrema, 1 y-extrema
|
||||
let bezier3 = Bezier::from_quadratic_coordinates(30., 130., 100., 25., 150., 130.);
|
||||
let [x_extrema3, y_extrema3] = bezier3.local_extrema();
|
||||
assert!(x_extrema3.is_empty());
|
||||
assert_eq!(y_extrema3.len(), 1);
|
||||
assert_eq!(x_extrema3.count(), 0);
|
||||
assert_eq!(y_extrema3.count(), 1);
|
||||
|
||||
// Test with 1 x-extrema, 1 y-extrema
|
||||
let bezier4 = Bezier::from_quadratic_coordinates(50., 70., 170., 35., 60., 150.);
|
||||
let [x_extrema4, y_extrema4] = bezier4.local_extrema();
|
||||
assert_eq!(x_extrema4.len(), 1);
|
||||
assert_eq!(y_extrema4.len(), 1);
|
||||
assert_eq!(x_extrema4.count(), 1);
|
||||
assert_eq!(y_extrema4.count(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -752,44 +750,44 @@ mod tests {
|
||||
// 0 x-extrema, 0 y-extrema
|
||||
let bezier1 = Bezier::from_cubic_coordinates(100., 105., 250., 250., 110., 150., 260., 260.);
|
||||
let [x_extrema1, y_extrema1] = bezier1.local_extrema();
|
||||
assert!(x_extrema1.is_empty());
|
||||
assert!(y_extrema1.is_empty());
|
||||
assert_eq!(x_extrema1.count(), 0);
|
||||
assert_eq!(y_extrema1.count(), 0);
|
||||
|
||||
// 1 x-extrema, 0 y-extrema
|
||||
let bezier2 = Bezier::from_cubic_coordinates(55., 145., 40., 40., 110., 110., 180., 40.);
|
||||
let [x_extrema2, y_extrema2] = bezier2.local_extrema();
|
||||
assert_eq!(x_extrema2.len(), 1);
|
||||
assert!(y_extrema2.is_empty());
|
||||
assert_eq!(x_extrema2.count(), 1);
|
||||
assert_eq!(y_extrema2.count(), 0);
|
||||
|
||||
// 1 x-extrema, 1 y-extrema
|
||||
let bezier3 = Bezier::from_cubic_coordinates(100., 105., 170., 10., 25., 20., 20., 120.);
|
||||
let [x_extrema3, y_extrema3] = bezier3.local_extrema();
|
||||
assert_eq!(x_extrema3.len(), 1);
|
||||
assert_eq!(y_extrema3.len(), 1);
|
||||
assert_eq!(x_extrema3.count(), 1);
|
||||
assert_eq!(y_extrema3.count(), 1);
|
||||
|
||||
// 1 x-extrema, 2 y-extrema
|
||||
let bezier4 = Bezier::from_cubic_coordinates(50., 90., 120., 16., 150., 190., 45., 150.);
|
||||
let [x_extrema4, y_extrema4] = bezier4.local_extrema();
|
||||
assert_eq!(x_extrema4.len(), 1);
|
||||
assert_eq!(y_extrema4.len(), 2);
|
||||
assert_eq!(x_extrema4.count(), 1);
|
||||
assert_eq!(y_extrema4.count(), 2);
|
||||
|
||||
// 2 x-extrema, 0 y-extrema
|
||||
let bezier5 = Bezier::from_cubic_coordinates(40., 170., 150., 160., 10., 10., 170., 10.);
|
||||
let [x_extrema5, y_extrema5] = bezier5.local_extrema();
|
||||
assert_eq!(x_extrema5.len(), 2);
|
||||
assert!(y_extrema5.is_empty());
|
||||
assert_eq!(x_extrema5.count(), 2);
|
||||
assert_eq!(y_extrema5.count(), 0);
|
||||
|
||||
// 2 x-extrema, 1 y-extrema
|
||||
let bezier6 = Bezier::from_cubic_coordinates(40., 170., 150., 160., 10., 10., 160., 45.);
|
||||
let [x_extrema6, y_extrema6] = bezier6.local_extrema();
|
||||
assert_eq!(x_extrema6.len(), 2);
|
||||
assert_eq!(y_extrema6.len(), 1);
|
||||
assert_eq!(x_extrema6.count(), 2);
|
||||
assert_eq!(y_extrema6.count(), 1);
|
||||
|
||||
// 2 x-extrema, 2 y-extrema
|
||||
let bezier7 = Bezier::from_cubic_coordinates(46., 60., 140., 10., 50., 160., 120., 120.);
|
||||
let [x_extrema7, y_extrema7] = bezier7.local_extrema();
|
||||
assert_eq!(x_extrema7.len(), 2);
|
||||
assert_eq!(y_extrema7.len(), 2);
|
||||
assert_eq!(x_extrema7.count(), 2);
|
||||
assert_eq!(y_extrema7.count(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -90,10 +90,10 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
|
||||
|
||||
// TODO: Consider the shared point between adjacent beziers.
|
||||
self.iter().enumerate().fold([Vec::new(), Vec::new()], |mut acc, elem| {
|
||||
let extremas = elem.1.local_extrema();
|
||||
let [x, y] = elem.1.local_extrema();
|
||||
// Convert t-values of bezier curve to t-values of subpath
|
||||
acc[0].extend(extremas[0].iter().map(|t| ((elem.0 as f64) + t) / number_of_curves).collect::<Vec<f64>>());
|
||||
acc[1].extend(extremas[1].iter().map(|t| ((elem.0 as f64) + t) / number_of_curves).collect::<Vec<f64>>());
|
||||
acc[0].extend(x.map(|t| ((elem.0 as f64) + t) / number_of_curves).collect::<Vec<f64>>());
|
||||
acc[1].extend(y.map(|t| ((elem.0 as f64) + t) / number_of_curves).collect::<Vec<f64>>());
|
||||
acc
|
||||
})
|
||||
}
|
||||
|
||||
@@ -93,37 +93,31 @@ pub fn compute_abc_for_cubic_through_points(start_point: DVec2, point_on_curve:
|
||||
|
||||
/// Return the index and the value of the closest point in the LUT compared to the provided point.
|
||||
pub fn get_closest_point_in_lut(lut: &[DVec2], point: DVec2) -> (usize, f64) {
|
||||
lut.iter()
|
||||
.enumerate()
|
||||
.map(|(i, p)| (i, point.distance_squared(*p)))
|
||||
.min_by(|x, y| (x.1).partial_cmp(&(y.1)).unwrap())
|
||||
.unwrap()
|
||||
lut.iter().enumerate().map(|(i, p)| (i, point.distance_squared(*p))).min_by(|x, y| (x.1).total_cmp(&(y.1))).unwrap()
|
||||
}
|
||||
|
||||
// TODO: Use an `Option` return type instead of a `Vec`
|
||||
/// Find the roots of the linear equation `ax + b`.
|
||||
pub fn solve_linear(a: f64, b: f64) -> Vec<f64> {
|
||||
let mut roots = Vec::new();
|
||||
pub fn solve_linear(a: f64, b: f64) -> [Option<f64>; 3] {
|
||||
// There exist roots when `a` is not 0
|
||||
if a.abs() > MAX_ABSOLUTE_DIFFERENCE {
|
||||
roots.push(-b / a);
|
||||
[Some(-b / a), None, None]
|
||||
} else {
|
||||
[None; 3]
|
||||
}
|
||||
roots
|
||||
}
|
||||
|
||||
// TODO: Use an `impl Iterator` return type instead of a `Vec`
|
||||
/// Find the roots of the linear equation `ax^2 + bx + c`.
|
||||
/// Precompute the `discriminant` (`b^2 - 4ac`) and `two_times_a` arguments prior to calling this function for efficiency purposes.
|
||||
pub fn solve_quadratic(discriminant: f64, two_times_a: f64, b: f64, c: f64) -> Vec<f64> {
|
||||
let mut roots = Vec::new();
|
||||
pub fn solve_quadratic(discriminant: f64, two_times_a: f64, b: f64, c: f64) -> [Option<f64>; 3] {
|
||||
let mut roots = [None; 3];
|
||||
if two_times_a.abs() <= STRICT_MAX_ABSOLUTE_DIFFERENCE {
|
||||
roots = solve_linear(b, c);
|
||||
} else if discriminant.abs() <= STRICT_MAX_ABSOLUTE_DIFFERENCE {
|
||||
roots.push(-b / (two_times_a));
|
||||
roots[0] = Some(-b / (two_times_a));
|
||||
} else if discriminant > 0. {
|
||||
let root_discriminant = discriminant.sqrt();
|
||||
roots.push((-b + root_discriminant) / (two_times_a));
|
||||
roots.push((-b - root_discriminant) / (two_times_a));
|
||||
roots[0] = Some((-b + root_discriminant) / (two_times_a));
|
||||
roots[1] = Some((-b - root_discriminant) / (two_times_a));
|
||||
}
|
||||
roots
|
||||
}
|
||||
@@ -139,8 +133,8 @@ fn cube_root(f: f64) -> f64 {
|
||||
|
||||
// TODO: Use an `impl Iterator` return type instead of a `Vec`
|
||||
/// Solve a cubic of the form `x^3 + px + q`, derivation from: <https://trans4mind.com/personal_development/mathematics/polynomials/cubicAlgebra.htm>.
|
||||
pub fn solve_reformatted_cubic(discriminant: f64, a: f64, p: f64, q: f64) -> Vec<f64> {
|
||||
let mut roots = Vec::new();
|
||||
pub fn solve_reformatted_cubic(discriminant: f64, a: f64, p: f64, q: f64) -> [Option<f64>; 3] {
|
||||
let mut roots = [None; 3];
|
||||
if discriminant.abs() <= STRICT_MAX_ABSOLUTE_DIFFERENCE {
|
||||
// When discriminant is 0 (check for approximation because of floating point errors), all roots are real, and 2 are repeated
|
||||
// filter out repeated roots (ie. roots whose distance is less than some epsilon)
|
||||
@@ -149,15 +143,15 @@ pub fn solve_reformatted_cubic(discriminant: f64, a: f64, p: f64, q: f64) -> Vec
|
||||
let root_1 = 2. * cube_root(-q_divided_by_2) - a_divided_by_3;
|
||||
let root_2 = cube_root(q_divided_by_2) - a_divided_by_3;
|
||||
if (root_1 - root_2).abs() > MIN_SEPARATION_VALUE {
|
||||
roots.push(root_1);
|
||||
roots[0] = Some(root_1);
|
||||
}
|
||||
roots.push(root_2);
|
||||
roots[1] = Some(root_2);
|
||||
} else if discriminant > 0. {
|
||||
// When discriminant > 0, there is one real and two imaginary roots
|
||||
let q_divided_by_2 = q / 2.;
|
||||
let square_root_discriminant = discriminant.powf(1. / 2.);
|
||||
|
||||
roots.push(cube_root(-q_divided_by_2 + square_root_discriminant) - cube_root(q_divided_by_2 + square_root_discriminant) - a / 3.);
|
||||
roots[0] = Some(cube_root(-q_divided_by_2 + square_root_discriminant) - cube_root(q_divided_by_2 + square_root_discriminant) - a / 3.);
|
||||
} else {
|
||||
// Otherwise, discriminant < 0 and there are three real roots
|
||||
let p_divided_by_3 = p / 3.;
|
||||
@@ -166,16 +160,16 @@ pub fn solve_reformatted_cubic(discriminant: f64, a: f64, p: f64, q: f64) -> Vec
|
||||
let phi = (-q / (2. * cube_root_r.powi(3))).acos();
|
||||
|
||||
let two_times_cube_root_r = 2. * cube_root_r;
|
||||
roots.push(two_times_cube_root_r * (phi / 3.).cos() - a_divided_by_3);
|
||||
roots.push(two_times_cube_root_r * ((phi + 2. * PI) / 3.).cos() - a_divided_by_3);
|
||||
roots.push(two_times_cube_root_r * ((phi + 4. * PI) / 3.).cos() - a_divided_by_3);
|
||||
roots[0] = Some(two_times_cube_root_r * (phi / 3.).cos() - a_divided_by_3);
|
||||
roots[1] = Some(two_times_cube_root_r * ((phi + 2. * PI) / 3.).cos() - a_divided_by_3);
|
||||
roots[2] = Some(two_times_cube_root_r * ((phi + 4. * PI) / 3.).cos() - a_divided_by_3);
|
||||
}
|
||||
roots
|
||||
}
|
||||
|
||||
// TODO: Use an `impl Iterator` return type instead of a `Vec`
|
||||
/// Solve a cubic of the form `ax^3 + bx^2 + ct + d`.
|
||||
pub fn solve_cubic(a: f64, b: f64, c: f64, d: f64) -> Vec<f64> {
|
||||
pub fn solve_cubic(a: f64, b: f64, c: f64, d: f64) -> [Option<f64>; 3] {
|
||||
if a.abs() <= STRICT_MAX_ABSOLUTE_DIFFERENCE {
|
||||
if b.abs() <= STRICT_MAX_ABSOLUTE_DIFFERENCE {
|
||||
// If both a and b are approximately 0, treat as a linear problem
|
||||
@@ -327,43 +321,47 @@ mod tests {
|
||||
a.len() == b.len() && a.into_iter().zip(b).all(|(a, b)| f64_compare(a, b, max_abs_diff))
|
||||
}
|
||||
|
||||
fn collect_roots(roots: [Option<f64>; 3]) -> Vec<f64> {
|
||||
roots.into_iter().flatten().collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_solve_linear() {
|
||||
// Line that is on the x-axis
|
||||
assert!(solve_linear(0., 0.).is_empty());
|
||||
assert!(collect_roots(solve_linear(0., 0.)).is_empty());
|
||||
// Line that is parallel to but not on the x-axis
|
||||
assert!(solve_linear(0., 1.).is_empty());
|
||||
assert!(collect_roots(solve_linear(0., 1.)).is_empty());
|
||||
// Line with a non-zero slope
|
||||
assert!(solve_linear(2., -8.) == vec![4.]);
|
||||
assert!(collect_roots(solve_linear(2., -8.)) == vec![4.]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_solve_cubic() {
|
||||
// discriminant == 0
|
||||
let roots1 = solve_cubic(1., 0., 0., 0.);
|
||||
let roots1 = collect_roots(solve_cubic(1., 0., 0., 0.));
|
||||
assert!(roots1 == vec![0.]);
|
||||
|
||||
let roots2 = solve_cubic(1., 3., 0., -4.);
|
||||
let roots2 = collect_roots(solve_cubic(1., 3., 0., -4.));
|
||||
assert!(roots2 == vec![1., -2.]);
|
||||
|
||||
// p == 0
|
||||
let roots3 = solve_cubic(1., 0., 0., -1.);
|
||||
let roots3 = collect_roots(solve_cubic(1., 0., 0., -1.));
|
||||
assert!(roots3 == vec![1.]);
|
||||
|
||||
// discriminant > 0
|
||||
let roots4 = solve_cubic(1., 3., 0., 2.);
|
||||
let roots4 = collect_roots(solve_cubic(1., 3., 0., 2.));
|
||||
assert!(f64_compare_vector(roots4, vec![-3.196], MAX_ABSOLUTE_DIFFERENCE));
|
||||
|
||||
// discriminant < 0
|
||||
let roots5 = solve_cubic(1., 3., 0., -1.);
|
||||
let roots5 = collect_roots(solve_cubic(1., 3., 0., -1.));
|
||||
assert!(f64_compare_vector(roots5, vec![0.532, -2.879, -0.653], MAX_ABSOLUTE_DIFFERENCE));
|
||||
|
||||
// quadratic
|
||||
let roots6 = solve_cubic(0., 3., 0., -3.);
|
||||
let roots6 = collect_roots(solve_cubic(0., 3., 0., -3.));
|
||||
assert!(roots6 == vec![1., -1.]);
|
||||
|
||||
// linear
|
||||
let roots7 = solve_cubic(0., 0., 1., -1.);
|
||||
let roots7 = collect_roots(solve_cubic(0., 0., 1., -1.));
|
||||
assert!(roots7 == vec![1.]);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user