Skip to content

Commit a29802d

Browse files
indierustyindierustyKeavon
authored
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 <[email protected]> Co-authored-by: Keavon Chambers <[email protected]>
1 parent 5e0e11b commit a29802d

File tree

4 files changed

+148
-31
lines changed

4 files changed

+148
-31
lines changed

node-graph/gcore/src/vector/misc.rs

+10
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
use dyn_any::DynAny;
2+
use glam::DVec2;
3+
use kurbo::Point;
24

35
/// Represents different ways of calculating the centroid.
46
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type)]
@@ -101,3 +103,11 @@ pub enum ArcType {
101103
Closed,
102104
PieSlice,
103105
}
106+
107+
pub fn point_to_dvec2(point: Point) -> DVec2 {
108+
DVec2 { x: point.x, y: point.y }
109+
}
110+
111+
pub fn dvec2_to_point(value: DVec2) -> Point {
112+
Point { x: value.x, y: value.y }
113+
}

node-graph/gcore/src/vector/vector_data.rs

+66-1
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@ mod attributes;
22
mod indexed;
33
mod modification;
44

5+
use super::misc::point_to_dvec2;
56
use super::style::{PathStyle, Stroke};
67
use crate::instances::Instances;
78
use crate::{AlphaBlending, Color, GraphicGroupTable};
89
pub use attributes::*;
9-
use bezier_rs::ManipulatorGroup;
10+
use bezier_rs::{BezierHandles, ManipulatorGroup};
1011
use core::borrow::Borrow;
1112
use dyn_any::DynAny;
1213
use glam::{DAffine2, DVec2};
@@ -176,6 +177,70 @@ impl VectorData {
176177
}
177178
}
178179

180+
/// Appends a Kurbo BezPath to the vector data.
181+
pub fn append_bezpath(&mut self, bezpath: kurbo::BezPath) {
182+
let mut first_point_index = None;
183+
let mut last_point_index = None;
184+
185+
let mut first_segment_id = None;
186+
let mut last_segment_id = None;
187+
188+
let mut point_id = self.point_domain.next_id();
189+
let mut segment_id = self.segment_domain.next_id();
190+
191+
let stroke_id = StrokeId::ZERO;
192+
let fill_id = FillId::ZERO;
193+
194+
for element in bezpath.elements() {
195+
match *element {
196+
kurbo::PathEl::MoveTo(point) => {
197+
let next_point_index = self.point_domain.ids().len();
198+
self.point_domain.push(point_id.next_id(), point_to_dvec2(point));
199+
first_point_index = Some(next_point_index);
200+
last_point_index = Some(next_point_index);
201+
}
202+
kurbo::PathEl::ClosePath => match (first_point_index, last_point_index) {
203+
(Some(first_point_index), Some(last_point_index)) => {
204+
let next_segment_id = segment_id.next_id();
205+
self.segment_domain.push(next_segment_id, first_point_index, last_point_index, BezierHandles::Linear, stroke_id);
206+
207+
let next_region_id = self.region_domain.next_id();
208+
self.region_domain.push(next_region_id, first_segment_id.unwrap()..=next_segment_id, fill_id);
209+
}
210+
_ => {
211+
error!("Empty bezpath cannot be closed.")
212+
}
213+
},
214+
_ => {}
215+
}
216+
217+
let mut append_path_element = |handle: BezierHandles, point: kurbo::Point| {
218+
let next_point_index = self.point_domain.ids().len();
219+
self.point_domain.push(point_id.next_id(), point_to_dvec2(point));
220+
221+
let next_segment_id = segment_id.next_id();
222+
self.segment_domain.push(segment_id.next_id(), last_point_index.unwrap(), next_point_index, handle, stroke_id);
223+
224+
last_point_index = Some(next_point_index);
225+
first_segment_id = Some(first_segment_id.unwrap_or(next_segment_id));
226+
last_segment_id = Some(next_segment_id);
227+
};
228+
229+
match *element {
230+
kurbo::PathEl::LineTo(point) => append_path_element(BezierHandles::Linear, point),
231+
kurbo::PathEl::QuadTo(handle, point) => append_path_element(BezierHandles::Quadratic { handle: point_to_dvec2(handle) }, point),
232+
kurbo::PathEl::CurveTo(handle_start, handle_end, point) => append_path_element(
233+
BezierHandles::Cubic {
234+
handle_start: point_to_dvec2(handle_start),
235+
handle_end: point_to_dvec2(handle_end),
236+
},
237+
point,
238+
),
239+
_ => {}
240+
}
241+
}
242+
}
243+
179244
/// Construct some new vector data from subpaths with an identity transform and black fill.
180245
pub fn from_subpaths(subpaths: impl IntoIterator<Item = impl Borrow<bezier_rs::Subpath<PointId>>>, preserve_id: bool) -> Self {
181246
let mut vector_data = Self::empty();

node-graph/gcore/src/vector/vector_data/attributes.rs

+42-4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::vector::misc::dvec2_to_point;
12
use crate::vector::vector_data::{HandleId, VectorData};
23
use bezier_rs::BezierHandles;
34
use core::iter::zip;
@@ -644,8 +645,7 @@ impl VectorData {
644645
})
645646
}
646647

