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
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::*;

View File

@@ -77,6 +77,7 @@ pub enum TaggedValue {
Footprint(graphene_core::transform::Footprint),
RenderOutput(RenderOutput),
Palette(Vec<Color>),
CentroidType(graphene_core::vector::misc::CentroidType),
}
#[allow(clippy::derived_hash_with_manual_eq)]
@@ -156,6 +157,7 @@ impl Hash for TaggedValue {
Self::Footprint(x) => x.hash(state),
Self::RenderOutput(x) => x.hash(state),
Self::Palette(x) => x.hash(state),
Self::CentroidType(x) => x.hash(state),
}
}
}
@@ -222,6 +224,7 @@ impl<'a> TaggedValue {
TaggedValue::Footprint(x) => Box::new(x),
TaggedValue::RenderOutput(x) => Box::new(x),
TaggedValue::Palette(x) => Box::new(x),
TaggedValue::CentroidType(x) => Box::new(x),
}
}
@@ -299,6 +302,7 @@ impl<'a> TaggedValue {
TaggedValue::Footprint(_) => concrete!(graphene_core::transform::Footprint),
TaggedValue::RenderOutput(_) => concrete!(RenderOutput),
TaggedValue::Palette(_) => concrete!(Vec<Color>),
TaggedValue::CentroidType(_) => concrete!(graphene_core::vector::misc::CentroidType),
}
}
@@ -366,6 +370,7 @@ impl<'a> TaggedValue {
}
x if x == TypeId::of::<graphene_core::transform::Footprint>() => Ok(TaggedValue::Footprint(*downcast(input).unwrap())),
x if x == TypeId::of::<Vec<Color>>() => Ok(TaggedValue::Palette(*downcast(input).unwrap())),
x if x == TypeId::of::<graphene_core::vector::misc::CentroidType>() => Ok(TaggedValue::CentroidType(*downcast(input).unwrap())),
_ => Err(format!("Cannot convert {:?} to TaggedValue", DynAny::type_name(input.as_ref()))),
}
}

View File

@@ -578,12 +578,18 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
async_node!(graphene_core::memo::EndLetNode<_, _>, input: WasmEditorApi, output: RenderOutput, params: [String]),
async_node!(graphene_core::memo::EndLetNode<_, _>, input: WasmEditorApi, output: RenderOutput, params: [Option<Color>]),
async_node!(graphene_core::memo::EndLetNode<_, _>, input: WasmEditorApi, output: RenderOutput, params: [Vec<Color>]),
async_node!(graphene_core::memo::EndLetNode<_, _>, input: WasmEditorApi, output: RenderOutput, params: [DVec2]),
async_node!(graphene_core::memo::EndLetNode<_, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [Footprint => VectorData]),
async_node!(graphene_core::memo::EndLetNode<_, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [Footprint => ImageFrame<Color>]),
async_node!(graphene_core::memo::EndLetNode<_, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [Footprint => Option<Color>]),
async_node!(graphene_core::memo::EndLetNode<_, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [Footprint => Vec<Color>]),
async_node!(graphene_core::memo::EndLetNode<_, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [Footprint => GraphicGroup]),
async_node!(graphene_core::memo::EndLetNode<_, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [Footprint => Artboard]),
async_node!(graphene_core::memo::EndLetNode<_, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [Footprint => f32]),
async_node!(graphene_core::memo::EndLetNode<_, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [Footprint => f64]),
async_node!(graphene_core::memo::EndLetNode<_, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [Footprint => bool]),
async_node!(graphene_core::memo::EndLetNode<_, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [Footprint => String]),
async_node!(graphene_core::memo::EndLetNode<_, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [Footprint => DVec2]),
async_node!(
graphene_core::memo::EndLetNode<_, _>,
input: WasmEditorApi,
@@ -774,6 +780,8 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
register_node!(graphene_core::vector::PoissonDiskPoints<_>, input: VectorData, params: [f64]),
register_node!(graphene_core::vector::LengthsOfSegmentsOfSubpaths, input: VectorData, params: []),
register_node!(graphene_core::vector::SplinesFromPointsNode, input: VectorData, params: []),
async_node!(graphene_core::vector::AreaNode<_>, input: (), output: f64, fn_params: [Footprint => VectorData]),
async_node!(graphene_core::vector::CentroidNode<_, _>, input: (), output: DVec2, fn_params: [Footprint => VectorData, () => graphene_core::vector::misc::CentroidType]),
async_node!(graphene_core::vector::MorphNode<_, _, _, _>, input: Footprint, output: VectorData, fn_params: [Footprint => VectorData, Footprint => VectorData, () => u32, () => f64]),
register_node!(graphene_core::vector::generator_nodes::CircleGenerator<_>, input: (), params: [f64]),
register_node!(graphene_core::vector::generator_nodes::EllipseGenerator<_, _>, input: (), params: [f64, f64]),