mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-08-04 13:30:48 +00:00
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:
parent
4587457bfa
commit
5a1c171fc3
18 changed files with 443 additions and 43 deletions
|
@ -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)]
|
||||
|
|
|
@ -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.);
|
||||
|
|
|
@ -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.));
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use super::*;
|
||||
use crate::consts::{DEFAULT_EUCLIDEAN_ERROR_BOUND, DEFAULT_LUT_STEP_SIZE};
|
||||
use crate::consts::{DEFAULT_EUCLIDEAN_ERROR_BOUND, DEFAULT_LUT_STEP_SIZE, MAX_ABSOLUTE_DIFFERENCE};
|
||||
use crate::utils::{SubpathTValue, TValue, TValueType};
|
||||
use glam::DVec2;
|
||||
|
||||
|
@ -30,11 +30,41 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
|
|||
self.iter().map(|bezier| bezier.length(tolerance)).sum()
|
||||
}
|
||||
|
||||
/// Return the approximation of the length centroid, together with the length, of the `Subpath`.
|
||||
///
|
||||
/// The length centroid is the center of mass for the arc length of the solid shape's perimeter.
|
||||
/// An infinitely thin wire forming the subpath's closed shape would balance at this point.
|
||||
///
|
||||
/// It will return `None` if no manipulator is present.
|
||||
/// - `tolerance` - Tolerance used to approximate the curve.
|
||||
/// - `always_closed` - consider the subpath as closed always.
|
||||
pub fn length_centroid_and_length(&self, tolerance: Option<f64>, always_closed: bool) -> Option<(DVec2, f64)> {
|
||||
if always_closed { self.iter_closed() } else { self.iter() }
|
||||
.map(|bezier| bezier.length_centroid_and_length(tolerance))
|
||||
.map(|(centroid, length)| (centroid * length, length))
|
||||
.reduce(|(centroid_part1, length1), (centroid_part2, length2)| (centroid_part1 + centroid_part2, length1 + length2))
|
||||
.map(|(centroid_part, length)| (centroid_part / length, length))
|
||||
}
|
||||
|
||||
/// Return the approximation of the length centroid of the `Subpath`.
|
||||
///
|
||||
/// The length centroid is the center of mass for the arc length of the solid shape's perimeter.
|
||||
/// An infinitely thin wire forming the subpath's closed shape would balance at this point.
|
||||
///
|
||||
/// It will return `None` if no manipulator is present.
|
||||
/// - `tolerance` - Tolerance used to approximate the curve.
|
||||
/// - `always_closed` - consider the subpath as closed always.
|
||||
/// <iframe frameBorder="0" width="100%" height="300px" src="https://graphite.rs/libraries/bezier-rs#subpath/length-centroid/solo" title="Length Centroid Demo"></iframe>
|
||||
pub fn length_centroid(&self, tolerance: Option<f64>, always_closed: bool) -> Option<DVec2> {
|
||||
self.length_centroid_and_length(tolerance, always_closed).map(|(centroid, _)| centroid)
|
||||
}
|
||||
|
||||
/// Return the area enclosed by the `Subpath` always considering it as a closed subpath. It will always give a positive value.
|
||||
///
|
||||
/// If the area is less than `error`, it will return zero.
|
||||
/// Because the calculation of area for self-intersecting path requires finding the intersections, the following parameters are used:
|
||||
/// - `error` - For intersections with non-linear beziers, `error` defines the threshold for bounding boxes to be considered an intersection point.
|
||||
/// - `minimum_separation`: the minimum difference two adjacent `t`-values must have when comparing adjacent `t`-values in sorted order.
|
||||
/// - `minimum_separation` - the minimum difference two adjacent `t`-values must have when comparing adjacent `t`-values in sorted order.
|
||||
/// If the comparison condition is not satisfied, the function takes the larger `t`-value of the two
|
||||
///
|
||||
/// **NOTE**: if an intersection were to occur within an `error` distance away from an anchor point, the algorithm will filter that intersection out.
|
||||
|
@ -62,19 +92,27 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
|
|||
})
|
||||
.sum();
|
||||
|
||||
if area.abs() < error.unwrap_or(MAX_ABSOLUTE_DIFFERENCE) {
|
||||
return 0.;
|
||||
}
|
||||
|
||||
area.abs()
|
||||
}
|
||||
|
||||
/// Return the centroid of the `Subpath` always considering it as a closed subpath.
|
||||
/// It will return `None` if no manipulator is present.
|
||||
/// Return the area centroid, together with the area, of the `Subpath` always considering it as a closed subpath. The area will always be a positive value.
|
||||
///
|
||||
/// Because the calculation of area for self-intersecting path requires finding the intersections, the following parameters are used:
|
||||
/// The area centroid is the center of mass for the area of a solid shape's interior.
|
||||
/// An infinitely flat material forming the subpath's closed shape would balance at this point.
|
||||
///
|
||||
/// It will return `None` if no manipulator is present. If the area is less than `error`, it will return `Some((DVec2::NAN, 0.))`.
|
||||
///
|
||||
/// Because the calculation of area and centroid for self-intersecting path requires finding the intersections, the following parameters are used:
|
||||
/// - `error` - For intersections with non-linear beziers, `error` defines the threshold for bounding boxes to be considered an intersection point.
|
||||
/// - `minimum_separation`: the minimum difference two adjacent `t`-values must have when comparing adjacent `t`-values in sorted order.
|
||||
/// If the comparison condition is not satisfied, the function takes the larger `t`-value of the two
|
||||
/// - `minimum_separation` - the minimum difference two adjacent `t`-values must have when comparing adjacent `t`-values in sorted order.
|
||||
/// If the comparison condition is not satisfied, the function takes the larger `t`-value of the two.
|
||||
///
|
||||
/// **NOTE**: if an intersection were to occur within an `error` distance away from an anchor point, the algorithm will filter that intersection out.
|
||||
pub fn centroid(&self, error: Option<f64>, minimum_separation: Option<f64>) -> Option<DVec2> {
|
||||
pub fn area_centroid_and_area(&self, error: Option<f64>, minimum_separation: Option<f64>) -> Option<(DVec2, f64)> {
|
||||
let all_intersections = self.all_self_intersections(error, minimum_separation);
|
||||
let mut current_sign: f64 = 1.;
|
||||
|
||||
|
@ -112,7 +150,35 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
|
|||
})
|
||||
.reduce(|(x1, y1, area1), (x2, y2, area2)| (x1 + x2, y1 + y2, area1 + area2))?;
|
||||
|
||||
Some(DVec2::new(x_sum / area, y_sum / area))
|
||||
if area.abs() < error.unwrap_or(MAX_ABSOLUTE_DIFFERENCE) {
|
||||
return Some((DVec2::NAN, 0.));
|
||||
}
|
||||
|
||||
Some((DVec2::new(x_sum / area, y_sum / area), area.abs()))
|
||||
}
|
||||
|
||||
/// Attempts to return the area centroid of the `Subpath` always considering it as a closed subpath. Falls back to length centroid if the area is zero.
|
||||
///
|
||||
/// The area centroid is the center of mass for the area of a solid shape's interior.
|
||||
/// An infinitely flat material forming the subpath's closed shape would balance at this point.
|
||||
///
|
||||
/// It will return `None` if no manipulator is present.
|
||||
/// Because the calculation of centroid for self-intersecting path requires finding the intersections, the following parameters are used:
|
||||
/// - `error` - For intersections with non-linear beziers, `error` defines the threshold for bounding boxes to be considered an intersection point.
|
||||
/// - `minimum_separation` - the minimum difference two adjacent `t`-values must have when comparing adjacent `t`-values in sorted order.
|
||||
/// - `tolerance` - Tolerance used to approximate the curve if it falls back to length centroid.
|
||||
/// If the comparison condition is not satisfied, the function takes the larger `t`-value of the two
|
||||
///
|
||||
/// **NOTE**: if an intersection were to occur within an `error` distance away from an anchor point, the algorithm will filter that intersection out.
|
||||
/// <iframe frameBorder="0" width="100%" height="300px" src="https://graphite.rs/libraries/bezier-rs#subpath/area-centroid/solo" title="Area Centroid Demo"></iframe>
|
||||
pub fn area_centroid(&self, error: Option<f64>, minimum_separation: Option<f64>, tolerance: Option<f64>) -> Option<DVec2> {
|
||||
let (centroid, area) = self.area_centroid_and_area(error, minimum_separation)?;
|
||||
|
||||
if area != 0. {
|
||||
Some(centroid)
|
||||
} else {
|
||||
self.length_centroid_and_length(tolerance, true).map(|(centroid, _)| centroid)
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts from a subpath (composed of multiple segments) to a point along a certain segment represented.
|
||||
|
@ -293,6 +359,39 @@ mod tests {
|
|||
assert_eq!(subpath.length(None), linear_bezier.length(None) + quadratic_bezier.length(None) + cubic_bezier.length(None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn length_centroid() {
|
||||
let start = DVec2::new(0., 0.);
|
||||
let end = DVec2::new(1., 1.);
|
||||
let handle = DVec2::new(0., 1.);
|
||||
|
||||
let mut subpath = Subpath::new(
|
||||
vec![
|
||||
ManipulatorGroup {
|
||||
anchor: start,
|
||||
in_handle: None,
|
||||
out_handle: Some(handle),
|
||||
id: EmptyId,
|
||||
},
|
||||
ManipulatorGroup {
|
||||
anchor: end,
|
||||
in_handle: None,
|
||||
out_handle: None,
|
||||
id: EmptyId,
|
||||
},
|
||||
],
|
||||
false,
|
||||
);
|
||||
|
||||
let expected_centroid = DVec2::new(0.4153039799983826, 0.5846960200016174);
|
||||
let epsilon = 0.00001;
|
||||
|
||||
assert!(subpath.length_centroid_and_length(None, true).unwrap().0.abs_diff_eq(expected_centroid, epsilon));
|
||||
|
||||
subpath.closed = true;
|
||||
assert!(subpath.length_centroid_and_length(None, true).unwrap().0.abs_diff_eq(expected_centroid, epsilon));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn area() {
|
||||
let start = DVec2::new(0., 0.);
|
||||
|
@ -327,7 +426,7 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn centroid() {
|
||||
fn area_centroid() {
|
||||
let start = DVec2::new(0., 0.);
|
||||
let end = DVec2::new(1., 1.);
|
||||
let handle = DVec2::new(0., 1.);
|
||||
|
@ -353,10 +452,10 @@ mod tests {
|
|||
let expected_centroid = DVec2::new(0.4, 0.6);
|
||||
let epsilon = 0.00001;
|
||||
|
||||
assert!(subpath.centroid(Some(0.001), Some(0.001)).unwrap().abs_diff_eq(expected_centroid, epsilon));
|
||||
assert!(subpath.area_centroid(Some(0.001), Some(0.001), None).unwrap().abs_diff_eq(expected_centroid, epsilon));
|
||||
|
||||
subpath.closed = true;
|
||||
assert!(subpath.centroid(Some(0.001), Some(0.001)).unwrap().abs_diff_eq(expected_centroid, epsilon));
|
||||
assert!(subpath.area_centroid(Some(0.001), Some(0.001), None).unwrap().abs_diff_eq(expected_centroid, epsilon));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue