mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-07-08 00:05:00 +00:00
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:
parent
5e0e11b4c1
commit
a29802de36
4 changed files with 148 additions and 31 deletions
|
@ -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 }
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue