From 06c3474e123881aa58871c05c8e3ce140ef14234 Mon Sep 17 00:00:00 2001 From: indierusty Date: Tue, 29 Jul 2025 10:09:07 +0530 Subject: [PATCH 01/11] add todo --- node-graph/gcore/src/vector/misc.rs | 4 ++++ node-graph/gcore/src/vector/vector_nodes.rs | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/node-graph/gcore/src/vector/misc.rs b/node-graph/gcore/src/vector/misc.rs index e0e514f8ea..e208984e9b 100644 --- a/node-graph/gcore/src/vector/misc.rs +++ b/node-graph/gcore/src/vector/misc.rs @@ -169,3 +169,7 @@ pub fn bezpath_from_manipulator_groups(manipulator_groups: &[ManipulatorGroup (Vec>, bool) { + todo!() +} diff --git a/node-graph/gcore/src/vector/vector_nodes.rs b/node-graph/gcore/src/vector/vector_nodes.rs index 275377c47e..a503775108 100644 --- a/node-graph/gcore/src/vector/vector_nodes.rs +++ b/node-graph/gcore/src/vector/vector_nodes.rs @@ -18,7 +18,7 @@ use crate::vector::style::{PaintOrder, StrokeAlign, StrokeCap, StrokeJoin}; use crate::vector::{FillId, RegionId}; use crate::{CloneVarArgs, Color, Context, Ctx, ExtractAll, GraphicElement, GraphicGroupTable, OwnedContextImpl}; -use bezier_rs::{BezierHandles, Join, ManipulatorGroup, Subpath}; +use bezier_rs::ManipulatorGroup; use core::f64::consts::PI; use core::hash::{Hash, Hasher}; use glam::{DAffine2, DVec2}; From a97943785982a790a7ded3859fd0c03fa41df150 Mon Sep 17 00:00:00 2001 From: indierusty Date: Wed, 30 Jul 2025 11:45:55 +0530 Subject: [PATCH 02/11] impl function to convert a bezpath to manipulator groups --- node-graph/gcore/src/vector/misc.rs | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/node-graph/gcore/src/vector/misc.rs b/node-graph/gcore/src/vector/misc.rs index e208984e9b..f04c2c1ff6 100644 --- a/node-graph/gcore/src/vector/misc.rs +++ b/node-graph/gcore/src/vector/misc.rs @@ -171,5 +171,29 @@ pub fn bezpath_from_manipulator_groups(manipulator_groups: &[ManipulatorGroup (Vec>, bool) { - todo!() + let mut manipulator_groups = Vec::new(); + let mut in_handle = None; + let mut is_closed = false; + + for element in bezpath.elements() { + let (manipulator_group, next_in_handle) = match *element { + kurbo::PathEl::MoveTo(point) => (ManipulatorGroup::new(point_to_dvec2(point), in_handle, None), None), + kurbo::PathEl::LineTo(point) => (ManipulatorGroup::new(point_to_dvec2(point), in_handle, None), None), + kurbo::PathEl::QuadTo(point, point1) => (ManipulatorGroup::new(point_to_dvec2(point), in_handle, Some(point_to_dvec2(point1))), None), + kurbo::PathEl::CurveTo(point, point1, point2) => (ManipulatorGroup::new(point_to_dvec2(point), in_handle, Some(point_to_dvec2(point1))), Some(point_to_dvec2(point2))), + kurbo::PathEl::ClosePath => { + is_closed = true; + break; + } + }; + + in_handle = next_in_handle; + manipulator_groups.push(manipulator_group); + } + + if let Some(first) = manipulator_groups.first_mut() { + first.in_handle = in_handle; + } + + (manipulator_groups, is_closed) } From 8966c4da7b0a5afe1c6d8b5dc492d0260eba4d51 Mon Sep 17 00:00:00 2001 From: indierusty Date: Wed, 30 Jul 2025 14:22:38 +0530 Subject: [PATCH 03/11] refactor few node impls --- node-graph/gcore/src/vector/vector_nodes.rs | 59 ++++++++++----------- 1 file changed, 27 insertions(+), 32 deletions(-) diff --git a/node-graph/gcore/src/vector/vector_nodes.rs b/node-graph/gcore/src/vector/vector_nodes.rs index a503775108..5509cf54f2 100644 --- a/node-graph/gcore/src/vector/vector_nodes.rs +++ b/node-graph/gcore/src/vector/vector_nodes.rs @@ -1,7 +1,7 @@ use super::algorithms::bezpath_algorithms::{self, position_on_bezpath, sample_polyline_on_bezpath, split_bezpath, tangent_on_bezpath}; use super::algorithms::offset_subpath::offset_subpath; use super::algorithms::spline::{solve_spline_first_handle_closed, solve_spline_first_handle_open}; -use super::misc::{CentroidType, point_to_dvec2}; +use super::misc::{CentroidType, bezpath_from_manipulator_groups, bezpath_to_manipulator_groups, point_to_dvec2}; use super::style::{Fill, Gradient, GradientStops, Stroke}; use super::{PointId, SegmentDomain, SegmentId, StrokeId, VectorData, VectorDataExt, VectorDataTable}; use crate::bounds::BoundingBox; @@ -22,7 +22,7 @@ use bezier_rs::ManipulatorGroup; use core::f64::consts::PI; use core::hash::{Hash, Hasher}; use glam::{DAffine2, DVec2}; -use kurbo::{Affine, BezPath, DEFAULT_ACCURACY, ParamCurve, PathEl, PathSeg, Shape}; +use kurbo::{Affine, BezPath, DEFAULT_ACCURACY, Line, ParamCurve, PathEl, PathSeg, Shape}; use rand::{Rng, SeedableRng}; use std::collections::hash_map::DefaultHasher; use std::f64::consts::TAU; @@ -465,34 +465,33 @@ async fn round_corners( // Grab the initial point ID as a stable starting point let mut initial_point_id = source.point_domain.ids().first().copied().unwrap_or(PointId::generate()); - for mut subpath in source.stroke_bezier_paths() { - subpath.apply_transform(source_transform); + for mut bezpath in source.stroke_bezpath_iter() { + bezpath.apply_affine(Affine::new(source_transform.to_cols_array())); + let (manipulator_groups, is_closed) = bezpath_to_manipulator_groups(&bezpath); // End if not enough points for corner rounding - if subpath.manipulator_groups().len() < 3 { - result.append_subpath(subpath, false); + if manipulator_groups.len() < 3 { + result.append_bezpath(bezpath); continue; } - let groups = subpath.manipulator_groups(); - let mut new_groups = Vec::new(); - let is_closed = subpath.closed(); + let mut new_manipulator_groups = Vec::new(); - for i in 0..groups.len() { + for i in 0..manipulator_groups.len() { // Skip first and last points for open paths - if !is_closed && (i == 0 || i == groups.len() - 1) { - new_groups.push(groups[i]); + if !is_closed && (i == 0 || i == manipulator_groups.len() - 1) { + new_manipulator_groups.push(manipulator_groups[i]); continue; } // Not the prettiest, but it makes the rest of the logic more readable - let prev_idx = if i == 0 { if is_closed { groups.len() - 1 } else { 0 } } else { i - 1 }; + let prev_idx = if i == 0 { if is_closed { manipulator_groups.len() - 1 } else { 0 } } else { i - 1 }; let curr_idx = i; - let next_idx = if i == groups.len() - 1 { if is_closed { 0 } else { i } } else { i + 1 }; + let next_idx = if i == manipulator_groups.len() - 1 { if is_closed { 0 } else { i } } else { i + 1 }; - let prev = groups[prev_idx].anchor; - let curr = groups[curr_idx].anchor; - let next = groups[next_idx].anchor; + let prev = manipulator_groups[prev_idx].anchor; + let curr = manipulator_groups[curr_idx].anchor; + let next = manipulator_groups[next_idx].anchor; let dir1 = (curr - prev).normalize_or(DVec2::X); let dir2 = (next - curr).normalize_or(DVec2::X); @@ -501,7 +500,7 @@ async fn round_corners( // Skip near-straight corners if theta > PI - min_angle_threshold.to_radians() { - new_groups.push(groups[curr_idx]); + new_manipulator_groups.push(manipulator_groups[curr_idx]); continue; } @@ -514,7 +513,7 @@ async fn round_corners( let p2 = curr + dir2 * distance_along_edge; // Add first point (coming into the rounded corner) - new_groups.push(ManipulatorGroup { + new_manipulator_groups.push(ManipulatorGroup { anchor: p1, in_handle: None, out_handle: Some(curr - dir1 * distance_along_edge * roundness), @@ -522,7 +521,7 @@ async fn round_corners( }); // Add second point (coming out of the rounded corner) - new_groups.push(ManipulatorGroup { + new_manipulator_groups.push(ManipulatorGroup { anchor: p2, in_handle: Some(curr + dir2 * distance_along_edge * roundness), out_handle: None, @@ -531,9 +530,9 @@ async fn round_corners( } // One subpath for each shape - let mut rounded_subpath = Subpath::new(new_groups, is_closed); - rounded_subpath.apply_transform(source_transform_inverse); - result.append_subpath(rounded_subpath, false); + let mut rounded_subpath = bezpath_from_manipulator_groups(&new_manipulator_groups, is_closed); + rounded_subpath.apply_affine(Affine::new(source_transform_inverse.to_cols_array())); + result.append_bezpath(rounded_subpath); } result.upstream_graphic_group = upstream_graphic_group; @@ -769,9 +768,9 @@ async fn auto_tangents( }); } - let mut softened_subpath = Subpath::new(new_groups, is_closed); - softened_subpath.apply_transform(transform.inverse()); - result.append_subpath(softened_subpath, true); + let mut softened_bezpath = bezpath_from_manipulator_groups(&new_groups, is_closed); + softened_bezpath.apply_affine(Affine::new(transform.inverse().to_cols_array())); + result.append_bezpath(softened_bezpath); } Instance { @@ -1900,15 +1899,11 @@ fn bevel_algorithm(mut vector_data: VectorData, vector_data_transform: DAffine2, let spilt_distance = calculate_distance_to_spilt(bezier, next_bezier, distance); if is_linear(&bezier) { - let start = point_to_dvec2(bezier.start()); - let end = point_to_dvec2(bezier.end()); - bezier = handles_to_segment(start, BezierHandles::Linear, end); + bezier = PathSeg::Line(Line::new(bezier.start(), bezier.end())); } if is_linear(&next_bezier) { - let start = point_to_dvec2(next_bezier.start()); - let end = point_to_dvec2(next_bezier.end()); - next_bezier = handles_to_segment(start, BezierHandles::Linear, end); + next_bezier = PathSeg::Line(Line::new(next_bezier.start(), next_bezier.end())); } let inverse_transform = if vector_data_transform.matrix2.determinant() != 0. { From 967c8c5ae95a5cf90889475632516fef7844e826 Mon Sep 17 00:00:00 2001 From: indierusty Date: Wed, 30 Jul 2025 15:48:08 +0530 Subject: [PATCH 04/11] refactor vector nodes test and few methods on VectorData struct --- node-graph/gcore/src/vector/vector_data.rs | 9 +- .../src/vector/vector_data/attributes.rs | 66 ++++++--------- node-graph/gcore/src/vector/vector_nodes.rs | 83 ++++++++++++------- 3 files changed, 85 insertions(+), 73 deletions(-) diff --git a/node-graph/gcore/src/vector/vector_data.rs b/node-graph/gcore/src/vector/vector_data.rs index 7f015a6adb..ccf6832beb 100644 --- a/node-graph/gcore/src/vector/vector_data.rs +++ b/node-graph/gcore/src/vector/vector_data.rs @@ -17,7 +17,7 @@ use core::hash::Hash; use dyn_any::DynAny; use glam::{DAffine2, DVec2}; pub use indexed::VectorDataIndex; -use kurbo::{Affine, Rect, Shape}; +use kurbo::{Affine, BezPath, Rect, Shape}; pub use modification::*; use std::collections::HashMap; @@ -195,6 +195,13 @@ impl VectorData { Self::from_subpaths([subpath], false) } + /// Construct some new vector data from a single bezpath with an identity transform and black fill. + pub fn from_bezpath(bezpath: BezPath) -> Self { + let mut vector_data = Self::default(); + vector_data.append_bezpath(bezpath); + vector_data + } + /// Construct some new vector data from subpaths with an identity transform and black fill. pub fn from_subpaths(subpaths: impl IntoIterator>>, preserve_id: bool) -> Self { let mut vector_data = Self::default(); diff --git a/node-graph/gcore/src/vector/vector_data/attributes.rs b/node-graph/gcore/src/vector/vector_data/attributes.rs index ea4460c78c..40d63bd90f 100644 --- a/node-graph/gcore/src/vector/vector_data/attributes.rs +++ b/node-graph/gcore/src/vector/vector_data/attributes.rs @@ -819,48 +819,8 @@ impl VectorData { Some(bezier_rs::Subpath::new(groups, closed)) } - /// 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) -> Option> { - let mut first_point = None; - let mut groups = Vec::new(); - let mut last: Option<(usize, BezierHandles)> = None; - - for (handle, start, end) in segments { - if last.is_some_and(|(previous_end, _)| previous_end != start) { - warn!("subpath_from_segments that were not continuous"); - return None; - } - first_point = Some(first_point.unwrap_or(start)); - - groups.push(ManipulatorGroup { - anchor: self.point_domain.positions()[start], - in_handle: last.and_then(|(_, handle)| handle.end()), - out_handle: handle.start(), - id: self.point_domain.ids()[start], - }); - - last = Some((end, handle)); - } - - let closed = groups.len() > 1 && last.map(|(point, _)| point) == first_point; - - if let Some((end, last_handle)) = last { - if closed { - groups[0].in_handle = last_handle.end(); - } else { - groups.push(ManipulatorGroup { - anchor: self.point_domain.positions()[end], - in_handle: last_handle.end(), - out_handle: None, - id: self.point_domain.ids()[end], - }); - } - } - Some(bezier_rs::Subpath::new(groups, closed)) - } - /// Construct a [`bezier_rs::Bezier`] curve for each region, skipping invalid regions. - pub fn region_bezier_paths(&self) -> impl Iterator)> + '_ { + pub fn region_manipulator_groups(&self) -> impl Iterator>)> + '_ { self.region_domain .id .iter() @@ -876,7 +836,29 @@ impl VectorData { .zip(self.segment_domain.end_point.get(range)?) .map(|((&handles, &start), &end)| (handles, start, end)); - self.subpath_from_segments(segments_iter).map(|subpath| (id, subpath)) + let mut manipulator_groups = Vec::new(); + let mut in_handle = None; + + for segment in segments_iter { + let (handles, start_point_index, _end_point_index) = segment; + let start_point_id = self.point_domain.id[start_point_index]; + let start_point = self.point_domain.position[start_point_index]; + + let (manipulator_group, next_in_handle) = match handles { + BezierHandles::Linear => (ManipulatorGroup::new_with_id(start_point, in_handle, None, start_point_id), None), + BezierHandles::Quadratic { handle } => (ManipulatorGroup::new_with_id(start_point, in_handle, Some(handle), start_point_id), None), + BezierHandles::Cubic { handle_start, handle_end } => (ManipulatorGroup::new_with_id(start_point, in_handle, Some(handle_start), start_point_id), Some(handle_end)), + }; + + in_handle = next_in_handle; + manipulator_groups.push(manipulator_group); + } + + if let Some(first) = manipulator_groups.first_mut() { + first.in_handle = in_handle; + } + + Some((id, manipulator_groups)) }) } diff --git a/node-graph/gcore/src/vector/vector_nodes.rs b/node-graph/gcore/src/vector/vector_nodes.rs index 5509cf54f2..64484715d4 100644 --- a/node-graph/gcore/src/vector/vector_nodes.rs +++ b/node-graph/gcore/src/vector/vector_nodes.rs @@ -18,7 +18,7 @@ use crate::vector::style::{PaintOrder, StrokeAlign, StrokeCap, StrokeJoin}; use crate::vector::{FillId, RegionId}; use crate::{CloneVarArgs, Color, Context, Ctx, ExtractAll, GraphicElement, GraphicGroupTable, OwnedContextImpl}; -use bezier_rs::ManipulatorGroup; +use bezier_rs::{Join, ManipulatorGroup}; use core::f64::consts::PI; use core::hash::{Hash, Hasher}; use glam::{DAffine2, DVec2}; @@ -2126,8 +2126,8 @@ mod test { } } - fn vector_node(data: Subpath) -> VectorDataTable { - VectorDataTable::new(VectorData::from_subpath(data)) + fn vector_node_from_bezpath(bezpath: BezPath) -> VectorDataTable { + VectorDataTable::new(VectorData::from_bezpath(bezpath)) } fn create_vector_data_instance(bezpath: BezPath, transform: DAffine2) -> Instance { @@ -2144,37 +2144,51 @@ mod test { async fn repeat() { let direction = DVec2::X * 1.5; let instances = 3; - let repeated = super::repeat(Footprint::default(), vector_node(Subpath::new_rect(DVec2::ZERO, DVec2::ONE)), direction, 0., instances).await; + let repeated = super::repeat( + Footprint::default(), + vector_node_from_bezpath(Rect::new(0., 0., 1., 1.).to_path(DEFAULT_ACCURACY)), + direction, + 0., + instances, + ) + .await; let vector_data = super::flatten_path(Footprint::default(), repeated).await; let vector_data = vector_data.instance_ref_iter().next().unwrap().instance; - assert_eq!(vector_data.region_bezier_paths().count(), 3); - for (index, (_, subpath)) in vector_data.region_bezier_paths().enumerate() { - assert!((subpath.manipulator_groups()[0].anchor - direction * index as f64 / (instances - 1) as f64).length() < 1e-5); + assert_eq!(vector_data.region_manipulator_groups().count(), 3); + for (index, (_, manipulator_groups)) in vector_data.region_manipulator_groups().enumerate() { + assert!((manipulator_groups[0].anchor - direction * index as f64 / (instances - 1) as f64).length() < 1e-5); } } #[tokio::test] async fn repeat_transform_position() { let direction = DVec2::new(12., 10.); let instances = 8; - let repeated = super::repeat(Footprint::default(), vector_node(Subpath::new_rect(DVec2::ZERO, DVec2::ONE)), direction, 0., instances).await; + let repeated = super::repeat( + Footprint::default(), + vector_node_from_bezpath(Rect::new(0., 0., 1., 1.).to_path(DEFAULT_ACCURACY)), + direction, + 0., + instances, + ) + .await; let vector_data = super::flatten_path(Footprint::default(), repeated).await; let vector_data = vector_data.instance_ref_iter().next().unwrap().instance; - assert_eq!(vector_data.region_bezier_paths().count(), 8); - for (index, (_, subpath)) in vector_data.region_bezier_paths().enumerate() { - assert!((subpath.manipulator_groups()[0].anchor - direction * index as f64 / (instances - 1) as f64).length() < 1e-5); + assert_eq!(vector_data.region_manipulator_groups().count(), 8); + for (index, (_, manipulator_groups)) in vector_data.region_manipulator_groups().enumerate() { + assert!((manipulator_groups[0].anchor - direction * index as f64 / (instances - 1) as f64).length() < 1e-5); } } #[tokio::test] async fn circular_repeat() { - let repeated = super::circular_repeat(Footprint::default(), vector_node(Subpath::new_rect(DVec2::NEG_ONE, DVec2::ONE)), 45., 4., 8).await; + let repeated = super::circular_repeat(Footprint::default(), vector_node_from_bezpath(Rect::new(-1., -1., 1., 1.).to_path(DEFAULT_ACCURACY)), 45., 4., 8).await; let vector_data = super::flatten_path(Footprint::default(), repeated).await; let vector_data = vector_data.instance_ref_iter().next().unwrap().instance; - assert_eq!(vector_data.region_bezier_paths().count(), 8); + assert_eq!(vector_data.region_manipulator_groups().count(), 8); - for (index, (_, subpath)) in vector_data.region_bezier_paths().enumerate() { + for (index, (_, manipulator_groups)) in vector_data.region_manipulator_groups().enumerate() { let expected_angle = (index as f64 + 1.) * 45.; - let center = (subpath.manipulator_groups()[0].anchor + subpath.manipulator_groups()[2].anchor) / 2.; + let center = (manipulator_groups[0].anchor + manipulator_groups[2].anchor) / 2.; let actual_angle = DVec2::Y.angle_to(center).to_degrees(); assert!((actual_angle - expected_angle).abs() % 360. < 1e-5, "Expected {expected_angle} found {actual_angle}"); @@ -2182,14 +2196,15 @@ mod test { } #[tokio::test] async fn bounding_box() { - let bounding_box = super::bounding_box((), vector_node(Subpath::new_rect(DVec2::NEG_ONE, DVec2::ONE))).await; + let bounding_box = super::bounding_box((), vector_node_from_bezpath(Rect::new(-1., -1., 1., 1.).to_path(DEFAULT_ACCURACY))).await; let bounding_box = bounding_box.instance_ref_iter().next().unwrap().instance; - assert_eq!(bounding_box.region_bezier_paths().count(), 1); - let subpath = bounding_box.region_bezier_paths().next().unwrap().1; - assert_eq!(&subpath.anchors()[..4], &[DVec2::NEG_ONE, DVec2::new(1., -1.), DVec2::ONE, DVec2::new(-1., 1.),]); + assert_eq!(bounding_box.region_manipulator_groups().count(), 1); + let manipulator_groups_anchors = bounding_box.region_manipulator_groups().next().unwrap().1.iter().map(|group| group.anchor).collect::>(); + + assert_eq!(&manipulator_groups_anchors[..4], &[DVec2::NEG_ONE, DVec2::new(1., -1.), DVec2::ONE, DVec2::new(-1., 1.),]); // Test a VectorData with non-zero rotation - let square = VectorData::from_subpath(Subpath::new_rect(DVec2::NEG_ONE, DVec2::ONE)); + let square = VectorData::from_bezpath(Rect::new(-1., -1., 1., 1.).to_path(DEFAULT_ACCURACY)); let mut square = VectorDataTable::new(square); *square.get_mut(0).unwrap().transform *= DAffine2::from_angle(std::f64::consts::FRAC_PI_4); let bounding_box = BoundingBoxNode { @@ -2198,34 +2213,42 @@ mod test { .eval(Footprint::default()) .await; let bounding_box = bounding_box.instance_ref_iter().next().unwrap().instance; - assert_eq!(bounding_box.region_bezier_paths().count(), 1); - let subpath = bounding_box.region_bezier_paths().next().unwrap().1; + assert_eq!(bounding_box.region_manipulator_groups().count(), 1); + let manipulator_groups_anchors = bounding_box.region_manipulator_groups().next().unwrap().1.iter().map(|group| group.anchor).collect::>(); + let expected_bounding_box = [DVec2::NEG_ONE, DVec2::new(1., -1.), DVec2::ONE, DVec2::new(-1., 1.)]; for i in 0..4 { - assert_eq!(subpath.anchors()[i], expected_bounding_box[i]); + assert_eq!(manipulator_groups_anchors[i], expected_bounding_box[i]); } } #[tokio::test] async fn copy_to_points() { - let points = Subpath::new_rect(DVec2::NEG_ONE * 10., DVec2::ONE * 10.); - let instance = Subpath::new_rect(DVec2::NEG_ONE, DVec2::ONE); + let points = Rect::new(-10., -10., 10., 10.).to_path(DEFAULT_ACCURACY); + let instance = Rect::new(-1., -1., 1., 1.).to_path(DEFAULT_ACCURACY); - let expected_points = VectorData::from_subpath(points.clone()).point_domain.positions().to_vec(); + let expected_points = VectorData::from_bezpath(points.clone()).point_domain.positions().to_vec(); - let copy_to_points = super::copy_to_points(Footprint::default(), vector_node(points), vector_node(instance), 1., 1., 0., 0, 0., 0).await; + let copy_to_points = super::copy_to_points(Footprint::default(), vector_node_from_bezpath(points), vector_node_from_bezpath(instance), 1., 1., 0., 0, 0., 0).await; let flatten_path = super::flatten_path(Footprint::default(), copy_to_points).await; let flattened_copy_to_points = flatten_path.instance_ref_iter().next().unwrap().instance; - assert_eq!(flattened_copy_to_points.region_bezier_paths().count(), expected_points.len()); + assert_eq!(flattened_copy_to_points.region_manipulator_groups().count(), expected_points.len()); - for (index, (_, subpath)) in flattened_copy_to_points.region_bezier_paths().enumerate() { + for (index, (_, manipulator_groups)) in flattened_copy_to_points.region_manipulator_groups().enumerate() { let offset = expected_points[index]; + let manipulator_groups_anchors = manipulator_groups.iter().map(|group| group.anchor).collect::>(); assert_eq!( - &subpath.anchors(), + &manipulator_groups_anchors, &[offset + DVec2::NEG_ONE, offset + DVec2::new(1., -1.), offset + DVec2::ONE, offset + DVec2::new(-1., 1.),] ); } } + + use bezier_rs::Subpath; + fn vector_node(data: Subpath) -> VectorDataTable { + VectorDataTable::new(VectorData::from_subpath(data)) + } + #[tokio::test] async fn sample_polyline() { let path = Subpath::from_bezier(&Bezier::from_cubic_dvec2(DVec2::ZERO, DVec2::ZERO, DVec2::X * 100., DVec2::X * 100.)); From 950cc52bf519f4179873f8ee8dff95ebb829132d Mon Sep 17 00:00:00 2001 From: indierusty Date: Thu, 31 Jul 2025 20:33:34 +0530 Subject: [PATCH 05/11] refactor tests --- .../vector/algorithms/bezpath_algorithms.rs | 37 +++-- node-graph/gcore/src/vector/misc.rs | 41 ++++- .../src/vector/vector_data/attributes.rs | 26 ++++ node-graph/gcore/src/vector/vector_nodes.rs | 146 ++++++++++-------- 4 files changed, 170 insertions(+), 80 deletions(-) diff --git a/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs b/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs index 109ef31e8a..8d21d5ec6e 100644 --- a/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs +++ b/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs @@ -1,8 +1,9 @@ +use std::path::Path; + use super::poisson_disk::poisson_disk_sample; -use crate::vector::algorithms::offset_subpath::MAX_ABSOLUTE_DIFFERENCE; use crate::vector::misc::{PointSpacingType, dvec2_to_point}; use glam::DVec2; -use kurbo::{BezPath, CubicBez, DEFAULT_ACCURACY, Line, ParamCurve, ParamCurveDeriv, PathEl, PathSeg, Point, QuadBez, Rect, Shape}; +use kurbo::{BezPath, DEFAULT_ACCURACY, Line, ParamCurve, ParamCurveDeriv, PathEl, PathSeg, Point, Rect, Shape}; /// Splits the [`BezPath`] at `t` value which lie in the range of [0, 1]. /// Returns [`None`] if the given [`BezPath`] has no segments or `t` is within f64::EPSILON of 0 or 1. @@ -175,6 +176,25 @@ pub fn t_value_to_parametric(bezpath: &BezPath, t: f64, euclidian: bool, segment bezpath_t_value_to_parametric(bezpath, BezPathTValue::GlobalParametric(t), segments_length) } +pub enum TValue { + Parametric(f64), + Euclidean(f64), +} + +pub fn trim_pathseg(segment: PathSeg, t1: TValue, t2: TValue) -> Option { + let t1 = eval_pathseg(segment, t1); + let t2 = eval_pathseg(segment, t2); + + if t1 > t2 { None } else { Some(segment.subsegment(t1..t2)) } +} + +pub fn eval_pathseg(segment: PathSeg, t_value: TValue) -> f64 { + match t_value { + TValue::Parametric(t) => t, + TValue::Euclidean(t) => eval_pathseg_euclidean(segment, t, DEFAULT_ACCURACY), + } +} + /// Finds the t value of point on the given path segment i.e fractional distance along the segment's total length. /// It uses a binary search to find the value `t` such that the ratio `length_up_to_t / total_length` approximates the input `distance`. pub fn eval_pathseg_euclidean(path_segment: PathSeg, distance: f64, accuracy: f64) -> f64 { @@ -315,16 +335,3 @@ pub fn poisson_disk_points(bezpath_index: usize, bezpaths: &[(BezPath, Rect)], s poisson_disk_sample(offset, width, height, separation_disk_diameter, point_in_shape_checker, line_intersect_shape_checker, rng) } - -/// Returns true if the Bezier curve is equivalent to a line. -/// -/// **NOTE**: This is different from simply checking if the segment is [`PathSeg::Line`] or [`PathSeg::Quad`] or [`PathSeg::Cubic`]. Bezier curve can also be a line if the control points are colinear to the start and end points. Therefore if the handles exceed the start and end point, it will still be considered as a line. -pub fn is_linear(segment: &PathSeg) -> bool { - let is_colinear = |a: Point, b: Point, c: Point| -> bool { ((b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x)).abs() < MAX_ABSOLUTE_DIFFERENCE }; - - match *segment { - PathSeg::Line(_) => true, - PathSeg::Quad(QuadBez { p0, p1, p2 }) => is_colinear(p0, p1, p2), - PathSeg::Cubic(CubicBez { p0, p1, p2, p3 }) => is_colinear(p0, p1, p3) && is_colinear(p0, p2, p3), - } -} diff --git a/node-graph/gcore/src/vector/misc.rs b/node-graph/gcore/src/vector/misc.rs index f04c2c1ff6..05bb8172ec 100644 --- a/node-graph/gcore/src/vector/misc.rs +++ b/node-graph/gcore/src/vector/misc.rs @@ -1,9 +1,11 @@ +use std::ops::Sub; + use bezier_rs::{BezierHandles, ManipulatorGroup, Subpath}; use dyn_any::DynAny; use glam::DVec2; -use kurbo::{BezPath, CubicBez, Line, PathSeg, Point, QuadBez}; +use kurbo::{BezPath, CubicBez, Line, ParamCurve, PathSeg, Point, QuadBez}; -use super::PointId; +use super::{PointId, algorithms::offset_subpath::MAX_ABSOLUTE_DIFFERENCE}; /// Represents different ways of calculating the centroid. #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)] @@ -197,3 +199,38 @@ pub fn bezpath_to_manipulator_groups(bezpath: &BezPath) -> (Vec bool { + let is_colinear = |a: Point, b: Point, c: Point| -> bool { ((b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x)).abs() < MAX_ABSOLUTE_DIFFERENCE }; + + match segment { + PathSeg::Line(_) => true, + PathSeg::Quad(QuadBez { p0, p1, p2 }) => is_colinear(p0, p1, p2), + PathSeg::Cubic(CubicBez { p0, p1, p2, p3 }) => is_colinear(p0, p1, p3) && is_colinear(p0, p2, p3), + } +} + +/// Get an iterator over the coordinates of all points in a path segment. +pub fn get_segment_points(segment: PathSeg) -> Vec { + match segment { + PathSeg::Line(line) => [line.p0, line.p1].to_vec(), + PathSeg::Quad(quad_bez) => [quad_bez.p0, quad_bez.p1, quad_bez.p2].to_vec(), + PathSeg::Cubic(cubic_bez) => [cubic_bez.p0, cubic_bez.p1, cubic_bez.p2, cubic_bez.p3].to_vec(), + } +} + +/// Returns true if the corresponding points of the two [PathSeg]'s are within the provided absolute value difference from each other. +pub fn pathseg_abs_diff_eq(seg1: PathSeg, seg2: PathSeg, max_abs_diff: f64) -> bool { + let seg1 = if is_linear(seg1) { PathSeg::Line(Line::new(seg1.start(), seg1.end())) } else { seg1 }; + let seg2 = if is_linear(seg2) { PathSeg::Line(Line::new(seg2.start(), seg2.end())) } else { seg2 }; + + let seg1_points = get_segment_points(seg1); + let seg2_points = get_segment_points(seg2); + + let cmp = |a: f64, b: f64| a.sub(b).abs() < max_abs_diff; + + seg1_points.len() == seg2_points.len() && seg1_points.into_iter().zip(seg2_points).all(|(a, b)| cmp(a.x, b.x) && cmp(a.y, b.y)) +} diff --git a/node-graph/gcore/src/vector/vector_data/attributes.rs b/node-graph/gcore/src/vector/vector_data/attributes.rs index 40d63bd90f..e2fbe9c1c7 100644 --- a/node-graph/gcore/src/vector/vector_data/attributes.rs +++ b/node-graph/gcore/src/vector/vector_data/attributes.rs @@ -3,6 +3,7 @@ use crate::vector::vector_data::{HandleId, VectorData}; use bezier_rs::{BezierHandles, ManipulatorGroup}; use dyn_any::DynAny; use glam::{DAffine2, DVec2}; +use kurbo::{CubicBez, Line, PathSeg, QuadBez}; use std::collections::HashMap; use std::hash::{Hash, Hasher}; use std::iter::zip; @@ -673,6 +674,18 @@ impl FoundSubpath { } impl VectorData { + /// Construct a [`kurbo::PathSeg`] by resolving the points from their ids. + fn path_segment_from_index(&self, start: usize, end: usize, handles: BezierHandles) -> PathSeg { + let start = dvec2_to_point(self.point_domain.positions()[start]); + let end = dvec2_to_point(self.point_domain.positions()[end]); + + match handles { + BezierHandles::Linear => PathSeg::Line(Line::new(start, end)), + BezierHandles::Quadratic { handle } => PathSeg::Quad(QuadBez::new(start, dvec2_to_point(handle), end)), + BezierHandles::Cubic { handle_start, handle_end } => PathSeg::Cubic(CubicBez::new(start, dvec2_to_point(handle_start), dvec2_to_point(handle_end), end)), + } + } + /// Construct a [`bezier_rs::Bezier`] curve spanning from the resolved position of the start and end points with the specified handles. fn segment_to_bezier_with_index(&self, start: usize, end: usize, handles: BezierHandles) -> bezier_rs::Bezier { let start = self.point_domain.positions()[start]; @@ -699,6 +712,19 @@ impl VectorData { (start_id, end_id, self.segment_to_bezier_with_index(start, end, self.segment_domain.handles[index])) } + /// Iterator over all of the [`bezier_rs::Bezier`] following the order that they are stored in the segment domain, skipping invalid segments. + pub fn segment_iter(&self) -> impl Iterator { + let to_segment = |(((&handles, &id), &start), &end)| (id, self.path_segment_from_index(start, end, handles), self.point_domain.ids()[start], self.point_domain.ids()[end]); + + self.segment_domain + .handles + .iter() + .zip(&self.segment_domain.id) + .zip(self.segment_domain.start_point()) + .zip(self.segment_domain.end_point()) + .map(to_segment) + } + /// Iterator over all of the [`bezier_rs::Bezier`] following the order that they are stored in the segment domain, skipping invalid segments. pub fn segment_bezier_iter(&self) -> impl Iterator + '_ { let to_bezier = |(((&handles, &id), &start), &end)| (id, self.segment_to_bezier_with_index(start, end, handles), self.point_domain.ids()[start], self.point_domain.ids()[end]); diff --git a/node-graph/gcore/src/vector/vector_nodes.rs b/node-graph/gcore/src/vector/vector_nodes.rs index 64484715d4..2ba2a2eb19 100644 --- a/node-graph/gcore/src/vector/vector_nodes.rs +++ b/node-graph/gcore/src/vector/vector_nodes.rs @@ -10,9 +10,9 @@ use crate::raster_types::{CPU, GPU, RasterDataTable}; use crate::registry::types::{Angle, Fraction, IntegerCount, Length, Multiplier, Percentage, PixelLength, PixelSize, SeedValue}; use crate::transform::{Footprint, ReferencePoint, Transform}; use crate::vector::PointDomain; -use crate::vector::algorithms::bezpath_algorithms::{eval_pathseg_euclidean, is_linear}; +use crate::vector::algorithms::bezpath_algorithms::eval_pathseg_euclidean; use crate::vector::algorithms::merge_by_distance::MergeByDistanceExt; -use crate::vector::misc::{MergeByDistanceAlgorithm, PointSpacingType}; +use crate::vector::misc::{MergeByDistanceAlgorithm, PointSpacingType, is_linear}; use crate::vector::misc::{handles_to_segment, segment_to_handles}; use crate::vector::style::{PaintOrder, StrokeAlign, StrokeCap, StrokeJoin}; use crate::vector::{FillId, RegionId}; @@ -1760,7 +1760,7 @@ fn bevel_algorithm(mut vector_data: VectorData, vector_data_transform: DAffine2, } fn calculate_distance_to_spilt(bezier1: PathSeg, bezier2: PathSeg, bevel_length: f64) -> f64 { - if is_linear(&bezier1) && is_linear(&bezier2) { + if is_linear(bezier1) && is_linear(bezier2) { let v1 = (bezier1.end() - bezier1.start()).normalize(); let v2 = (bezier1.end() - bezier2.end()).normalize(); @@ -1898,11 +1898,11 @@ fn bevel_algorithm(mut vector_data: VectorData, vector_data_transform: DAffine2, let spilt_distance = calculate_distance_to_spilt(bezier, next_bezier, distance); - if is_linear(&bezier) { + if is_linear(bezier) { bezier = PathSeg::Line(Line::new(bezier.start(), bezier.end())); } - if is_linear(&next_bezier) { + if is_linear(next_bezier) { next_bezier = PathSeg::Line(Line::new(next_bezier.start(), next_bezier.end())); } @@ -2110,9 +2110,14 @@ async fn centroid(ctx: impl Ctx + CloneVarArgs + ExtractAll, vector_data: impl N #[cfg(test)] mod test { use super::*; - use crate::Node; - use bezier_rs::Bezier; - use kurbo::Rect; + use crate::{ + Node, + vector::{ + algorithms::bezpath_algorithms::{TValue, trim_pathseg}, + misc::pathseg_abs_diff_eq, + }, + }; + use kurbo::{CubicBez, Ellipse, Point, Rect}; use std::pin::Pin; #[derive(Clone)] @@ -2244,15 +2249,10 @@ mod test { } } - use bezier_rs::Subpath; - fn vector_node(data: Subpath) -> VectorDataTable { - VectorDataTable::new(VectorData::from_subpath(data)) - } - #[tokio::test] async fn sample_polyline() { - let path = Subpath::from_bezier(&Bezier::from_cubic_dvec2(DVec2::ZERO, DVec2::ZERO, DVec2::X * 100., DVec2::X * 100.)); - let sample_polyline = super::sample_polyline(Footprint::default(), vector_node(path), PointSpacingType::Separation, 30., 0, 0., 0., false, vec![100.]).await; + let path = BezPath::from_vec(vec![PathEl::MoveTo(Point::ZERO), PathEl::CurveTo(Point::ZERO, Point::new(100., 0.), Point::new(100., 0.))]); + let sample_polyline = super::sample_polyline(Footprint::default(), vector_node_from_bezpath(path), PointSpacingType::Separation, 30., 0, 0., 0., false, vec![100.]).await; let sample_polyline = sample_polyline.instance_ref_iter().next().unwrap().instance; assert_eq!(sample_polyline.point_domain.positions().len(), 4); for (pos, expected) in sample_polyline.point_domain.positions().iter().zip([DVec2::X * 0., DVec2::X * 30., DVec2::X * 60., DVec2::X * 90.]) { @@ -2261,8 +2261,8 @@ mod test { } #[tokio::test] async fn sample_polyline_adaptive_spacing() { - let path = Subpath::from_bezier(&Bezier::from_cubic_dvec2(DVec2::ZERO, DVec2::ZERO, DVec2::X * 100., DVec2::X * 100.)); - let sample_polyline = super::sample_polyline(Footprint::default(), vector_node(path), PointSpacingType::Separation, 18., 0, 45., 10., true, vec![100.]).await; + let path = BezPath::from_vec(vec![PathEl::MoveTo(Point::ZERO), PathEl::CurveTo(Point::ZERO, Point::new(100., 0.), Point::new(100., 0.))]); + let sample_polyline = super::sample_polyline(Footprint::default(), vector_node_from_bezpath(path), PointSpacingType::Separation, 18., 0, 45., 10., true, vec![100.]).await; let sample_polyline = sample_polyline.instance_ref_iter().next().unwrap().instance; assert_eq!(sample_polyline.point_domain.positions().len(), 4); for (pos, expected) in sample_polyline.point_domain.positions().iter().zip([DVec2::X * 45., DVec2::X * 60., DVec2::X * 75., DVec2::X * 90.]) { @@ -2273,7 +2273,7 @@ mod test { async fn poisson() { let poisson_points = super::poisson_disk_points( Footprint::default(), - vector_node(Subpath::new_ellipse(DVec2::NEG_ONE * 50., DVec2::ONE * 50.)), + vector_node_from_bezpath(Ellipse::from_rect(Rect::new(-50., -50., 50., 50.)).to_path(DEFAULT_ACCURACY)), 10. * std::f64::consts::SQRT_2, 0, ) @@ -2290,8 +2290,8 @@ mod test { } #[tokio::test] async fn segment_lengths() { - let subpath = Subpath::from_bezier(&Bezier::from_cubic_dvec2(DVec2::ZERO, DVec2::ZERO, DVec2::X * 100., DVec2::X * 100.)); - let lengths = subpath_segment_lengths(Footprint::default(), vector_node(subpath)).await; + let bezpath = BezPath::from_vec(vec![PathEl::MoveTo(Point::ZERO), PathEl::CurveTo(Point::ZERO, Point::new(100., 0.), Point::new(100., 0.))]); + let lengths = subpath_segment_lengths(Footprint::default(), vector_node_from_bezpath(bezpath)).await; assert_eq!(lengths, vec![100.]); } #[tokio::test] @@ -2308,16 +2308,16 @@ mod test { } #[tokio::test] async fn spline() { - let spline = super::spline(Footprint::default(), vector_node(Subpath::new_rect(DVec2::ZERO, DVec2::ONE * 100.))).await; + let spline = super::spline(Footprint::default(), vector_node_from_bezpath(Rect::new(0., 0., 100., 100.).to_path(DEFAULT_ACCURACY))).await; let spline = spline.instance_ref_iter().next().unwrap().instance; - assert_eq!(spline.stroke_bezier_paths().count(), 1); + assert_eq!(spline.stroke_bezpath_iter().count(), 1); assert_eq!(spline.point_domain.positions(), &[DVec2::ZERO, DVec2::new(100., 0.), DVec2::new(100., 100.), DVec2::new(0., 100.)]); } #[tokio::test] async fn morph() { - let source = Subpath::new_rect(DVec2::ZERO, DVec2::ONE * 100.); - let target = Subpath::new_ellipse(DVec2::NEG_ONE * 100., DVec2::ZERO); - let morphed = super::morph(Footprint::default(), vector_node(source), vector_node(target), 0.5).await; + let source = Rect::new(0., 0., 100., 100.).to_path(DEFAULT_ACCURACY); + let target = Ellipse::from_rect(Rect::new(-100., -100., 0., 0.)).to_path(DEFAULT_ACCURACY); + let morphed = super::morph(Footprint::default(), vector_node_from_bezpath(source), vector_node_from_bezpath(target), 0.5).await; let morphed = morphed.instance_ref_iter().next().unwrap().instance; assert_eq!( &morphed.point_domain.positions()[..4], @@ -2326,63 +2326,76 @@ mod test { } #[track_caller] - fn contains_segment(vector: VectorData, target: Bezier) { - let segments = vector.segment_bezier_iter().map(|x| x.1); - let count = segments.filter(|bezier| bezier.abs_diff_eq(&target, 0.01) || bezier.reversed().abs_diff_eq(&target, 0.01)).count(); + fn contains_segment(vector: VectorData, target: PathSeg) { + let segments = vector.segment_iter().map(|x| x.1); + let count = segments + .filter(|segment| pathseg_abs_diff_eq(*segment, target, 0.01) || pathseg_abs_diff_eq(segment.reverse(), target, 0.01)) + .count(); + assert_eq!( count, 1, "Expected exactly one matching segment for {target:?}, but found {count}. The given segments are: {:#?}", - vector.segment_bezier_iter().collect::>() + vector.segment_iter().collect::>() ); } #[tokio::test] async fn bevel_rect() { - let source = Subpath::new_rect(DVec2::ZERO, DVec2::ONE * 100.); - let beveled = super::bevel(Footprint::default(), vector_node(source), 2_f64.sqrt() * 10.); + let source = Rect::new(0., 0., 100., 100.).to_path(DEFAULT_ACCURACY); + let beveled = super::bevel(Footprint::default(), vector_node_from_bezpath(source), 2_f64.sqrt() * 10.); let beveled = beveled.instance_ref_iter().next().unwrap().instance; assert_eq!(beveled.point_domain.positions().len(), 8); assert_eq!(beveled.segment_domain.ids().len(), 8); // Segments - contains_segment(beveled.clone(), Bezier::from_linear_dvec2(DVec2::new(10., 0.), DVec2::new(90., 0.))); - contains_segment(beveled.clone(), Bezier::from_linear_dvec2(DVec2::new(10., 100.), DVec2::new(90., 100.))); - contains_segment(beveled.clone(), Bezier::from_linear_dvec2(DVec2::new(0., 10.), DVec2::new(0., 90.))); - contains_segment(beveled.clone(), Bezier::from_linear_dvec2(DVec2::new(100., 10.), DVec2::new(100., 90.))); + contains_segment(beveled.clone(), PathSeg::Line(Line::new(Point::new(10., 0.), Point::new(90., 0.)))); + contains_segment(beveled.clone(), PathSeg::Line(Line::new(Point::new(10., 100.), Point::new(90., 100.)))); + contains_segment(beveled.clone(), PathSeg::Line(Line::new(Point::new(0., 10.), Point::new(0., 90.)))); + contains_segment(beveled.clone(), PathSeg::Line(Line::new(Point::new(100., 10.), Point::new(100., 90.)))); // Joins - contains_segment(beveled.clone(), Bezier::from_linear_dvec2(DVec2::new(10., 0.), DVec2::new(0., 10.))); - contains_segment(beveled.clone(), Bezier::from_linear_dvec2(DVec2::new(90., 0.), DVec2::new(100., 10.))); - contains_segment(beveled.clone(), Bezier::from_linear_dvec2(DVec2::new(100., 90.), DVec2::new(90., 100.))); - contains_segment(beveled.clone(), Bezier::from_linear_dvec2(DVec2::new(10., 100.), DVec2::new(0., 90.))); + contains_segment(beveled.clone(), PathSeg::Line(Line::new(Point::new(10., 0.), Point::new(0., 10.)))); + contains_segment(beveled.clone(), PathSeg::Line(Line::new(Point::new(90., 0.), Point::new(100., 10.)))); + contains_segment(beveled.clone(), PathSeg::Line(Line::new(Point::new(100., 90.), Point::new(90., 100.)))); + contains_segment(beveled.clone(), PathSeg::Line(Line::new(Point::new(10., 100.), Point::new(0., 90.)))); } #[tokio::test] async fn bevel_open_curve() { - let curve = Bezier::from_cubic_dvec2(DVec2::ZERO, DVec2::new(10., 0.), DVec2::new(10., 100.), DVec2::X * 100.); - let source = Subpath::from_beziers(&[Bezier::from_linear_dvec2(DVec2::X * -100., DVec2::ZERO), curve], false); - let beveled = super::bevel((), vector_node(source), 2_f64.sqrt() * 10.); + let curve = PathSeg::Cubic(CubicBez::new(Point::ZERO, Point::new(10., 0.), Point::new(10., 100.), Point::new(100., 0.))); + + let mut source = BezPath::new(); + source.move_to(Point::new(-100., 0.)); + source.line_to(Point::ZERO); + source.push(curve.as_path_el()); + + let beveled = super::bevel((), vector_node_from_bezpath(source), 2_f64.sqrt() * 10.); let beveled = beveled.instance_ref_iter().next().unwrap().instance; assert_eq!(beveled.point_domain.positions().len(), 4); assert_eq!(beveled.segment_domain.ids().len(), 3); // Segments - contains_segment(beveled.clone(), Bezier::from_linear_dvec2(DVec2::new(-8.2, 0.), DVec2::new(-100., 0.))); - let trimmed = curve.trim(bezier_rs::TValue::Euclidean(8.2 / curve.length(Some(0.00001))), bezier_rs::TValue::Parametric(1.)); + contains_segment(beveled.clone(), PathSeg::Line(Line::new(Point::new(-8.2, 0.), Point::new(-100., 0.)))); + let trimmed = trim_pathseg(curve, TValue::Euclidean(8.2 / curve.perimeter(DEFAULT_ACCURACY)), TValue::Parametric(1.)).unwrap(); contains_segment(beveled.clone(), trimmed); // Join - contains_segment(beveled.clone(), Bezier::from_linear_dvec2(DVec2::new(-8.2, 0.), trimmed.start)); + contains_segment(beveled.clone(), PathSeg::Line(Line::new(Point::new(-8.2, 0.), trimmed.start()))); } #[tokio::test] async fn bevel_with_transform() { - let curve = Bezier::from_cubic_dvec2(DVec2::ZERO, DVec2::new(10., 0.), DVec2::new(10., 100.), DVec2::new(100., 0.)); - let source = Subpath::::from_beziers(&[Bezier::from_linear_dvec2(DVec2::new(-100., 0.), DVec2::ZERO), curve], false); - let vector_data = VectorData::from_subpath(source); + let curve = PathSeg::Cubic(CubicBez::new(Point::ZERO, Point::new(10., 0.), Point::new(10., 100.), Point::new(100., 0.))); + + let mut source = BezPath::new(); + source.move_to(Point::new(-100., 0.)); + source.line_to(Point::ZERO); + source.push(curve.as_path_el()); + + let vector_data = VectorData::from_bezpath(source); let mut vector_data_table = VectorDataTable::new(vector_data.clone()); *vector_data_table.get_mut(0).unwrap().transform = DAffine2::from_scale_angle_translation(DVec2::splat(10.), 1., DVec2::new(99., 77.)); @@ -2394,40 +2407,47 @@ mod test { assert_eq!(beveled.segment_domain.ids().len(), 3); // Segments - contains_segment(beveled.clone(), Bezier::from_linear_dvec2(DVec2::new(-8.2, 0.), DVec2::new(-100., 0.))); - let trimmed = curve.trim(bezier_rs::TValue::Euclidean(8.2 / curve.length(Some(0.00001))), bezier_rs::TValue::Parametric(1.)); + contains_segment(beveled.clone(), PathSeg::Line(Line::new(Point::new(-8.2, 0.), Point::new(-100., 0.)))); + let trimmed = trim_pathseg(curve, TValue::Euclidean(8.2 / curve.perimeter(DEFAULT_ACCURACY)), TValue::Parametric(1.)).unwrap(); contains_segment(beveled.clone(), trimmed); // Join - contains_segment(beveled.clone(), Bezier::from_linear_dvec2(DVec2::new(-8.2, 0.), trimmed.start)); + contains_segment(beveled.clone(), PathSeg::Line(Line::new(Point::new(-8.2, 0.), trimmed.start()))); } #[tokio::test] async fn bevel_too_high() { - let source = Subpath::from_anchors([DVec2::ZERO, DVec2::new(100., 0.), DVec2::new(100., 100.), DVec2::new(0., 100.)], false); - let beveled = super::bevel(Footprint::default(), vector_node(source), 999.); + let mut source = BezPath::new(); + source.move_to(Point::ZERO); + source.line_to(Point::new(100., 0.)); + source.line_to(Point::new(100., 100.)); + source.line_to(Point::new(0., 100.)); + + let beveled = super::bevel(Footprint::default(), vector_node_from_bezpath(source), 999.); let beveled = beveled.instance_ref_iter().next().unwrap().instance; assert_eq!(beveled.point_domain.positions().len(), 6); assert_eq!(beveled.segment_domain.ids().len(), 5); // Segments - contains_segment(beveled.clone(), Bezier::from_linear_dvec2(DVec2::new(0., 0.), DVec2::new(50., 0.))); - contains_segment(beveled.clone(), Bezier::from_linear_dvec2(DVec2::new(100., 50.), DVec2::new(100., 50.))); - contains_segment(beveled.clone(), Bezier::from_linear_dvec2(DVec2::new(100., 50.), DVec2::new(50., 100.))); + contains_segment(beveled.clone(), PathSeg::Line(Line::new(Point::new(0., 0.), Point::new(50., 0.)))); + contains_segment(beveled.clone(), PathSeg::Line(Line::new(Point::new(100., 50.), Point::new(100., 50.)))); + contains_segment(beveled.clone(), PathSeg::Line(Line::new(Point::new(100., 50.), Point::new(50., 100.)))); // Joins - contains_segment(beveled.clone(), Bezier::from_linear_dvec2(DVec2::new(50., 0.), DVec2::new(100., 50.))); - contains_segment(beveled.clone(), Bezier::from_linear_dvec2(DVec2::new(100., 50.), DVec2::new(50., 100.))); + contains_segment(beveled.clone(), PathSeg::Line(Line::new(Point::new(50., 0.), Point::new(100., 50.)))); + contains_segment(beveled.clone(), PathSeg::Line(Line::new(Point::new(100., 50.), Point::new(50., 100.)))); } #[tokio::test] async fn bevel_repeated_point() { - let line = Bezier::from_linear_dvec2(DVec2::ZERO, DVec2::new(100., 0.)); - let point = Bezier::from_cubic_dvec2(DVec2::new(100., 0.), DVec2::ZERO, DVec2::ZERO, DVec2::new(100., 0.)); - let curve = Bezier::from_cubic_dvec2(DVec2::new(100., 0.), DVec2::new(110., 0.), DVec2::new(110., 200.), DVec2::new(200., 0.)); - let subpath = Subpath::from_beziers(&[line, point, curve], false); - let beveled_table = super::bevel(Footprint::default(), vector_node(subpath), 5.); + let line = PathSeg::Line(Line::new(Point::ZERO, Point::new(100., 0.))); + let point = PathSeg::Cubic(CubicBez::new(Point::new(100., 0.), Point::ZERO, Point::ZERO, Point::new(100., 0.))); + let curve = PathSeg::Cubic(CubicBez::new(Point::new(100., 0.), Point::new(110., 0.), Point::new(110., 200.), Point::new(200., 0.))); + + let subpath = BezPath::from_path_segments([line, point, curve].into_iter()); + + let beveled_table = super::bevel(Footprint::default(), vector_node_from_bezpath(subpath), 5.); let beveled = beveled_table.instance_ref_iter().next().unwrap().instance; assert_eq!(beveled.point_domain.positions().len(), 6); From c4ca14f661783a360e785c4b788e6a138f98a4af Mon Sep 17 00:00:00 2001 From: indierusty Date: Thu, 31 Jul 2025 20:51:04 +0530 Subject: [PATCH 06/11] remove unused import --- node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs b/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs index 8d21d5ec6e..e0750caf54 100644 --- a/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs +++ b/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs @@ -1,5 +1,3 @@ -use std::path::Path; - use super::poisson_disk::poisson_disk_sample; use crate::vector::misc::{PointSpacingType, dvec2_to_point}; use glam::DVec2; From 2ef05a0cc7e96b673fe0697eb2e10f33d6176b48 Mon Sep 17 00:00:00 2001 From: indierusty Date: Fri, 1 Aug 2025 09:56:21 +0530 Subject: [PATCH 07/11] simplify and fix morph node test --- node-graph/gcore/src/vector/vector_nodes.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/node-graph/gcore/src/vector/vector_nodes.rs b/node-graph/gcore/src/vector/vector_nodes.rs index 2ba2a2eb19..6e115c6709 100644 --- a/node-graph/gcore/src/vector/vector_nodes.rs +++ b/node-graph/gcore/src/vector/vector_nodes.rs @@ -2316,12 +2316,12 @@ mod test { #[tokio::test] async fn morph() { let source = Rect::new(0., 0., 100., 100.).to_path(DEFAULT_ACCURACY); - let target = Ellipse::from_rect(Rect::new(-100., -100., 0., 0.)).to_path(DEFAULT_ACCURACY); + let target = Rect::new(-100., -100., 0., 0.).to_path(DEFAULT_ACCURACY); let morphed = super::morph(Footprint::default(), vector_node_from_bezpath(source), vector_node_from_bezpath(target), 0.5).await; let morphed = morphed.instance_ref_iter().next().unwrap().instance; assert_eq!( &morphed.point_domain.positions()[..4], - vec![DVec2::new(-25., -50.), DVec2::new(50., -25.), DVec2::new(25., 50.), DVec2::new(-50., 25.)] + vec![DVec2::new(-50.0, -50.0), DVec2::new(50.0, -50.0), DVec2::new(50.0, 50.0), DVec2::new(-50.0, 50.0)] ); } From 487ac1d017ead4ac942537c35f01a8ed8f4098fb Mon Sep 17 00:00:00 2001 From: indierusty Date: Fri, 1 Aug 2025 10:53:50 +0530 Subject: [PATCH 08/11] rename vars and comment --- .../gcore/src/vector/algorithms/bezpath_algorithms.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs b/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs index c5634399f8..a77a87d125 100644 --- a/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs +++ b/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs @@ -194,6 +194,7 @@ pub enum TValue { Euclidean(f64), } +/// Return the subsegment for the given [TValue] range. Returns None if parametric value of `t1` is greater than `t2`. pub fn trim_pathseg(segment: PathSeg, t1: TValue, t2: TValue) -> Option { let t1 = eval_pathseg(segment, t1); let t2 = eval_pathseg(segment, t2); @@ -210,12 +211,12 @@ pub fn eval_pathseg(segment: PathSeg, t_value: TValue) -> f64 { /// Finds the t value of point on the given path segment i.e fractional distance along the segment's total length. /// It uses a binary search to find the value `t` such that the ratio `length_up_to_t / total_length` approximates the input `distance`. -pub fn eval_pathseg_euclidean(path_segment: PathSeg, distance: f64, accuracy: f64) -> f64 { +pub fn eval_pathseg_euclidean(segment: PathSeg, distance: f64, accuracy: f64) -> f64 { let mut low_t = 0.; let mut mid_t = 0.5; let mut high_t = 1.; - let total_length = path_segment.perimeter(accuracy); + let total_length = segment.perimeter(accuracy); if !total_length.is_finite() || total_length <= f64::EPSILON { return 0.; @@ -224,7 +225,7 @@ pub fn eval_pathseg_euclidean(path_segment: PathSeg, distance: f64, accuracy: f6 let distance = distance.clamp(0., 1.); while high_t - low_t > accuracy { - let current_length = path_segment.subsegment(0.0..mid_t).perimeter(accuracy); + let current_length = segment.subsegment(0.0..mid_t).perimeter(accuracy); let current_distance = current_length / total_length; if current_distance > distance { From b51aa4a956d609672bef2e8696736db9715d2ef7 Mon Sep 17 00:00:00 2001 From: indierusty Date: Fri, 1 Aug 2025 11:21:49 +0530 Subject: [PATCH 09/11] refactor bezpath_to_parametric function --- .../vector/algorithms/bezpath_algorithms.rs | 54 ++++++++----------- node-graph/gcore/src/vector/vector_nodes.rs | 15 ++++-- 2 files changed, 32 insertions(+), 37 deletions(-) diff --git a/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs b/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs index a77a87d125..1a7ac6c587 100644 --- a/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs +++ b/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs @@ -56,25 +56,26 @@ pub fn split_bezpath_at_segment(bezpath: &BezPath, segment_index: usize, t: f64) } /// Splits the [`BezPath`] at a `t` value which lies in the range of [0, 1]. -/// Returns [`None`] if the given [`BezPath`] has no segments or `t` is within f64::EPSILON of 0 or 1. -pub fn split_bezpath(bezpath: &BezPath, t: f64, euclidian: bool) -> Option<(BezPath, BezPath)> { - if t <= f64::EPSILON || (1. - t) <= f64::EPSILON || bezpath.segments().count() == 0 { +/// Returns [`None`] if the given [`BezPath`] has no segments. +pub fn split_bezpath(bezpath: &BezPath, t_value: TValue) -> Option<(BezPath, BezPath)> { + if bezpath.segments().count() == 0 { return None; } // Get the segment which lies at the split. - let (segment_index, t) = t_value_to_parametric(bezpath, t, euclidian, None); + let (segment_index, t) = eval_bezpath(bezpath, t_value, None); split_bezpath_at_segment(bezpath, segment_index, t) } -pub fn evaluate_bezpath(bezpath: &BezPath, t: f64, euclidian: bool, segments_length: Option<&[f64]>) -> Point { - let (segment_index, t) = t_value_to_parametric(bezpath, t, euclidian, segments_length); +pub fn evaluate_bezpath(bezpath: &BezPath, t_value: TValue, segments_length: Option<&[f64]>) -> Point { + let (segment_index, t) = eval_bezpath(bezpath, t_value, segments_length); bezpath.get_seg(segment_index + 1).unwrap().eval(t) } -pub fn tangent_on_bezpath(bezpath: &BezPath, t: f64, euclidian: bool, segments_length: Option<&[f64]>) -> Point { - let (segment_index, t) = t_value_to_parametric(bezpath, t, euclidian, segments_length); +pub fn tangent_on_bezpath(bezpath: &BezPath, t_value: TValue, segments_length: Option<&[f64]>) -> Point { + let (segment_index, t) = eval_bezpath(bezpath, t_value, segments_length); let segment = bezpath.get_seg(segment_index + 1).unwrap(); + match segment { PathSeg::Line(line) => line.deriv().eval(t), PathSeg::Quad(quad_bez) => quad_bez.deriv().eval(t), @@ -180,15 +181,7 @@ pub fn sample_polyline_on_bezpath( Some(sample_bezpath) } -pub fn t_value_to_parametric(bezpath: &BezPath, t: f64, euclidian: bool, segments_length: Option<&[f64]>) -> (usize, f64) { - if euclidian { - let (segment_index, t) = bezpath_t_value_to_parametric(bezpath, BezPathTValue::GlobalEuclidean(t), segments_length); - let segment = bezpath.get_seg(segment_index + 1).unwrap(); - return (segment_index, eval_pathseg_euclidean(segment, t, DEFAULT_ACCURACY)); - } - bezpath_t_value_to_parametric(bezpath, BezPathTValue::GlobalParametric(t), segments_length) -} - +#[derive(Debug, Clone, Copy)] pub enum TValue { Parametric(f64), Euclidean(f64), @@ -242,7 +235,7 @@ pub fn eval_pathseg_euclidean(segment: PathSeg, distance: f64, accuracy: f64) -> /// Converts from a bezpath (composed of multiple segments) to a point along a certain segment represented. /// The returned tuple represents the segment index and the `t` value along that segment. /// Both the input global `t` value and the output `t` value are in euclidean space, meaning there is a constant rate of change along the arc length. -fn global_euclidean_to_local_euclidean(bezpath: &BezPath, global_t: f64, lengths: &[f64], total_length: f64) -> (usize, f64) { +fn eval_bazpath_to_euclidean(bezpath: &BezPath, global_t: f64, lengths: &[f64], total_length: f64) -> (usize, f64) { let mut accumulator = 0.; for (index, length) in lengths.iter().enumerate() { let length_ratio = length / total_length; @@ -254,19 +247,14 @@ fn global_euclidean_to_local_euclidean(bezpath: &BezPath, global_t: f64, lengths (bezpath.segments().count() - 1, 1.) } -enum BezPathTValue { - GlobalEuclidean(f64), - GlobalParametric(f64), -} - -/// Convert a [BezPathTValue] to a parametric `(segment_index, t)` tuple. -/// - Asserts that `t` values contained within the `SubpathTValue` argument lie in the range [0, 1]. -fn bezpath_t_value_to_parametric(bezpath: &BezPath, t: BezPathTValue, precomputed_segments_length: Option<&[f64]>) -> (usize, f64) { +/// Convert a [TValue] to a parametric `(segment_index, t)` tuple. +/// - Asserts that `t` values contained within the `TValue` argument lie in the range [0, 1]. +fn eval_bezpath(bezpath: &BezPath, t: TValue, precomputed_segments_length: Option<&[f64]>) -> (usize, f64) { let segment_count = bezpath.segments().count(); assert!(segment_count >= 1); match t { - BezPathTValue::GlobalEuclidean(t) => { + TValue::Euclidean(t) => { let computed_segments_length; let segments_length = if let Some(segments_length) = precomputed_segments_length { @@ -278,16 +266,18 @@ fn bezpath_t_value_to_parametric(bezpath: &BezPath, t: BezPathTValue, precompute let total_length = segments_length.iter().sum(); - global_euclidean_to_local_euclidean(bezpath, t, segments_length, total_length) + let (segment_index, t) = eval_bazpath_to_euclidean(bezpath, t, segments_length, total_length); + let segment = bezpath.get_seg(segment_index + 1).unwrap(); + (segment_index, eval_pathseg_euclidean(segment, t, DEFAULT_ACCURACY)) } - BezPathTValue::GlobalParametric(global_t) => { - assert!((0.0..=1.).contains(&global_t)); + TValue::Parametric(t) => { + assert!((0.0..=1.).contains(&t)); - if global_t == 1. { + if t == 1. { return (segment_count - 1, 1.); } - let scaled_t = global_t * segment_count as f64; + let scaled_t = t * segment_count as f64; let segment_index = scaled_t.floor() as usize; let t = scaled_t - segment_index as f64; diff --git a/node-graph/gcore/src/vector/vector_nodes.rs b/node-graph/gcore/src/vector/vector_nodes.rs index f604621af2..708ccfb9a5 100644 --- a/node-graph/gcore/src/vector/vector_nodes.rs +++ b/node-graph/gcore/src/vector/vector_nodes.rs @@ -1,4 +1,4 @@ -use super::algorithms::bezpath_algorithms::{self, evaluate_bezpath, sample_polyline_on_bezpath, split_bezpath, tangent_on_bezpath}; +use super::algorithms::bezpath_algorithms::{self, TValue, evaluate_bezpath, sample_polyline_on_bezpath, split_bezpath, tangent_on_bezpath}; use super::algorithms::offset_subpath::offset_bezpath; use super::algorithms::spline::{solve_spline_first_handle_closed, solve_spline_first_handle_open}; use super::misc::{CentroidType, bezpath_from_manipulator_groups, bezpath_to_manipulator_groups, point_to_dvec2}; @@ -1221,8 +1221,9 @@ async fn split_path(_: impl Ctx, mut vector_data: VectorDataTable, progress: Fra result_vector_data.append_bezpath(bezpath.clone()); } let t = if t_value == bezpath_count { 1. } else { t_value.fract() }; + let t = if euclidian { TValue::Euclidean(t) } else { TValue::Parametric(t) }; - if let Some((first, second)) = split_bezpath(&bezpath, t, euclidian) { + if let Some((first, second)) = split_bezpath(&bezpath, t) { result_vector_data.append_bezpath(first); result_vector_data.append_bezpath(second); } else { @@ -1323,9 +1324,11 @@ async fn position_on_path( bezpaths.get_mut(index).map_or(DVec2::ZERO, |(bezpath, transform)| { let t = if progress == bezpath_count { 1. } else { progress.fract() }; + let t = if euclidian { TValue::Euclidean(t) } else { TValue::Parametric(t) }; + bezpath.apply_affine(Affine::new(transform.to_cols_array())); - point_to_dvec2(evaluate_bezpath(bezpath, t, euclidian, None)) + point_to_dvec2(evaluate_bezpath(bezpath, t, None)) }) } @@ -1360,12 +1363,14 @@ async fn tangent_on_path( bezpaths.get_mut(index).map_or(0., |(bezpath, transform)| { let t = if progress == bezpath_count { 1. } else { progress.fract() }; + let t_value = |t: f64| if euclidian { TValue::Euclidean(t) } else { TValue::Parametric(t) }; + bezpath.apply_affine(Affine::new(transform.to_cols_array())); - let mut tangent = point_to_dvec2(tangent_on_bezpath(bezpath, t, euclidian, None)); + let mut tangent = point_to_dvec2(tangent_on_bezpath(bezpath, t_value(t), None)); if tangent == DVec2::ZERO { let t = t + if t > 0.5 { -0.001 } else { 0.001 }; - tangent = point_to_dvec2(tangent_on_bezpath(bezpath, t, euclidian, None)); + tangent = point_to_dvec2(tangent_on_bezpath(bezpath, t_value(t), None)); } if tangent == DVec2::ZERO { return 0.; From 0d2052eda71b40eb1f4a7c4860046b6125d6e2e0 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Fri, 1 Aug 2025 23:00:02 -0700 Subject: [PATCH 10/11] Code review --- node-graph/gcore/src/vector/misc.rs | 13 ++++++------- node-graph/gcore/src/vector/vector_data.rs | 2 +- node-graph/gcore/src/vector/vector_nodes.rs | 14 +++++--------- 3 files changed, 12 insertions(+), 17 deletions(-) diff --git a/node-graph/gcore/src/vector/misc.rs b/node-graph/gcore/src/vector/misc.rs index 05bb8172ec..113a1a7ab4 100644 --- a/node-graph/gcore/src/vector/misc.rs +++ b/node-graph/gcore/src/vector/misc.rs @@ -1,11 +1,10 @@ -use std::ops::Sub; - +use super::PointId; +use super::algorithms::offset_subpath::MAX_ABSOLUTE_DIFFERENCE; use bezier_rs::{BezierHandles, ManipulatorGroup, Subpath}; use dyn_any::DynAny; use glam::DVec2; use kurbo::{BezPath, CubicBez, Line, ParamCurve, PathSeg, Point, QuadBez}; - -use super::{PointId, algorithms::offset_subpath::MAX_ABSOLUTE_DIFFERENCE}; +use std::ops::Sub; /// Represents different ways of calculating the centroid. #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)] @@ -200,9 +199,9 @@ pub fn bezpath_to_manipulator_groups(bezpath: &BezPath) -> (Vec bool { let is_colinear = |a: Point, b: Point, c: Point| -> bool { ((b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x)).abs() < MAX_ABSOLUTE_DIFFERENCE }; @@ -222,7 +221,7 @@ pub fn get_segment_points(segment: PathSeg) -> Vec { } } -/// Returns true if the corresponding points of the two [PathSeg]'s are within the provided absolute value difference from each other. +/// Returns true if the corresponding points of the two [`PathSeg`]s are within the provided absolute value difference from each other. pub fn pathseg_abs_diff_eq(seg1: PathSeg, seg2: PathSeg, max_abs_diff: f64) -> bool { let seg1 = if is_linear(seg1) { PathSeg::Line(Line::new(seg1.start(), seg1.end())) } else { seg1 }; let seg2 = if is_linear(seg2) { PathSeg::Line(Line::new(seg2.start(), seg2.end())) } else { seg2 }; diff --git a/node-graph/gcore/src/vector/vector_data.rs b/node-graph/gcore/src/vector/vector_data.rs index ccf6832beb..1cc981bba2 100644 --- a/node-graph/gcore/src/vector/vector_data.rs +++ b/node-graph/gcore/src/vector/vector_data.rs @@ -195,7 +195,7 @@ impl VectorData { Self::from_subpaths([subpath], false) } - /// Construct some new vector data from a single bezpath with an identity transform and black fill. + /// Construct some new vector data from a single [`BezPath`] with an identity transform and black fill. pub fn from_bezpath(bezpath: BezPath) -> Self { let mut vector_data = Self::default(); vector_data.append_bezpath(bezpath); diff --git a/node-graph/gcore/src/vector/vector_nodes.rs b/node-graph/gcore/src/vector/vector_nodes.rs index 708ccfb9a5..c7af5a38d4 100644 --- a/node-graph/gcore/src/vector/vector_nodes.rs +++ b/node-graph/gcore/src/vector/vector_nodes.rs @@ -17,7 +17,6 @@ use crate::vector::misc::{handles_to_segment, segment_to_handles}; use crate::vector::style::{PaintOrder, StrokeAlign, StrokeCap, StrokeJoin}; use crate::vector::{FillId, RegionId}; use crate::{CloneVarArgs, Color, Context, Ctx, ExtractAll, GraphicElement, GraphicGroupTable, OwnedContextImpl}; - use bezier_rs::ManipulatorGroup; use core::f64::consts::PI; use core::hash::{Hash, Hasher}; @@ -667,6 +666,7 @@ async fn auto_tangents( source: VectorDataTable, /// The amount of spread for the auto-tangents, from 0 (sharp corner) to 1 (full spread). #[default(0.5)] + // TODO: Make this a soft range to allow any value to be typed in outside the slider range of 0 to 1 #[range((0., 1.))] spread: f64, /// If active, existing non-zero handles won't be affected. @@ -2116,13 +2116,9 @@ async fn centroid(ctx: impl Ctx + CloneVarArgs + ExtractAll, vector_data: impl N #[cfg(test)] mod test { use super::*; - use crate::{ - Node, - vector::{ - algorithms::bezpath_algorithms::{TValue, trim_pathseg}, - misc::pathseg_abs_diff_eq, - }, - }; + use crate::Node; + use crate::vector::algorithms::bezpath_algorithms::{TValue, trim_pathseg}; + use crate::vector::misc::pathseg_abs_diff_eq; use kurbo::{CubicBez, Ellipse, Point, Rect}; use std::pin::Pin; @@ -2327,7 +2323,7 @@ mod test { let morphed = morphed.instance_ref_iter().next().unwrap().instance; assert_eq!( &morphed.point_domain.positions()[..4], - vec![DVec2::new(-50.0, -50.0), DVec2::new(50.0, -50.0), DVec2::new(50.0, 50.0), DVec2::new(-50.0, 50.0)] + vec![DVec2::new(-50., -50.), DVec2::new(50., -50.), DVec2::new(50., 50.), DVec2::new(-50., 50.)] ); } From abcf6fc90472c656aa615af1da1f9b51f6c81350 Mon Sep 17 00:00:00 2001 From: indierusty Date: Sat, 2 Aug 2025 12:21:58 +0530 Subject: [PATCH 11/11] fix bezpath_to_manipulator_groups function --- node-graph/gcore/src/vector/misc.rs | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/node-graph/gcore/src/vector/misc.rs b/node-graph/gcore/src/vector/misc.rs index 113a1a7ab4..ef582ff6dd 100644 --- a/node-graph/gcore/src/vector/misc.rs +++ b/node-graph/gcore/src/vector/misc.rs @@ -172,30 +172,34 @@ pub fn bezpath_from_manipulator_groups(manipulator_groups: &[ManipulatorGroup (Vec>, bool) { - let mut manipulator_groups = Vec::new(); - let mut in_handle = None; + let mut manipulator_groups = Vec::>::new(); let mut is_closed = false; for element in bezpath.elements() { - let (manipulator_group, next_in_handle) = match *element { - kurbo::PathEl::MoveTo(point) => (ManipulatorGroup::new(point_to_dvec2(point), in_handle, None), None), - kurbo::PathEl::LineTo(point) => (ManipulatorGroup::new(point_to_dvec2(point), in_handle, None), None), - kurbo::PathEl::QuadTo(point, point1) => (ManipulatorGroup::new(point_to_dvec2(point), in_handle, Some(point_to_dvec2(point1))), None), - kurbo::PathEl::CurveTo(point, point1, point2) => (ManipulatorGroup::new(point_to_dvec2(point), in_handle, Some(point_to_dvec2(point1))), Some(point_to_dvec2(point2))), + let manipulator_group = match *element { + kurbo::PathEl::MoveTo(point) => ManipulatorGroup::new(point_to_dvec2(point), None, None), + kurbo::PathEl::LineTo(point) => ManipulatorGroup::new(point_to_dvec2(point), None, None), + kurbo::PathEl::QuadTo(point, point1) => ManipulatorGroup::new(point_to_dvec2(point1), Some(point_to_dvec2(point)), None), + kurbo::PathEl::CurveTo(point, point1, point2) => { + if let Some(last_maipulator_group) = manipulator_groups.last_mut() { + last_maipulator_group.out_handle = Some(point_to_dvec2(point)); + } + ManipulatorGroup::new(point_to_dvec2(point2), Some(point_to_dvec2(point1)), None) + } kurbo::PathEl::ClosePath => { + if let Some(last_group) = manipulator_groups.pop() { + if let Some(first_group) = manipulator_groups.first_mut() { + first_group.out_handle = last_group.in_handle; + } + } is_closed = true; break; } }; - in_handle = next_in_handle; manipulator_groups.push(manipulator_group); } - if let Some(first) = manipulator_groups.first_mut() { - first.in_handle = in_handle; - } - (manipulator_groups, is_closed) }