Add Area and Centroid nodes (#1749)

* initial attempt for area node

* allow node preview for more types

* make AreaNode sync and add CentroidNode

* cargo fmt

* preview of DVec2

* make the nodes async again

* use segment domain instead of region domain

* modify the check for linearity

* create a limit for area in centroid calculation

* cargo fmt

* reverse unnecessary changes

* add threshold to area calculation too.

* handle zero area edge case

* add todo comment

* implement 1D centroid and use it as fallback

* formatting floats to skip last zero

* add Centroid Type radio button to Centroid Node

* rename docs to use area and perimeter centroid

* add web demos for perimeter centroid

* add tests for perimeter centroid

* add fallback to use average of points

* Fix for broken area

* missing fixes

* Code review and rename Perimeter Centroid to Length Centroid

* Use dummy footprint in Area and Centroid nodes

* add doc and todo to clarify when `is_linear` fails

* use epsilon instead of zero

---------

Co-authored-by: 0hypercube <0hypercube@gmail.com>
Co-authored-by: Keavon Chambers <keavon@keavon.com>
Co-authored-by: Dennis Kobert <dennis@kobert.dev>
This commit is contained in:
Elbert Ronnie
2024-05-22 13:11:11 -07:00
committed by GitHub
co-authored by 0hypercube Keavon Chambers Dennis Kobert
parent 4587457bfa
commit 5a1c171fc3
18 changed files with 443 additions and 43 deletions
+13
View File
@@ -218,6 +218,19 @@ impl Bezier {
self.get_points().all(|point| point.abs_diff_eq(start, MAX_ABSOLUTE_DIFFERENCE))
}
/// Returns true if the Bezier curve is equivalent to a line.
///
/// **NOTE**: This is different from simply checking if the handle is [`BezierHandles::Linear`]. A [`Quadratic`](BezierHandles::Quadratic) or [`Cubic`](BezierHandles::Cubic) Bezier curve can also be a line if the handles are colinear to the start and end points. Therefore if the handles exceed the start and end point, it will still be considered as a line.
pub fn is_linear(&self) -> bool {
let is_colinear = |a: DVec2, b: DVec2, c: DVec2| -> bool { ((b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x)).abs() < MAX_ABSOLUTE_DIFFERENCE };
match self.handles {
BezierHandles::Linear => true,
BezierHandles::Quadratic { handle } => is_colinear(self.start, handle, self.end),
BezierHandles::Cubic { handle_start, handle_end } => is_colinear(self.start, handle_start, self.end) && is_colinear(self.start, handle_end, self.end),
}
}
}
#[cfg(test)]
+88
View File
@@ -179,6 +179,75 @@ impl Bezier {
}
}
/// Return an approximation of the length centroid, together with the length, of the bezier curve.
///
/// The length centroid is the center of mass for the arc length of the Bezier segment.
/// An infinitely thin wire forming the Bezier segment's shape would balance at this point.
///
/// - `tolerance` - Tolerance used to approximate the curve.
pub fn length_centroid_and_length(&self, tolerance: Option<f64>) -> (DVec2, f64) {
match self.handles {
BezierHandles::Linear => ((self.start + self.end()) / 2., (self.start - self.end).length()),
BezierHandles::Quadratic { handle } => {
// Use Casteljau subdivision, noting that the length is more than the straight line distance from start to end but less than the straight line distance through the handles
fn recurse(a0: DVec2, a1: DVec2, a2: DVec2, tolerance: f64, level: u8) -> (f64, DVec2) {
let lower = a0.distance(a2);
let upper = a0.distance(a1) + a1.distance(a2);
if upper - lower <= 2. * tolerance || level >= 8 {
let length = (lower + upper) / 2.;
return (length, length * (a0 + a1 + a2) / 3.);
}
let b1 = 0.5 * (a0 + a1);
let c1 = 0.5 * (a1 + a2);
let b2 = 0.5 * (b1 + c1);
let (length1, centroid_part1) = recurse(a0, b1, b2, 0.5 * tolerance, level + 1);
let (length2, centroid_part2) = recurse(b2, c1, a2, 0.5 * tolerance, level + 1);
(length1 + length2, centroid_part1 + centroid_part2)
}
let (length, centroid_parts) = recurse(self.start, handle, self.end, tolerance.unwrap_or_default(), 0);
(centroid_parts / length, length)
}
BezierHandles::Cubic { handle_start, handle_end } => {
// Use Casteljau subdivision, noting that the length is more than the straight line distance from start to end but less than the straight line distance through the handles
fn recurse(a0: DVec2, a1: DVec2, a2: DVec2, a3: DVec2, tolerance: f64, level: u8) -> (f64, DVec2) {
let lower = a0.distance(a3);
let upper = a0.distance(a1) + a1.distance(a2) + a2.distance(a3);
if upper - lower <= 2. * tolerance || level >= 8 {
let length = (lower + upper) / 2.;
return (length, length * (a0 + a1 + a2 + a3) / 4.);
}
let b1 = 0.5 * (a0 + a1);
let t0 = 0.5 * (a1 + a2);
let c1 = 0.5 * (a2 + a3);
let b2 = 0.5 * (b1 + t0);
let c2 = 0.5 * (t0 + c1);
let b3 = 0.5 * (b2 + c2);
let (length1, centroid_part1) = recurse(a0, b1, b2, b3, 0.5 * tolerance, level + 1);
let (length2, centroid_part2) = recurse(b3, c2, c1, a3, 0.5 * tolerance, level + 1);
(length1 + length2, centroid_part1 + centroid_part2)
}
let (length, centroid_parts) = recurse(self.start, handle_start, handle_end, self.end, tolerance.unwrap_or_default(), 0);
(centroid_parts / length, length)
}
}
}
/// Return an approximation of the length centroid of the Bezier curve.
///
/// The length centroid is the center of mass for the arc length of the Bezier segment.
/// An infinitely thin wire with the Bezier segment's shape would balance at this point.
///
/// - `tolerance` - Tolerance used to approximate the curve.
/// <iframe frameBorder="0" width="100%" height="300px" src="https://graphite.rs/libraries/bezier-rs#bezier/length-centroid/solo" title="Length Centroid Demo"></iframe>
pub fn length_centroid(&self, tolerance: Option<f64>) -> DVec2 {
self.length_centroid_and_length(tolerance).0
}
/// Returns the parametric `t`-value that corresponds to the closest point on the curve to the provided point.
/// <iframe frameBorder="0" width="100%" height="300px" src="https://graphite.rs/libraries/bezier-rs#bezier/project/solo" title="Project Demo"></iframe>
pub fn project(&self, point: DVec2) -> f64 {
@@ -260,6 +329,25 @@ mod tests {
assert!(utils::f64_compare(bezier_cubic.length(None), 199., 1e-2));
}
#[test]
fn test_length_centroid() {
let p1 = DVec2::new(30., 50.);
let p2 = DVec2::new(140., 30.);
let p3 = DVec2::new(160., 170.);
let p4 = DVec2::new(77., 129.);
let bezier_linear = Bezier::from_linear_dvec2(p1, p2);
assert!(bezier_linear.length_centroid_and_length(None).0.abs_diff_eq((p1 + p2) / 2., MAX_ABSOLUTE_DIFFERENCE));
let bezier_quadratic = Bezier::from_quadratic_dvec2(p1, p2, p3);
let expected = DVec2::new(112.81017736920136, 87.98713052477228);
assert!(bezier_quadratic.length_centroid_and_length(None).0.abs_diff_eq(expected, MAX_ABSOLUTE_DIFFERENCE));
let bezier_cubic = Bezier::from_cubic_dvec2(p1, p2, p3, p4);
let expected = DVec2::new(95.23597072432115, 88.0645175770206);
assert!(bezier_cubic.length_centroid_and_length(None).0.abs_diff_eq(expected, MAX_ABSOLUTE_DIFFERENCE));
}
#[test]
fn test_project() {
let bezier1 = Bezier::from_cubic_coordinates(4., 4., 23., 45., 10., 30., 56., 90.);
+7 -1
View File
@@ -404,7 +404,13 @@ impl Bezier {
/// - `error` - For intersections where the provided bezier is non-linear, `error` defines the threshold for bounding boxes to be considered an intersection point.
pub fn unfiltered_intersections(&self, other: &Bezier, error: Option<f64>) -> Vec<[f64; 2]> {
let error = error.unwrap_or(0.5);
if other.handles == BezierHandles::Linear {
// TODO: This implementation does not handle the case of line-like bezier curves properly. Two line-like bezier curves which have the same slope
// should not return any intersection points but the current implementation returns many of them. This results in the area of line not being zero.
// Using `is_linear` does prevent it but only in cases where the line-like cubic bezier has it handles at exactly the same position as the start
// and end points. In future, the below algorithm needs to be changed to account for all possible cases.
if other.is_linear() {
// Rotate the bezier and the line by the angle that the line makes with the x axis
let line_directional_vector = other.end - other.start;
let angle = line_directional_vector.angle_between(DVec2::new(0., 1.));