Skip to content

Refactor the Solidify Stroke node implementation to use the Kurbo API #2608

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Apr 22, 2025
10 changes: 10 additions & 0 deletions node-graph/gcore/src/vector/misc.rs
Original file line number Diff line number Diff line change
@@ -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)]
Expand Down Expand Up @@ -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 }
}
67 changes: 66 additions & 1 deletion node-graph/gcore/src/vector/vector_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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<Item = impl Borrow<bezier_rs::Subpath<PointId>>>, preserve_id: bool) -> Self {
let mut vector_data = Self::empty();
Expand Down
46 changes: 42 additions & 4 deletions node-graph/gcore/src/vector/vector_data/attributes.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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));
Expand All @@ -660,6 +660,44 @@ impl VectorData {
}
}

/// Construct a [`bezier_rs::Bezier`] curve for stroke.
pub fn stroke_bezier_paths(&self) -> impl Iterator<Item = bezier_rs::Subpath<PointId>> {
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<Item = kurbo::BezPath> {
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<Item = bezier_rs::ManipulatorGroup<PointId>> + '_ {
self.stroke_bezier_paths().flat_map(|mut path| std::mem::take(path.manipulator_groups_mut()))
Expand Down Expand Up @@ -746,7 +784,7 @@ pub struct StrokePathIter<'a> {
}

impl Iterator for StrokePathIter<'_> {
type Item = bezier_rs::Subpath<PointId>;
type Item = (Vec<bezier_rs::ManipulatorGroup<PointId>>, bool);

fn next(&mut self) -> Option<Self::Item> {
let current_start = if let Some((index, _)) = self.points.iter().enumerate().skip(self.skip).find(|(_, val)| val.connected() == 1) {
Expand Down Expand Up @@ -805,7 +843,7 @@ impl Iterator for StrokePathIter<'_> {
}
}

Some(bezier_rs::Subpath::new(groups, closed))
Some((groups, closed))
}
}

Expand Down
56 changes: 30 additions & 26 deletions node-graph/gcore/src/vector/vector_nodes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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.
Expand Down
Loading