ifc_lite_geometry/scan_outline/
mod.rs1mod fit;
42mod grid;
43mod morph;
44mod options;
45mod simplify;
46mod snap;
47mod square;
48mod support;
49mod topology;
50mod trace;
51
52#[cfg(test)]
53#[path = "synth_tests.rs"]
54mod synth_tests;
55#[cfg(test)]
56#[path = "scan_outline_tests.rs"]
57mod tests;
58
59use crate::geom2d::polygon_area;
60pub use options::{
61 ScanOutlineDiagnostics, ScanOutlineOptions, MAX_CELLS_LIMIT, MAX_SNAP_DISTANCE_CELLS, MAX_VERTEX_MOVE_CELLS,
62 MIN_CELL_SIZE_LIMIT,
63};
64
65pub const MAX_GAP_LIMIT: f64 = 0.5;
68
69#[derive(Clone, Debug, Default, PartialEq)]
77#[non_exhaustive]
78pub struct ScanOutline {
79 pub rings: Vec<Vec<[f64; 2]>>,
80 pub shape_offsets: Vec<usize>,
81 pub parents: Vec<Option<usize>>,
84 pub diagnostics: ScanOutlineDiagnostics,
85}
86
87#[derive(Clone, Copy, Debug, PartialEq)]
89pub struct PlaneFrame {
90 pub origin: [f64; 3],
91 pub u_axis: [f64; 3],
92 pub v_axis: [f64; 3],
93}
94
95impl PlaneFrame {
96 pub fn to_world(&self, p: [f64; 2]) -> [f64; 3] {
97 let (o, u, v) = (self.origin, self.u_axis, self.v_axis);
98 [
99 o[0] + p[0] * u[0] + p[1] * v[0],
100 o[1] + p[0] * u[1] + p[1] * v[1],
101 o[2] + p[0] * u[2] + p[1] * v[2],
102 ]
103 }
104}
105
106impl ScanOutline {
107 pub fn world_rings(&self, frame: &PlaneFrame) -> Vec<Vec<[f64; 3]>> {
109 self.rings
110 .iter()
111 .map(|ring| ring.iter().map(|p| frame.to_world(*p)).collect())
112 .collect()
113 }
114}
115
116pub fn trace_scan_outline(xy: &[f32], opts: &ScanOutlineOptions) -> Result<ScanOutline, String> {
121 opts.validate()?;
122 let mut diag = ScanOutlineDiagnostics {
123 input_points: xy.len() / 2,
124 ..Default::default()
125 };
126 let max_gap = if opts.max_gap > MAX_GAP_LIMIT {
127 diag.max_gap_clamped = true;
128 MAX_GAP_LIMIT
129 } else {
130 opts.max_gap
131 };
132
133 let Some(binned) = grid::bin_points(xy, opts, max_gap, &mut diag) else {
134 return Ok(ScanOutline { diagnostics: diag, ..Default::default() });
135 };
136 let geo = binned.geometry;
137 let close_radius = grid::close_radius_cells(max_gap, geo.cell);
138 let mut mask = binned.occupied;
139 morph::close(&mut mask, geo.width, geo.height, close_radius);
140 morph::open_by_reconstruction(&mut mask, geo.width, geo.height, opts.open_radius_cells);
141 morph::fill_saddles(&mut mask, geo.width, geo.height);
142 let cell_area = geo.cell * geo.cell;
143 let labels = morph::filter_components(
144 &mut mask,
145 geo.width,
146 geo.height,
147 area_cells(opts.min_component_area, cell_area),
148 area_cells(opts.min_hole_area, cell_area),
149 );
150 diag.components_dropped = labels.dropped;
151 diag.holes_filled = labels.filled;
152 diag.solid_cells = mask.iter().filter(|&&m| m != 0).count();
153
154 let traced = trace::trace_rings(&mask, &labels, &geo);
155 let lattice = traced.rings;
156 let parents = traced.parents;
157
158 let eps = opts.simplify_tolerance_cells * geo.cell;
159 let (mut rings, reinsertions) = simplify::simplify_rings(&lattice, &parents, eps);
160 diag.simplify_reinsertions = reinsertions;
161
162 let max_move = opts.max_vertex_move_cells * geo.cell;
163 let support_cells = opts.max_vertex_move_cells.max(0.5 * close_radius as f64) + 1.0;
166 let support = support::Support::new(&mask, &geo, support_cells);
167 if opts.snap {
168 let index = grid::PointIndex::build(xy, &geo);
169 let ctx = snap::SnapContext { index: &index, xy, cell: geo.cell, close_radius_cells: close_radius, opts };
170 let snapped = snap::snap_rings(&rings, &ctx);
171 diag.snapped_edges = snapped.fitted_edges;
172 let mut references = snapped.references;
176 let mut moved = snapped.rings;
177 diag.reverted_moves += topology::restore_invalid_rings(&mut references, &mut moved, &rings, &parents, Some(&support));
178 diag.reverted_moves += topology::repair_moves(&mut moved, &references, &parents, Some(&support));
179 rings = moved;
180 }
181 if opts.square {
182 let before = rings.clone();
183 let squared = square::square_rings(&rings, geo.cell, opts);
184 diag.dominant_angle_deg = squared.dominant_angle_deg;
185 let mut moved = squared.rings.clone();
186 diag.reverted_moves += cap_moves(&mut moved, &before, max_move);
187 diag.reverted_moves += topology::repair_moves(&mut moved, &before, &parents, Some(&support));
188 diag.squared_edges = surviving_squared_edges(&moved, &squared.rings, &squared.squared);
190 rings = moved;
191 }
192 topology::drop_collinear_vertices(&mut rings, &parents);
193
194 let (rings, shape_offsets, parents) = group_shapes(rings, &parents);
195 diag.ring_count = rings.len();
196 diag.outer_ring_count = shape_offsets.len();
197 diag.hole_ring_count = rings.len() - shape_offsets.len();
198 diag.vertex_count = rings.iter().map(Vec::len).sum();
199 Ok(ScanOutline { rings, shape_offsets, parents, diagnostics: diag })
200}
201
202fn area_cells(area: f64, cell_area: f64) -> usize {
203 (area / cell_area).ceil() as usize
204}
205
206fn cap_moves(moved: &mut [Vec<[f64; 2]>], before: &[Vec<[f64; 2]>], max_move: f64) -> usize {
210 let mut undone = 0;
211 for (ring, old) in moved.iter_mut().zip(before) {
212 if ring.len() != old.len() {
213 continue;
214 }
215 for (p, q) in ring.iter_mut().zip(old) {
216 let d = ((p[0] - q[0]).powi(2) + (p[1] - q[1]).powi(2)).sqrt();
217 if *p != *q && (!d.is_finite() || d > max_move) {
218 *p = *q;
219 undone += 1;
220 }
221 }
222 }
223 undone
224}
225
226fn surviving_squared_edges(rings: &[Vec<[f64; 2]>], squared: &[Vec<[f64; 2]>], flags: &[Vec<bool>]) -> usize {
228 let mut count = 0;
229 for ((ring, target), flags) in rings.iter().zip(squared).zip(flags) {
230 let n = ring.len();
231 for (k, &flag) in flags.iter().enumerate() {
232 if flag && ring[k] == target[k] && ring[(k + 1) % n] == target[(k + 1) % n] {
233 count += 1;
234 }
235 }
236 }
237 count
238}
239
240fn group_shapes(
242 rings: Vec<Vec<[f64; 2]>>,
243 parents: &[Option<usize>],
244) -> (Vec<Vec<[f64; 2]>>, Vec<usize>, Vec<Option<usize>>) {
245 let is_outer: Vec<bool> = rings.iter().map(|r| polygon_area(r) > 0.0).collect();
246 let mut order = Vec::with_capacity(rings.len());
247 let mut offsets = Vec::new();
248 let mut holes_of: Vec<Vec<usize>> = vec![Vec::new(); rings.len()];
249 for (i, p) in parents.iter().enumerate() {
250 if !is_outer[i] {
251 if let Some(p) = p {
252 holes_of[*p].push(i);
253 }
254 }
255 }
256 for i in 0..rings.len() {
257 if is_outer[i] {
258 offsets.push(order.len());
259 order.push(i);
260 order.extend_from_slice(&holes_of[i]);
261 }
262 }
263 let mut new_index = vec![usize::MAX; rings.len()];
264 for (new, old) in order.iter().enumerate() {
265 new_index[*old] = new;
266 }
267 let new_parents = order.iter().map(|old| parents[*old].map(|p| new_index[p])).collect();
268 let mut slots: Vec<Option<Vec<[f64; 2]>>> = rings.into_iter().map(Some).collect();
269 let new_rings = order.iter().map(|old| slots[*old].take().unwrap_or_default()).collect();
270 (new_rings, offsets, new_parents)
271}