New node: 'Extrude' (#3414)

* Add extrude node

* Code review

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
James Lindsay 2025-11-27 02:50:10 +00:00 committed by GitHub
parent 5ebf6d6bc0
commit 8383a3afac
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 230 additions and 8 deletions

View File

@ -23,7 +23,7 @@ use graphene_std::raster::{
use graphene_std::table::{Table, TableRow}; use graphene_std::table::{Table, TableRow};
use graphene_std::text::{Font, TextAlign}; use graphene_std::text::{Font, TextAlign};
use graphene_std::transform::{Footprint, ReferencePoint, Transform}; 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}; use graphene_std::vector::style::{Fill, FillChoice, FillType, GradientStops, GradientType, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
pub(crate) fn string_properties(text: &str) -> Vec<LayoutGroup> { pub(crate) fn string_properties(text: &str) -> Vec<LayoutGroup> {
@ -219,6 +219,7 @@ pub(crate) fn property_from_type(
Some(x) if x == TypeId::of::<ArcType>() => enum_choice::<ArcType>().for_socket(default_info).property_row(), Some(x) if x == TypeId::of::<ArcType>() => enum_choice::<ArcType>().for_socket(default_info).property_row(),
Some(x) if x == TypeId::of::<TextAlign>() => enum_choice::<TextAlign>().for_socket(default_info).property_row(), Some(x) if x == TypeId::of::<TextAlign>() => enum_choice::<TextAlign>().for_socket(default_info).property_row(),
Some(x) if x == TypeId::of::<MergeByDistanceAlgorithm>() => enum_choice::<MergeByDistanceAlgorithm>().for_socket(default_info).property_row(), Some(x) if x == TypeId::of::<MergeByDistanceAlgorithm>() => enum_choice::<MergeByDistanceAlgorithm>().for_socket(default_info).property_row(),
Some(x) if x == TypeId::of::<ExtrudeJoiningAlgorithm>() => enum_choice::<ExtrudeJoiningAlgorithm>().for_socket(default_info).property_row(),
Some(x) if x == TypeId::of::<PointSpacingType>() => enum_choice::<PointSpacingType>().for_socket(default_info).property_row(), Some(x) if x == TypeId::of::<PointSpacingType>() => enum_choice::<PointSpacingType>().for_socket(default_info).property_row(),
Some(x) if x == TypeId::of::<BooleanOperation>() => enum_choice::<BooleanOperation>().for_socket(default_info).property_row(), Some(x) if x == TypeId::of::<BooleanOperation>() => enum_choice::<BooleanOperation>().for_socket(default_info).property_row(),
Some(x) if x == TypeId::of::<CentroidType>() => enum_choice::<CentroidType>().for_socket(default_info).property_row(), Some(x) if x == TypeId::of::<CentroidType>() => enum_choice::<CentroidType>().for_socket(default_info).property_row(),

View File

@ -245,6 +245,7 @@ tagged_value! {
GridType(vector::misc::GridType), GridType(vector::misc::GridType),
ArcType(vector::misc::ArcType), ArcType(vector::misc::ArcType),
MergeByDistanceAlgorithm(vector::misc::MergeByDistanceAlgorithm), MergeByDistanceAlgorithm(vector::misc::MergeByDistanceAlgorithm),
ExtrudeJoiningAlgorithm(vector::misc::ExtrudeJoiningAlgorithm),
PointSpacingType(vector::misc::PointSpacingType), PointSpacingType(vector::misc::PointSpacingType),
SpiralType(vector::misc::SpiralType), SpiralType(vector::misc::SpiralType),
#[serde(alias = "LineCap")] #[serde(alias = "LineCap")]

View File

@ -134,6 +134,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
async_node!(graphene_core::memo::MonitorNode<_, _, _>, 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::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::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::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::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::FillType]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::style::GradientType]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::style::GradientType]),
@ -220,6 +221,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
async_node!(graphene_core::memo::MemoNode<_, _>, 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::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::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::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::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::StrokeCap]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::StrokeJoin]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::StrokeJoin]),

View File

@ -83,6 +83,16 @@ pub enum MergeByDistanceAlgorithm {
Topological, 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)] #[repr(C)]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)]
#[widget(Radio)] #[widget(Radio)]

View File

@ -300,6 +300,10 @@ impl SegmentDomain {
self.end_point[segment_index] = new; 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] { pub fn handles(&self) -> &[BezierHandles] {
&self.handles &self.handles
} }

View File

