mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-10-04 03:08:12 +08: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:
co-authored by
0hypercube
Keavon Chambers
Dennis Kobert
parent
4587457bfa
commit
5a1c171fc3
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user