647-
/// Construct a [`bezier_rs::Bezier`] curve for stroke.
648-
pub fn stroke_bezier_paths(&self) -> StrokePathIter<'_> {
648+
fn build_stroke_path_iter(&self) -> StrokePathIter {
649649
let mut points = vec![StrokePathIterPointMetadata::default(); self.point_domain.ids().len()];
650650
for (segment_index, (&start, &end)) in self.segment_domain.start_point.iter().zip(&self.segment_domain.end_point).enumerate() {
651651
points[start].set(StrokePathIterPointSegmentMetadata::new(segment_index, false));
@@ -660,6 +660,44 @@ impl VectorData {
660660
}
661661
}
662662

663+
/// Construct a [`bezier_rs::Bezier`] curve for stroke.
664+
pub fn stroke_bezier_paths(&self) -> impl Iterator<Item = bezier_rs::Subpath<PointId>> {
665+
self.build_stroke_path_iter().into_iter().map(|(group, closed)| bezier_rs::Subpath::new(group, closed))
666+
}
667+
668+
/// Construct a [`kurbo::BezPath`] curve for stroke.
669+
pub fn stroke_bezpath_iter(&self) -> impl Iterator<Item = kurbo::BezPath> {
670+
self.build_stroke_path_iter().into_iter().map(|(group, closed)| {
671+
let mut bezpath = kurbo::BezPath::new();
672+
let mut out_handle;
673+
674+
let Some(first) = group.first() else { return bezpath };
675+
bezpath.move_to(dvec2_to_point(first.anchor));
676+
out_handle = first.out_handle;
677+
678+
for manipulator in group.iter().skip(1) {
679+
match (out_handle, manipulator.in_handle) {
680+
(Some(handle_start), Some(handle_end)) => bezpath.curve_to(dvec2_to_point(handle_start), dvec2_to_point(handle_end), dvec2_to_point(manipulator.anchor)),
681+
(None, None) => bezpath.line_to(dvec2_to_point(manipulator.anchor)),
682+
(None, Some(handle)) => bezpath.quad_to(dvec2_to_point(handle), dvec2_to_point(manipulator.anchor)),
683+
(Some(handle), None) => bezpath.quad_to(dvec2_to_point(handle), dvec2_to_point(manipulator.anchor)),
684+
}
685+
out_handle = manipulator.out_handle;
686+
}
687+
688+
if closed {
689+
match (out_handle, first.in_handle) {
690+
(Some(handle_start), Some(handle_end)) => bezpath.curve_to(dvec2_to_point(handle_start), dvec2_to_point(handle_end), dvec2_to_point(first.anchor)),
691+
(None, None) => bezpath.line_to(dvec2_to_point(first.anchor)),
692+
(None, Some(handle)) => bezpath.quad_to(dvec2_to_point(handle), dvec2_to_point(first.anchor)),
693+
(Some(handle), None) => bezpath.quad_to(dvec2_to_point(handle), dvec2_to_point(first.anchor)),
694+
}
695+
bezpath.close_path();
696+
}
697+
bezpath
698+
})
699+
}
700+
663701
/// Construct an iterator [`bezier_rs::ManipulatorGroup`] for stroke.
664702
pub fn manipulator_groups(&self) -> impl Iterator<Item = bezier_rs::ManipulatorGroup<PointId>> + '_ {
665703
self.stroke_bezier_paths().flat_map(|mut path| std::mem::take(path.manipulator_groups_mut()))
@@ -746,7 +784,7 @@ pub struct StrokePathIter<'a> {
746784
}
747785

748786
impl Iterator for StrokePathIter<'_> {
749-
type Item = bezier_rs::Subpath<PointId>;
787+
type Item = (Vec<bezier_rs::ManipulatorGroup<PointId>>, bool);
750788

