projective_grid/shared/grow/params.rs
1//! Grow policy trait, tuning, and result types.
2//!
3//! This module owns the growth *contract*: the [`SquareAttachPolicy`] trait
4//! that lets a caller plug pattern-specific invariants into the otherwise
5//! pure-geometry candidate search, the per-candidate [`Admit`] /
6//! [`LabelledNeighbour`] / [`FillEdgeCtx`] data carriers, the [`GrowParams`]
7//! tolerances, and the [`GrowResult`] output container. It deliberately holds
8//! no algorithm — the candidate-search / ambiguity helpers live in
9//! [`super`](crate::shared::grow) and the prediction geometry in
10//! [`super::predict`] — so the policy contract can be read without wading
11//! through the search loop. Tier: advanced engine (semver-exempt pre-1.0).
12
13use nalgebra::{Point2, Vector2};
14use std::collections::{HashMap, HashSet};
15
16/// Per-candidate decision from a [`SquareAttachPolicy`].
17#[derive(Clone, Copy, Debug, PartialEq, Eq)]
18pub enum Admit {
19 /// Accept this candidate at the given grid cell.
20 Accept,
21 /// Reject this candidate; the generic code may move on to the
22 /// next nearest (if any).
23 Reject,
24}
25
26/// Information about an existing labelled neighbour, passed to the
27/// policy during candidate evaluation.
28#[derive(Clone, Copy, Debug)]
29pub struct LabelledNeighbour {
30 /// Index of the neighbour corner in the caller's position array.
31 pub idx: usize,
32 /// The neighbour's `(i, j)` grid cell.
33 pub at: (i32, i32),
34 /// The neighbour's position in image pixels.
35 pub position: Point2<f32>,
36}
37
38/// Caller-supplied attachment policy for the square-lattice growth helpers.
39///
40/// Implementations typically hold references to the caller's feature
41/// data (axes, labels, strengths) plus tuning parameters, and use `idx`
42/// to look up the relevant per-feature record inside each callback.
43pub trait SquareAttachPolicy {
44 /// Is this corner index a possible candidate at all? Called
45 /// once per corner when the KD-tree is built.
46 fn is_eligible(&self, idx: usize) -> bool;
47
48 /// Optional caller-defined label required at grid cell `(i, j)`.
49 /// Return `None` for no constraint.
50 fn required_label_at(&self, i: i32, j: i32) -> Option<u8>;
51
52 /// Return the label of the corner at `idx`. Must agree with
53 /// `required_label_at` at attachment time. Called during
54 /// candidate filtering.
55 fn label_of(&self, idx: usize) -> Option<u8>;
56
57 /// Accept or reject a candidate for attachment at grid cell
58 /// `at` given its geometric prediction and existing labelled
59 /// neighbours. Called per candidate in order of increasing
60 /// distance to `prediction`.
61 fn accept_candidate(
62 &self,
63 idx: usize,
64 at: (i32, i32),
65 prediction: Point2<f32>,
66 neighbours: &[LabelledNeighbour],
67 ) -> Admit;
68
69 /// Soft per-edge check: is the induced edge between the just-
70 /// attached candidate and one of its cardinal-labelled neighbours
71 /// admissible? At least one cardinal edge must pass for the
72 /// attachment to stick; otherwise the position is marked a hole
73 /// and the candidate is rolled back.
74 ///
75 /// Default: accept all edges (no soft check).
76 fn edge_ok(
77 &self,
78 _candidate_idx: usize,
79 _neighbour_idx: usize,
80 _at_candidate: (i32, i32),
81 _at_neighbour: (i32, i32),
82 ) -> bool {
83 true
84 }
85
86 /// Optional widened eligibility used by the fill-pass booster.
87 ///
88 /// Defaults to [`Self::is_eligible`]; patterns whose precision
89 /// core admits only `Clustered` corners but want to admit a few
90 /// near-cluster corners during the booster pass override this to
91 /// expand the admissible set. The fill pass calls this when
92 /// building its KD-tree; the regular grow / boundary-extension
93 /// passes ignore it.
94 fn eligible_for_fill(&self, idx: usize) -> bool {
95 self.is_eligible(idx)
96 }
97
98 /// Optional fill-pass edge check that has access to the full
99 /// labelled set and the position table via [`FillEdgeCtx`].
100 ///
101 /// The default delegates to [`Self::edge_ok`], ignoring the extra
102 /// context. Pattern implementations that need a directional edge
103 /// metric (e.g., a strongly anisotropic component where the
104 /// horizontal pitch is much larger than the vertical pitch and a
105 /// scalar `cell_size` rejects legitimate vertical extrapolations)
106 /// override this to consult the labelled set when computing the
107 /// expected edge length.
108 ///
109 /// Only invoked by [`crate::shared::fill::fill_grid_holes`]; the
110 /// regular grow and boundary-extension passes call [`Self::edge_ok`]
111 /// directly.
112 fn fill_edge_ok(&self, ctx: FillEdgeCtx<'_>) -> bool {
113 self.edge_ok(
114 ctx.candidate_idx,
115 ctx.neighbour_idx,
116 ctx.at_candidate,
117 ctx.at_neighbour,
118 )
119 }
120}
121
122/// Context passed to [`SquareAttachPolicy::fill_edge_ok`].
123///
124/// Bundles every piece of state the policy needs to make a
125/// labelled-set-aware edge decision: the candidate + cardinal
126/// neighbour indices, their `(i, j)` cells, the full labelled map,
127/// the corner position array, and the scalar fallback cell size.
128#[non_exhaustive]
129#[derive(Clone, Copy)]
130pub struct FillEdgeCtx<'a> {
131 /// Index of the candidate corner being evaluated.
132 pub candidate_idx: usize,
133 /// Index of the already-labelled cardinal neighbour.
134 pub neighbour_idx: usize,
135 /// The candidate's prospective `(i, j)` cell.
136 pub at_candidate: (i32, i32),
137 /// The cardinal neighbour's `(i, j)` cell.
138 pub at_neighbour: (i32, i32),
139 /// The full `(i, j) → corner_idx` labelled map at this point in the grow.
140 pub labelled: &'a HashMap<(i32, i32), usize>,
141 /// Corner positions in image pixels, indexed by the values of `labelled`.
142 pub positions: &'a [Point2<f32>],
143 /// Scalar fallback cell size in pixels, used when no local estimate exists.
144 pub cell_size: f32,
145}
146
147/// Tolerances for the square-lattice growth helpers.
148#[non_exhaustive]
149#[derive(Clone, Copy, Debug)]
150pub struct GrowParams {
151 /// Candidate-search radius (fraction of `cell_size`) around each
152 /// prediction. Applies when the target is being **interpolated**
153 /// between labelled neighbours on opposite sides.
154 pub attach_search_rel: f32,
155 /// Ambiguity factor: if the second-nearest candidate is within
156 /// `factor × nearest_distance`, the attachment is skipped.
157 pub attach_ambiguity_factor: f32,
158 /// Multiplier on `attach_search_rel` when the target is being
159 /// **extrapolated** outward from the labelled set (every labelled
160 /// neighbour sits on the same side of the target along at least one
161 /// axis). Defaults to 2.0 — opens the search up enough to absorb
162 /// the perspective-foreshortening overshoot at the image edge while
163 /// still rejecting off-lattice candidates that sit several cell-
164 /// widths away.
165 pub boundary_search_factor: f32,
166}
167
168impl Default for GrowParams {
169 fn default() -> Self {
170 Self {
171 attach_search_rel: 0.35,
172 attach_ambiguity_factor: 1.5,
173 boundary_search_factor: 2.0,
174 }
175 }
176}
177
178impl GrowParams {
179 /// Construct grow parameters from the interpolation search radius and
180 /// ambiguity factor; `boundary_search_factor` keeps its default.
181 pub fn new(attach_search_rel: f32, attach_ambiguity_factor: f32) -> Self {
182 Self {
183 attach_search_rel,
184 attach_ambiguity_factor,
185 ..Self::default()
186 }
187 }
188}
189
190/// Outcome of a grow pass.
191#[derive(Debug, Default)]
192pub struct GrowResult {
193 /// `(i, j) → corner_index` map of accepted labels. Rebased so the
194 /// bounding-box minimum is `(0, 0)`.
195 pub labelled: HashMap<(i32, i32), usize>,
196 /// Inverse map.
197 pub by_corner: HashMap<usize, (i32, i32)>,
198 /// Positions with ≥ 2 candidates inside the ambiguity window.
199 pub ambiguous: HashSet<(i32, i32)>,
200 /// Positions with no accepted candidate.
201 pub holes: HashSet<(i32, i32)>,
202 /// Grid `i`-axis vector (pixels per cell) carried forward — overlays
203 /// and boosters use it.
204 pub axis_i: Vector2<f32>,
205 /// Grid `j`-axis vector (pixels per cell) carried forward — overlays
206 /// and boosters use it.
207 pub axis_j: Vector2<f32>,
208 /// Mod-2 `i` component of the coordinate shift removed by the
209 /// final rebase.
210 ///
211 /// `bfs_grow` walks in seed-local coordinates, then subtracts the
212 /// labelled bounding-box minimum so output coordinates start at
213 /// `(0, 0)`. Callers with an alternating label rule can add these
214 /// mod-2 components back when evaluating labels in post-rebase
215 /// coordinates. Callers without an alternating rule can ignore
216 /// these fields.
217 pub rebase_i_mod2: i32,
218 /// See [`Self::rebase_i_mod2`].
219 pub rebase_j_mod2: i32,
220}