diff --git a/node-graph/gcore/src/vector/misc.rs b/node-graph/gcore/src/vector/misc.rs index eb1f2ba20c..4d2d6cb53d 100644 --- a/node-graph/gcore/src/vector/misc.rs +++ b/node-graph/gcore/src/vector/misc.rs @@ -1,4 +1,6 @@ use dyn_any::DynAny; +use glam::DVec2; +use kurbo::Point; /// Represents different ways of calculating the centroid. #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type)] @@ -101,3 +103,11 @@ pub enum ArcType { Closed, PieSlice, } + +pub fn point_to_dvec2(point: Point) -> DVec2 { + DVec2 { x: point.x, y: point.y } +} + +pub fn dvec2_to_point(value: DVec2) -> Point { + Point { x: value.x, y: value.y } +} diff --git a/node-graph/gcore/src/vector/vector_data.rs b/node-graph/gcore/src/vector/vector_data.rs index e2a701fba6..e92f3ff51d 100644 --- a/node-graph/gcore/src/vector/vector_data.rs +++ b/node-graph/gcore/src/vector/vector_data.rs @@ -2,11 +2,12 @@ mod attributes; mod indexed; mod modification; +use super::misc::point_to_dvec2; use super::style::{PathStyle, Stroke}; use crate::instances::Instances; use crate::{AlphaBlending, Color, GraphicGroupTable}; pub use attributes::*; -use bezier_rs::ManipulatorGroup; +use bezier_rs::{BezierHandles, ManipulatorGroup}; use core::borrow::Borrow; use dyn_any::DynAny; use glam::{DAffine2, DVec2}; @@ -176,6 +177,70 @@ impl VectorData { } } + /// Appends a Kurbo BezPath to the vector data. + pub fn append_bezpath(&mut self, bezpath: kurbo::BezPath) { + let mut first_point_index = None; + let mut last_point_index = None; + + let mut first_segment_id = None; + let mut last_segment_id = None; + + let mut point_id = self.point_domain.next_id(); + let mut segment_id = self.segment_domain.next_id(); + + let stroke_id = StrokeId::ZERO; + let fill_id = FillId::ZERO; + + for element in bezpath.elements() { + match *element { + kurbo::PathEl::MoveTo(point) => { + let next_point_index = self.point_domain.ids().len(); + self.point_domain.push(point_id.next_id(), point_to_dvec2(point)); + first_point_index = Some(next_point_index); + last_point_index = Some(next_point_index); + } + kurbo::PathEl::ClosePath => match (first_point_index, last_point_index) { + (Some(first_point_index), Some(last_point_index)) => { + let next_segment_id = segment_id.next_id(); + self.segment_domain.push(next_segment_id, first_point_index, last_point_index, BezierHandles::Linear, stroke_id); + + let next_region_id = self.region_domain.next_id(); + self.region_domain.push(next_region_id, first_segment_id.unwrap()..=next_segment_id, fill_id); + } + _ => { + error!("Empty bezpath cannot be closed.") + } + }, + _ => {} + } + + let mut append_path_element = |handle: BezierHandles, point: kurbo::Point| { + let next_point_index = self.point_domain.ids().len(); + self.point_domain.push(point_id.next_id(), point_to_dvec2(point)); + + let next_segment_id = segment_id.next_id(); + self.segment_domain.push(segment_id.next_id(), last_point_index.unwrap(), next_point_index, handle, stroke_id); + + last_point_index = Some(next_point_index); + first_segment_id = Some(first_segment_id.unwrap_or(next_segment_id)); + last_segment_id = Some(next_segment_id); + }; + + match *element { + kurbo::PathEl::LineTo(point) => append_path_element(BezierHandles::Linear, point), + kurbo::PathEl::QuadTo(handle, point) => append_path_element(BezierHandles::Quadratic { handle: point_to_dvec2(handle) }, point), + kurbo::PathEl::CurveTo(handle_start, handle_end, point) => append_path_element( + BezierHandles::Cubic { + handle_start: point_to_dvec2(handle_start), + handle_end: point_to_dvec2(handle_end), + }, + point, + ), + _ => {} + } + } + } + /// Construct some new vector data from subpaths with an identity transform and black fill. pub fn from_subpaths(subpaths: impl IntoIterator>>, preserve_id: bool) -> Self { let mut vector_data = Self::empty(); diff --git a/node-graph/gcore/src/vector/vector_data/attributes.rs b/node-graph/gcore/src/vector/vector_data/attributes.rs index 3b84eb8193..6d2f2b54e1 100644 --- a/node-graph/gcore/src/vector/vector_data/attributes.rs +++ b/node-graph/gcore/src/vector/vector_data/attributes.rs @@ -1,3 +1,4 @@ +use crate::vector::misc::dvec2_to_point; use crate::vector::vector_data::{HandleId, VectorData}; use bezier_rs::BezierHandles; use core::iter::zip; @@ -644,8 +645,7 @@ impl VectorData { }) } - /// Construct a [`bezier_rs::Bezier`] curve for stroke. - pub fn stroke_bezier_paths(&self) -> StrokePathIter<'_> { + fn build_stroke_path_iter(&self) -> StrokePathIter { let mut points = vec![StrokePathIterPointMetadata::default(); self.point_domain.ids().len()]; for (segment_index, (&start, &end)) in self.segment_domain.start_point.iter().zip(&self.segment_domain.end_point).enumerate() { points[start].set(StrokePathIterPointSegmentMetadata::new(segment_index, false)); @@ -660,6 +660,44 @@ impl VectorData { } } + /// Construct a [`bezier_rs::Bezier`] curve for stroke. + pub fn stroke_bezier_paths(&self) -> impl Iterator> { + self.build_stroke_path_iter().into_iter().map(|(group, closed)| bezier_rs::Subpath::new(group, closed)) + } + + /// Construct a [`kurbo::BezPath`] curve for stroke. + pub fn stroke_bezpath_iter(&self) -> impl Iterator { + self.build_stroke_path_iter().into_iter().map(|(group, closed)| { + let mut bezpath = kurbo::BezPath::new(); + let mut out_handle; + + let Some(first) = group.first() else { return bezpath }; + bezpath.move_to(dvec2_to_point(first.anchor)); + out_handle = first.out_handle; + + for manipulator in group.iter().skip(1) { + match (out_handle, manipulator.in_handle) { + (Some(handle_start), Some(handle_end)) => bezpath.curve_to(dvec2_to_point(handle_start), dvec2_to_point(handle_end), dvec2_to_point(manipulator.anchor)), + (None, None) => bezpath.line_to(dvec2_to_point(manipulator.anchor)), + (None, Some(handle)) => bezpath.quad_to(dvec2_to_point(handle), dvec2_to_point(manipulator.anchor)), + (Some(handle), None) => bezpath.quad_to(dvec2_to_point(handle), dvec2_to_point(manipulator.anchor)), + } + out_handle = manipulator.out_handle; + } + + if closed { + match (out_handle, first.in_handle) { + (Some(handle_start), Some(handle_end)) => bezpath.curve_to(dvec2_to_point(handle_start), dvec2_to_point(handle_end), dvec2_to_point(first.anchor)), + (None, None) => bezpath.line_to(dvec2_to_point(first.anchor)), + (None, Some(handle)) => bezpath.quad_to(dvec2_to_point(handle), dvec2_to_point(first.anchor)), + (Some(handle), None) => bezpath.quad_to(dvec2_to_point(handle), dvec2_to_point(first.anchor)), + } + bezpath.close_path(); + } + bezpath + }) + } + /// Construct an iterator [`bezier_rs::ManipulatorGroup`] for stroke. pub fn manipulator_groups(&self) -> impl Iterator> + '_ { self.stroke_bezier_paths().flat_map(|mut path| std::mem::take(path.manipulator_groups_mut())) @@ -746,7 +784,7 @@ pub struct StrokePathIter<'a> { } impl Iterator for StrokePathIter<'_> { - type Item = bezier_rs::Subpath; + type Item = (Vec>, bool); fn next(&mut self) -> Option { let current_start = if let Some((index, _)) = self.points.iter().enumerate().skip(self.skip).find(|(_, val)| val.connected() == 1) { @@ -805,7 +843,7 @@ impl Iterator for StrokePathIter<'_> { } } - Some(bezier_rs::Subpath::new(groups, closed)) + Some((groups, closed)) } } diff --git a/node-graph/gcore/src/vector/vector_nodes.rs b/node-graph/gcore/src/vector/vector_nodes.rs index d0fda86e8d..7a1554dee5 100644 --- a/node-graph/gcore/src/vector/vector_nodes.rs +++ b/node-graph/gcore/src/vector/vector_nodes.rs @@ -9,7 +9,7 @@ use crate::transform::{Footprint, Transform, TransformMut}; use crate::vector::PointDomain; use crate::vector::style::{LineCap, LineJoin}; use crate::{CloneVarArgs, Color, Context, Ctx, ExtractAll, GraphicElement, GraphicGroupTable, OwnedContextImpl}; -use bezier_rs::{Cap, Join, ManipulatorGroup, Subpath, SubpathTValue, TValue}; +use bezier_rs::{Join, ManipulatorGroup, Subpath, SubpathTValue, TValue}; use core::f64::consts::PI; use glam::{DAffine2, DVec2}; use rand::{Rng, SeedableRng}; @@ -1021,34 +1021,38 @@ async fn solidify_stroke(_: impl Ctx, vector_data: VectorDataTable) -> VectorDat let vector_data = vector_data.one_instance().instance; let stroke = vector_data.style.stroke().clone().unwrap_or_default(); - let subpaths = vector_data.stroke_bezier_paths(); + let bezpaths = vector_data.stroke_bezpath_iter(); let mut result = VectorData::empty(); - // Perform operation on all subpaths in this shape. - for subpath in subpaths { - // Taking the existing stroke data and passing it to Bezier-rs to generate new fill paths. - let stroke_radius = stroke.weight / 2.; - let join = match stroke.line_join { - LineJoin::Miter => Join::Miter(Some(stroke.line_join_miter_limit)), - LineJoin::Bevel => Join::Bevel, - LineJoin::Round => Join::Round, - }; - let cap = match stroke.line_cap { - LineCap::Butt => Cap::Butt, - LineCap::Round => Cap::Round, - LineCap::Square => Cap::Square, - }; - let solidified = subpath.outline(stroke_radius, join, cap); + // Taking the existing stroke data and passing it to kurbo::stroke to generate new fill paths. + let join = match stroke.line_join { + LineJoin::Miter => kurbo::Join::Miter, + LineJoin::Bevel => kurbo::Join::Bevel, + LineJoin::Round => kurbo::Join::Round, + }; + let cap = match stroke.line_cap { + LineCap::Butt => kurbo::Cap::Butt, + LineCap::Round => kurbo::Cap::Round, + LineCap::Square => kurbo::Cap::Square, + }; + let dash_offset = stroke.dash_offset; + let dash_pattern = stroke.dash_lengths; + let miter_limit = stroke.line_join_miter_limit; - // This is where we determine whether we have a closed or open path. Ex: Oval vs line segment. - if solidified.1.is_some() { - // Two closed subpaths, closed shape. Add both subpaths. - result.append_subpath(solidified.0, false); - result.append_subpath(solidified.1.unwrap(), false); - } else { - // One closed subpath, open path. - result.append_subpath(solidified.0, false); - } + let stroke_style = kurbo::Stroke::new(stroke.weight) + .with_caps(cap) + .with_join(join) + .with_dashes(dash_offset, dash_pattern) + .with_miter_limit(miter_limit); + + let stroke_options = kurbo::StrokeOpts::default(); + + // 0.25 is balanced between performace and accuracy of the curve. + const STROKE_TOLERANCE: f64 = 0.25; + + for path in bezpaths { + let solidified = kurbo::stroke(path, &stroke_style, &stroke_options, STROKE_TOLERANCE); + result.append_bezpath(solidified); } // We set our fill to our stroke's color, then clear our stroke.