Combine 'Merge by Distance' nodes with a choice of algorithm; clean up 'Boolean Operation' node result with merge-by-distance

This commit is contained in:
Keavon Chambers 2025-06-22 04:22:29 -07:00
parent c1b15fcfdf
commit b5975e92b2
7 changed files with 167 additions and 112 deletions

View file

@ -23,8 +23,8 @@ use graphene_std::text::Font;
use graphene_std::transform::{Footprint, ReferencePoint}; use graphene_std::transform::{Footprint, ReferencePoint};
use graphene_std::vector::VectorDataTable; use graphene_std::vector::VectorDataTable;
use graphene_std::vector::generator_nodes::grid; use graphene_std::vector::generator_nodes::grid;
use graphene_std::vector::misc::ArcType;
use graphene_std::vector::misc::CentroidType; use graphene_std::vector::misc::CentroidType;
use graphene_std::vector::misc::{ArcType, MergeByDistanceAlgorithm};
use graphene_std::vector::misc::{BooleanOperation, GridType}; use graphene_std::vector::misc::{BooleanOperation, GridType};
use graphene_std::vector::style::{Fill, FillChoice, FillType, GradientStops}; use graphene_std::vector::style::{Fill, FillChoice, FillType, GradientStops};
use graphene_std::vector::style::{GradientType, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin}; 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::<StrokeAlign>() => enum_choice::<StrokeAlign>().for_socket(default_info).property_row(), Some(x) if x == TypeId::of::<StrokeAlign>() => enum_choice::<StrokeAlign>().for_socket(default_info).property_row(),
Some(x) if x == TypeId::of::<PaintOrder>() => enum_choice::<PaintOrder>().for_socket(default_info).property_row(), Some(x) if x == TypeId::of::<PaintOrder>() => enum_choice::<PaintOrder>().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::<ArcType>() => enum_choice::<ArcType>().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::<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(),
Some(x) if x == TypeId::of::<LuminanceCalculation>() => enum_choice::<LuminanceCalculation>().for_socket(default_info).property_row(), Some(x) if x == TypeId::of::<LuminanceCalculation>() => enum_choice::<LuminanceCalculation>().for_socket(default_info).property_row(),

View file

@ -1032,6 +1032,48 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
document.network_interface.replace_reference_name(node_id, network_path, "Auto-Tangents".to_string()); 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 // TODO: Eventually remove this document upgrade code

View file

