From 8ab0a64921ed992fe635d49594b03d0136857560 Mon Sep 17 00:00:00 2001 From: hypercube <0hypercube@gmail.com> Date: Sun, 23 Nov 2025 20:25:48 +0000 Subject: [PATCH 1/2] Add extrude node --- .../document/node_graph/node_properties.rs | 3 +- node-graph/graph-craft/src/document/value.rs | 1 + .../interpreted-executor/src/node_registry.rs | 2 + .../libraries/vector-types/src/vector/misc.rs | 10 + .../src/vector/vector_attributes.rs | 4 + node-graph/nodes/vector/src/vector_nodes.rs | 208 +++++++++++++++++- 6 files changed, 226 insertions(+), 2 deletions(-) diff --git a/editor/src/messages/portfolio/document/node_graph/node_properties.rs b/editor/src/messages/portfolio/document/node_graph/node_properties.rs index a0def2ebf3..9c35d4a529 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -23,7 +23,7 @@ use graphene_std::raster::{ use graphene_std::table::{Table, TableRow}; use graphene_std::text::{Font, TextAlign}; use graphene_std::transform::{Footprint, ReferencePoint, Transform}; -use graphene_std::vector::misc::{ArcType, CentroidType, GridType, MergeByDistanceAlgorithm, PointSpacingType, SpiralType}; +use graphene_std::vector::misc::{ArcType, CentroidType, ExtrudeJoiningAlgorithm, GridType, MergeByDistanceAlgorithm, PointSpacingType, SpiralType}; use graphene_std::vector::style::{Fill, FillChoice, FillType, GradientStops, GradientType, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin}; pub(crate) fn string_properties(text: &str) -> Vec { @@ -219,6 +219,7 @@ pub(crate) fn property_from_type( Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), + Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index 565dbc8649..48f15c0422 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -245,6 +245,7 @@ tagged_value! { GridType(vector::misc::GridType), ArcType(vector::misc::ArcType), MergeByDistanceAlgorithm(vector::misc::MergeByDistanceAlgorithm), + ExtrudeJoiningAlgorithm(vector::misc::ExtrudeJoiningAlgorithm), PointSpacingType(vector::misc::PointSpacingType), SpiralType(vector::misc::SpiralType), #[serde(alias = "LineCap")] diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index 9fb82e29a7..c916e394c6 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -134,6 +134,7 @@ fn node_registry() -> HashMap, input: Context, fn_params: [Context => graphene_std::vector::misc::GridType]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::ArcType]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::MergeByDistanceAlgorithm]), + async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::ExtrudeJoiningAlgorithm]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::PointSpacingType]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::style::FillType]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::style::GradientType]), @@ -220,6 +221,7 @@ fn node_registry() -> HashMap, input: Context, fn_params: [Context => graphene_std::vector::misc::GridType]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::ArcType]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::MergeByDistanceAlgorithm]), + async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::ExtrudeJoiningAlgorithm]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::PointSpacingType]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::StrokeCap]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::StrokeJoin]), diff --git a/node-graph/libraries/vector-types/src/vector/misc.rs b/node-graph/libraries/vector-types/src/vector/misc.rs index e0ab874831..f42d445d51 100644 --- a/node-graph/libraries/vector-types/src/vector/misc.rs +++ b/node-graph/libraries/vector-types/src/vector/misc.rs @@ -83,6 +83,16 @@ pub enum MergeByDistanceAlgorithm { Topological, } +#[repr(C)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)] +#[widget(Radio)] +pub enum ExtrudeJoiningAlgorithm { + All, + #[default] + Extrema, + None, +} + #[repr(C)] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)] #[widget(Radio)] diff --git a/node-graph/libraries/vector-types/src/vector/vector_attributes.rs b/node-graph/libraries/vector-types/src/vector/vector_attributes.rs index 3c8d4adf1e..5d2549aa45 100644 --- a/node-graph/libraries/vector-types/src/vector/vector_attributes.rs +++ b/node-graph/libraries/vector-types/src/vector/vector_attributes.rs @@ -300,6 +300,10 @@ impl SegmentDomain { self.end_point[segment_index] = new; } + pub fn set_handles(&mut self, segment_index: usize, new: BezierHandles) { + self.handles[segment_index] = new; + } + pub fn handles(&self) -> &[BezierHandles] { &self.handles } diff --git a/node-graph/nodes/vector/src/vector_nodes.rs b/node-graph/nodes/vector/src/vector_nodes.rs index 947b508637..387f6a53d5 100644 --- a/node-graph/nodes/vector/src/vector_nodes.rs +++ b/node-graph/nodes/vector/src/vector_nodes.rs @@ -21,7 +21,7 @@ use vector_types::vector::algorithms::bezpath_algorithms::{self, TValue, evaluat use vector_types::vector::algorithms::merge_by_distance::MergeByDistanceExt; use vector_types::vector::algorithms::offset_subpath::offset_bezpath; use vector_types::vector::algorithms::spline::{solve_spline_first_handle_closed, solve_spline_first_handle_open}; -use vector_types::vector::misc::{CentroidType, bezpath_from_manipulator_groups, bezpath_to_manipulator_groups, point_to_dvec2}; +use vector_types::vector::misc::{CentroidType, ExtrudeJoiningAlgorithm, bezpath_from_manipulator_groups, bezpath_to_manipulator_groups, point_to_dvec2}; use vector_types::vector::misc::{MergeByDistanceAlgorithm, PointSpacingType, is_linear}; use vector_types::vector::misc::{handles_to_segment, segment_to_handles}; use vector_types::vector::style::{Fill, Gradient, GradientStops, Stroke}; @@ -580,6 +580,212 @@ pub fn merge_by_distance( } } +pub mod extrude_algorithms { + use kurbo::{ParamCurve, ParamCurveDeriv}; + + /// Convert [`vector_types::subpath::Bezier`] to [`kurbo::PathSeg`] + fn bezier_to_path_seg(bezier: vector_types::subpath::Bezier) -> kurbo::PathSeg { + let [start, end] = [(bezier.start().x, bezier.start().y), (bezier.end().x, bezier.end().y)]; + match bezier.handles { + vector_types::subpath::BezierHandles::Linear => kurbo::Line::new(start, end).into(), + vector_types::subpath::BezierHandles::Quadratic { handle } => kurbo::QuadBez::new(start, (handle.x, handle.y), end).into(), + vector_types::subpath::BezierHandles::Cubic { handle_start, handle_end } => kurbo::CubicBez::new(start, (handle_start.x, handle_start.y), (handle_end.x, handle_end.y), end).into(), + } + } + + /// Convert [`kurbo::CubicBez`] to [`vector_types::subpath::BezierHandles`] + fn cubic_to_handles(cubic_bez: kurbo::CubicBez) -> vector_types::subpath::BezierHandles { + vector_types::subpath::BezierHandles::Cubic { + handle_start: glam::DVec2::new(cubic_bez.p1.x, cubic_bez.p1.y), + handle_end: glam::DVec2::new(cubic_bez.p2.x, cubic_bez.p2.y), + } + } + + /// Find the t values to split (where the tangent changes to be on the other side of the direction) + fn find_splits(cubic_segment: kurbo::CubicBez, direction: glam::DVec2) -> impl Iterator { + let derivative = cubic_segment.deriv(); + let convert = |x: kurbo::Point| glam::DVec2::new(x.x, x.y); + let derivative_pts = [derivative.p0, derivative.p1, derivative.p2].map(convert); + + let t_squared = derivative_pts[0] - 2. * derivative_pts[1] + derivative_pts[2]; + let t_scalar = -2. * derivative_pts[0] + 2. * derivative_pts[1]; + let contant = derivative_pts[0]; + + kurbo::common::solve_quadratic(contant.perp_dot(direction), t_scalar.perp_dot(direction), t_squared.perp_dot(direction)) + .into_iter() + .filter(|&t| t > 1e-6 && t < 1. - 1e-6) + } + + /// Split so segements they do not have tangents on both sides of the direction vector + fn split(vector: &mut graphic_types::Vector, direction: glam::DVec2) { + let segment_count = vector.segment_domain.ids().len(); + let mut next_point = vector.point_domain.next_id(); + let mut next_segment = vector.segment_domain.next_id(); + for segment_index in 0..segment_count { + let (_, _, bezier) = vector.segment_points_from_index(segment_index); + let mut start_index = vector.segment_domain.start_point()[segment_index]; + let pathseg = bezier_to_path_seg(bezier).to_cubic(); + let mut start_t = 0.; + for split_t in find_splits(pathseg, direction) { + let [first, second] = [pathseg.subsegment(start_t..split_t), pathseg.subsegment(split_t..1.)]; + let [first_handles, second_handles] = [first, second].map(cubic_to_handles); + let middle_point = next_point.next_id(); + let start_segment = next_segment.next_id(); + + let middle_point_index = vector.point_domain.len(); + vector.point_domain.push(middle_point, glam::DVec2::new(first.end().x, first.end().y)); + vector + .segment_domain + .push(start_segment, start_index, middle_point_index, first_handles, vector_types::vector::StrokeId::ZERO); + vector.segment_domain.set_start_point(segment_index, middle_point_index); + vector.segment_domain.set_handles(segment_index, second_handles); + + start_t = split_t; + start_index = middle_point_index; + } + } + } + + /// Copy all segements with the offset of `direction` + fn offset_copy_all_segments(vector: &mut graphic_types::Vector, direction: glam::DVec2) { + let points_count = vector.point_domain.ids().len(); + let mut next_point = vector.point_domain.next_id(); + for index in 0..points_count { + vector.point_domain.push(next_point.next_id(), vector.point_domain.positions()[index] + direction); + } + + let segment_count = vector.segment_domain.ids().len(); + let mut next_segment = vector.segment_domain.next_id(); + for index in 0..segment_count { + vector.segment_domain.push( + next_segment.next_id(), + vector.segment_domain.start_point()[index] + points_count, + vector.segment_domain.end_point()[index] + points_count, + vector.segment_domain.handles()[index].apply_transformation(|x| x + direction), + vector.segment_domain.stroke()[index], + ); + } + } + + /// Join points from the original to the copied that are on alternate sides of the direction + fn join_extrema_edges(vector: &mut graphic_types::Vector, direction: glam::DVec2) { + #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] + enum Found { + #[default] + None, + Positive, + Negative, + Both, + Invalid, + } + impl Found { + fn update(&mut self, value: f64) { + *self = match (*self, value > 0.) { + (Found::None, true) => Found::Positive, + (Found::None, false) => Found::Negative, + (Found::Positive, true) | (Found::Negative, false) => Found::Both, + _ => Found::Invalid, + }; + } + } + let first_half_points = vector.point_domain.len() / 2; + let mut points = vec![Found::None; first_half_points]; + let first_half_segments = vector.segment_domain.ids().len() / 2; + for segment_id in 0..first_half_segments { + let index = [vector.segment_domain.start_point()[segment_id], vector.segment_domain.end_point()[segment_id]]; + let position = index.map(|index| vector.point_domain.positions()[index]); + + if position[0].abs_diff_eq(position[1], 1e-6) { + continue; // Skip zero length segments + } + + points[index[0]].update(direction.perp_dot(position[1] - position[0])); + points[index[1]].update(direction.perp_dot(position[0] - position[1])); + } + + let mut next_segment = vector.segment_domain.next_id(); + for index in 0..first_half_points { + if points[index] != Found::Both { + continue; + } + vector.segment_domain.push( + next_segment.next_id(), + index, + index + first_half_points, + vector_types::subpath::BezierHandles::Linear, + vector_types::vector::StrokeId::ZERO, + ); + } + } + + /// Join all points from the original to the copied + fn join_all(vector: &mut graphic_types::Vector) { + let mut next_segment = vector.segment_domain.next_id(); + let first_half = vector.point_domain.len() / 2; + for index in 0..first_half { + vector.segment_domain.push( + next_segment.next_id(), + index, + index + first_half, + vector_types::subpath::BezierHandles::Linear, + vector_types::vector::StrokeId::ZERO, + ); + } + } + + pub fn extrude(vector: &mut graphic_types::Vector, direction: glam::DVec2, joining_algorithm: vector_types::vector::misc::ExtrudeJoiningAlgorithm) { + split(vector, direction); + offset_copy_all_segments(vector, direction); + match joining_algorithm { + vector_types::vector::misc::ExtrudeJoiningAlgorithm::Extrema => join_extrema_edges(vector, direction), + vector_types::vector::misc::ExtrudeJoiningAlgorithm::All => join_all(vector), + vector_types::vector::misc::ExtrudeJoiningAlgorithm::None => {} + } + } + + #[cfg(test)] + mod extrude_tests { + use glam::DVec2; + use kurbo::{ParamCurve, ParamCurveDeriv}; + + #[test] + fn split_cubic() { + let l1 = kurbo::CubicBez::new((0., 0.), (100., 0.), (100., 100.), (0., 100.)); + assert_eq!(super::find_splits(l1, DVec2::Y).collect::>(), vec![0.5]); + assert!(super::find_splits(l1, DVec2::X).collect::>().is_empty()); + + let l2 = kurbo::CubicBez::new((0., 0.), (0., 0.), (100., 0.), (100., 0.)); + assert!(super::find_splits(l2, DVec2::X).collect::>().is_empty()); + + let l3 = kurbo::PathSeg::Line(kurbo::Line::new((0., 0.), (100., 0.))); + assert!(super::find_splits(l3.to_cubic(), DVec2::X).collect::>().is_empty()); + + let l4 = kurbo::CubicBez::new((0., 0.), (100., -10.), (100., 110.), (0., 100.)); + let splits = super::find_splits(l4, DVec2::X).map(|t| l4.deriv().eval(t)).collect::>(); + assert_eq!(splits.len(), 2); + assert!(splits.iter().all(|&deriv| deriv.y.abs() < 1e-8), "{splits:?}"); + } + + #[test] + fn split_vector() { + let curve = kurbo::PathSeg::Cubic(kurbo::CubicBez::new((0., 0.), (100., -10.), (100., 110.), (0., 100.))); + let mut vector = graphic_types::Vector::from_bezpath(kurbo::BezPath::from_path_segments([curve].into_iter())); + super::split(&mut vector, DVec2::X); + assert_eq!(vector.segment_ids().len(), 3); + assert_eq!(vector.point_domain.ids().len(), 4); + } + } +} + +/// Attempt to inscribe circles at the anchors (that have exactly two segments connected). +#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] +async fn extrude(_: impl Ctx, mut source: Table, direction: DVec2, joining_algorithm: ExtrudeJoiningAlgorithm) -> Table { + for TableRowMut { element: source, .. } in source.iter_mut() { + extrude_algorithms::extrude(source, direction, joining_algorithm); + } + source +} + #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] async fn box_warp(_: impl Ctx, content: Table, #[expose] rectangle: Table) -> Table { let Some((target, target_transform)) = rectangle.get(0).map(|rect| (rect.element, rect.transform)) else { From 47483182a0c0f998f37e8a9f5c89a3f306187922 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Wed, 26 Nov 2025 17:11:50 -0800 Subject: [PATCH 2/2] Code review --- node-graph/nodes/vector/src/vector_nodes.rs | 108 ++++++++++---------- 1 file changed, 53 insertions(+), 55 deletions(-) diff --git a/node-graph/nodes/vector/src/vector_nodes.rs b/node-graph/nodes/vector/src/vector_nodes.rs index 387f6a53d5..8ec8855e69 100644 --- a/node-graph/nodes/vector/src/vector_nodes.rs +++ b/node-graph/nodes/vector/src/vector_nodes.rs @@ -581,51 +581,57 @@ pub fn merge_by_distance( } pub mod extrude_algorithms { + use glam::DVec2; use kurbo::{ParamCurve, ParamCurveDeriv}; + use vector_types::subpath::BezierHandles; + use vector_types::vector::StrokeId; + use vector_types::vector::misc::ExtrudeJoiningAlgorithm; - /// Convert [`vector_types::subpath::Bezier`] to [`kurbo::PathSeg`] + /// Convert [`vector_types::subpath::Bezier`] to [`kurbo::PathSeg`]. fn bezier_to_path_seg(bezier: vector_types::subpath::Bezier) -> kurbo::PathSeg { let [start, end] = [(bezier.start().x, bezier.start().y), (bezier.end().x, bezier.end().y)]; match bezier.handles { - vector_types::subpath::BezierHandles::Linear => kurbo::Line::new(start, end).into(), - vector_types::subpath::BezierHandles::Quadratic { handle } => kurbo::QuadBez::new(start, (handle.x, handle.y), end).into(), - vector_types::subpath::BezierHandles::Cubic { handle_start, handle_end } => kurbo::CubicBez::new(start, (handle_start.x, handle_start.y), (handle_end.x, handle_end.y), end).into(), + BezierHandles::Linear => kurbo::Line::new(start, end).into(), + BezierHandles::Quadratic { handle } => kurbo::QuadBez::new(start, (handle.x, handle.y), end).into(), + BezierHandles::Cubic { handle_start, handle_end } => kurbo::CubicBez::new(start, (handle_start.x, handle_start.y), (handle_end.x, handle_end.y), end).into(), } } - /// Convert [`kurbo::CubicBez`] to [`vector_types::subpath::BezierHandles`] - fn cubic_to_handles(cubic_bez: kurbo::CubicBez) -> vector_types::subpath::BezierHandles { - vector_types::subpath::BezierHandles::Cubic { - handle_start: glam::DVec2::new(cubic_bez.p1.x, cubic_bez.p1.y), - handle_end: glam::DVec2::new(cubic_bez.p2.x, cubic_bez.p2.y), + /// Convert [`kurbo::CubicBez`] to [`vector_types::subpath::BezierHandles`]. + fn cubic_to_handles(cubic_bez: kurbo::CubicBez) -> BezierHandles { + BezierHandles::Cubic { + handle_start: DVec2::new(cubic_bez.p1.x, cubic_bez.p1.y), + handle_end: DVec2::new(cubic_bez.p2.x, cubic_bez.p2.y), } } - /// Find the t values to split (where the tangent changes to be on the other side of the direction) - fn find_splits(cubic_segment: kurbo::CubicBez, direction: glam::DVec2) -> impl Iterator { + /// Find the `t` values to split (where the tangent changes to be on the other side of the direction). + fn find_splits(cubic_segment: kurbo::CubicBez, direction: DVec2) -> impl Iterator { let derivative = cubic_segment.deriv(); - let convert = |x: kurbo::Point| glam::DVec2::new(x.x, x.y); - let derivative_pts = [derivative.p0, derivative.p1, derivative.p2].map(convert); + let convert = |x: kurbo::Point| DVec2::new(x.x, x.y); + let derivative_points = [derivative.p0, derivative.p1, derivative.p2].map(convert); - let t_squared = derivative_pts[0] - 2. * derivative_pts[1] + derivative_pts[2]; - let t_scalar = -2. * derivative_pts[0] + 2. * derivative_pts[1]; - let contant = derivative_pts[0]; + let t_squared = derivative_points[0] - 2. * derivative_points[1] + derivative_points[2]; + let t_scalar = -2. * derivative_points[0] + 2. * derivative_points[1]; + let constant = derivative_points[0]; - kurbo::common::solve_quadratic(contant.perp_dot(direction), t_scalar.perp_dot(direction), t_squared.perp_dot(direction)) + kurbo::common::solve_quadratic(constant.perp_dot(direction), t_scalar.perp_dot(direction), t_squared.perp_dot(direction)) .into_iter() .filter(|&t| t > 1e-6 && t < 1. - 1e-6) } - /// Split so segements they do not have tangents on both sides of the direction vector - fn split(vector: &mut graphic_types::Vector, direction: glam::DVec2) { + /// Split so segments no longer have tangents on both sides of the direction vector. + fn split(vector: &mut graphic_types::Vector, direction: DVec2) { let segment_count = vector.segment_domain.ids().len(); let mut next_point = vector.point_domain.next_id(); let mut next_segment = vector.segment_domain.next_id(); + for segment_index in 0..segment_count { let (_, _, bezier) = vector.segment_points_from_index(segment_index); let mut start_index = vector.segment_domain.start_point()[segment_index]; let pathseg = bezier_to_path_seg(bezier).to_cubic(); let mut start_t = 0.; + for split_t in find_splits(pathseg, direction) { let [first, second] = [pathseg.subsegment(start_t..split_t), pathseg.subsegment(split_t..1.)]; let [first_handles, second_handles] = [first, second].map(cubic_to_handles); @@ -633,10 +639,8 @@ pub mod extrude_algorithms { let start_segment = next_segment.next_id(); let middle_point_index = vector.point_domain.len(); - vector.point_domain.push(middle_point, glam::DVec2::new(first.end().x, first.end().y)); - vector - .segment_domain - .push(start_segment, start_index, middle_point_index, first_handles, vector_types::vector::StrokeId::ZERO); + vector.point_domain.push(middle_point, DVec2::new(first.end().x, first.end().y)); + vector.segment_domain.push(start_segment, start_index, middle_point_index, first_handles, StrokeId::ZERO); vector.segment_domain.set_start_point(segment_index, middle_point_index); vector.segment_domain.set_handles(segment_index, second_handles); @@ -646,8 +650,8 @@ pub mod extrude_algorithms { } } - /// Copy all segements with the offset of `direction` - fn offset_copy_all_segments(vector: &mut graphic_types::Vector, direction: glam::DVec2) { + /// Copy all segments with the offset of `direction`. + fn offset_copy_all_segments(vector: &mut graphic_types::Vector, direction: DVec2) { let points_count = vector.point_domain.ids().len(); let mut next_point = vector.point_domain.next_id(); for index in 0..points_count { @@ -667,8 +671,8 @@ pub mod extrude_algorithms { } } - /// Join points from the original to the copied that are on alternate sides of the direction - fn join_extrema_edges(vector: &mut graphic_types::Vector, direction: glam::DVec2) { + /// Join points from the original to the copied that are on opposite sides of the direction. + fn join_extrema_edges(vector: &mut graphic_types::Vector, direction: DVec2) { #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] enum Found { #[default] @@ -678,6 +682,7 @@ pub mod extrude_algorithms { Both, Invalid, } + impl Found { fn update(&mut self, value: f64) { *self = match (*self, value > 0.) { @@ -688,9 +693,11 @@ pub mod extrude_algorithms { }; } } + let first_half_points = vector.point_domain.len() / 2; let mut points = vec![Found::None; first_half_points]; let first_half_segments = vector.segment_domain.ids().len() / 2; + for segment_id in 0..first_half_segments { let index = [vector.segment_domain.start_point()[segment_id], vector.segment_domain.end_point()[segment_id]]; let position = index.map(|index| vector.point_domain.positions()[index]); @@ -704,42 +711,34 @@ pub mod extrude_algorithms { } let mut next_segment = vector.segment_domain.next_id(); - for index in 0..first_half_points { - if points[index] != Found::Both { + for (index, &point) in points.iter().enumerate().take(first_half_points) { + if point != Found::Both { continue; } - vector.segment_domain.push( - next_segment.next_id(), - index, - index + first_half_points, - vector_types::subpath::BezierHandles::Linear, - vector_types::vector::StrokeId::ZERO, - ); + + vector + .segment_domain + .push(next_segment.next_id(), index, index + first_half_points, BezierHandles::Linear, StrokeId::ZERO); } } - /// Join all points from the original to the copied + /// Join all points from the original to the copied. fn join_all(vector: &mut graphic_types::Vector) { let mut next_segment = vector.segment_domain.next_id(); let first_half = vector.point_domain.len() / 2; for index in 0..first_half { - vector.segment_domain.push( - next_segment.next_id(), - index, - index + first_half, - vector_types::subpath::BezierHandles::Linear, - vector_types::vector::StrokeId::ZERO, - ); + vector.segment_domain.push(next_segment.next_id(), index, index + first_half, BezierHandles::Linear, StrokeId::ZERO); } } - pub fn extrude(vector: &mut graphic_types::Vector, direction: glam::DVec2, joining_algorithm: vector_types::vector::misc::ExtrudeJoiningAlgorithm) { + pub fn extrude(vector: &mut graphic_types::Vector, direction: DVec2, joining_algorithm: ExtrudeJoiningAlgorithm) { split(vector, direction); offset_copy_all_segments(vector, direction); + match joining_algorithm { - vector_types::vector::misc::ExtrudeJoiningAlgorithm::Extrema => join_extrema_edges(vector, direction), - vector_types::vector::misc::ExtrudeJoiningAlgorithm::All => join_all(vector), - vector_types::vector::misc::ExtrudeJoiningAlgorithm::None => {} + ExtrudeJoiningAlgorithm::Extrema => join_extrema_edges(vector, direction), + ExtrudeJoiningAlgorithm::All => join_all(vector), + ExtrudeJoiningAlgorithm::None => {} } } @@ -777,7 +776,6 @@ pub mod extrude_algorithms { } } -/// Attempt to inscribe circles at the anchors (that have exactly two segments connected). #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] async fn extrude(_: impl Ctx, mut source: Table, direction: DVec2, joining_algorithm: ExtrudeJoiningAlgorithm) -> Table { for TableRowMut { element: source, .. } in source.iter_mut() { @@ -1837,7 +1835,7 @@ async fn morph( let target_segment_len = target_bezpath.segments().count(); let source_segment_len = source_bezpath.segments().count(); - // Insert new segments to align the number of segments in sorce_bezpath and target_bezpath. + // Insert new segments to align the number of segments in source_bezpath and target_bezpath. make_new_segments(&mut source_bezpath, target_segment_len.max(source_segment_len) - source_segment_len); make_new_segments(&mut target_bezpath, source_segment_len.max(target_segment_len) - target_segment_len); @@ -1942,7 +1940,7 @@ fn bevel_algorithm(mut vector: Vector, transform: DAffine2, distance: f64) -> Ve segments_connected_count[point_index] += 1; } - // Zero out points without exactly two connectors. These are ignored + // Zero out points without exactly two connectors. These are ignored. for count in &mut segments_connected_count { if *count != 2 { *count = 0; @@ -1970,7 +1968,7 @@ fn bevel_algorithm(mut vector: Vector, transform: DAffine2, distance: f64) -> Ve } } - fn calculate_distance_to_spilt(bezier1: PathSeg, bezier2: PathSeg, bevel_length: f64) -> f64 { + fn calculate_distance_to_split(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(); @@ -2107,7 +2105,7 @@ fn bevel_algorithm(mut vector: Vector, transform: DAffine2, distance: f64) -> Ve let mut next_bezier = handles_to_segment(next_start, *next_handles, next_end); next_bezier = Affine::new(transform.to_cols_array()) * next_bezier; - let spilt_distance = calculate_distance_to_spilt(bezier, next_bezier, distance); + let calculated_split_distance = calculate_distance_to_split(bezier, next_bezier, distance); if is_linear(bezier) { bezier = PathSeg::Line(Line::new(bezier.start(), bezier.end())); @@ -2140,7 +2138,7 @@ fn bevel_algorithm(mut vector: Vector, transform: DAffine2, distance: f64) -> Ve let valid_length = length > 1e-10; if segments_connected[*end_point] > 0 && valid_length { // Apply the bevel to the end - let distance = spilt_distance.min(original_length.min(next_original_length) / 2.); + let distance = calculated_split_distance.min(original_length.min(next_original_length) / 2.); bezier = split_distance(bezier.reverse(), distance, length).reverse(); if index == 0 && next_index == 1 { @@ -2159,7 +2157,7 @@ fn bevel_algorithm(mut vector: Vector, transform: DAffine2, distance: f64) -> Ve 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.); + let distance = calculated_split_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.);