Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion libraries/bezier-rs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ mod symmetrical_basis;
mod utils;

pub use bezier::*;
pub use consts::*;
pub use subpath::*;
pub use symmetrical_basis::*;
pub use utils::{Cap, Join, SubpathTValue, TValue, TValueType};
pub use utils::{Cap, Join, SubpathTValue, TValue, TValueType, line_intersection};
109 changes: 98 additions & 11 deletions node-graph/gcore/src/vector/vector_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ 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};
pub use indexed::VectorDataIndex;
use kurbo::BezPath;
pub use modification::*;
use std::collections::HashMap;

Expand Down Expand Up @@ -176,6 +177,100 @@ impl VectorData {
}
}

// # TODO:
pub fn as_bezpath(&self) -> BezPath {
for i in 0..self.segment_domain.handles().len() {}
todo!()
}

// # TODO: clean up and test
pub fn append_bezpath(&mut self, bezpath: &BezPath) {
let [mut first_seg, mut last_seg] = [None, None];
let [mut first_point, mut last_point] = [None, None];
let mut point_id = self.point_domain.next_id();
let mut segment_id = self.segment_domain.next_id();

let fill_id = FillId::ZERO;
let stroke_id = StrokeId::ZERO;

for elm in bezpath.elements().iter() {
match elm {
kurbo::PathEl::MoveTo(point) => {
let point_id = point_id.next_id();
self.point_domain.push(point_id, DVec2::new(point.x, point.y));

let index = self.point_domain.ids().len() - 1;

last_point = Some(index);
first_point = Some(first_point.unwrap_or(index));
}
kurbo::PathEl::LineTo(point) => {
let start = last_point.unwrap();
let point_id = point_id.next_id();

self.point_domain.push(point_id, DVec2::new(point.x, point.y));
let index = self.point_domain.ids().len() - 1;

let seg_id = segment_id.next_id();
first_seg = Some(first_seg.unwrap_or(seg_id));
last_seg = Some(seg_id);

self.segment_domain.push(seg_id, start, index, BezierHandles::Linear, stroke_id);
last_point = Some(index);
// first_point = Some(first_point.unwrap_or(index)); // is it needed?
}
kurbo::PathEl::QuadTo(handle, point) => {
let start = last_point.unwrap();
let point_id = point_id.next_id();

self.point_domain.push(point_id, DVec2::new(point.x, point.y));
let index = self.point_domain.ids().len() - 1;

let seg_id = segment_id.next_id();
first_seg = Some(first_seg.unwrap_or(seg_id));
last_seg = Some(seg_id);

let handle = DVec2::new(handle.x, handle.y);
self.segment_domain.push(seg_id, start, index, BezierHandles::Quadratic { handle }, stroke_id);

last_point = Some(index);
// first_point = Some(first_point.unwrap_or(index));
}
kurbo::PathEl::CurveTo(handle_start, handle_end, end_point) => {
let start = last_point.unwrap();
let end_point_id = point_id.next_id();

self.point_domain.push(end_point_id, DVec2::new(end_point.x, end_point.y));
let index = self.point_domain.ids().len() - 1;

let seg_id = segment_id.next_id();
first_seg = Some(first_seg.unwrap_or(seg_id));
last_seg = Some(seg_id);

let handle_start = DVec2::new(handle_start.x, handle_start.y);
let handle_end = DVec2::new(handle_end.x, handle_end.y);

self.segment_domain.push(seg_id, start, index, BezierHandles::Cubic { handle_start, handle_end }, stroke_id);

last_point = Some(index);
// first_point = Some(first_point.unwrap_or(index)); // is it needed?
}
kurbo::PathEl::ClosePath => {
if let (Some(first_id), Some(last_id)) = (first_point, last_point) {
let id = segment_id.next_id();
first_seg = Some(first_seg.unwrap_or(id));
last_seg = Some(id);
self.segment_domain.push(id, last_id, first_id, BezierHandles::Linear, stroke_id);
}

if let [Some(first_seg), Some(last_seg)] = [first_seg, last_seg] {
self.region_domain.push(self.region_domain.next_id(), first_seg..=last_seg, fill_id);
}
}
}
}
}

/// 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 Expand Up @@ -334,22 +429,14 @@ impl VectorData {
let (start_point_id, _, _) = self.segment_points_from_id(*segment_id)?;
let start_index = self.point_domain.resolve_id(start_point_id)?;

