1use crate::square::grow::{
44 collect_labelled_neighbours, predict_from_neighbours, Admit, FillEdgeCtx, GrowResult,
45 GrowValidator,
46};
47use kiddo::{KdTree, SquaredEuclidean};
48use nalgebra::Point2;
49
50#[non_exhaustive]
52#[derive(Clone, Copy, Debug)]
53pub struct FillParams {
54 pub attach_search_rel: f32,
57 pub attach_ambiguity_factor: f32,
60 pub max_iters: usize,
66}
67
68impl Default for FillParams {
69 fn default() -> Self {
70 Self {
71 attach_search_rel: 0.35,
72 attach_ambiguity_factor: 1.5,
73 max_iters: 1,
74 }
75 }
76}
77
78impl FillParams {
79 pub fn new(attach_search_rel: f32, attach_ambiguity_factor: f32, max_iters: usize) -> Self {
82 Self {
83 attach_search_rel,
84 attach_ambiguity_factor,
85 max_iters,
86 }
87 }
88}
89
90#[non_exhaustive]
92#[derive(Clone, Debug, Default)]
93pub struct FillStats {
94 pub added: usize,
96 pub iterations: usize,
98 pub attached_indices: Vec<usize>,
103 pub attached_cells: Vec<(i32, i32)>,
105}
106
107pub fn fill_grid_holes<V: GrowValidator>(
118 positions: &[Point2<f32>],
119 grow: &mut GrowResult,
120 cell_size: f32,
121 params: &FillParams,
122 validator: &V,
123) -> FillStats {
124 let mut stats = FillStats::default();
125 if grow.labelled.is_empty() {
126 return stats;
127 }
128
129 for _iter in 0..params.max_iters.max(1) {
130 stats.iterations += 1;
131 let (tree, slot_to_corner) = build_fill_tree(positions, grow, validator);
132 let ctx = FillCtx {
133 positions,
134 cell_size,
135 params,
136 tree: &tree,
137 slot_to_corner: &slot_to_corner,
138 validator,
139 };
140
141 let cells = enumerate_fill_cells(grow);
142 let mut added_this_iter = 0usize;
143 for cell in cells {
144 if grow.labelled.contains_key(&cell) {
145 continue;
146 }
147 if let Some(attached_idx) = try_fill_cell(cell, grow, &ctx) {
148 stats.attached_indices.push(attached_idx);
149 stats.attached_cells.push(cell);
150 added_this_iter += 1;
151 }
152 }
153 stats.added += added_this_iter;
154 if added_this_iter == 0 {
155 break;
156 }
157 }
158
159 stats
160}
161
162fn build_fill_tree<V: GrowValidator>(
163 positions: &[Point2<f32>],
164 grow: &GrowResult,
165 validator: &V,
166) -> (KdTree<f32, 2>, Vec<usize>) {
167 let mut tree: KdTree<f32, 2> = KdTree::new();
168 let mut slot_to_corner: Vec<usize> = Vec::new();
169 for (idx, pos) in positions.iter().enumerate() {
170 if validator.eligible_for_fill(idx) && !grow.by_corner.contains_key(&idx) {
171 tree.add(&[pos.x, pos.y], slot_to_corner.len() as u64);
172 slot_to_corner.push(idx);
173 }
174 }
175 (tree, slot_to_corner)
176}
177
178fn enumerate_fill_cells(grow: &GrowResult) -> Vec<(i32, i32)> {
181 use std::collections::HashSet;
182 let mut out: HashSet<(i32, i32)> = HashSet::new();
183 let (mut min_i, mut max_i, mut min_j, mut max_j) = (i32::MAX, i32::MIN, i32::MAX, i32::MIN);
184 for &(i, j) in grow.labelled.keys() {
185 min_i = min_i.min(i);
186 max_i = max_i.max(i);
187 min_j = min_j.min(j);
188 max_j = max_j.max(j);
189 }
190
191 for j in min_j..=max_j {
193 for i in min_i..=max_i {
194 if !grow.labelled.contains_key(&(i, j)) {
195 out.insert((i, j));
196 }
197 }
198 }
199
200 for j in min_j..=max_j {
203 out.insert((min_i - 1, j));
204 out.insert((max_i + 1, j));
205 }
206 for i in min_i..=max_i {
207 out.insert((i, min_j - 1));
208 out.insert((i, max_j + 1));
209 }
210
211 out.into_iter().collect()
212}
213
214struct FillCtx<'a, V: GrowValidator> {
220 positions: &'a [Point2<f32>],
221 cell_size: f32,
222 params: &'a FillParams,
223 tree: &'a KdTree<f32, 2>,
224 slot_to_corner: &'a [usize],
225 validator: &'a V,
226}
227
228fn try_fill_cell<V: GrowValidator>(
229 cell: (i32, i32),
230 grow: &mut GrowResult,
231 ctx: &FillCtx<'_, V>,
232) -> Option<usize> {
233 let positions = ctx.positions;
234 let cell_size = ctx.cell_size;
235 let params = ctx.params;
236 let tree = ctx.tree;
237 let slot_to_corner = ctx.slot_to_corner;
238 let validator = ctx.validator;
239 let neighbours = collect_labelled_neighbours(cell, 1, &grow.labelled, positions);
244 if neighbours.is_empty() {
245 return None;
246 }
247
248 let pred = predict_from_neighbours(
253 cell,
254 &neighbours,
255 grow.axis_i,
256 grow.axis_j,
257 cell_size,
258 &grow.labelled,
259 positions,
260 );
261
262 let required_label = validator.required_label_at(cell.0, cell.1);
265
266 let search_r = params.attach_search_rel * cell_size;
268 let r2 = search_r * search_r;
269 let mut hits: Vec<(usize, f32)> = Vec::new();
270 for nn in tree
271 .within_unsorted::<SquaredEuclidean>(&[pred.x, pred.y], r2)
272 .into_iter()
273 {
274 let slot = nn.item as usize;
275 let idx = slot_to_corner[slot];
276 if grow.by_corner.contains_key(&idx) {
277 continue;
278 }
279 if let Some(req) = required_label {
280 if validator.label_of(idx) != Some(req) {
281 continue;
282 }
283 }
284 if matches!(
285 validator.accept_candidate(idx, cell, pred, &neighbours),
286 Admit::Reject
287 ) {
288 continue;
289 }
290 hits.push((idx, nn.distance.sqrt()));
291 }
292 hits.sort_by(|a, b| a.1.total_cmp(&b.1));
293
294 let candidate_idx = match hits.len() {
295 0 => return None,
296 1 => hits[0].0,
297 _ => {
298 let d0 = hits[0].1.max(f32::EPSILON);
299 let d1 = hits[1].1;
300 if d1 / d0 < params.attach_ambiguity_factor {
301 return None;
302 }
303 hits[0].0
304 }
305 };
306
307 if !any_cardinal_fill_edge_ok(
313 candidate_idx,
314 cell,
315 positions,
316 &grow.labelled,
317 validator,
318 cell_size,
319 ) {
320 return None;
321 }
322
323 grow.labelled.insert(cell, candidate_idx);
325 grow.by_corner.insert(candidate_idx, cell);
326 Some(candidate_idx)
327}
328
329fn any_cardinal_fill_edge_ok<V: GrowValidator>(
330 c_idx: usize,
331 pos: (i32, i32),
332 positions: &[Point2<f32>],
333 labelled: &std::collections::HashMap<(i32, i32), usize>,
334 validator: &V,
335 cell_size: f32,
336) -> bool {
337 let mut found_any = false;
338 for (di, dj) in [(1, 0), (-1, 0), (0, 1), (0, -1)] {
339 let neigh = (pos.0 + di, pos.1 + dj);
340 if let Some(&n_idx) = labelled.get(&neigh) {
341 found_any = true;
342 let ctx = FillEdgeCtx {
343 candidate_idx: c_idx,
344 neighbour_idx: n_idx,
345 at_candidate: pos,
346 at_neighbour: neigh,
347 labelled,
348 positions,
349 cell_size,
350 };
351 if validator.fill_edge_ok(ctx) {
352 return true;
353 }
354 }
355 }
356 !found_any
357}
358
359#[cfg(test)]
360mod tests {
361 use super::*;
362 use crate::square::grow::{Admit, LabelledNeighbour};
363 use std::collections::HashMap;
364
365 struct OpenValidator;
368
369 impl GrowValidator for OpenValidator {
370 fn is_eligible(&self, _idx: usize) -> bool {
371 true
372 }
373 fn required_label_at(&self, _i: i32, _j: i32) -> Option<u8> {
374 None
375 }
376 fn label_of(&self, _idx: usize) -> Option<u8> {
377 None
378 }
379 fn accept_candidate(
380 &self,
381 _idx: usize,
382 _at: (i32, i32),
383 _prediction: Point2<f32>,
384 _neighbours: &[LabelledNeighbour],
385 ) -> Admit {
386 Admit::Accept
387 }
388 }
389
390 #[test]
391 fn fill_pass_attaches_interior_hole() {
392 let s = 20.0_f32;
394 let mut positions: Vec<Point2<f32>> = Vec::new();
395 for j in 0..3 {
396 for i in 0..3 {
397 positions.push(Point2::new(50.0 + i as f32 * s, 50.0 + j as f32 * s));
398 }
399 }
400 let mut labelled: HashMap<(i32, i32), usize> = HashMap::new();
401 let mut by_corner: HashMap<usize, (i32, i32)> = HashMap::new();
402 for j in 0..3 {
403 for i in 0..3 {
404 if (i, j) == (1, 1) {
405 continue;
406 }
407 let idx = (j * 3 + i) as usize;
408 labelled.insert((i, j), idx);
409 by_corner.insert(idx, (i, j));
410 }
411 }
412 let mut grow = GrowResult {
413 labelled,
414 by_corner,
415 ambiguous: Default::default(),
416 holes: Default::default(),
417 axis_i: nalgebra::Vector2::new(1.0, 0.0),
418 axis_j: nalgebra::Vector2::new(0.0, 1.0),
419 parity_shift_i: 0,
420 parity_shift_j: 0,
421 };
422 let stats = fill_grid_holes(
423 &positions,
424 &mut grow,
425 s,
426 &FillParams::default(),
427 &OpenValidator,
428 );
429 assert_eq!(stats.added, 1);
430 assert_eq!(grow.labelled.get(&(1, 1)), Some(&4));
431 assert_eq!(stats.attached_indices, vec![4]);
432 assert_eq!(stats.attached_cells, vec![(1, 1)]);
433 }
434}