From 441fbfc38c80c3dd3c2900604285013d01d58ce5 Mon Sep 17 00:00:00 2001 From: afrdbaig7 Date: Mon, 23 Mar 2026 18:06:01 +0530 Subject: [PATCH 1/3] Fix crashes + structural issues in Bevel and add colinear metadata detection - Fixes missing colinear metadata in generators by parsing geometry actively. - Adds post-processing for boolean outputs. - Defines current limitation: Identity is not preserved through boolean ops. - Removed index-based iteration in Bevel node to prevent out-of-bounds panics without removing Bevel's segment modifying identities. --- .../vector-types/src/vector/vector_types.rs | 32 +++ node-graph/nodes/path-bool/src/lib.rs | 1 + .../nodes/vector/src/generator_nodes.rs | 35 ++- node-graph/nodes/vector/src/vector_nodes.rs | 219 +++++++++++------- 4 files changed, 185 insertions(+), 102 deletions(-) diff --git a/node-graph/libraries/vector-types/src/vector/vector_types.rs b/node-graph/libraries/vector-types/src/vector/vector_types.rs index e504bb7199..101c1a884c 100644 --- a/node-graph/libraries/vector-types/src/vector/vector_types.rs +++ b/node-graph/libraries/vector-types/src/vector/vector_types.rs @@ -427,6 +427,38 @@ impl Vector { } } + pub fn detect_colinear_manipulators(&mut self) + where + Upstream: 'static, + { + self.colinear_manipulators.clear(); + for index in 0..self.point_domain.ids().len() { + let point_pos = self.point_domain.positions()[index]; + + let mut connected = Vec::new(); + for seg_id in self.segment_domain.start_connected(index) { + connected.push(HandleId::primary(seg_id)); + } + for seg_id in self.segment_domain.end_connected(index) { + connected.push(HandleId::end(seg_id)); + } + + if connected.len() == 2 { + let h1 = connected[0]; + let h2 = connected[1]; + + if let (Some(pos1), Some(pos2)) = (h1.to_manipulator_point().get_position(self), h2.to_manipulator_point().get_position(self)) { + let vec1 = (pos1 - point_pos).normalize_or_zero(); + let vec2 = (pos2 - point_pos).normalize_or_zero(); + + if vec1.dot(vec2) < -0.99999 { + self.colinear_manipulators.push([h1, h2]); + } + } + } + } + } + pub fn concat(&mut self, additional: &Self, transform_of_additional: DAffine2, collision_hash_seed: u64) { let point_map = additional .point_domain diff --git a/node-graph/nodes/path-bool/src/lib.rs b/node-graph/nodes/path-bool/src/lib.rs index 3a80a44189..55482d8dc6 100644 --- a/node-graph/nodes/path-bool/src/lib.rs +++ b/node-graph/nodes/path-bool/src/lib.rs @@ -141,6 +141,7 @@ fn boolean_operation_on_vector_table<'a>(vector: impl DoubleEndedIterator Table { let radius = radius.abs(); - Table::new_from_element(Vector::from_subpath(subpath::Subpath::new_ellipse(DVec2::splat(-radius), DVec2::splat(radius)))) + let mut circle = Vector::from_subpath(subpath::Subpath::new_ellipse(DVec2::splat(-radius), DVec2::splat(radius))); + set_ellipse_colinear_manipulators(&mut circle); + Table::new_from_element(circle) } /// Generates an arc shape forming a portion of a circle which may be open, closed, or a pie slice. @@ -66,7 +79,7 @@ fn arc( sweep_angle: Angle, arc_type: ArcType, ) -> Table { - Table::new_from_element(Vector::from_subpath(subpath::Subpath::new_arc( + let mut vector = Vector::from_subpath(subpath::Subpath::new_arc( radius, start_angle / 360. * std::f64::consts::TAU, sweep_angle / 360. * std::f64::consts::TAU, @@ -75,7 +88,9 @@ fn arc( ArcType::Closed => subpath::ArcType::Closed, ArcType::PieSlice => subpath::ArcType::PieSlice, }, - ))) + )); + vector.detect_colinear_manipulators(); + Table::new_from_element(vector) } /// Generates a spiral shape that winds from an inner to an outer radius. @@ -90,14 +105,16 @@ fn spiral( #[default(25)] outer_radius: f64, #[default(90.)] angular_resolution: f64, ) -> Table { - Table::new_from_element(Vector::from_subpath(subpath::Subpath::new_spiral( + let mut vector = Vector::from_subpath(subpath::Subpath::new_spiral( inner_radius, outer_radius, turns, start_angle.to_radians(), angular_resolution.to_radians(), spiral_type, - ))) + )); + vector.detect_colinear_manipulators(); + Table::new_from_element(vector) } /// Generates an ellipse shape (an oval or stretched circle) with the chosen radii. @@ -117,13 +134,7 @@ fn ellipse( let corner2 = radius; let mut ellipse = Vector::from_subpath(subpath::Subpath::new_ellipse(corner1, corner2)); - - let len = ellipse.segment_domain.ids().len(); - for i in 0..len { - ellipse - .colinear_manipulators - .push([HandleId::end(ellipse.segment_domain.ids()[i]), HandleId::primary(ellipse.segment_domain.ids()[(i + 1) % len])]); - } + set_ellipse_colinear_manipulators(&mut ellipse); Table::new_from_element(ellipse) } diff --git a/node-graph/nodes/vector/src/vector_nodes.rs b/node-graph/nodes/vector/src/vector_nodes.rs index c101b9dfdf..804652362e 100644 --- a/node-graph/nodes/vector/src/vector_nodes.rs +++ b/node-graph/nodes/vector/src/vector_nodes.rs @@ -1793,6 +1793,8 @@ async fn spline(_: impl Ctx, content: Table) -> Table { } row.element.segment_domain = segment_domain; + // Clear stale colinear_manipulators since all segment IDs have been replaced + row.element.colinear_manipulators.clear(); Some(row) }) .collect() @@ -2173,135 +2175,157 @@ fn bevel_algorithm(mut vector: Vector, transform: DAffine2, distance: f64) -> Ve split_distance } - fn sort_segments(segment_domain: &SegmentDomain) -> Vec { + 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); - } + let mut paths = Vec::new(); + let mut unvisited_segments: std::collections::HashSet = (0..segment_domain.ids().len()).collect(); + + while !unvisited_segments.is_empty() { + let first = *unvisited_segments.iter().next().unwrap(); + unvisited_segments.remove(&first); + + let mut path = vec![first]; + + loop { + let last = *path.last().unwrap(); + // Find next segment + if let Some(&next) = unvisited_segments.iter().find(|&&p| start_points[p] == end_points[last]) { + path.push(next); + unvisited_segments.remove(&next); + } else { + break; } - None => break, } - } - - if segment_domain_length != sorted_segments.len() { - for i in 0..segment_domain_length { - if !sorted_segments.contains(&i) { - sorted_segments.push(i); + + // Try to extend backwards + loop { + let first = *path.first().unwrap(); + if let Some(&prev) = unvisited_segments.iter().find(|&&p| end_points[p] == start_points[first]) { + path.insert(0, prev); + unvisited_segments.remove(&prev); + } else { + break; } } + + paths.push(path); } - sorted_segments + paths } fn update_existing_segments(vector: &mut Vector, transform: DAffine2, distance: f64, segments_connected: &mut [usize]) -> Vec<[usize; 2]> { let mut next_id = vector.point_domain.next_id(); let mut new_segments = Vec::new(); - let sorted_segments = sort_segments(&vector.segment_domain); + let paths = sort_segments(&vector.segment_domain); let segment_domain = &mut vector.segment_domain; - let segment_domain_length = segment_domain.ids().len(); - let mut first_original_length = 0.; - let mut first_length = 0.; - let mut prev_original_length = 0.; - let mut prev_length = 0.; + for path in paths { + let mut first_original_length = 0.; + let mut first_length = 0.; + let mut prev_original_length = 0.; + let mut prev_length = 0.; + + let path_len = path.len(); + for i in 0..path_len { + let index = path[i]; + let (next_index, is_connected) = if i == path_len - 1 { + let is_closed = segment_domain.start_point()[path[0]] == segment_domain.end_point()[path[path_len - 1]]; + (path[0], is_closed) + } else { + (path[i + 1], true) + }; - 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; + if !is_connected || index == next_index { + continue; + } - let start = vector.point_domain.positions()[*start_point]; - let end = vector.point_domain.positions()[*end_point]; + let pair_handles_and_points = segment_domain.pair_handles_and_points_mut_by_index(index, next_index); + let (handles, start_point, end_point, next_handles, next_start_point, next_end_point) = pair_handles_and_points; - let mut bezier = handles_to_segment(start, *handles, end); - bezier = Affine::new(transform.to_cols_array()) * bezier; + let start = vector.point_domain.positions()[*start_point]; + let end = vector.point_domain.positions()[*end_point]; - let next_start = vector.point_domain.positions()[*next_start_point]; - let next_end = vector.point_domain.positions()[*next_end_point]; + let mut bezier = handles_to_segment(start, *handles, end); + bezier = Affine::new(transform.to_cols_array()) * bezier; - let mut next_bezier = handles_to_segment(next_start, *next_handles, next_end); - next_bezier = Affine::new(transform.to_cols_array()) * next_bezier; + let next_start = vector.point_domain.positions()[*next_start_point]; + let next_end = vector.point_domain.positions()[*next_end_point]; - let calculated_split_distance = calculate_distance_to_split(bezier, next_bezier, distance); + let mut next_bezier = handles_to_segment(next_start, *next_handles, next_end); + next_bezier = Affine::new(transform.to_cols_array()) * next_bezier; - if is_linear(bezier) { - bezier = PathSeg::Line(Line::new(bezier.start(), bezier.end())); - } + let calculated_split_distance = calculate_distance_to_split(bezier, next_bezier, distance); - if is_linear(next_bezier) { - next_bezier = PathSeg::Line(Line::new(next_bezier.start(), next_bezier.end())); - } + if is_linear(bezier) { + bezier = PathSeg::Line(Line::new(bezier.start(), bezier.end())); + } - let inverse_transform = if transform.matrix2.determinant() != 0. { transform.inverse() } else { Default::default() }; + if is_linear(next_bezier) { + next_bezier = PathSeg::Line(Line::new(next_bezier.start(), next_bezier.end())); + } - if index == 0 && next_index == 1 { - first_original_length = bezier.perimeter(DEFAULT_ACCURACY); - first_length = first_original_length; - } + let inverse_transform = if transform.matrix2.determinant() != 0. { transform.inverse() } else { Default::default() }; - let (original_length, length) = if index == 0 { - (bezier.perimeter(DEFAULT_ACCURACY), bezier.perimeter(DEFAULT_ACCURACY)) - } else { - (prev_original_length, prev_length) - }; + if i == 0 { + first_original_length = bezier.perimeter(DEFAULT_ACCURACY); + first_length = first_original_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)) - }; + let (original_length, length) = if i == 0 { + (bezier.perimeter(DEFAULT_ACCURACY), bezier.perimeter(DEFAULT_ACCURACY)) + } else { + (prev_original_length, prev_length) + }; + + let (next_original_length, mut next_length) = if i == path_len - 1 { + (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] > 0 && valid_length { - // Apply the bevel to the end - let distance = calculated_split_distance.min(original_length.min(next_original_length) / 2.); - bezier = split_distance(bezier.reverse(), distance, length).reverse(); + // Only split if the length is big enough to make it worthwhile + let valid_length = length > 1e-10; + if segments_connected[*end_point] > 0 && valid_length { + // Apply the bevel to the end + 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 { - first_length = (length - distance).max(0.); + if i == 0 { + first_length = (length - distance).max(0.); + } + + // Update the end position + let pos = inverse_transform.transform_point2(point_to_dvec2(bezier.end())); + create_or_modify_point(&mut vector.point_domain, segments_connected, pos, end_point, &mut next_id, &mut new_segments); } - // Update the end position - let pos = inverse_transform.transform_point2(point_to_dvec2(bezier.end())); - create_or_modify_point(&mut vector.point_domain, segments_connected, pos, end_point, &mut next_id, &mut new_segments); - } + // Update the handles + *handles = segment_to_handles(&bezier).apply_transformation(|p| inverse_transform.transform_point2(p)); - // Update the handles - *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 = 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.); - // 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 = 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.); + // Update the start position + let pos = inverse_transform.transform_point2(point_to_dvec2(next_bezier.start())); - // Update the start position - let pos = inverse_transform.transform_point2(point_to_dvec2(next_bezier.start())); + create_or_modify_point(&mut vector.point_domain, segments_connected, pos, next_start_point, &mut next_id, &mut new_segments); - create_or_modify_point(&mut vector.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)); + } - // 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; } - - prev_original_length = next_original_length; - prev_length = next_length; } new_segments @@ -2320,6 +2344,21 @@ fn bevel_algorithm(mut vector: Vector, transform: DAffine2, distance: f64) -> Ve let mut segments_connected = segments_connected_count(&vector); let new_segments = update_existing_segments(&mut vector, transform, distance, &mut segments_connected); insert_new_segments(&mut vector, &new_segments); + + // Clean up colinear_manipulators: remove entries that reference + // segments which no longer exist or have become linear after beveling. + // Collect valid non-linear segment IDs first to avoid borrow conflicts. + let valid_nonlinear_segments: std::collections::HashSet = vector + .segment_domain + .ids() + .iter() + .zip(vector.segment_domain.handles()) + .filter(|(_, handles)| !matches!(handles, BezierHandles::Linear)) + .map(|(&id, _)| id) + .collect(); + vector.colinear_manipulators.retain(|[h1, h2]| { + valid_nonlinear_segments.contains(&h1.segment) && valid_nonlinear_segments.contains(&h2.segment) + }); } vector From 0e2e7f8fa206a377e88f288c9d7821b8a425485e Mon Sep 17 00:00:00 2001 From: afrdbaig7 Date: Mon, 23 Mar 2026 18:13:47 +0530 Subject: [PATCH 2/3] Run cargo fmt to please the CI --- node-graph/nodes/vector/src/vector_nodes.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/node-graph/nodes/vector/src/vector_nodes.rs b/node-graph/nodes/vector/src/vector_nodes.rs index 804652362e..80afd1e3d3 100644 --- a/node-graph/nodes/vector/src/vector_nodes.rs +++ b/node-graph/nodes/vector/src/vector_nodes.rs @@ -2185,9 +2185,9 @@ fn bevel_algorithm(mut vector: Vector, transform: DAffine2, distance: f64) -> Ve while !unvisited_segments.is_empty() { let first = *unvisited_segments.iter().next().unwrap(); unvisited_segments.remove(&first); - + let mut path = vec![first]; - + loop { let last = *path.last().unwrap(); // Find next segment @@ -2198,7 +2198,7 @@ fn bevel_algorithm(mut vector: Vector, transform: DAffine2, distance: f64) -> Ve break; } } - + // Try to extend backwards loop { let first = *path.first().unwrap(); @@ -2356,9 +2356,9 @@ fn bevel_algorithm(mut vector: Vector, transform: DAffine2, distance: f64) -> Ve .filter(|(_, handles)| !matches!(handles, BezierHandles::Linear)) .map(|(&id, _)| id) .collect(); - vector.colinear_manipulators.retain(|[h1, h2]| { - valid_nonlinear_segments.contains(&h1.segment) && valid_nonlinear_segments.contains(&h2.segment) - }); + vector + .colinear_manipulators + .retain(|[h1, h2]| valid_nonlinear_segments.contains(&h1.segment) && valid_nonlinear_segments.contains(&h2.segment)); } vector From 1ee91aa031eaacd7f8a435bfba251d0122546eab Mon Sep 17 00:00:00 2001 From: afrdbaig7 Date: Mon, 23 Mar 2026 18:58:35 +0530 Subject: [PATCH 3/3] Eliminate HashSet runtime dependencies to isolate WASM build failures --- node-graph/nodes/vector/src/vector_nodes.rs | 24 +++++++++++---------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/node-graph/nodes/vector/src/vector_nodes.rs b/node-graph/nodes/vector/src/vector_nodes.rs index 80afd1e3d3..f958f72ff8 100644 --- a/node-graph/nodes/vector/src/vector_nodes.rs +++ b/node-graph/nodes/vector/src/vector_nodes.rs @@ -2180,31 +2180,33 @@ fn bevel_algorithm(mut vector: Vector, transform: DAffine2, distance: f64) -> Ve let end_points = segment_domain.end_point(); let mut paths = Vec::new(); - let mut unvisited_segments: std::collections::HashSet = (0..segment_domain.ids().len()).collect(); + let mut unvisited_segments: Vec = (0..segment_domain.ids().len()).collect(); while !unvisited_segments.is_empty() { - let first = *unvisited_segments.iter().next().unwrap(); - unvisited_segments.remove(&first); - + let first = unvisited_segments[0]; + unvisited_segments.retain(|&x| x != first); + let mut path = vec![first]; - + loop { let last = *path.last().unwrap(); // Find next segment - if let Some(&next) = unvisited_segments.iter().find(|&&p| start_points[p] == end_points[last]) { + if let Some(next_idx) = unvisited_segments.iter().position(|&p| start_points[p] == end_points[last]) { + let next = unvisited_segments[next_idx]; path.push(next); - unvisited_segments.remove(&next); + unvisited_segments.retain(|&x| x != next); } else { break; } } - + // Try to extend backwards loop { let first = *path.first().unwrap(); - if let Some(&prev) = unvisited_segments.iter().find(|&&p| end_points[p] == start_points[first]) { + if let Some(prev_idx) = unvisited_segments.iter().position(|&p| end_points[p] == start_points[first]) { + let prev = unvisited_segments[prev_idx]; path.insert(0, prev); - unvisited_segments.remove(&prev); + unvisited_segments.retain(|&x| x != prev); } else { break; } @@ -2348,7 +2350,7 @@ fn bevel_algorithm(mut vector: Vector, transform: DAffine2, distance: f64) -> Ve // Clean up colinear_manipulators: remove entries that reference // segments which no longer exist or have become linear after beveling. // Collect valid non-linear segment IDs first to avoid borrow conflicts. - let valid_nonlinear_segments: std::collections::HashSet = vector + let valid_nonlinear_segments: Vec = vector .segment_domain .ids() .iter()