@ -5,7 +5,7 @@ use rustc_hash::FxHashSet;
impl VectorData { impl VectorData {
/// Collapse all points with edges shorter than the specified distance /// 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 // Treat self as an undirected graph
let indices = VectorDataIndex::build_from(self); let indices = VectorDataIndex::build_from(self);

View file

@ -94,6 +94,15 @@ pub enum ArcType {
PieSlice, 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 { pub fn point_to_dvec2(point: Point) -> DVec2 {
DVec2 { x: point.x, y: point.y } DVec2 { x: point.x, y: point.y }
} }

View file

@ -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::registry::types::{Angle, Fraction, IntegerCount, Length, Multiplier, Percentage, PixelLength, PixelSize, SeedValue};
use crate::renderer::GraphicElementRendered; use crate::renderer::GraphicElementRendered;
use crate::transform::{Footprint, ReferencePoint, Transform}; 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::style::{PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
use crate::vector::{FillId, PointDomain, RegionId}; use crate::vector::{FillId, PointDomain, RegionId};
use crate::{CloneVarArgs, Color, Context, Ctx, ExtractAll, GraphicElement, GraphicGroupTable, OwnedContextImpl}; use crate::{CloneVarArgs, Color, Context, Ctx, ExtractAll, GraphicElement, GraphicGroupTable, OwnedContextImpl};
@ -549,135 +549,146 @@ async fn round_corners(
result_table result_table
} }
#[node_macro::node(name("Spatial Merge by Distance"), category("Debug"), path(graphene_core::vector))] #[node_macro::node(name("Merge by Distance"), category("Vector"), path(graphene_core::vector))]
async fn spatial_merge_by_distance( pub fn merge_by_distance(
_: impl Ctx, _: impl Ctx,
vector_data: VectorDataTable, vector_data: VectorDataTable,
#[default(0.1)] #[default(0.1)]
#[hard_min(0.0001)] #[hard_min(0.0001)]
distance: f64, distance: Length,
algorithm: MergeByDistanceAlgorithm,
) -> VectorDataTable { ) -> VectorDataTable {
let mut result_table = VectorDataTable::default(); let mut result_table = VectorDataTable::default();
for mut vector_data_instance in vector_data.instance_iter() { match algorithm {
let vector_data_transform = vector_data_instance.transform; MergeByDistanceAlgorithm::Spatial => {
let vector_data = vector_data_instance.instance; 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 // Find min x and y for grid cell normalization
let mut min_x = f64::MAX; let mut min_x = f64::MAX;
let mut min_y = f64::MAX; let mut min_y = f64::MAX;
// Calculate mins without collecting all positions // Calculate mins without collecting all positions
for &pos in vector_data.point_domain.positions() { for &pos in vector_data.point_domain.positions() {
let transformed_pos = vector_data_transform.transform_point2(pos); let transformed_pos = vector_data_transform.transform_point2(pos);
min_x = min_x.min(transformed_pos.x); min_x = min_x.min(transformed_pos.x);
min_y = min_y.min(transformed_pos.y); min_y = min_y.min(transformed_pos.y);
} }
// Create a spatial grid with cell size of 'distance' // Create a spatial grid with cell size of 'distance'
use std::collections::HashMap; use std::collections::HashMap;
let mut grid: HashMap<(i32, i32), Vec<usize>> = HashMap::new(); let mut grid: HashMap<(i32, i32), Vec<usize>> = HashMap::new();
// Add points to grid cells without collecting all positions first // Add points to grid cells without collecting all positions first
for i in 0..point_count { for i in 0..point_count {
let pos = vector_data_transform.transform_point2(vector_data.point_domain.positions()[i]); 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_x = ((pos.x - min_x) / distance).floor() as i32;
let grid_y = ((pos.y - min_y) / 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 // Create point index mapping for merged points
let mut point_index_map = vec![None; point_count]; let mut point_index_map = vec![None; point_count];
let mut merged_positions = Vec::new(); let mut merged_positions = Vec::new();
let mut merged_indices = Vec::new(); let mut merged_indices = Vec::new();
// Process each point // Process each point
for i in 0..point_count { for i in 0..point_count {
// Skip points that have already been processed // Skip points that have already been processed
if point_index_map[i].is_some() { if point_index_map[i].is_some() {
continue; continue;
} }
let pos_i = vector_data_transform.transform_point2(vector_data.point_domain.positions()[i]); 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_x = ((pos_i.x - min_x) / distance).floor() as i32;
let grid_y = ((pos_i.y - min_y) / 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) // Check only neighboring cells (3x3 grid around current cell)
for dx in -1..=1 { for dx in -1..=1 {
for dy in -1..=1 { for dy in -1..=1 {
let neighbor_cell = (grid_x + dx, grid_y + dy); let neighbor_cell = (grid_x + dx, grid_y + dy);
if let Some(indices) = grid.get(&neighbor_cell) { if let Some(indices) = grid.get(&neighbor_cell) {
for &j in indices { for &j in indices {
if j > i && point_index_map[j].is_none() { if j > i && point_index_map[j].is_none() {
let pos_j = vector_data_transform.transform_point2(vector_data.point_domain.positions()[j]); let pos_j = vector_data_transform.transform_point2(vector_data.point_domain.positions()[j]);
if pos_i.distance(pos_j) <= distance { if pos_i.distance(pos_j) <= distance {
group.push(j); 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 // Create new point domain with merged points
let merged_position = group let mut new_point_domain = PointDomain::new();
.iter() for (idx, pos) in merged_indices.into_iter().zip(merged_positions) {
.map(|&idx| vector_data_transform.transform_point2(vector_data.point_domain.positions()[idx])) new_point_domain.push(idx, pos);
.fold(DVec2::ZERO, |sum, pos| sum + pos) }
/ group.len() as f64;
let merged_position = vector_data_transform.inverse().transform_point2(merged_position); // Update segment domain
let merged_index = merged_positions.len(); 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); // Get new indices for start and end points
merged_indices.push(vector_data.point_domain.ids()[group[0]]); let new_start = point_index_map[start].unwrap();
let new_end = point_index_map[end].unwrap();
// Update mapping for all points in the group // Skip segments where start and end points were merged
for &idx in &group { if new_start != new_end {
point_index_map[idx] = Some(merged_index); 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);
} }
} }
MergeByDistanceAlgorithm::Topological => {
// Create new point domain with merged points for mut source_instance in vector_data.instance_iter() {
let mut new_point_domain = PointDomain::new(); source_instance.instance.merge_by_distance(distance);
for (idx, pos) in merged_indices.into_iter().zip(merged_positions) { result_table.push(source_instance);
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);
} }
} }
// 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 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)) 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))] #[node_macro::node(category("Vector"), path(graphene_core::vector))]
async fn area(ctx: impl Ctx + CloneVarArgs + ExtractAll, vector_data: impl Node<Context<'static>, Output = VectorDataTable>) -> f64 { async fn area(ctx: impl Ctx + CloneVarArgs + ExtractAll, vector_data: impl Node<Context<'static>, Output = VectorDataTable>) -> f64 {
let new_ctx = OwnedContextImpl::from(ctx).with_footprint(Footprint::default()).into_context(); let new_ctx = OwnedContextImpl::from(ctx).with_footprint(Footprint::default()).into_context();

View file

@ -233,6 +233,7 @@ tagged_value! {
SelectiveColorChoice(graphene_core::raster::SelectiveColorChoice), SelectiveColorChoice(graphene_core::raster::SelectiveColorChoice),
GridType(graphene_core::vector::misc::GridType), GridType(graphene_core::vector::misc::GridType),
ArcType(graphene_core::vector::misc::ArcType), ArcType(graphene_core::vector::misc::ArcType),
MergeByDistanceAlgorithm(graphene_core::vector::misc::MergeByDistanceAlgorithm),
#[serde(alias = "LineCap")] #[serde(alias = "LineCap")]
StrokeCap(graphene_core::vector::style::StrokeCap), StrokeCap(graphene_core::vector::style::StrokeCap),
#[serde(alias = "LineJoin")] #[serde(alias = "LineJoin")]

View file

@ -41,6 +41,9 @@ async fn boolean_operation<I: Into<GraphicGroupTable> + 'n + Send + Clone>(
VectorData::transform(result_vector_data.instance, transform); VectorData::transform(result_vector_data.instance, transform);
result_vector_data.instance.style.set_stroke_transform(DAffine2::IDENTITY); result_vector_data.instance.style.set_stroke_transform(DAffine2::IDENTITY);
result_vector_data.instance.upstream_graphic_group = Some(group_of_paths.clone()); 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 result_vector_data_table