751789
fn next(&mut self) -> Option<Self::Item> {
752790
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<'_> {
805843
}
806844
}
807845

808-
Some(bezier_rs::Subpath::new(groups, closed))
846+
Some((groups, closed))
809847
}
810848
}
811849

node-graph/gcore/src/vector/vector_nodes.rs

+30-26
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use crate::transform::{Footprint, Transform, TransformMut};
99
use crate::vector::PointDomain;
1010
use crate::vector::style::{LineCap, LineJoin};
1111
use crate::{CloneVarArgs, Color, Context, Ctx, ExtractAll, GraphicElement, GraphicGroupTable, OwnedContextImpl};
12-
use bezier_rs::{Cap, Join, ManipulatorGroup, Subpath, SubpathTValue, TValue};
12+
use bezier_rs::{Join, ManipulatorGroup, Subpath, SubpathTValue, TValue};
1313
use core::f64::consts::PI;
1414
use glam::{DAffine2, DVec2};
1515
use rand::{Rng, SeedableRng};
@@ -1021,34 +1021,38 @@ async fn solidify_stroke(_: impl Ctx, vector_data: VectorDataTable) -> VectorDat
10211021
let vector_data = vector_data.one_instance().instance;
10221022

10231023
let stroke = vector_data.style.stroke().clone().unwrap_or_default();
1024-
let subpaths = vector_data.stroke_bezier_paths();
1024+
let bezpaths = vector_data.stroke_bezpath_iter();
10251025
let mut result = VectorData::empty();
10261026

1027-
// Perform operation on all subpaths in this shape.
1028-
for subpath in subpaths {
1029-
// Taking the existing stroke data and passing it to Bezier-rs to generate new fill paths.
1030-
let stroke_radius = stroke.weight / 2.;
1031-
let join = match stroke.line_join {
1032-
LineJoin::Miter => Join::Miter(Some(stroke.line_join_miter_limit)),
1033-
LineJoin::Bevel => Join::Bevel,
1034-
LineJoin::Round => Join::Round,
1035-
};
1036-
let cap = match stroke.line_cap {
1037-
LineCap::Butt => Cap::Butt,
1038-
LineCap::Round => Cap::Round,
1039-
LineCap::Square => Cap::Square,
1040-
};
1041-
let solidified = subpath.outline(stroke_radius, join, cap);
1027+
// Taking the existing stroke data and passing it to kurbo::stroke to generate new fill paths.
1028+
let join = match stroke.line_join {
1029+
LineJoin::Miter => kurbo::Join::Miter,
1030+
LineJoin::Bevel => kurbo::Join::Bevel,
1031+
LineJoin::Round => kurbo::Join::Round,
1032+
};
1033+
let cap = match stroke.line_cap {
1034+
LineCap::Butt => kurbo::Cap::Butt,
1035+
LineCap::Round => kurbo::Cap::Round,
1036+
LineCap::Square => kurbo::Cap::Square,
1037+
};
1038+
let dash_offset = stroke.dash_offset;
1039+
let dash_pattern = stroke.dash_lengths;
1040+
let miter_limit = stroke.line_join_miter_limit;
10421041

1043-
// This is where we determine whether we have a closed or open path. Ex: Oval vs line segment.
1044-
if solidified.1.is_some() {
1045-
// Two closed subpaths, closed shape. Add both subpaths.
1046-
result.append_subpath(solidified.0, false);
1047-
result.append_subpath(solidified.1.unwrap(), false);
1048-
} else {
1049-
// One closed subpath, open path.
1050-
result.append_subpath(solidified.0, false);
1051-
}
1042+
let stroke_style = kurbo::Stroke::new(stroke.weight)
1043+
.with_caps(cap)
1044+
.with_join(join)
1045+
.with_dashes(dash_offset, dash_pattern)
1046+
.with_miter_limit(miter_limit);
1047+
1048+
let stroke_options = kurbo::StrokeOpts::default();
1049+
1050+
// 0.25 is balanced between performace and accuracy of the curve.
1051+
const STROKE_TOLERANCE: f64 = 0.25;
1052+
1053+
for path in bezpaths {
1054+
let solidified = kurbo::stroke(path, &stroke_style, &stroke_options, STROKE_TOLERANCE);
1055+
result.append_bezpath(solidified);
10521056
}
10531057

10541058
// We set our fill to our stroke's color, then clear our stroke.

0 commit comments

Comments
 (0)