From b5975e92b26ffd5efe8c7397f230b6eb8c942152 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Sun, 22 Jun 2025 04:22:29 -0700 Subject: [PATCH] Combine 'Merge by Distance' nodes with a choice of algorithm; clean up 'Boolean Operation' node result with merge-by-distance --- .../document/node_graph/node_properties.rs | 3 +- .../portfolio/portfolio_message_handler.rs | 42 ++++ .../vector/algorithms/merge_by_distance.rs | 2 +- node-graph/gcore/src/vector/misc.rs | 9 + node-graph/gcore/src/vector/vector_nodes.rs | 219 +++++++++--------- node-graph/graph-craft/src/document/value.rs | 1 + node-graph/gstd/src/vector.rs | 3 + 7 files changed, 167 insertions(+), 112 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 a61c33a37..13d17755a 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -23,8 +23,8 @@ use graphene_std::text::Font; use graphene_std::transform::{Footprint, ReferencePoint}; use graphene_std::vector::VectorDataTable; use graphene_std::vector::generator_nodes::grid; -use graphene_std::vector::misc::ArcType; use graphene_std::vector::misc::CentroidType; +use graphene_std::vector::misc::{ArcType, MergeByDistanceAlgorithm}; use graphene_std::vector::misc::{BooleanOperation, GridType}; use graphene_std::vector::style::{Fill, FillChoice, FillType, GradientStops}; use graphene_std::vector::style::{GradientType, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin}; @@ -238,6 +238,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/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index dac5225fd..e834f0da2 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -1032,6 +1032,48 @@ impl MessageHandler> for PortfolioMes document.network_interface.replace_reference_name(node_id, network_path, "Auto-Tangents".to_string()); } + + if reference == "Merge by Distance" && inputs_count == 2 { + let node_definition = resolve_document_node_type("Merge by Distance").unwrap(); + let new_node_template = node_definition.default_node_template(); + let document_node = new_node_template.document_node; + document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone()); + document + .network_interface + .replace_implementation_metadata(node_id, network_path, new_node_template.persistent_node_metadata); + + let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path); + + document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path); + document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path); + document.network_interface.set_input( + &InputConnector::node(*node_id, 2), + NodeInput::value(TaggedValue::MergeByDistanceAlgorithm(graphene_std::vector::misc::MergeByDistanceAlgorithm::Topological), false), + network_path, + ); + } + + if reference == "Spatial Merge by Distance" { + let node_definition = resolve_document_node_type("Merge by Distance").unwrap(); + let new_node_template = node_definition.default_node_template(); + let document_node = new_node_template.document_node; + document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone()); + document + .network_interface + .replace_implementation_metadata(node_id, network_path, new_node_template.persistent_node_metadata); + + let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path); + + document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path); + document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path); + document.network_interface.set_input( + &InputConnector::node(*node_id, 2), + NodeInput::value(TaggedValue::MergeByDistanceAlgorithm(graphene_std::vector::misc::MergeByDistanceAlgorithm::Spatial), false), + network_path, + ); + + document.network_interface.replace_reference_name(node_id, network_path, "Merge by Distance".to_string()); + } } // TODO: Eventually remove this document upgrade code diff --git a/node-graph/gcore/src/vector/algorithms/merge_by_distance.rs b/node-graph/gcore/src/vector/algorithms/merge_by_distance.rs index c7fb75eaf..99a94b3f5 100644 --- a/node-graph/gcore/src/vector/algorithms/merge_by_distance.rs +++ b/node-graph/gcore/src/vector/algorithms/merge_by_distance.rs @@ -5,7 +5,7 @@ use rustc_hash::FxHashSet; impl VectorData { /// Collapse all points with edges shorter than the specified distance - pub(crate) fn merge_by_distance(&mut self, distance: f64) { + pub fn merge_by_distance(&mut self, distance: f64) { // Treat self as an undirected graph let indices = VectorDataIndex::build_from(self); diff --git a/node-graph/gcore/src/vector/misc.rs b/node-graph/gcore/src/vector/misc.rs index 8e275c97d..cd012746e 100644 --- a/node-graph/gcore/src/vector/misc.rs +++ b/node-graph/gcore/src/vector/misc.rs @@ -94,6 +94,15 @@ pub enum ArcType { PieSlice, } +#[repr(C)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)] +#[widget(Radio)] +pub enum MergeByDistanceAlgorithm { + #[default] + Spatial, + Topological, +} + pub fn point_to_dvec2(point: Point) -> DVec2 { DVec2 { x: point.x, y: point.y } } diff --git a/node-graph/gcore/src/vector/vector_nodes.rs b/node-graph/gcore/src/vector/vector_nodes.rs index e78a2acd0..6d710c256 100644 --- a/node-graph/gcore/src/vector/vector_nodes.rs +++ b/node-graph/gcore/src/vector/vector_nodes.rs @@ -9,7 +9,7 @@ use crate::raster_types::{CPU, RasterDataTable}; use crate::registry::types::{Angle, Fraction, IntegerCount, Length, Multiplier, Percentage, PixelLength, PixelSize, SeedValue}; use crate::renderer::GraphicElementRendered; use crate::transform::{Footprint, ReferencePoint, Transform}; -use crate::vector::misc::dvec2_to_point; +use crate::vector::misc::{MergeByDistanceAlgorithm, dvec2_to_point}; use crate::vector::style::{PaintOrder, StrokeAlign, StrokeCap, StrokeJoin}; use crate::vector::{FillId, PointDomain, RegionId}; use crate::{CloneVarArgs, Color, Context, Ctx, ExtractAll, GraphicElement, GraphicGroupTable, OwnedContextImpl}; @@ -549,135 +549,146 @@ async fn round_corners( result_table } -#[node_macro::node(name("Spatial Merge by Distance"), category("Debug"), path(graphene_core::vector))] -async fn spatial_merge_by_distance( +#[node_macro::node(name("Merge by Distance"), category("Vector"), path(graphene_core::vector))] +pub fn merge_by_distance( _: impl Ctx, vector_data: VectorDataTable, #[default(0.1)] #[hard_min(0.0001)] - distance: f64, + distance: Length, + algorithm: MergeByDistanceAlgorithm, ) -> VectorDataTable { let mut result_table = VectorDataTable::default(); - for mut vector_data_instance in vector_data.instance_iter() { - let vector_data_transform = vector_data_instance.transform; - let vector_data = vector_data_instance.instance; + match algorithm { + MergeByDistanceAlgorithm::Spatial => { + for mut vector_data_instance in vector_data.instance_iter() { + let vector_data_transform = vector_data_instance.transform; + let vector_data = vector_data_instance.instance; - let point_count = vector_data.point_domain.positions().len(); + let point_count = vector_data.point_domain.positions().len(); - // Find min x and y for grid cell normalization - let mut min_x = f64::MAX; - let mut min_y = f64::MAX; + // Find min x and y for grid cell normalization + let mut min_x = f64::MAX; + let mut min_y = f64::MAX; - // Calculate mins without collecting all positions - for &pos in vector_data.point_domain.positions() { - let transformed_pos = vector_data_transform.transform_point2(pos); - min_x = min_x.min(transformed_pos.x); - min_y = min_y.min(transformed_pos.y); - } + // Calculate mins without collecting all positions + for &pos in vector_data.point_domain.positions() { + let transformed_pos = vector_data_transform.transform_point2(pos); + min_x = min_x.min(transformed_pos.x); + min_y = min_y.min(transformed_pos.y); + } - // Create a spatial grid with cell size of 'distance' - use std::collections::HashMap; - let mut grid: HashMap<(i32, i32), Vec> = HashMap::new(); + // Create a spatial grid with cell size of 'distance' + use std::collections::HashMap; + let mut grid: HashMap<(i32, i32), Vec> = HashMap::new(); - // Add points to grid cells without collecting all positions first - for i in 0..point_count { - let pos = vector_data_transform.transform_point2(vector_data.point_domain.positions()[i]); - let grid_x = ((pos.x - min_x) / distance).floor() as i32; - let grid_y = ((pos.y - min_y) / distance).floor() as i32; + // Add points to grid cells without collecting all positions first + for i in 0..point_count { + let pos = vector_data_transform.transform_point2(vector_data.point_domain.positions()[i]); + let grid_x = ((pos.x - min_x) / distance).floor() as i32; + let grid_y = ((pos.y - min_y) / distance).floor() as i32; - grid.entry((grid_x, grid_y)).or_default().push(i); - } + grid.entry((grid_x, grid_y)).or_default().push(i); + } - // Create point index mapping for merged points - let mut point_index_map = vec![None; point_count]; - let mut merged_positions = Vec::new(); - let mut merged_indices = Vec::new(); + // Create point index mapping for merged points + let mut point_index_map = vec![None; point_count]; + let mut merged_positions = Vec::new(); + let mut merged_indices = Vec::new(); - // Process each point - for i in 0..point_count { - // Skip points that have already been processed - if point_index_map[i].is_some() { - continue; - } + // Process each point + for i in 0..point_count { + // Skip points that have already been processed + if point_index_map[i].is_some() { + continue; + } - let pos_i = vector_data_transform.transform_point2(vector_data.point_domain.positions()[i]); - let grid_x = ((pos_i.x - min_x) / distance).floor() as i32; - let grid_y = ((pos_i.y - min_y) / distance).floor() as i32; + let pos_i = vector_data_transform.transform_point2(vector_data.point_domain.positions()[i]); + let grid_x = ((pos_i.x - min_x) / distance).floor() as i32; + let grid_y = ((pos_i.y - min_y) / distance).floor() as i32; - let mut group = vec![i]; + let mut group = vec![i]; - // Check only neighboring cells (3x3 grid around current cell) - for dx in -1..=1 { - for dy in -1..=1 { - let neighbor_cell = (grid_x + dx, grid_y + dy); + // Check only neighboring cells (3x3 grid around current cell) + for dx in -1..=1 { + for dy in -1..=1 { + let neighbor_cell = (grid_x + dx, grid_y + dy); - if let Some(indices) = grid.get(&neighbor_cell) { - for &j in indices { - if j > i && point_index_map[j].is_none() { - let pos_j = vector_data_transform.transform_point2(vector_data.point_domain.positions()[j]); - if pos_i.distance(pos_j) <= distance { - group.push(j); + if let Some(indices) = grid.get(&neighbor_cell) { + for &j in indices { + if j > i && point_index_map[j].is_none() { + let pos_j = vector_data_transform.transform_point2(vector_data.point_domain.positions()[j]); + if pos_i.distance(pos_j) <= distance { + group.push(j); + } + } } } } } + + // Create merged point - calculate positions as needed + let merged_position = group + .iter() + .map(|&idx| vector_data_transform.transform_point2(vector_data.point_domain.positions()[idx])) + .fold(DVec2::ZERO, |sum, pos| sum + pos) + / group.len() as f64; + + let merged_position = vector_data_transform.inverse().transform_point2(merged_position); + let merged_index = merged_positions.len(); + + merged_positions.push(merged_position); + merged_indices.push(vector_data.point_domain.ids()[group[0]]); + + // Update mapping for all points in the group + for &idx in &group { + point_index_map[idx] = Some(merged_index); + } } - } - // Create merged point - calculate positions as needed - let merged_position = group - .iter() - .map(|&idx| vector_data_transform.transform_point2(vector_data.point_domain.positions()[idx])) - .fold(DVec2::ZERO, |sum, pos| sum + pos) - / group.len() as f64; + // Create new point domain with merged points + let mut new_point_domain = PointDomain::new(); + for (idx, pos) in merged_indices.into_iter().zip(merged_positions) { + new_point_domain.push(idx, pos); + } - let merged_position = vector_data_transform.inverse().transform_point2(merged_position); - let merged_index = merged_positions.len(); + // Update segment domain + let mut new_segment_domain = SegmentDomain::new(); + for segment_idx in 0..vector_data.segment_domain.ids().len() { + let id = vector_data.segment_domain.ids()[segment_idx]; + let start = vector_data.segment_domain.start_point()[segment_idx]; + let end = vector_data.segment_domain.end_point()[segment_idx]; + let handles = vector_data.segment_domain.handles()[segment_idx]; + let stroke = vector_data.segment_domain.stroke()[segment_idx]; - merged_positions.push(merged_position); - merged_indices.push(vector_data.point_domain.ids()[group[0]]); + // Get new indices for start and end points + let new_start = point_index_map[start].unwrap(); + let new_end = point_index_map[end].unwrap(); - // Update mapping for all points in the group - for &idx in &group { - point_index_map[idx] = Some(merged_index); + // Skip segments where start and end points were merged + if new_start != new_end { + new_segment_domain.push(id, new_start, new_end, handles, stroke); + } + } + + // Create new vector data + let mut result = vector_data.clone(); + result.point_domain = new_point_domain; + result.segment_domain = new_segment_domain; + + // Create and return the result + vector_data_instance.instance = result; + vector_data_instance.source_node_id = None; + result_table.push(vector_data_instance); } } - - // Create new point domain with merged points - let mut new_point_domain = PointDomain::new(); - for (idx, pos) in merged_indices.into_iter().zip(merged_positions) { - new_point_domain.push(idx, pos); - } - - // Update segment domain - let mut new_segment_domain = SegmentDomain::new(); - for segment_idx in 0..vector_data.segment_domain.ids().len() { - let id = vector_data.segment_domain.ids()[segment_idx]; - let start = vector_data.segment_domain.start_point()[segment_idx]; - let end = vector_data.segment_domain.end_point()[segment_idx]; - let handles = vector_data.segment_domain.handles()[segment_idx]; - let stroke = vector_data.segment_domain.stroke()[segment_idx]; - - // Get new indices for start and end points - let new_start = point_index_map[start].unwrap(); - let new_end = point_index_map[end].unwrap(); - - // Skip segments where start and end points were merged - if new_start != new_end { - new_segment_domain.push(id, new_start, new_end, handles, stroke); + MergeByDistanceAlgorithm::Topological => { + for mut source_instance in vector_data.instance_iter() { + source_instance.instance.merge_by_distance(distance); + result_table.push(source_instance); } } - - // Create new vector data - let mut result = vector_data.clone(); - result.point_domain = new_point_domain; - result.segment_domain = new_segment_domain; - - // Create and return the result - vector_data_instance.instance = result; - vector_data_instance.source_node_id = None; - result_table.push(vector_data_instance); } result_table @@ -1859,18 +1870,6 @@ fn point_inside(_: impl Ctx, source: VectorDataTable, point: DVec2) -> bool { source.instance_iter().any(|instance| instance.instance.check_point_inside_shape(instance.transform, point)) } -#[node_macro::node(name("Merge by Distance"), category("Vector"), path(graphene_core::vector))] -fn merge_by_distance(_: impl Ctx, source: VectorDataTable, #[default(10.)] distance: Length) -> VectorDataTable { - let mut result_table = VectorDataTable::default(); - - for mut source_instance in source.instance_iter() { - source_instance.instance.merge_by_distance(distance); - result_table.push(source_instance); - } - - result_table -} - #[node_macro::node(category("Vector"), path(graphene_core::vector))] async fn area(ctx: impl Ctx + CloneVarArgs + ExtractAll, vector_data: impl Node, Output = VectorDataTable>) -> f64 { let new_ctx = OwnedContextImpl::from(ctx).with_footprint(Footprint::default()).into_context(); diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index 94aa05342..f641555d9 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -233,6 +233,7 @@ tagged_value! { SelectiveColorChoice(graphene_core::raster::SelectiveColorChoice), GridType(graphene_core::vector::misc::GridType), ArcType(graphene_core::vector::misc::ArcType), + MergeByDistanceAlgorithm(graphene_core::vector::misc::MergeByDistanceAlgorithm), #[serde(alias = "LineCap")] StrokeCap(graphene_core::vector::style::StrokeCap), #[serde(alias = "LineJoin")] diff --git a/node-graph/gstd/src/vector.rs b/node-graph/gstd/src/vector.rs index 1e84ed7a9..6d7ea1ce9 100644 --- a/node-graph/gstd/src/vector.rs +++ b/node-graph/gstd/src/vector.rs @@ -41,6 +41,9 @@ async fn boolean_operation + 'n + Send + Clone>( VectorData::transform(result_vector_data.instance, transform); result_vector_data.instance.style.set_stroke_transform(DAffine2::IDENTITY); result_vector_data.instance.upstream_graphic_group = Some(group_of_paths.clone()); + + // Clean up the boolean operation result by merging duplicated points + result_vector_data.instance.merge_by_distance(0.001); } result_vector_data_table