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:
0HyperCube
2024-01-13 14:32:10 +00:00
committed by GitHub
co-authored by Keavon Chambers
parent 78a1bb17cd
commit 456ca170a4
40 changed files with 2170 additions and 475 deletions
+36 -38
View File
@@ -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]