1use crate::shared::grow::{
44 collect_labelled_neighbours, predict_from_neighbours, Admit, FillEdgeCtx, GrowResult,
45 SquareAttachPolicy,
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: SquareAttachPolicy>(
118 positions: &[Point2<f32>],
119 grow: &mut GrowResult,
120 cell_size: f32,
121 params: &FillParams,
122 policy: &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, policy);
132 let ctx = FillCtx {
133 positions,
134 cell_size,
135 params,
136 tree: &tree,
137 slot_to_corner: &slot_to_corner,
138 policy,
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: SquareAttachPolicy>(
163 positions: &[Point2<f32>],
164 grow: &GrowResult,
165 policy: &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 policy.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 let mut cells: Vec<(i32, i32)> = out.into_iter().collect();
221 cells.sort_unstable();
222 cells
223}
224
225struct FillCtx<'a, V: SquareAttachPolicy> {
231 positions: &'a [Point2<f32>],
232 cell_size: f32,
233 params: &'a FillParams,
234 tree: &'a KdTree<f32, 2>,
235 slot_to_corner: &'a [usize],
236 policy: &'a V,
237}
238
239fn try_fill_cell<V: SquareAttachPolicy>(
240 cell: (i32, i32),
241 grow: &mut GrowResult,
242 ctx: &FillCtx<'_, V>,
243) -> Option<usize> {
244 let positions = ctx.positions;
245 let cell_size = ctx.cell_size;
246 let params = ctx.params;
247 let tree = ctx.tree;
248 let slot_to_corner = ctx.slot_to_corner;
249 let policy = ctx.policy;
250 let neighbours = collect_labelled_neighbours(cell, 1, &grow.labelled, positions);
255 if neighbours.is_empty() {
256 return None;
257 }
258
259 let pred = predict_from_neighbours(
264 cell,
265 &neighbours,
266 grow.axis_i,
267 grow.axis_j,
268 cell_size,
269 &grow.labelled,
270 positions,
271 );
272
273 let required_label = policy.required_label_at(cell.0, cell.1);
275
276 let search_r = params.attach_search_rel * cell_size;
278 let r2 = search_r * search_r;
279 let mut hits: Vec<(usize, f32)> = Vec::new();
280 for nn in tree
281 .within_unsorted::<SquaredEuclidean>(&[pred.x, pred.y], r2)
282 .into_iter()
283 {
284 let slot = nn.item as usize;
285 let idx = slot_to_corner[slot];
286 if grow.by_corner.contains_key(&idx) {
287 continue;
288 }
289 if let Some(req) = required_label {
290 if policy.label_of(idx) != Some(req) {
291 continue;
292 }
293 }
294 if matches!(
295 policy.accept_candidate(idx, cell, pred, &neighbours),
296 Admit::Reject
297 ) {
298 continue;
299 }
300 hits.push((idx, nn.distance.sqrt()));
301 }
302 hits.sort_by(|a, b| a.1.total_cmp(&b.1));
303
304 let candidate_idx = match hits.len() {
305 0 => return None,
306 1 => hits[0].0,
307 _ => {
308 let d0 = hits[0].1.max(f32::EPSILON);
309 let d1 = hits[1].1;
310 if d1 / d0 < params.attach_ambiguity_factor {
311 return None;
312 }
313 hits[0].0
314 }
315 };
316
317 if !any_cardinal_fill_edge_ok(
323 candidate_idx,
324 cell,
325 positions,
326 &grow.labelled,
327 policy,
328 cell_size,
329 ) {
330 return None;
331 }
332
333 grow.labelled.insert(cell, candidate_idx);
335 grow.by_corner.insert(candidate_idx, cell);
336 Some(candidate_idx)
337}
338
339fn any_cardinal_fill_edge_ok<V: SquareAttachPolicy>(
340 c_idx: usize,
341 pos: (i32, i32),
342 positions: &[Point2<f32>],
343 labelled: &std::collections::HashMap<(i32, i32), usize>,
344 policy: &V,
345 cell_size: f32,
346) -> bool {
347 let mut found_any = false;
348 for (di, dj) in [(1, 0), (-1, 0), (0, 1), (0, -1)] {
349 let neigh = (pos.0 + di, pos.1 + dj);
350 if let Some(&n_idx) = labelled.get(&neigh) {
351 found_any = true;
352 let ctx = FillEdgeCtx {
353 candidate_idx: c_idx,
354 neighbour_idx: n_idx,
355 at_candidate: pos,
356 at_neighbour: neigh,
357 labelled,
358 positions,
359 cell_size,
360 };
361 if policy.fill_edge_ok(ctx) {
362 return true;
363 }
364 }
365 }
366 !found_any
367}
368
369#[cfg(test)]
370mod tests {
371 use super::*;
372 use crate::shared::grow::{Admit, LabelledNeighbour};
373 use std::collections::HashMap;
374
375 struct OpenValidator;
378
379 impl SquareAttachPolicy for OpenValidator {
380 fn is_eligible(&self, _idx: usize) -> bool {
381 true
382 }
383 fn required_label_at(&self, _i: i32, _j: i32) -> Option<u8> {
384 None
385 }
386 fn label_of(&self, _idx: usize) -> Option<u8> {
387 None
388 }
389 fn accept_candidate(
390 &self,
391 _idx: usize,
392 _at: (i32, i32),
393 _prediction: Point2<f32>,
394 _neighbours: &[LabelledNeighbour],
395 ) -> Admit {
396 Admit::Accept
397 }
398 }
399
400 #[test]
401 fn fill_pass_attaches_interior_hole() {
402 let s = 20.0_f32;
404 let mut positions: Vec<Point2<f32>> = Vec::new();
405 for j in 0..3 {
406 for i in 0..3 {
407 positions.push(Point2::new(50.0 + i as f32 * s, 50.0 + j as f32 * s));
408 }
409 }
410 let mut labelled: HashMap<(i32, i32), usize> = HashMap::new();
411 let mut by_corner: HashMap<usize, (i32, i32)> = HashMap::new();
412 for j in 0..3 {
413 for i in 0..3 {
414 if (i, j) == (1, 1) {
415 continue;
416 }
417 let idx = (j * 3 + i) as usize;
418 labelled.insert((i, j), idx);
419 by_corner.insert(idx, (i, j));
420 }
421 }
422 let mut grow = GrowResult {
423 labelled,
424 by_corner,
425 ambiguous: Default::default(),
426 holes: Default::default(),
427 axis_i: nalgebra::Vector2::new(1.0, 0.0),
428 axis_j: nalgebra::Vector2::new(0.0, 1.0),
429 rebase_i_mod2: 0,
430 rebase_j_mod2: 0,
431 };
432 let stats = fill_grid_holes(
433 &positions,
434 &mut grow,
435 s,
436 &FillParams::default(),
437 &OpenValidator,
438 );
439 assert_eq!(stats.added, 1);
440 assert_eq!(grow.labelled.get(&(1, 1)), Some(&4));
441 assert_eq!(stats.attached_indices, vec![4]);
442 assert_eq!(stats.attached_cells, vec![(1, 1)]);
443 }
444}