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-23 01:41:11 +05:30 committed by GitHub
parent 4587457bfa
commit 5a1c171fc3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 443 additions and 43 deletions

View file

@ -595,6 +595,7 @@ impl Primitive for String {}
impl Primitive for bool {}
impl Primitive for f32 {}
impl Primitive for f64 {}
impl Primitive for DVec2 {}
fn text_attributes(attributes: &mut SvgRenderAttrs) {
attributes.push("fill", "white");

View file

@ -0,0 +1,11 @@
use dyn_any::{DynAny, StaticType};
/// Represents different ways of calculating the centroid.
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type)]
pub enum CentroidType {
/// The center of mass for the area of a solid shape's interior, as if made out of an infinitely flat material.
#[default]
Area,
/// The center of mass for the arc length of a curved shape's perimeter, as if made out of an infinitely thin wire.
Length,
}

View file

@ -1,5 +1,6 @@
pub mod brush_stroke;
pub mod generator_nodes;
pub mod misc;
pub mod style;
pub use style::PathStyle;

View file

@ -1,3 +1,4 @@
use super::misc::CentroidType;
use super::style::{Fill, FillType, Gradient, GradientType, Stroke};
use super::{PointId, SegmentId, StrokeId, VectorData};
use crate::renderer::GraphicElementRendered;
@ -530,6 +531,75 @@ async fn morph<SourceFuture: Future<Output = VectorData>, TargetFuture: Future<O
result
}
#[derive(Debug, Clone, Copy)]
pub struct AreaNode<VectorData> {
vector_data: VectorData,
}
#[node_macro::node_fn(AreaNode)]
async fn area_node<Fut: Future<Output = VectorData>>(empty: (), vector_data: impl Node<Footprint, Output = Fut>) -> f64 {
let vector_data = self.vector_data.eval(Footprint::default()).await;
let mut area = 0.;
let scale = vector_data.transform.decompose_scale();
for subpath in vector_data.stroke_bezier_paths() {
area += subpath.area(Some(1e-3), Some(1e-3));
}
area * scale[0] * scale[1]
}
#[derive(Debug, Clone, Copy)]
pub struct CentroidNode<VectorData, CentroidType> {
vector_data: VectorData,
centroid_type: CentroidType,
}
#[node_macro::node_fn(CentroidNode)]
async fn centroid_node<Fut: Future<Output = VectorData>>(empty: (), vector_data: impl Node<Footprint, Output = Fut>, centroid_type: CentroidType) -> DVec2 {
let vector_data = self.vector_data.eval(Footprint::default()).await;
if centroid_type == CentroidType::Area {
let mut area = 0.;
let mut centroid = DVec2::ZERO;
for subpath in vector_data.stroke_bezier_paths() {
if let Some((subpath_centroid, subpath_area)) = subpath.area_centroid_and_area(Some(1e-3), Some(1e-3)) {
if subpath_area == 0. {
continue;
}
area += subpath_area;
centroid += subpath_area * subpath_centroid;
}
}
if area != 0. {
centroid /= area;
return vector_data.transform().transform_point2(centroid);
}
}
let mut length = 0.;
let mut centroid = DVec2::ZERO;
for subpath in vector_data.stroke_bezier_paths() {
if let Some((subpath_centroid, subpath_length)) = subpath.length_centroid_and_length(None, true) {
length += subpath_length;
centroid += subpath_length * subpath_centroid;
}
}
if length != 0. {
centroid /= length;
return vector_data.transform().transform_point2(centroid);
}
let positions = vector_data.point_domain.positions();
if !positions.is_empty() {
let centroid = positions.iter().sum::<DVec2>() / (positions.len() as f64);
return vector_data.transform().transform_point2(centroid);
}
DVec2::ZERO
}
#[cfg(test)]
mod test {
use super::*;