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

* impl append_bezpath method to push a kurbo bezier path to vector data.

* refactor stroke_bezier_paths method and StrokePathIter iterator implementation

* refactor

* impl VectorData method to get strokes iterator of kurbo bezpath

* impl solidify stroke node

* refactor

* use StrokeOptLevel::Optimized for generation stroke fill

* add miter limit and dashes

* fix naming

---------

Co-authored-by: indierusty <priyaayadav@gmail.com>
Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
Priyanshu 2025-04-22 14:02:58 +05:30 committed by GitHub
parent 5e0e11b4c1
commit a29802de36
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 148 additions and 31 deletions

View file

@ -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 }
}

View file

@ -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<Item = impl Borrow<bezier_rs::Subpath<PointId>>>, preserve_id: bool) -> Self {
let mut vector_data = Self::empty();

View file

@ -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<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()))
@ -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) {
@ -805,7 +843,7 @@ impl Iterator for StrokePathIter<'_> {
}
}
Some(bezier_rs::Subpath::new(groups, closed))
Some((groups, closed))
}
}

View file

@ -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.