From c5800aa96ec1e6b8ea3a2b9121cfeea6f9403274 Mon Sep 17 00:00:00 2001 From: Mohd Mohsin <136162998+MohdMohsin97@users.noreply.github.com> Date: Mon, 14 Jul 2025 03:15:51 +0530 Subject: [PATCH] Fix the Bevel node so it applies a consistent bevel size regardless of angle (#2293) * feat: update the bevel algorithm for same bevel length * Fix: Resolves issue in bevel algorithm * fix bevel algorithm * Feat : update the bevel algorithm for bezier curve * Feat: Add support for rounded and inward-rounded Bevel options * fix bevel algo for curves * Nits * refactor area node * refactor close path node * small refactor of 'check_point_inside_shape' method * copy is_linear function from bezier-rs lib * refactor bevel node implementation * cleanup * refactor bevel implementation * fix transformation * cleanup * update the bevel algorithm * Code review * Clean up setup logic for failing test * update tests to reflect new algorithm * Decimal integer nits --------- Co-authored-by: Keavon Chambers Co-authored-by: indierusty --- .../vector/algorithms/bezpath_algorithms.rs | 16 +- node-graph/gcore/src/vector/misc.rs | 37 +- node-graph/gcore/src/vector/vector_data.rs | 26 +- .../src/vector/vector_data/attributes.rs | 29 ++ node-graph/gcore/src/vector/vector_nodes.rs | 322 +++++++++++++----- 5 files changed, 329 insertions(+), 101 deletions(-) diff --git a/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs b/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs index fa8af0f6d..109ef31e8 100644 --- a/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs +++ b/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs @@ -1,7 +1,8 @@ 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, DEFAULT_ACCURACY, Line, ParamCurve, ParamCurveDeriv, PathEl, PathSeg, Point, Rect, Shape}; +use kurbo::{BezPath, CubicBez, DEFAULT_ACCURACY, Line, ParamCurve, ParamCurveDeriv, PathEl, PathSeg, Point, QuadBez, 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. @@ -314,3 +315,16 @@ 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 4196c45d7..64e4f012e 100644 --- a/node-graph/gcore/src/vector/misc.rs +++ b/node-graph/gcore/src/vector/misc.rs @@ -1,6 +1,7 @@ +use bezier_rs::BezierHandles; use dyn_any::DynAny; use glam::DVec2; -use kurbo::Point; +use kurbo::{CubicBez, Line, PathSeg, Point, QuadBez}; /// 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)] @@ -96,3 +97,37 @@ pub fn point_to_dvec2(point: Point) -> DVec2 { pub fn dvec2_to_point(value: DVec2) -> Point { Point { x: value.x, y: value.y } } + +pub fn segment_to_handles(segment: &PathSeg) -> BezierHandles { + match *segment { + PathSeg::Line(_) => BezierHandles::Linear, + PathSeg::Quad(QuadBez { p0: _, p1, p2: _ }) => BezierHandles::Quadratic { handle: point_to_dvec2(p1) }, + PathSeg::Cubic(CubicBez { p0: _, p1, p2, p3: _ }) => BezierHandles::Cubic { + handle_start: point_to_dvec2(p1), + handle_end: point_to_dvec2(p2), + }, + } +} + +pub fn handles_to_segment(start: DVec2, handles: BezierHandles, end: DVec2) -> PathSeg { + match handles { + bezier_rs::BezierHandles::Linear => { + let p0 = dvec2_to_point(start); + let p1 = dvec2_to_point(end); + PathSeg::Line(Line::new(p0, p1)) + } + bezier_rs::BezierHandles::Quadratic { handle } => { + let p0 = dvec2_to_point(start); + let p1 = dvec2_to_point(handle); + let p2 = dvec2_to_point(end); + PathSeg::Quad(QuadBez::new(p0, p1, p2)) + } + bezier_rs::BezierHandles::Cubic { handle_start, handle_end } => { + let p0 = dvec2_to_point(start); + let p1 = dvec2_to_point(handle_start); + let p2 = dvec2_to_point(handle_end); + let p3 = dvec2_to_point(end); + PathSeg::Cubic(CubicBez::new(p0, p1, p2, p3)) + } + } +} diff --git a/node-graph/gcore/src/vector/vector_data.rs b/node-graph/gcore/src/vector/vector_data.rs index c9888984c..352378454 100644 --- a/node-graph/gcore/src/vector/vector_data.rs +++ b/node-graph/gcore/src/vector/vector_data.rs @@ -226,10 +226,10 @@ impl VectorData { pub fn close_subpaths(&mut self) { let segments_to_add: Vec<_> = self - .stroke_bezier_paths() - .filter(|subpath| !subpath.closed) - .filter_map(|subpath| { - let (first, last) = subpath.manipulator_groups().first().zip(subpath.manipulator_groups().last())?; + .build_stroke_path_iter() + .filter(|(_, closed)| !closed) + .filter_map(|(manipulator_groups, _)| { + let (first, last) = manipulator_groups.first().zip(manipulator_groups.last())?; let (start, end) = self.point_domain.resolve_id(first.id).zip(self.point_domain.resolve_id(last.id))?; Some((start, end)) }) @@ -370,7 +370,7 @@ impl VectorData { } pub fn check_point_inside_shape(&self, vector_data_transform: DAffine2, point: DVec2) -> bool { - let bez_paths: Vec<_> = self + let number = self .stroke_bezpath_iter() .map(|mut bezpath| { // TODO: apply transform to points instead of modifying the paths @@ -379,19 +379,9 @@ impl VectorData { let bbox = bezpath.bounding_box(); (bezpath, bbox) }) - .collect(); - - // Check against all paths the point is contained in to compute the correct winding number - let mut number = 0; - - for (shape, bbox) in bez_paths { - if bbox.x0 > point.x || bbox.y0 > point.y || bbox.x1 < point.x || bbox.y1 < point.y { - continue; - } - - let winding = shape.winding(dvec2_to_point(point)); - number += winding; - } + .filter(|(_, bbox)| bbox.contains(dvec2_to_point(point))) + .map(|(bezpath, _)| bezpath.winding(dvec2_to_point(point))) + .sum::(); // Non-zero fill rule number != 0 diff --git a/node-graph/gcore/src/vector/vector_data/attributes.rs b/node-graph/gcore/src/vector/vector_data/attributes.rs index bb2ca1f91..ea4460c78 100644 --- a/node-graph/gcore/src/vector/vector_data/attributes.rs +++ b/node-graph/gcore/src/vector/vector_data/attributes.rs @@ -440,6 +440,35 @@ impl SegmentDomain { let handles = self.handles.iter_mut(); zip(ids, zip(start_point, zip(end_point, handles))).map(|(id, (start_point, (end_point, handles)))| (id, start_point, end_point, handles)) } + + pub(crate) fn pair_handles_and_points_mut_by_index( + &mut self, + index1: usize, + index2: usize, + ) -> (&mut bezier_rs::BezierHandles, &mut usize, &mut usize, &mut bezier_rs::BezierHandles, &mut usize, &mut usize) { + // Use split_at_mut to avoid multiple mutable borrows of the same slice + let (handles_first, handles_second) = self.handles.split_at_mut(index2.max(index1)); + let (start_first, start_second) = self.start_point.split_at_mut(index2.max(index1)); + let (end_first, end_second) = self.end_point.split_at_mut(index2.max(index1)); + + let (h1, h2) = if index1 < index2 { + (&mut handles_first[index1], &mut handles_second[0]) + } else { + (&mut handles_second[0], &mut handles_first[index2]) + }; + let (sp1, sp2) = if index1 < index2 { + (&mut start_first[index1], &mut start_second[0]) + } else { + (&mut start_second[0], &mut start_first[index2]) + }; + let (ep1, ep2) = if index1 < index2 { + (&mut end_first[index1], &mut end_second[0]) + } else { + (&mut end_second[0], &mut end_first[index2]) + }; + + (h1, sp1, ep1, h2, sp2, ep2) + } } #[derive(Clone, Debug, Default, PartialEq, Hash, DynAny, serde::Serialize, serde::Deserialize)] diff --git a/node-graph/gcore/src/vector/vector_nodes.rs b/node-graph/gcore/src/vector/vector_nodes.rs index b3b47e48e..6db9b992b 100644 --- a/node-graph/gcore/src/vector/vector_nodes.rs +++ b/node-graph/gcore/src/vector/vector_nodes.rs @@ -9,19 +9,23 @@ use crate::instances::{Instance, InstanceMut, Instances}; 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::merge_by_distance::MergeByDistanceExt; use crate::vector::misc::{MergeByDistanceAlgorithm, PointSpacingType}; +use crate::vector::misc::{handles_to_segment, segment_to_handles}; use crate::vector::style::{PaintOrder, StrokeAlign, StrokeCap, StrokeJoin}; -use crate::vector::{FillId, PointDomain, RegionId}; +use crate::vector::{FillId, RegionId}; use crate::{CloneVarArgs, Color, Context, Ctx, ExtractAll, GraphicElement, GraphicGroupTable, OwnedContextImpl}; -use bezier_rs::{Join, ManipulatorGroup, Subpath}; + +use bezier_rs::{BezierHandles, Join, ManipulatorGroup, Subpath}; +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 rand::{Rng, SeedableRng}; use std::collections::hash_map::DefaultHasher; -use std::f64::consts::PI; use std::f64::consts::TAU; -use std::hash::{Hash, Hasher}; /// Implemented for types that can be converted to an iterator of vector data. /// Used for the fill and stroke node so they can be used on VectorData or GraphicGroup @@ -1674,9 +1678,7 @@ async fn morph(_: impl Ctx, source: VectorDataTable, #[expose] target: VectorDat source_path.apply_affine(Affine::new(source_transform.to_cols_array())); // Skip if the path has no segments else get the point at the end of the path. - let Some(end) = source_path.segments().last().and_then(|element| Some(element.end())) else { - continue; - }; + let Some(end) = source_path.segments().last().map(|element| element.end()) else { continue }; for element in source_path.elements_mut() { match element { @@ -1701,9 +1703,7 @@ async fn morph(_: impl Ctx, source: VectorDataTable, #[expose] target: VectorDat target_path.apply_affine(Affine::new(source_transform.to_cols_array())); // Skip if the path has no segments else get the point at the start of the path. - let Some(start) = target_path.segments().next().and_then(|element| Some(element.start())) else { - continue; - }; + let Some(start) = target_path.segments().next().map(|element| element.start()) else { continue }; for element in target_path.elements_mut() { match element { @@ -1736,10 +1736,9 @@ async fn morph(_: impl Ctx, source: VectorDataTable, #[expose] target: VectorDat fn bevel_algorithm(mut vector_data: VectorData, vector_data_transform: DAffine2, 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).clamp(0., 1.), EUCLIDEAN_ERROR, length); - bezier.split(bezier_rs::TValue::Parametric(parametric))[1] + fn split_distance(bezier: PathSeg, distance: f64, length: f64) -> PathSeg { + let parametric = eval_pathseg_euclidean(bezier, (distance / length).clamp(0., 1.), DEFAULT_ACCURACY); + bezier.subsegment(parametric..1.) } /// Produces a list that corresponds with the point ID. The value is how many segments are connected. @@ -1778,65 +1777,233 @@ 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) { + let v1 = (bezier1.end() - bezier1.start()).normalize(); + let v2 = (bezier1.end() - bezier2.end()).normalize(); + + let dot_product = v1.dot(v2); + let angle_rad = dot_product.acos(); + + return bevel_length / (2. * (angle_rad / 2.).sin()); + } + + let length1 = bezier1.perimeter(DEFAULT_ACCURACY); + let length2 = bezier2.perimeter(DEFAULT_ACCURACY); + + let max_split = length1.min(length2); + + let mut split_distance = 0.; + let mut best_diff = f64::MAX; + let mut current_best_distance = 0.; + + let clamp_and_round = |value: f64| ((value * 1000.).round() / 1000.).clamp(0., 1.); + + const INITIAL_SAMPLES: usize = 50; + for i in 0..=INITIAL_SAMPLES { + let distance_sample = max_split * (i as f64 / INITIAL_SAMPLES as f64); + + let x_point_t = eval_pathseg_euclidean(bezier1, 1. - clamp_and_round(distance_sample / length1), DEFAULT_ACCURACY); + let y_point_t = eval_pathseg_euclidean(bezier2, clamp_and_round(distance_sample / length2), DEFAULT_ACCURACY); + + let x_point = bezier1.eval(x_point_t); + let y_point = bezier2.eval(y_point_t); + + let distance = x_point.distance(y_point); + let diff = (bevel_length - distance).abs(); + + if diff < best_diff { + best_diff = diff; + current_best_distance = distance_sample; + } + + if bevel_length - distance < 0. { + split_distance = distance_sample; + + if i > 0 { + let prev_sample = max_split * ((i - 1) as f64 / INITIAL_SAMPLES as f64); + + const REFINE_STEPS: usize = 10; + for j in 1..=REFINE_STEPS { + let refined_sample = prev_sample + (distance_sample - prev_sample) * (j as f64 / REFINE_STEPS as f64); + + let x_point_t = eval_pathseg_euclidean(bezier1, 1. - (refined_sample / length1).clamp(0., 1.), DEFAULT_ACCURACY); + let y_point_t = eval_pathseg_euclidean(bezier2, (refined_sample / length2).clamp(0., 1.), DEFAULT_ACCURACY); + + let x_point = bezier1.eval(x_point_t); + let y_point = bezier2.eval(y_point_t); + + let distance = x_point.distance(y_point); + + if bevel_length - distance < 0. { + split_distance = refined_sample; + break; + } + } + } + break; + } + } + + if split_distance == 0. && current_best_distance > 0. { + split_distance = current_best_distance; + } + + split_distance + } + + fn sort_segments(segment_domain: &SegmentDomain) -> Vec { + let start_points = segment_domain.start_point(); + let end_points = segment_domain.end_point(); + + let mut sorted_segments = vec![0]; + let segment_domain_length = segment_domain.ids().len(); + + for _ in 0..segment_domain_length { + match sorted_segments.last() { + Some(&last) => { + if let Some(index) = start_points.iter().position(|&p| p == end_points[last]) { + if index == 0 { + break; + } + sorted_segments.push(index); + } + } + None => break, + } + } + + if segment_domain_length != sorted_segments.len() { + for i in 0..segment_domain_length as usize { + if !sorted_segments.contains(&i) { + sorted_segments.push(i); + } + } + } + + sorted_segments + } + fn update_existing_segments(vector_data: &mut VectorData, vector_data_transform: DAffine2, distance: f64, segments_connected: &mut [usize]) -> 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, - }; + let sorted_segments = sort_segments(&vector_data.segment_domain); + let segment_domain = &mut vector_data.segment_domain; + let segment_domain_length = segment_domain.ids().len(); - if bezier.is_linear() { - bezier.handles = bezier_rs::BezierHandles::Linear; + let mut first_original_length = 0.; + let mut first_length = 0.; + let mut prev_original_length = 0.; + let mut prev_length = 0.; + + for i in 0..segment_domain_length { + let (index, next_index) = if i == segment_domain_length - 1 { (i, 0) } else { (i, i + 1) }; + let pair_handles_and_points = segment_domain.pair_handles_and_points_mut_by_index(sorted_segments[index], sorted_segments[next_index]); + let (handles, start_point, end_point, next_handles, next_start_point, next_end_point) = pair_handles_and_points; + + let start = vector_data.point_domain.positions()[*start_point]; + let end = vector_data.point_domain.positions()[*end_point]; + + let mut bezier = handles_to_segment(start, *handles, end); + bezier = Affine::new(vector_data_transform.to_cols_array()) * bezier; + + let next_start = vector_data.point_domain.positions()[*next_start_point]; + let next_end = vector_data.point_domain.positions()[*next_end_point]; + + let mut next_bezier = handles_to_segment(next_start, *next_handles, next_end); + next_bezier = Affine::new(vector_data_transform.to_cols_array()) * next_bezier; + + 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 = bezier.apply_transformation(|p| vector_data_transform.transform_point2(p)); + + 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); + } + 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; - - // Only split if the length is big enough to make it worthwhile - let valid_length = length > 1e-10; - if segments_connected[*start_point_index] > 0 && valid_length { - // Apply the bevel to the start - let distance = distance.min(original_length / 2.); - bezier = split_distance(bezier, distance, 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 index == 0 && next_index == 1 { + first_original_length = bezier.perimeter(DEFAULT_ACCURACY); + first_length = first_original_length; } + let (original_length, length) = if index == 0 { + (bezier.perimeter(DEFAULT_ACCURACY), bezier.perimeter(DEFAULT_ACCURACY)) + } else { + (prev_original_length, prev_length) + }; + + let (next_original_length, mut next_length) = if index == segment_domain_length - 1 && next_index == 0 { + (first_original_length, first_length) + } else { + (next_bezier.perimeter(DEFAULT_ACCURACY), next_bezier.perimeter(DEFAULT_ACCURACY)) + }; + // Only split if the length is big enough to make it worthwhile let valid_length = length > 1e-10; - if segments_connected[*end_point_index] > 0 && valid_length { + if segments_connected[*end_point] > 0 && valid_length { // Apply the bevel to the end - let distance = distance.min(original_length / 2.); - bezier = split_distance(bezier.reversed(), distance, length).reversed(); + let distance = spilt_distance.min(original_length.min(next_original_length) / 2.); + bezier = split_distance(bezier.reverse(), distance, length).reverse(); + + if index == 0 && next_index == 1 { + first_length = (length - distance).max(0.); + } + // 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); + let pos = inverse_transform.transform_point2(point_to_dvec2(bezier.end())); + create_or_modify_point(&mut vector_data.point_domain, segments_connected, pos, end_point, &mut next_id, &mut new_segments); } + // Update the handles - *handles = bezier.handles.apply_transformation(|p| inverse_transform.transform_point2(p)); + *handles = segment_to_handles(&bezier).apply_transformation(|p| inverse_transform.transform_point2(p)); + + // Only split if the length is big enough to make it worthwhile + let valid_length = next_length > 1e-10; + if segments_connected[*next_start_point] > 0 && valid_length { + // Apply the bevel to the start + let distance = spilt_distance.min(next_original_length.min(original_length) / 2.); + next_bezier = split_distance(next_bezier, distance, next_length); + next_length = (next_length - distance).max(0.); + + // Update the start position + let pos = inverse_transform.transform_point2(point_to_dvec2(next_bezier.start())); + + create_or_modify_point(&mut vector_data.point_domain, segments_connected, pos, next_start_point, &mut next_id, &mut new_segments); + + // Update the handles + *next_handles = segment_to_handles(&next_bezier).apply_transformation(|p| inverse_transform.transform_point2(p)); + } + + prev_original_length = next_original_length; + prev_length = next_length; } + 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 handles = bezier_rs::BezierHandles::Linear; + vector_data.segment_domain.push(next_id.next_id(), start, end, handles, StrokeId::ZERO); } } - let mut segments_connected = segments_connected_count(&vector_data); - let new_segments = update_existing_segments(&mut vector_data, vector_data_transform, distance, &mut segments_connected); - insert_new_segments(&mut vector_data, &new_segments); + if distance > 1. && vector_data.segment_domain.ids().len() > 1 { + let mut segments_connected = segments_connected_count(&vector_data); + let new_segments = update_existing_segments(&mut vector_data, vector_data_transform, distance, &mut segments_connected); + insert_new_segments(&mut vector_data, &new_segments); + } vector_data } @@ -1848,7 +2015,7 @@ fn bevel(_: impl Ctx, source: VectorDataTable, #[default(10.)] distance: Length) for source_instance in source.instance_iter() { result_table.push(Instance { instance: bevel_algorithm(source_instance.instance, source_instance.transform, distance), - ..Default::default() + ..source_instance }); } @@ -1904,7 +2071,7 @@ async fn area(ctx: impl Ctx + CloneVarArgs + ExtractAll, vector_data: impl Node< .instance_ref_iter() .map(|vector_data_instance| { let scale = vector_data_instance.transform.decompose_scale(); - vector_data_instance.instance.stroke_bezier_paths().map(|subpath| subpath.area(Some(1e-3), Some(1e-3))).sum::() * scale.x * scale.y + vector_data_instance.instance.stroke_bezpath_iter().map(|subpath| subpath.area() * scale.x * scale.y).sum::() }) .sum() } @@ -2180,66 +2347,66 @@ mod test { #[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.); + let beveled = super::bevel(Footprint::default(), vector_node(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(5., 0.), DVec2::new(95., 0.))); - contains_segment(beveled.clone(), Bezier::from_linear_dvec2(DVec2::new(5., 100.), DVec2::new(95., 100.))); - contains_segment(beveled.clone(), Bezier::from_linear_dvec2(DVec2::new(0., 5.), DVec2::new(0., 95.))); - contains_segment(beveled.clone(), Bezier::from_linear_dvec2(DVec2::new(100., 5.), DVec2::new(100., 95.))); + 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.))); // Joins - contains_segment(beveled.clone(), Bezier::from_linear_dvec2(DVec2::new(5., 0.), DVec2::new(0., 5.))); - contains_segment(beveled.clone(), Bezier::from_linear_dvec2(DVec2::new(95., 0.), DVec2::new(100., 5.))); - contains_segment(beveled.clone(), Bezier::from_linear_dvec2(DVec2::new(100., 95.), DVec2::new(95., 100.))); - contains_segment(beveled.clone(), Bezier::from_linear_dvec2(DVec2::new(5., 100.), DVec2::new(0., 95.))); + 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.))); } #[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), 5.); + let beveled = super::bevel((), vector_node(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(-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.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(), trimmed); // Join - contains_segment(beveled.clone(), Bezier::from_linear_dvec2(DVec2::new(-5., 0.), trimmed.start)); + contains_segment(beveled.clone(), Bezier::from_linear_dvec2(DVec2::new(-8.2, 0.), trimmed.start)); } #[tokio::test] async fn bevel_with_transform() { - let curve = Bezier::from_cubic_dvec2(DVec2::new(0., 0.), DVec2::new(1., 0.), DVec2::new(1., 10.), DVec2::new(10., 0.)); - let source = Subpath::::from_beziers(&[Bezier::from_linear_dvec2(DVec2::new(-10., 0.), DVec2::ZERO), curve], false); + 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 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.)); - let beveled = super::bevel((), VectorDataTable::new(vector_data), 5.); + let beveled = super::bevel((), VectorDataTable::new(vector_data), 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(-5., 0.), DVec2::new(-10., 0.))); - let trimmed = curve.trim(bezier_rs::TValue::Euclidean(5. / curve.length(Some(0.00001))), bezier_rs::TValue::Parametric(1.)); + 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(), trimmed); // Join - contains_segment(beveled.clone(), Bezier::from_linear_dvec2(DVec2::new(-5., 0.), trimmed.start)); + contains_segment(beveled.clone(), Bezier::from_linear_dvec2(DVec2::new(-8.2, 0.), trimmed.start)); } #[tokio::test] @@ -2263,21 +2430,14 @@ mod test { #[tokio::test] async fn bevel_repeated_point() { - let curve = Bezier::from_cubic_dvec2(DVec2::ZERO, DVec2::new(10., 0.), DVec2::new(10., 100.), DVec2::X * 100.); - let point = Bezier::from_cubic_dvec2(DVec2::ZERO, DVec2::ZERO, DVec2::ZERO, DVec2::ZERO); - let source = Subpath::from_beziers(&[Bezier::from_linear_dvec2(DVec2::X * -100., DVec2::ZERO), point, curve], false); - let beveled = super::bevel(Footprint::default(), vector_node(source), 5.); - let beveled = beveled.instance_ref_iter().next().unwrap().instance; + 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 beveled = beveled_table.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(-100., 0.), DVec2::new(-5., 0.))); - contains_segment(beveled.clone(), Bezier::from_linear_dvec2(DVec2::new(-5., 0.), DVec2::new(0., 0.))); - contains_segment(beveled.clone(), point); - let [start, end] = curve.split(bezier_rs::TValue::Euclidean(5. / curve.length(Some(0.00001)))); - contains_segment(beveled.clone(), Bezier::from_linear_dvec2(start.start, start.end)); - contains_segment(beveled.clone(), end); } }