self.segment_domain.end_connected(start_index).find(|&id| id != *segment_id).map(|id| (start_point_id, id)).or(self
.segment_domain
.start_connected(start_index)
.find(|&id| id != *segment_id)
.map(|id| (start_point_id, id)))
self.segment_domain.end_connected(start_index).find(|&id| id != *segment_id).map(|id| (start_point_id, id))
}
ManipulatorPointId::EndHandle(segment_id) => {
// For end handle, find segments starting at our end point
let (_, end_point_id, _) = self.segment_points_from_id(*segment_id)?;
let end_index = self.point_domain.resolve_id(end_point_id)?;

self.segment_domain.start_connected(end_index).find(|&id| id != *segment_id).map(|id| (end_point_id, id)).or(self
.segment_domain
.end_connected(end_index)
.find(|&id| id != *segment_id)
.map(|id| (end_point_id, id)))
self.segment_domain.start_connected(end_index).find(|&id| id != *segment_id).map(|id| (end_point_id, id))
}
ManipulatorPointId::Anchor(_) => None,
}
Expand Down
21 changes: 21 additions & 0 deletions node-graph/gcore/src/vector/vector_data/attributes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use bezier_rs::BezierHandles;
use core::iter::zip;
use dyn_any::DynAny;
use glam::{DAffine2, DVec2};
use kurbo::{CubicBez, Line, PathSeg, QuadBez};
use std::collections::HashMap;
use std::hash::{Hash, Hasher};

Expand Down Expand Up @@ -583,6 +584,26 @@ impl VectorData {
.map(to_bezier)
}

/// Iterator over all of the [`kurbo::PathSeg`] following the order that they are stored in the segment domain, skipping invalid segments.
pub fn path_segment_iter(&self) -> impl Iterator<Item = PathSeg> {
self.segment_domain
.handles
.iter()
.zip(self.segment_domain.start_point())
.zip(self.segment_domain.end_point())
.map(|((&handle, &start), &end)| {
let start = self.point_domain.positions()[start];
let end = self.point_domain.positions()[end];
match handle {
BezierHandles::Linear => PathSeg::Line(Line::new((start.x, start.y), (end.x, end.y))),
BezierHandles::Quadratic { handle } => PathSeg::Quad(QuadBez::new((start.x, start.y), (handle.x, handle.y), (end.x, end.y))),
BezierHandles::Cubic { handle_start, handle_end } => {
PathSeg::Cubic(CubicBez::new((start.x, start.y), (handle_start.x, handle_start.y), (handle_end.x, handle_end.y), (end.x, end.y)))
}
}
})
}

/// Construct a [`bezier_rs::Bezier`] curve from an iterator of segments with (handles, start point, end point). Returns None if any ids are invalid or if the segments are not continuous.
fn subpath_from_segments(&self, segments: impl Iterator<Item = (bezier_rs::BezierHandles, usize, usize)>) -> Option<bezier_rs::Subpath<PointId>> {
let mut first_point = None;
Expand Down
111 changes: 97 additions & 14 deletions node-graph/gcore/src/vector/vector_nodes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@ use crate::transform::{Footprint, Transform, TransformMut};
use crate::vector::PointDomain;
use crate::vector::style::LineJoin;
use crate::{CloneVarArgs, Color, Context, Ctx, ExtractAll, GraphicElement, GraphicGroupTable, OwnedContextImpl};
use bezier_rs::{Cap, Join, ManipulatorGroup, Subpath, SubpathTValue, TValue};
use bezier_rs::{Bezier, BezierHandles, Cap, Join, ManipulatorGroup, Subpath, SubpathTValue, TValue};
use core::f64::consts::PI;
use glam::{DAffine2, DVec2};
use kurbo::offset::CubicOffset;
use kurbo::simplify::SimplifyBezPath;
use kurbo::{Affine, BezPath, CubicBez, ParamCurve, ParamCurveFit, PathSeg, Point, Shape};
use rand::{Rng, SeedableRng};

/// Implemented for types that can be converted to an iterator of vector data.
Expand Down Expand Up @@ -950,33 +953,113 @@ async fn bounding_box(_: impl Ctx, vector_data: VectorDataTable) -> VectorDataTa
VectorDataTable::new(result)
}

