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 195cae25e4..46805288b9 100644 --- a/node-graph/gcore/src/vector/algorithms/merge_by_distance.rs +++ b/node-graph/gcore/src/vector/algorithms/merge_by_distance.rs @@ -105,6 +105,11 @@ impl MergeByDistanceExt for Vector { fn merge_by_distance_spatial(&mut self, transform: DAffine2, distance: f64) { let point_count = self.point_domain.positions().len(); + if !(distance.is_finite() && distance > 0.0) { + // Nothing to merge or invalid input; bail safely + return; + } + // Find min x and y for grid cell normalization let mut min_x = f64::MAX; let mut min_y = f64::MAX; @@ -123,8 +128,14 @@ impl MergeByDistanceExt for Vector { // Add points to grid cells without collecting all positions first for i in 0..point_count { let pos = transform.transform_point2(self.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; + let gx = ((pos.x - min_x) / distance).floor(); + let gy = ((pos.y - min_y) / distance).floor(); + if !gx.is_finite() || !gy.is_finite() { + continue; + } + + let grid_x = gx.clamp(i32::MIN as f64, i32::MAX as f64) as i32; + let grid_y = gy.clamp(i32::MIN as f64, i32::MAX as f64) as i32; grid.entry((grid_x, grid_y)).or_default().push(i); } @@ -142,17 +153,27 @@ impl MergeByDistanceExt for Vector { } let pos_i = transform.transform_point2(self.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 gx = ((pos_i.x - min_x) / distance).floor(); + let gy = ((pos_i.y - min_y) / distance).floor(); + + if !gx.is_finite() || !gy.is_finite() { + point_index_map[i] = Some(merged_positions.len()); + merged_positions.push(self.point_domain.positions()[i]); + merged_indices.push(self.point_domain.ids()[i]); + continue; + } + + let grid_x = gx.clamp(i32::MIN as f64, i32::MAX as f64) as i32; + let grid_y = gy.clamp(i32::MIN as f64, i32::MAX as f64) as i32; 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); - - if let Some(indices) = grid.get(&neighbor_cell) { + let nx = grid_x.saturating_add(dx); + let ny = grid_y.saturating_add(dy); + if let Some(indices) = grid.get(&(nx, ny)) { for &j in indices { if j > i && point_index_map[j].is_none() { let pos_j = transform.transform_point2(self.point_domain.positions()[j]); @@ -166,17 +187,17 @@ impl MergeByDistanceExt for Vector { } // Create merged point - calculate positions as needed - let merged_position = group + let merged_world = group .iter() .map(|&idx| transform.transform_point2(self.point_domain.positions()[idx])) .fold(DVec2::ZERO, |sum, pos| sum + pos) / group.len() as f64; + let merged_local = transform.inverse().transform_point2(merged_world); - let merged_position = transform.inverse().transform_point2(merged_position); + let rep_id = group.iter().map(|&idx| self.point_domain.ids()[idx]).min().unwrap(); let merged_index = merged_positions.len(); - - merged_positions.push(merged_position); - merged_indices.push(self.point_domain.ids()[group[0]]); + merged_positions.push(merged_local); + merged_indices.push(rep_id); // Update mapping for all points in the group for &idx in &group { @@ -184,6 +205,14 @@ impl MergeByDistanceExt for Vector { } } + for i in 0..point_count { + if point_index_map[i].is_none() { + point_index_map[i] = Some(merged_positions.len()); + merged_positions.push(self.point_domain.positions()[i]); + merged_indices.push(self.point_domain.ids()[i]); + } + } + // 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) { diff --git a/node-graph/gpath-bool/src/lib.rs b/node-graph/gpath-bool/src/lib.rs index df3a089414..6ab5ad2739 100644 --- a/node-graph/gpath-bool/src/lib.rs +++ b/node-graph/gpath-bool/src/lib.rs @@ -333,35 +333,86 @@ fn to_path(vector: &Vector, transform: DAffine2) -> Vec fn to_path_segments(path: &mut Vec, subpath: &Subpath, transform: DAffine2) { use path_bool::PathSegment; - let mut global_start = None; + + const EPSILON: f64 = 1e-8; + const COORD_LIMIT: f64 = 1.0e6; + #[inline] + fn finite_clamped(mut v: DVec2) -> Option { + if !v.x.is_finite() || !v.y.is_finite() { + return None; + } + v.x = v.x.clamp(-COORD_LIMIT, COORD_LIMIT); + v.y = v.y.clamp(-COORD_LIMIT, COORD_LIMIT); + Some(v) + } + + let transform_point = |pos: DVec2| -> Option { + let p = transform.transform_point2(pos); + let q = p * EPSILON.recip(); + let q = q.round(); + let q = q * EPSILON; + finite_clamped(q) + }; + + let mut global_start: Option = None; let mut global_end = DVec2::ZERO; + let mut any_segment = false; for bezier in subpath.iter() { - const EPS: f64 = 1e-8; - let transform_point = |pos: DVec2| transform.transform_point2(pos).mul(EPS.recip()).round().mul(EPS); - let PathSegPoints { p0, p1, p2, p3 } = pathseg_points(bezier); - let p0 = transform_point(p0); - let p1 = p1.map(transform_point); - let p2 = p2.map(transform_point); - let p3 = transform_point(p3); + let p0 = match transform_point(p0) { + Some(v) => v, + None => continue, + }; + let p3 = match transform_point(p3) { + Some(v) => v, + None => continue, + }; + let p1 = match p1 { + Some(h) => transform_point(h), + None => None, + }; + let p2 = match p2 { + Some(h) => transform_point(h), + None => None, + }; if global_start.is_none() { global_start = Some(p0); } global_end = p3; - let segment = match (p1, p2) { - (None, None) => PathSegment::Line(p0, p3), - (None, Some(p2)) | (Some(p2), None) => PathSegment::Quadratic(p0, p2, p3), - (Some(p1), Some(p2)) => PathSegment::Cubic(p0, p1, p2, p3), + let seg = match (p1, p2) { + (None, None) => { + if (p3 - p0).length_squared() == 0.0 { + continue; + } + PathSegment::Line(p0, p3) + } + (None, Some(p2)) | (Some(p2), None) => { + if (p3 - p0).length_squared() == 0.0 && (p2 - p0).length_squared() == 0.0 { + continue; + } + PathSegment::Quadratic(p0, p2, p3) + } + (Some(p1), Some(p2)) => { + if (p3 - p0).length_squared() == 0.0 && (p1 - p0).length_squared() == 0.0 && (p2 - p0).length_squared() == 0.0 { + continue; + }; + PathSegment::Cubic(p0, p1, p2, p3) + } }; - - path.push(segment); + path.push(seg); + any_segment = true; } - if let Some(start) = global_start { - path.push(PathSegment::Line(global_end, start)); + + if any_segment { + if let Some(start) = global_start { + if (global_end - start).length_squared() > 0.0 { + path.push(PathSegment::Line(global_end, start)) + } + } } }