Skip to main content

ogeom_heal/
small.rs

1//! Small faces and small solids, removed where a caller names the size
2//! below which they are noise.
3//!
4//! A face the size of a tolerance is no feature: an exchange round trip or
5//! a boolean at the edge of its resolution leaves spots (a face that fits
6//! in a ball of that size) and strips (a face whose two long sides stand
7//! that close along their length). Neither is removed by [`fix_shape`],
8//! which drops nothing the model says is there; this is the step that does,
9//! at a size the caller states.
10//!
11//! A spot collapses to a point: its edges go and its vertices become one.
12//! A strip collapses to an edge: its short ends go, one long side stands
13//! for both, and its neighbours meet on it. Either way the neighbours'
14//! geometry does not move; the vertices and edges that close the gap widen
15//! to own it, as the containment rule asks, and trims are fitted where an
16//! edge now bounds a face it did not.
17//!
18//! [`fix_shape`]: crate::fix_shape()
19
20use std::collections::HashMap;
21
22use ogeom_algo::{Built, History, edge_vertices, volume_properties};
23use ogeom_core::{OgeomResult, Tolerance, Tolerances, ogeom_bail};
24use ogeom_geom::Curve3d as _;
25use ogeom_math::Point;
26use ogeom_mesh::Deflection;
27use ogeom_topo::{EdgeRepr, Model, Shape, ShapeType, TShapeId, explore_unique};
28
29use crate::{Reshape, fix_face_pcurves};
30
31/// What [`fix_small_faces`] removed.
32#[derive(Debug, Clone)]
33pub struct SmallFaces {
34    /// The shape without them, and what became of every input.
35    pub built: Built,
36    /// Faces collapsed to a point.
37    pub spots: usize,
38    /// Faces collapsed to an edge.
39    pub strips: usize,
40}
41
42/// Remove every face of `shape` smaller than `size`: a spot (it fits in a
43/// ball of that diameter) collapses to a point, and a strip (two long
44/// sides within `size` of each other along their length, every other side
45/// shorter than `size`) collapses to one of its long sides.
46///
47/// # Errors
48///
49/// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if
50/// `size` is not a distance or the rebuilt shape comes out empty.
51pub fn fix_small_faces(
52    model: &mut Model,
53    shape: &Shape,
54    size: f64,
55    tol: Tolerances,
56) -> OgeomResult<SmallFaces> {
57    if !size.is_finite() || size <= tol.confusion() {
58        ogeom_bail!(
59            Construction,
60            "a small-face size of {size} is not a distance"
61        );
62    }
63    // Collapses merge vertices and substitute edges node by node, and a
64    // node placed twice (a prism's far cap is its near cap moved) would be
65    // merged at both places at once. Such a shape is baked first, every
66    // occurrence its own node in world space.
67    let placed_twice = explore_unique(model, shape, ShapeType::Edge)?
68        .iter()
69        .any(|e| !e.location().is_identity());
70    let start = if placed_twice && model.kind_of(shape)? == ShapeType::Solid {
71        ogeom_algo::baked_shape(model, shape, tol)?
72    } else {
73        Built::new(shape.clone(), History::identity())
74    };
75    // Passes until none removes anything: a face beside one just removed
76    // waits for the next pass, where it stands as the last one left it.
77    let mut total = SmallFaces {
78        built: start,
79        spots: 0,
80        strips: 0,
81    };
82    for _ in 0..16 {
83        let pass = one_pass(model, &total.built.shape, size, tol)?;
84        if pass.spots + pass.strips == 0 {
85            break;
86        }
87        total = SmallFaces {
88            built: Built::new(
89                pass.built.shape,
90                total.built.history.then(&pass.built.history),
91            ),
92            spots: total.spots + pass.spots,
93            strips: total.strips + pass.strips,
94        };
95    }
96    Ok(total)
97}
98
99fn one_pass(
100    model: &mut Model,
101    shape: &Shape,
102    size: f64,
103    tol: Tolerances,
104) -> OgeomResult<SmallFaces> {
105    let mut reshape = Reshape::new();
106    // Vertex merges, resolved through earlier ones: a survivor's survivor.
107    let mut survivor: HashMap<TShapeId, Shape> = HashMap::new();
108    let root = |survivor: &HashMap<TShapeId, Shape>, v: &Shape| -> Shape {
109        let mut current = v.clone();
110        while let Some(next) = survivor.get(&current.node()) {
111            if next.node() == current.node() {
112                break;
113            }
114            current = next.clone();
115        }
116        current
117    };
118    let mut gone: Vec<TShapeId> = Vec::new();
119    let (mut spots, mut strips) = (0, 0);
120
121    for face in explore_unique(model, shape, ShapeType::Face)? {
122        let edges = explore_unique(model, &face, ShapeType::Edge)?;
123        if edges.iter().any(|e| gone.contains(&e.node())) {
124            // A neighbour already collapsed onto this face's boundary; the
125            // next pass sees the rebuilt face.
126            continue;
127        }
128        let mut samples: Vec<(Shape, Vec<Point>)> = Vec::with_capacity(edges.len());
129        for edge in &edges {
130            samples.push((edge.clone(), edge_points(model, edge, tol)?));
131        }
132        let all: Vec<Point> = samples
133            .iter()
134            .flat_map(|(_, p)| p.iter().copied())
135            .collect();
136        if all.is_empty() {
137            continue;
138        }
139        let spread = all
140            .iter()
141            .flat_map(|p| all.iter().map(move |q| p.distance(*q)))
142            .fold(0.0_f64, f64::max);
143
144        if spread < size {
145            // A spot: every edge goes, every vertex becomes the first.
146            let mut vertices = explore_unique(model, &face, ShapeType::Vertex)?.into_iter();
147            let Some(first) = vertices.next() else {
148                continue;
149            };
150            let keep = root(&survivor, &first);
151            for v in vertices {
152                let drop = root(&survivor, &v);
153                if !drop.is_same(&keep) {
154                    survivor.insert(drop.node(), keep.clone());
155                }
156            }
157            for edge in &edges {
158                reshape.remove(edge);
159                gone.push(edge.node());
160            }
161            reshape.remove(&face);
162            spots += 1;
163            continue;
164        }
165
166        // A strip: two long sides, every other side short, the long ones
167        // within `size` of each other all along.
168        let long: Vec<usize> = (0..samples.len())
169            .filter(|&i| polyline_length(&samples[i].1) >= size)
170            .collect();
171        let [a, b] = long.as_slice() else {
172            continue;
173        };
174        if explore_unique(model, &face, ShapeType::Wire)?.len() != 1 {
175            continue;
176        }
177        let (side_a, side_b) = (&samples[*a], &samples[*b]);
178        let apart = hausdorff(&side_a.1, &side_b.1);
179        if apart >= size {
180            continue;
181        }
182        let (Some((a0, a1)), Some((b0, b1))) = (
183            edge_vertices(model, &side_a.0)?,
184            edge_vertices(model, &side_b.0)?,
185        ) else {
186            continue;
187        };
188        let at = |v: &Shape| -> OgeomResult<Point> {
189            let Some(data) = model.node(v).and_then(|n| n.data().as_vertex()) else {
190                ogeom_bail!(Construction, "a vertex holds no point");
191            };
192            Ok(v.transform(model.datums())?.apply(data.point))
193        };
194        // Which of b's ends faces which of a's: the same way round or the
195        // other.
196        let same_way = at(&a0)?.distance(at(&b0)?) + at(&a1)?.distance(at(&b1)?)
197            <= at(&a0)?.distance(at(&b1)?) + at(&a1)?.distance(at(&b0)?);
198        let (to0, to1) = if same_way { (&b0, &b1) } else { (&b1, &b0) };
199        for (from, to) in [(&a0, to0), (&a1, to1)] {
200            let (keep, drop) = (root(&survivor, to), root(&survivor, from));
201            if !drop.is_same(&keep) {
202                survivor.insert(drop.node(), keep.clone());
203            }
204        }
205        for (i, (edge, _)) in samples.iter().enumerate() {
206            if i != *b {
207                gone.push(edge.node());
208            }
209            if i != *a && i != *b {
210                reshape.remove(edge);
211            }
212        }
213        // Side a stands for side b wherever a neighbour held it, and owns
214        // the width it spans.
215        let stand_in = if same_way {
216            side_b.0.clone()
217        } else {
218            side_b.0.reversed()
219        };
220        model.widen(&side_b.0, Tolerance::new(apart + tol.confusion())?)?;
221        reshape.replace(&side_a.0, stand_in);
222        reshape.remove(&face);
223        strips += 1;
224    }
225
226    // Each absorbed vertex's survivor widens to reach where it stood.
227    for vertex in explore_unique(model, shape, ShapeType::Vertex)? {
228        let to = root(&survivor, &vertex);
229        if to.is_same(&vertex) {
230            continue;
231        }
232        let from = {
233            let Some(data) = model.node(&vertex).and_then(|n| n.data().as_vertex()) else {
234                continue;
235            };
236            (
237                vertex.transform(model.datums())?.apply(data.point),
238                data.tolerance.get(),
239            )
240        };
241        let Some(data) = model.node(&to).and_then(|n| n.data().as_vertex()) else {
242            continue;
243        };
244        let here = to.transform(model.datums())?.apply(data.point);
245        model.widen(
246            &to,
247            Tolerance::new((from.0.distance(here) + from.1).max(tol.confusion()))?,
248        )?;
249        reshape.replace(&vertex, to);
250    }
251
252    if reshape.is_empty() {
253        return Ok(SmallFaces {
254            built: Built::new(shape.clone(), History::identity()),
255            spots,
256            strips,
257        });
258    }
259    let built = reshape.apply(model, shape)?;
260    // Edges now bounding faces they did not are given their trims there.
261    for face in explore_unique(model, &built.shape, ShapeType::Face)? {
262        fix_face_pcurves(model, &face, tol.confusion() * 1e7, tol)?;
263    }
264    ogeom_algo::restore_containment(model, &built.shape)?;
265    Ok(SmallFaces {
266        built,
267        spots,
268        strips,
269    })
270}
271
272/// Remove every solid of `shape` enclosing less than `volume`: the debris
273/// an exchange or a boolean leaves beside the part.
274///
275/// # Errors
276///
277/// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if
278/// `volume` is not positive, a solid's volume cannot be measured, or every
279/// solid would go.
280pub fn remove_small_solids(
281    model: &mut Model,
282    shape: &Shape,
283    volume: f64,
284    tol: Tolerances,
285) -> OgeomResult<(Built, usize)> {
286    if !volume.is_finite() || volume <= 0.0 {
287        ogeom_bail!(
288            Construction,
289            "a small-solid volume of {volume} is not positive"
290        );
291    }
292    let solids = explore_unique(model, shape, ShapeType::Solid)?;
293    let mut reshape = Reshape::new();
294    let mut removed = 0;
295    for solid in &solids {
296        let measured = volume_properties(model, solid, Deflection::default(), tol)?.mass;
297        if measured < volume {
298            reshape.remove(solid);
299            removed += 1;
300        }
301    }
302    if removed == 0 {
303        return Ok((Built::new(shape.clone(), History::identity()), 0));
304    }
305    if removed == solids.len() {
306        ogeom_bail!(
307            Construction,
308            "every solid is smaller than {volume}; removing them leaves nothing"
309        );
310    }
311    Ok((reshape.apply(model, shape)?, removed))
312}
313
314/// Points along an edge, placed.
315fn edge_points(model: &Model, edge: &Shape, tol: Tolerances) -> OgeomResult<Vec<Point>> {
316    let Some(data) = model.node(edge).and_then(|n| n.data().as_edge()) else {
317        return Ok(Vec::new());
318    };
319    if data.degenerate {
320        return Ok(Vec::new());
321    }
322    let Some(EdgeRepr::Curve3d { curve, range, .. }) = data.curve3d() else {
323        return Ok(Vec::new());
324    };
325    let Some(geometry) = model.geometry().curve(*curve) else {
326        return Ok(Vec::new());
327    };
328    let placement = edge.transform(model.datums())?;
329    let mut out = Vec::with_capacity(17);
330    for i in 0..=16 {
331        let t = range.0 + (range.1 - range.0) * f64::from(i) / 16.0;
332        out.push(placement.apply(geometry.point_at(t, tol)?));
333    }
334    Ok(out)
335}
336
337fn polyline_length(points: &[Point]) -> f64 {
338    points.windows(2).map(|w| w[0].distance(w[1])).sum()
339}
340
341/// The largest distance from either polyline to the other.
342fn hausdorff(a: &[Point], b: &[Point]) -> f64 {
343    let nearest = |p: Point, line: &[Point]| -> f64 {
344        line.windows(2)
345            .map(|w| {
346                let d = w[1] - w[0];
347                let dd = d.dot(d);
348                let s = if dd > 0.0 {
349                    ((p - w[0]).dot(d) / dd).clamp(0.0, 1.0)
350                } else {
351                    0.0
352                };
353                p.distance(w[0] + d * s)
354            })
355            .fold(f64::INFINITY, f64::min)
356    };
357    let one = a.iter().map(|p| nearest(*p, b)).fold(0.0_f64, f64::max);
358    let other = b.iter().map(|p| nearest(*p, a)).fold(0.0_f64, f64::max);
359    one.max(other)
360}