fn offset(cubics: &[CubicBez], distance: f64, join: kurbo::Join, miter_limit: Option<f64>) -> BezPath {
let mut offset_paths = Vec::new();
for cubic in cubics {
let cubic_offset = CubicOffset::new_regularized(*cubic, -distance, 3.);
let offset_segment = kurbo::fit_to_bezpath(&cubic_offset, 1e-3);
offset_paths.push(offset_segment);
}

let mut new_paths = Vec::new();

for i in 0..offset_paths.len() - 1 {
let j = i + 1;
let bez1 = offset_paths[i].clone();
let bez2 = offset_paths[j].clone();

let last_seg = bez1.segments().last().unwrap();
let first_seg = bez2.segments().nth(0).unwrap();

new_paths.push(bez1.clone());

if last_seg.end().distance(first_seg.start()) < 0.001 {
//////// NOTE
continue;
}

let out_tangent = SimplifyBezPath::new(last_seg.path_elements(0.1).into_iter()).sample_pt_tangent(1., 1.).tangent;
let in_tangent = SimplifyBezPath::new(first_seg.path_elements(0.1).into_iter()).sample_pt_tangent(0., 1.).tangent;

let angle = DVec2::new(out_tangent.x, out_tangent.y).angle_to(DVec2::new(in_tangent.x, in_tangent.y));
info!("{:?}", (i, j, angle.to_degrees()));

let mut apply_join = true;
if (angle > 0. && distance > 0.) || (angle < 0. && distance < 0.) {
// if let Some((clipped_bez1, clipped_bez2)) = clip_bezpath(&bez1, &bez2) {
// offset_bezpath[i] = clipped_bez1;
// offset_bezpath[j] = clipped_bez2;
// apply_join = false;
// }
}

if apply_join {
match join {
kurbo::Join::Bevel => {}
kurbo::Join::Miter => {}
kurbo::Join::Round => {}
}
}
}

if let Some(last) = offset_paths.get(offset_paths.len() - 1) {
new_paths.push(last.clone())
}

let mut bezpath = BezPath::new();

for i in 0..new_paths.len() {
if i == 0 {
bezpath.extend(&new_paths[i]);
continue;
}

let last_point = bezpath.get_seg(bezpath.elements().len() - 1).unwrap().end();
let next_point = new_paths[i].get_seg(1).unwrap().start();

if last_point.distance_squared(next_point) > 1. {
bezpath.line_to(next_point);
}
for elm in new_paths[i].elements().iter().skip(1) {
bezpath.push(*elm);
}
}

bezpath
}

#[node_macro::node(category("Vector"), path(graphene_core::vector), properties("offset_path_properties"))]
async fn offset_path(_: impl Ctx, vector_data: VectorDataTable, distance: f64, line_join: LineJoin, #[default(4.)] miter_limit: f64) -> VectorDataTable {
let vector_data_transform = vector_data.transform();
let vector_data = vector_data.one_instance().instance;

let subpaths = vector_data.stroke_bezier_paths();

let mut result = VectorData::empty();
result.style = vector_data.style.clone();
result.style.set_stroke_transform(DAffine2::IDENTITY);

// Perform operation on all subpaths in this shape.
let mut cubic_bez = Vec::new();

for mut subpath in subpaths {
subpath.apply_transform(vector_data_transform);
for bezier in subpath.iter() {
let Bezier { start, end, handles } = bezier.to_cubic();
let BezierHandles::Cubic { handle_start, handle_end } = handles else { continue };
let cubic = CubicBez::new((start.x, start.y), (handle_start.x, handle_start.y), (handle_end.x, handle_end.y), (end.x, end.y));
cubic_bez.push(cubic);
}
}

// Taking the existing stroke data and passing it to Bezier-rs to generate new paths.
let subpath_out = subpath.offset(
-distance,
match line_join {
LineJoin::Miter => Join::Miter(Some(miter_limit)),
LineJoin::Bevel => Join::Bevel,
LineJoin::Round => Join::Round,
},
);
let join = match line_join {
LineJoin::Miter => kurbo::Join::Miter,
LineJoin::Bevel => kurbo::Join::Bevel,
LineJoin::Round => kurbo::Join::Round,
};

// One closed subpath, open path.
result.append_subpath(subpath_out, false);
}
let offset_path = offset(&cubic_bez, distance, join, Some(miter_limit));

result.append_bezpath(&offset_path);

VectorDataTable::new(result)
}
Expand Down