diff --git a/libraries/bezier-rs/src/bezier/core.rs b/libraries/bezier-rs/src/bezier/core.rs index ea1930f66..9124012a7 100644 --- a/libraries/bezier-rs/src/bezier/core.rs +++ b/libraries/bezier-rs/src/bezier/core.rs @@ -201,8 +201,11 @@ impl Bezier { /// Returns true if the corresponding points of the two `Bezier`s are within the provided absolute value difference from each other. /// The points considered includes the start, end, and any relevant handles. pub fn abs_diff_eq(&self, other: &Bezier, max_abs_diff: f64) -> bool { - let self_points = self.get_points().collect::>(); - let other_points = other.get_points().collect::>(); + let a = if self.is_linear() { Self::from_linear_dvec2(self.start, self.end) } else { *self }; + let b = if other.is_linear() { Self::from_linear_dvec2(other.start, other.end) } else { *other }; + + let self_points = a.get_points().collect::>(); + let other_points = b.get_points().collect::>(); self_points.len() == other_points.len() && self_points.into_iter().zip(other_points).all(|(a, b)| a.abs_diff_eq(b, max_abs_diff)) } diff --git a/libraries/bezier-rs/src/bezier/mod.rs b/libraries/bezier-rs/src/bezier/mod.rs index c6eeb6ccd..fb2c11b6a 100644 --- a/libraries/bezier-rs/src/bezier/mod.rs +++ b/libraries/bezier-rs/src/bezier/mod.rs @@ -102,7 +102,7 @@ impl BezierHandles { } #[must_use] - pub fn flipped(self) -> Self { + pub fn reversed(self) -> Self { match self { BezierHandles::Cubic { handle_start, handle_end } => Self::Cubic { handle_start: handle_end, diff --git a/libraries/bezier-rs/src/bezier/transform.rs b/libraries/bezier-rs/src/bezier/transform.rs index 9db2106cf..d65cfb74e 100644 --- a/libraries/bezier-rs/src/bezier/transform.rs +++ b/libraries/bezier-rs/src/bezier/transform.rs @@ -605,6 +605,16 @@ impl Bezier { (arcs, low) } + + /// Reverses the direction of the bézier. + #[must_use] + pub fn reversed(self) -> Self { + Self { + start: self.end, + end: self.start, + handles: self.handles.reversed(), + } + } } #[cfg(test)] diff --git a/node-graph/gcore/src/vector/vector_data/attributes.rs b/node-graph/gcore/src/vector/vector_data/attributes.rs index 03492b6af..42ce9ff65 100644 --- a/node-graph/gcore/src/vector/vector_data/attributes.rs +++ b/node-graph/gcore/src/vector/vector_data/attributes.rs @@ -144,6 +144,10 @@ impl PointDomain { self.id.iter().copied().zip(self.positions.iter_mut()) } + pub fn set_position(&mut self, index: usize, position: DVec2) { + self.positions[index] = position; + } + pub fn ids(&self) -> &[PointId] { &self.id } @@ -270,6 +274,14 @@ impl SegmentDomain { &self.end_point } + pub fn set_start_point(&mut self, segment_index: usize, new: usize) { + self.start_point[segment_index] = new; + } + + pub fn set_end_point(&mut self, segment_index: usize, new: usize) { + self.end_point[segment_index] = new; + } + pub fn handles(&self) -> &[bezier_rs::BezierHandles] { &self.handles } @@ -310,6 +322,11 @@ impl SegmentDomain { nested.map(|(((&a, b), &c), &d)| (a, b, c, d)) } + pub(crate) fn handles_and_points_mut(&mut self) -> impl Iterator { + let nested = self.handles.iter_mut().zip(&mut self.start_point).zip(&mut self.end_point); + nested.map(|((a, b), c)| (a, b, c)) + } + pub fn stroke_mut(&mut self) -> impl Iterator { self.ids.iter().copied().zip(self.stroke.iter_mut()) } @@ -501,12 +518,16 @@ impl super::VectorData { /// Tries to convert a segment with the specified id to the start and end points and a [`bezier_rs::Bezier`], returning None if the id is invalid. pub fn segment_points_from_id(&self, id: SegmentId) -> Option<(PointId, PointId, bezier_rs::Bezier)> { - let index: usize = self.segment_domain.id_to_index(id)?; + Some(self.segment_points_from_index(self.segment_domain.id_to_index(id)?)) + } + + /// Tries to convert a segment with the specified index to the start and end points and a [`bezier_rs::Bezier`]. + pub fn segment_points_from_index(&self, index: usize) -> (PointId, PointId, bezier_rs::Bezier) { let start = self.segment_domain.start_point[index]; let end = self.segment_domain.end_point[index]; let start_id = self.point_domain.ids()[start]; let end_id = self.point_domain.ids()[end]; - Some((start_id, end_id, self.segment_to_bezier_with_index(start, end, self.segment_domain.handles[index]))) + (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. @@ -722,7 +743,7 @@ impl<'a> Iterator for StrokePathIter<'a> { let mut handles = self.vector_data.segment_domain.handles()[val.segment_index]; if val.start_from_end { - handles = handles.flipped(); + handles = handles.reversed(); } let next_point_index = if val.start_from_end { self.vector_data.segment_domain.start_point()[val.segment_index] diff --git a/node-graph/gcore/src/vector/vector_nodes.rs b/node-graph/gcore/src/vector/vector_nodes.rs index 1613534ce..1e889ac17 100644 --- a/node-graph/gcore/src/vector/vector_nodes.rs +++ b/node-graph/gcore/src/vector/vector_nodes.rs @@ -5,6 +5,7 @@ use crate::registry::types::{Angle, Fraction, IntegerCount, Length, SeedValue}; use crate::renderer::GraphicElementRendered; use crate::transform::{Footprint, Transform, TransformMut}; use crate::vector::style::LineJoin; +use crate::vector::PointDomain; use crate::{Color, GraphicElement, GraphicGroup}; use bezier_rs::{Cap, Join, Subpath, SubpathTValue, TValue}; @@ -292,7 +293,7 @@ async fn circular_repeat, angle_offset: Angle, - #[default(5)] radius: Length, + #[default(5)] radius: f64, #[default(5)] instances: IntegerCount, ) -> GraphicGroup { let instance = instance.eval(footprint).await; @@ -861,6 +862,123 @@ async fn morph( result } +fn bevel_algorithm(mut vector_data: VectorData, distance: f64) -> VectorData { + // Splits a bézier curve based on a distance measurement + fn split_distance(bezier: bezier_rs::Bezier, distance: f64, length: f64) -> bezier_rs::Bezier { + const EUCLIDEAN_ERROR: f64 = 0.001; + let parametric = bezier.euclidean_to_parametric_with_total_length(distance / length, EUCLIDEAN_ERROR, length); + bezier.split(bezier_rs::TValue::Parametric(parametric))[1] + } + + /// Produces a list that correspons with the point id. The value is how many segments are connected. + fn segments_connected_count(vector_data: &VectorData) -> Vec { + // Count the number of segments connectign to each point. + let mut segments_connected_count = vec![0; vector_data.point_domain.ids().len()]; + for &point_index in vector_data.segment_domain.start_point().iter().chain(vector_data.segment_domain.end_point()) { + segments_connected_count[point_index] += 1; + } + + // Zero out points without exactly two connectors. These are ignored + for count in &mut segments_connected_count { + if *count != 2 { + *count = 0; + } + } + segments_connected_count + } + + /// Updates the index so that it points at a point with the position. If nobody else will look at the index, the original point is updated. Otherwise a new point is created. + fn create_or_modify_point(point_domain: &mut PointDomain, segments_connected_count: &mut [u8], pos: DVec2, index: &mut usize, next_id: &mut PointId, new_segments: &mut Vec<[usize; 2]>) { + segments_connected_count[*index] -= 1; + if segments_connected_count[*index] == 0 { + // If nobody else is going to look at this point, we're alright to modify it + point_domain.set_position(*index, pos); + } else { + let new_index = point_domain.ids().len(); + let original_index = *index; + + // Create a new point (since someone will wish to look at the point in the original position in future) + *index = new_index; + point_domain.push(next_id.next_id(), pos); + + // Add a new segment to be created later + new_segments.push([new_index, original_index]) + } + } + + fn update_existing_segments(vector_data: &mut VectorData, distance: f64, segments_connected: &mut [u8]) -> Vec<[usize; 2]> { + let mut next_id = vector_data.point_domain.next_id(); + let mut new_segments = Vec::new(); + + for (handles, start_point_index, end_point_index) in vector_data.segment_domain.handles_and_points_mut() { + // Convert the original segment to a bezier + let mut bezier = bezier_rs::Bezier { + start: vector_data.point_domain.positions()[*start_point_index], + end: vector_data.point_domain.positions()[*end_point_index], + handles: *handles, + }; + + if bezier.is_linear() { + bezier.handles = bezier_rs::BezierHandles::Linear; + } + bezier = bezier.apply_transformation(|p| vector_data.transform.transform_point2(p)); + let inverse_transform = (vector_data.transform.matrix2.determinant() != 0.).then(|| vector_data.transform.inverse()).unwrap_or_default(); + + let original_length = bezier.length(None); + let mut length = original_length; + + if segments_connected[*start_point_index] > 0 { + // Apply the bevel to the start + bezier = split_distance(bezier, distance.min(original_length / 2.), length); + length = (length - distance).max(0.); + // Update the start position + let pos = inverse_transform.transform_point2(bezier.start); + create_or_modify_point(&mut vector_data.point_domain, segments_connected, pos, start_point_index, &mut next_id, &mut new_segments); + } + if segments_connected[*end_point_index] > 0 { + // Apply the bevel to the end + bezier = split_distance(bezier.reversed(), distance.min(original_length / 2.), length).reversed(); + // Update the end position + let pos = inverse_transform.transform_point2(bezier.end); + create_or_modify_point(&mut vector_data.point_domain, segments_connected, pos, end_point_index, &mut next_id, &mut new_segments); + } + // Update the handles + *handles = bezier.handles.apply_transformation(|p| inverse_transform.transform_point2(p)); + } + new_segments + } + + fn insert_new_segments(vector_data: &mut VectorData, new_segments: &[[usize; 2]]) { + let mut next_id = vector_data.segment_domain.next_id(); + for &[start, end] in new_segments { + vector_data.segment_domain.push(next_id.next_id(), start, end, bezier_rs::BezierHandles::Linear, StrokeId::ZERO); + } + } + + let mut segments_connected = segments_connected_count(&vector_data); + let new_segments = update_existing_segments(&mut vector_data, distance, &mut segments_connected); + insert_new_segments(&mut vector_data, &new_segments); + + vector_data +} + +#[node_macro::node(category("Vector"), path(graphene_core::vector))] +async fn bevel( + #[implementations( + (), + Footprint, + )] + footprint: F, + #[implementations( + () -> VectorData, + Footprint -> VectorData, + )] + source: impl Node, + #[default(10.)] distance: Length, +) -> VectorData { + bevel_algorithm(source.eval(footprint).await, distance) +} + #[node_macro::node(category("Vector"), path(graphene_core::vector))] async fn area(_: (), vector_data: impl Node) -> f64 { let vector_data = vector_data.eval(Footprint::default()).await; @@ -1076,4 +1194,88 @@ mod test { vec![DVec2::new(-25., -50.), DVec2::new(50., -25.), DVec2::new(25., 50.), DVec2::new(-50., 25.)] ); } + + #[track_caller] + fn contains_segment(vector: &VectorData, target: bezier_rs::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(); + assert_eq!(count, 1, "Incorrect number of {target:#?} in {:#?}", vector.segment_bezier_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), 5.).await; + assert_eq!(beveled.point_domain.positions().len(), 8); + assert_eq!(beveled.segment_domain.ids().len(), 8); + + // Segments + contains_segment(&beveled, bezier_rs::Bezier::from_linear_dvec2(DVec2::new(5., 0.), DVec2::new(95., 0.))); + contains_segment(&beveled, bezier_rs::Bezier::from_linear_dvec2(DVec2::new(5., 100.), DVec2::new(95., 100.))); + contains_segment(&beveled, bezier_rs::Bezier::from_linear_dvec2(DVec2::new(0., 5.), DVec2::new(0., 95.))); + contains_segment(&beveled, bezier_rs::Bezier::from_linear_dvec2(DVec2::new(100., 5.), DVec2::new(100., 95.))); + + // Joins + contains_segment(&beveled, bezier_rs::Bezier::from_linear_dvec2(DVec2::new(5., 0.), DVec2::new(0., 5.))); + contains_segment(&beveled, bezier_rs::Bezier::from_linear_dvec2(DVec2::new(95., 0.), DVec2::new(100., 5.))); + contains_segment(&beveled, bezier_rs::Bezier::from_linear_dvec2(DVec2::new(100., 95.), DVec2::new(95., 100.))); + contains_segment(&beveled, bezier_rs::Bezier::from_linear_dvec2(DVec2::new(5., 100.), DVec2::new(0., 95.))); + } + + #[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(Footprint::default(), &vector_node(source), 5.).await; + + assert_eq!(beveled.point_domain.positions().len(), 4); + assert_eq!(beveled.segment_domain.ids().len(), 3); + + // Segments + contains_segment(&beveled, bezier_rs::Bezier::from_linear_dvec2(DVec2::new(-5., 0.), DVec2::new(-100., 0.))); + let trimmed = curve.trim(bezier_rs::TValue::Euclidean(5. / curve.length(Some(0.00001))), bezier_rs::TValue::Parametric(1.)); + contains_segment(&beveled, trimmed); + + // Join + contains_segment(&beveled, bezier_rs::Bezier::from_linear_dvec2(DVec2::new(-5., 0.), trimmed.start)); + } + + #[tokio::test] + async fn bevel_with_transform() { + let curve = Bezier::from_cubic_dvec2(DVec2::ZERO, DVec2::new(1., 0.), DVec2::new(1., 10.), DVec2::X * 10.); + let source = Subpath::::from_beziers(&[Bezier::from_linear_dvec2(DVec2::X * -10., DVec2::ZERO), curve], false); + let mut vector_data = VectorData::from_subpath(source); + let transform = DAffine2::from_scale_angle_translation(DVec2::splat(10.), 1., DVec2::new(99., 77.)); + vector_data.transform = transform; + let beveled = super::bevel(Footprint::default(), &FutureWrapperNode(vector_data), 5.).await; + + assert_eq!(beveled.point_domain.positions().len(), 4); + assert_eq!(beveled.segment_domain.ids().len(), 3); + assert_eq!(beveled.transform, transform); + + // Segments + contains_segment(&beveled, bezier_rs::Bezier::from_linear_dvec2(DVec2::new(-0.5, 0.), DVec2::new(-10., 0.))); + let trimmed = curve.trim(bezier_rs::TValue::Euclidean(0.5 / curve.length(Some(0.00001))), bezier_rs::TValue::Parametric(1.)); + contains_segment(&beveled, trimmed); + + // Join + contains_segment(&beveled, bezier_rs::Bezier::from_linear_dvec2(DVec2::new(-0.5, 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.).await; + assert_eq!(beveled.point_domain.positions().len(), 6); + assert_eq!(beveled.segment_domain.ids().len(), 5); + + // Segments + contains_segment(&beveled, bezier_rs::Bezier::from_linear_dvec2(DVec2::new(0., 0.), DVec2::new(50., 0.))); + contains_segment(&beveled, bezier_rs::Bezier::from_linear_dvec2(DVec2::new(100., 50.), DVec2::new(100., 50.))); + contains_segment(&beveled, bezier_rs::Bezier::from_linear_dvec2(DVec2::new(100., 50.), DVec2::new(50., 100.))); + + // Joins + contains_segment(&beveled, bezier_rs::Bezier::from_linear_dvec2(DVec2::new(50., 0.), DVec2::new(100., 50.))); + contains_segment(&beveled, bezier_rs::Bezier::from_linear_dvec2(DVec2::new(100., 50.), DVec2::new(50., 100.))); + } }