@ -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::merge_by_distance::MergeByDistanceExt;
use vector_types::vector::algorithms::offset_subpath::offset_bezpath; 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::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::{MergeByDistanceAlgorithm, PointSpacingType, is_linear};
use vector_types::vector::misc::{handles_to_segment, segment_to_handles}; use vector_types::vector::misc::{handles_to_segment, segment_to_handles};
use vector_types::vector::style::{Fill, Gradient, GradientStops, Stroke}; use vector_types::vector::style::{Fill, Gradient, GradientStops, Stroke};
@ -580,6 +580,210 @@ 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`].
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 {
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) -> 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: DVec2) -> impl Iterator<Item = f64> {
let derivative = cubic_segment.deriv();
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_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(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 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);
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, 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);
start_t = split_t;
start_index = middle_point_index;
}
}
}
/// 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 {
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 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]
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, &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, BezierHandles::Linear, 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, BezierHandles::Linear, StrokeId::ZERO);
}
}
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 {
ExtrudeJoiningAlgorithm::Extrema => join_extrema_edges(vector, direction),
ExtrudeJoiningAlgorithm::All => join_all(vector),
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<f64>>(), vec![0.5]);
assert!(super::find_splits(l1, DVec2::X).collect::<Vec<f64>>().is_empty());
let l2 = kurbo::CubicBez::new((0., 0.), (0., 0.), (100., 0.), (100., 0.));
assert!(super::find_splits(l2, DVec2::X).collect::<Vec<f64>>().is_empty());
let l3 = kurbo::PathSeg::Line(kurbo::Line::new((0., 0.), (100., 0.)));
assert!(super::find_splits(l3.to_cubic(), DVec2::X).collect::<Vec<f64>>().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::<Vec<_>>();
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);
}
}
}
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
async fn extrude(_: impl Ctx, mut source: Table<Vector>, direction: DVec2, joining_algorithm: ExtrudeJoiningAlgorithm) -> Table<Vector> {
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))] #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
async fn box_warp(_: impl Ctx, content: Table<Vector>, #[expose] rectangle: Table<Vector>) -> Table<Vector> { async fn box_warp(_: impl Ctx, content: Table<Vector>, #[expose] rectangle: Table<Vector>) -> Table<Vector> {
let Some((target, target_transform)) = rectangle.get(0).map(|rect| (rect.element, rect.transform)) else { let Some((target, target_transform)) = rectangle.get(0).map(|rect| (rect.element, rect.transform)) else {
@ -1631,7 +1835,7 @@ async fn morph<I: IntoGraphicTable + 'n + Send + Clone>(
let target_segment_len = target_bezpath.segments().count(); let target_segment_len = target_bezpath.segments().count();
let source_segment_len = source_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 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); make_new_segments(&mut target_bezpath, source_segment_len.max(target_segment_len) - target_segment_len);
@ -1736,7 +1940,7 @@ fn bevel_algorithm(mut vector: Vector, transform: DAffine2, distance: f64) -> Ve
segments_connected_count[point_index] += 1; 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 { for count in &mut segments_connected_count {
if *count != 2 { if *count != 2 {
*count = 0; *count = 0;
@ -1764,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) { if is_linear(bezier1) && is_linear(bezier2) {
let v1 = (bezier1.end() - bezier1.start()).normalize(); let v1 = (bezier1.end() - bezier1.start()).normalize();
let v2 = (bezier1.end() - bezier2.end()).normalize(); let v2 = (bezier1.end() - bezier2.end()).normalize();
@ -1901,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); let mut next_bezier = handles_to_segment(next_start, *next_handles, next_end);
next_bezier = Affine::new(transform.to_cols_array()) * next_bezier; 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) { if is_linear(bezier) {
bezier = PathSeg::Line(Line::new(bezier.start(), bezier.end())); bezier = PathSeg::Line(Line::new(bezier.start(), bezier.end()));
@ -1934,7 +2138,7 @@ fn bevel_algorithm(mut vector: Vector, transform: DAffine2, distance: f64) -> Ve
let valid_length = length > 1e-10; let valid_length = length > 1e-10;
if segments_connected[*end_point] > 0 && valid_length { if segments_connected[*end_point] > 0 && valid_length {
// Apply the bevel to the end // 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(); bezier = split_distance(bezier.reverse(), distance, length).reverse();
if index == 0 && next_index == 1 { if index == 0 && next_index == 1 {
@ -1953,7 +2157,7 @@ fn bevel_algorithm(mut vector: Vector, transform: DAffine2, distance: f64) -> Ve
let valid_length = next_length > 1e-10; let valid_length = next_length > 1e-10;
if segments_connected[*next_start_point] > 0 && valid_length { if segments_connected[*next_start_point] > 0 && valid_length {
// Apply the bevel to the start // 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_bezier = split_distance(next_bezier, distance, next_length);
next_length = (next_length - distance).max(0.); next_length = (next_length - distance).max(0.);