|
1 |
| -use crate::vector::{PointId, VectorData, VectorDataIndex}; |
2 |
| -use glam::DVec2; |
| 1 | +use crate::vector::{PointDomain, PointId, SegmentDomain, VectorData, VectorDataIndex}; |
| 2 | +use glam::{DAffine2, DVec2}; |
3 | 3 | use petgraph::prelude::UnGraphMap;
|
4 | 4 | use rustc_hash::FxHashSet;
|
5 | 5 |
|
6 | 6 | impl VectorData {
|
7 | 7 | /// Collapse all points with edges shorter than the specified distance
|
8 |
| - pub fn merge_by_distance(&mut self, distance: f64) { |
| 8 | + pub fn merge_by_distance_topological(&mut self, distance: f64) { |
9 | 9 | // Treat self as an undirected graph
|
10 | 10 | let indices = VectorDataIndex::build_from(self);
|
11 | 11 |
|
| 12 | + // TODO: We lose information on the winding order by using an undirected graph. Switch to a directed graph and fix the algorithm to handle that. |
12 | 13 | // Graph containing only short edges, referencing the data graph
|
13 | 14 | let mut short_edges = UnGraphMap::new();
|
| 15 | + |
14 | 16 | for segment_id in self.segment_ids().iter().copied() {
|
15 | 17 | let length = indices.segment_chord_length(segment_id);
|
16 | 18 | if length < distance {
|
@@ -92,4 +94,116 @@ impl VectorData {
|
92 | 94 | self.segment_domain.retain(|id| !segments_to_delete.contains(id), usize::MAX);
|
93 | 95 | self.point_domain.retain(&mut self.segment_domain, |id| !points_to_delete.contains(id));
|
94 | 96 | }
|
| 97 | + |
| 98 | + pub fn merge_by_distance_spatial(&mut self, transform: DAffine2, distance: f64) { |
| 99 | + let point_count = self.point_domain.positions().len(); |
| 100 | + |
| 101 | + // Find min x and y for grid cell normalization |
| 102 | + let mut min_x = f64::MAX; |
| 103 | + let mut min_y = f64::MAX; |
| 104 | + |
| 105 | + // Calculate mins without collecting all positions |
| 106 | + for &pos in self.point_domain.positions() { |
| 107 | + let transformed_pos = transform.transform_point2(pos); |
| 108 | + min_x = min_x.min(transformed_pos.x); |
| 109 | + min_y = min_y.min(transformed_pos.y); |
| 110 | + } |
| 111 | + |
| 112 | + // Create a spatial grid with cell size of 'distance' |
| 113 | + use std::collections::HashMap; |
| 114 | + let mut grid: HashMap<(i32, i32), Vec<usize>> = HashMap::new(); |
| 115 | + |
| 116 | + // Add points to grid cells without collecting all positions first |
| 117 | + for i in 0..point_count { |
| 118 | + let pos = transform.transform_point2(self.point_domain.positions()[i]); |
| 119 | + let grid_x = ((pos.x - min_x) / distance).floor() as i32; |
| 120 | + let grid_y = ((pos.y - min_y) / distance).floor() as i32; |
| 121 | + |
| 122 | + grid.entry((grid_x, grid_y)).or_default().push(i); |
| 123 | + } |
| 124 | + |
| 125 | + // Create point index mapping for merged points |
| 126 | + let mut point_index_map = vec![None; point_count]; |
| 127 | + let mut merged_positions = Vec::new(); |
| 128 | + let mut merged_indices = Vec::new(); |
| 129 | + |
| 130 | + // Process each point |
| 131 | + for i in 0..point_count { |
| 132 | + // Skip points that have already been processed |
| 133 | + if point_index_map[i].is_some() { |
| 134 | + continue; |
| 135 | + } |
| 136 | + |
| 137 | + let pos_i = transform.transform_point2(self.point_domain.positions()[i]); |
| 138 | + let grid_x = ((pos_i.x - min_x) / distance).floor() as i32; |
| 139 | + let grid_y = ((pos_i.y - min_y) / distance).floor() as i32; |
| 140 | + |
| 141 | + let mut group = vec![i]; |
| 142 | + |
| 143 | + // Check only neighboring cells (3x3 grid around current cell) |
| 144 | + for dx in -1..=1 { |
| 145 | + for dy in -1..=1 { |
| 146 | + let neighbor_cell = (grid_x + dx, grid_y + dy); |
| 147 | + |
| 148 | + if let Some(indices) = grid.get(&neighbor_cell) { |
| 149 | + for &j in indices { |
| 150 | + if j > i && point_index_map[j].is_none() { |
| 151 | + let pos_j = transform.transform_point2(self.point_domain.positions()[j]); |
| 152 | + if pos_i.distance(pos_j) <= distance { |
| 153 | + group.push(j); |
| 154 | + } |
| 155 | + } |
| 156 | + } |
| 157 | + } |
| 158 | + } |
| 159 | + } |
| 160 | + |
| 161 | + // Create merged point - calculate positions as needed |
| 162 | + let merged_position = group |
| 163 | + .iter() |
| 164 | + .map(|&idx| transform.transform_point2(self.point_domain.positions()[idx])) |
| 165 | + .fold(DVec2::ZERO, |sum, pos| sum + pos) |
| 166 | + / group.len() as f64; |
| 167 | + |
| 168 | + let merged_position = transform.inverse().transform_point2(merged_position); |
| 169 | + let merged_index = merged_positions.len(); |
| 170 | + |
| 171 | + merged_positions.push(merged_position); |
| 172 | + merged_indices.push(self.point_domain.ids()[group[0]]); |
| 173 | + |
| 174 | + // Update mapping for all points in the group |
| 175 | + for &idx in &group { |
| 176 | + point_index_map[idx] = Some(merged_index); |
| 177 | + } |
| 178 | + } |
| 179 | + |
| 180 | + // Create new point domain with merged points |
| 181 | + let mut new_point_domain = PointDomain::new(); |
| 182 | + for (idx, pos) in merged_indices.into_iter().zip(merged_positions) { |
| 183 | + new_point_domain.push(idx, pos); |
| 184 | + } |
| 185 | + |
| 186 | + // Update segment domain |
| 187 | + let mut new_segment_domain = SegmentDomain::new(); |
| 188 | + for segment_idx in 0..self.segment_domain.ids().len() { |
| 189 | + let id = self.segment_domain.ids()[segment_idx]; |
| 190 | + let start = self.segment_domain.start_point()[segment_idx]; |
| 191 | + let end = self.segment_domain.end_point()[segment_idx]; |
| 192 | + let handles = self.segment_domain.handles()[segment_idx]; |
| 193 | + let stroke = self.segment_domain.stroke()[segment_idx]; |
| 194 | + |
| 195 | + // Get new indices for start and end points |
| 196 | + let new_start = point_index_map[start].unwrap(); |
| 197 | + let new_end = point_index_map[end].unwrap(); |
| 198 | + |
| 199 | + // Skip segments where start and end points were merged |
| 200 | + if new_start != new_end { |
| 201 | + new_segment_domain.push(id, new_start, new_end, handles, stroke); |
| 202 | + } |
| 203 | + } |
| 204 | + |
| 205 | + // Create new vector data |
| 206 | + self.point_domain = new_point_domain; |
| 207 | + self.segment_domain = new_segment_domain; |
| 208 | + } |
95 | 209 | }
|
0 commit comments