Skip to main content

ogeom_heal/
fix_shape.rs

1//! One entry point over a shape nobody promised was well-formed.
2//!
3//! The exchange readers heal what their corpus exhibits, inline and in a
4//! fixed order; a shape built by a caller, or read from a file the corpus
5//! does not resemble, has no such pass. This is it: diagnose, mend what a
6//! mend is known for, diagnose again, and say what changed and what did
7//! not. Nothing is moved that the model's own tolerances do not already
8//! call the same place, and nothing is dropped that the model says is
9//! there: a face with area is a face, whatever its shape.
10//!
11//! What is mended, in order:
12//!
13//! - a wire whose edges are not end to end is put in the order that
14//!   walks them, where one exists;
15//! - an edge shorter than its own vertices' tolerances (the two ends
16//!   the same point by the model's own admission) is collapsed, its two
17//!   vertices made one;
18//! - an edge with no pcurve on a face it bounds is given the trim
19//!   projection can honestly fit, as the readers do;
20//! - loose faces (a compound of them, or an open shell) are sewn where
21//!   they share edges;
22//! - tolerances are tightened to what the geometry needs.
23//!
24//! What is not, and where it lives: small faces and small solids stay
25//! (removing a face opens the shell it is in; that is defeaturing, and
26//! the boolean crate has it for the features it knows), and a face across
27//! a grid of patches is not rebuilt as one.
28
29use std::collections::HashMap;
30
31use ogeom_algo::{
32    Diagnosis, History, check, edge_vertices, linear_properties, make_wire, order_edges, sew,
33};
34use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
35use ogeom_mesh::Deflection;
36use ogeom_topo::{Model, Shape, ShapeType, TShapeId, explore_unique};
37
38use crate::{Reshape, fix_face_pcurves, reduce_tolerances};
39
40/// What [`fix_shape`] did, and what it found before and after.
41#[derive(Debug, Clone)]
42pub struct FixReport {
43    /// The diagnosis the shape came in with.
44    pub before: Diagnosis,
45    /// The diagnosis it leaves with. Not necessarily valid: what has no
46    /// mend here is still reported.
47    pub after: Diagnosis,
48    /// Wires whose edges were put end to end.
49    pub wires_reordered: usize,
50    /// Edges collapsed to a vertex.
51    pub edges_collapsed: usize,
52    /// Edges given a pcurve on a face they bound.
53    pub edges_trimmed: usize,
54    /// Edge pairs sewn, and edges still free after, when sewing ran.
55    pub sewn: Option<(usize, usize)>,
56    /// Tolerances tightened.
57    pub tolerances_reduced: usize,
58    /// Tolerances widened so that every vertex is at least as loose as the
59    /// edges it bounds and every edge as the faces it bounds.
60    pub tolerances_widened: usize,
61}
62
63/// A fixed shape: the result, its history, and the report.
64#[derive(Debug, Clone)]
65pub struct Fixed {
66    /// The shape as mended. The input where nothing was.
67    pub shape: Shape,
68    /// What became of every input node.
69    pub history: History,
70    /// What was done and what remains.
71    pub report: FixReport,
72}
73
74/// The cap on how far a fitted trim may sit from its surface: the
75/// readers' own, a millimetre at unit scale.
76const TRIM_CAP: f64 = 1e7;
77
78/// Mend `shape` where a mend is known for what is wrong with it.
79///
80/// See the module documentation for what is and is not done.
81///
82/// # Errors
83///
84/// [`OgeomError::Dangling`](ogeom_core::OgeomError::Dangling) if a handle
85/// fails to resolve; [`OgeomError::Construction`](ogeom_core::OgeomError::Construction)
86/// if a rebuilt container comes out empty.
87pub fn fix_shape(model: &mut Model, shape: &Shape, tol: Tolerances) -> OgeomResult<Fixed> {
88    let before = check(model, shape, tol)?;
89    let mut history = History::identity();
90    let mut current = shape.clone();
91
92    // Wires and small edges, in one rebuild.
93    let mut reshape = Reshape::new();
94    let wires_reordered = reorder_wires(model, &current, &mut reshape, tol)?;
95    let edges_collapsed = collapse_small_edges(model, &current, &mut reshape, tol)?;
96    if !reshape.is_empty() {
97        let built = reshape.apply(model, &current)?;
98        history = history.then(&built.history);
99        current = built.shape;
100    }
101
102    // Trims for edges that have none on a face they bound.
103    let mut edges_trimmed = 0;
104    for face in explore_unique(model, &current, ShapeType::Face)? {
105        let trims = fix_face_pcurves(model, &face, tol.confusion() * TRIM_CAP, tol)?;
106        edges_trimmed += trims.fitted;
107    }
108
109    // Loose faces sewn: a compound of faces, or an open shell.
110    let mut sewn = None;
111    let kind = model.kind_of(&current)?;
112    if matches!(kind, ShapeType::Compound | ShapeType::Shell) {
113        let faces = explore_unique(model, &current, ShapeType::Face)?;
114        if !faces.is_empty() {
115            let result = sew(model, &faces, tol)?;
116            sewn = Some((result.joined, result.free_edges.len()));
117            // Rebuilt when sewing joined anything, or when the faces
118            // already closed among themselves and only the container said
119            // otherwise: a compound of faces that is a shell becomes one.
120            if result.joined > 0 || (kind == ShapeType::Compound && result.free_edges.is_empty()) {
121                let rebuilt = match (kind, result.shells.len()) {
122                    (ShapeType::Shell, 1) => result.shells[0].clone(),
123                    _ => ogeom_algo::make_compound(model, &result.shells)?.shape,
124                };
125                let mut step = result.history;
126                step.modify(&current, rebuilt.clone());
127                history = history.then(&step);
128                current = rebuilt;
129            }
130        }
131    }
132
133    let tolerances_reduced = reduce_tolerances(model, &current, tol)?;
134    // Last, because every step above may leave a vertex tighter than an
135    // edge it bounds (a reduction tightens edges and faces, never below
136    // what they bound, but a shape can arrive broken), and containment is
137    // established only by widening what is bounded.
138    let tolerances_widened = ogeom_algo::restore_containment(model, &current)?;
139    let after = check(model, &current, tol)?;
140    Ok(Fixed {
141        shape: current,
142        history,
143        report: FixReport {
144            before,
145            after,
146            wires_reordered,
147            edges_collapsed,
148            edges_trimmed,
149            sewn,
150            tolerances_reduced,
151            tolerances_widened,
152        },
153    })
154}
155
156/// Stage a rebuilt wire for every wire whose edges do not walk end to end
157/// but can be put in an order that does.
158fn reorder_wires(
159    model: &mut Model,
160    shape: &Shape,
161    reshape: &mut Reshape,
162    tol: Tolerances,
163) -> OgeomResult<usize> {
164    let mut count = 0;
165    for wire in explore_unique(model, shape, ShapeType::Wire)? {
166        let edges = model.ordered_children_of(&wire)?;
167        if edges.len() < 2 || walks_end_to_end(model, &edges, tol)? {
168            continue;
169        }
170        // A bag that is no path at all (a branch, a gap) is left as it
171        // is and reported by the diagnosis; reordering cannot mend it.
172        let Ok(ordered) = order_edges(model, &edges, tol) else {
173            continue;
174        };
175        let rebuilt = make_wire(model, &ordered, tol)?.shape;
176        reshape.replace(&wire, rebuilt);
177        count += 1;
178    }
179    Ok(count)
180}
181
182/// Whether consecutive edges meet, the last back at the first.
183fn walks_end_to_end(model: &Model, edges: &[Shape], tol: Tolerances) -> OgeomResult<bool> {
184    for i in 0..edges.len() {
185        let (Some((_, end)), Some((next, _))) = (
186            edge_vertices(model, &edges[i])?,
187            edge_vertices(model, &edges[(i + 1) % edges.len()])?,
188        ) else {
189            return Ok(false);
190        };
191        if !end.is_same(&next) && !model.same_position(&end, &next, tol)? {
192            return Ok(false);
193        }
194    }
195    Ok(true)
196}
197
198/// Stage the collapse of every edge shorter than its vertices' tolerances:
199/// the edge goes, and its far vertex becomes its near one.
200///
201/// A run of such edges collapses to one vertex, not a chain of
202/// substitutions: the survivor of each merge is found through the merges
203/// before it.
204fn collapse_small_edges(
205    model: &mut Model,
206    shape: &Shape,
207    reshape: &mut Reshape,
208    tol: Tolerances,
209) -> OgeomResult<usize> {
210    let mut survivor: HashMap<TShapeId, Shape> = HashMap::new();
211    fn root(survivor: &HashMap<TShapeId, Shape>, v: &Shape) -> Shape {
212        let mut current = v.clone();
213        while let Some(next) = survivor.get(&current.node()) {
214            if next.node() == current.node() {
215                break;
216            }
217            current = next.clone();
218        }
219        current
220    }
221    let mut count = 0;
222    for edge in explore_unique(model, shape, ShapeType::Edge)? {
223        let Some((a, b)) = edge_vertices(model, &edge)? else {
224            continue;
225        };
226        if a.is_same(&b) {
227            // A closed edge is a loop, however short.
228            continue;
229        }
230        let reach = [&a, &b]
231            .iter()
232            .filter_map(|v| model.tolerance_of(v).ok().flatten())
233            .map(|t| t.get())
234            .fold(tol.confusion(), f64::max);
235        let length = linear_properties(model, &edge, Deflection::default(), tol)?.mass;
236        if length > reach {
237            continue;
238        }
239        let (keep, drop) = (root(&survivor, &a), root(&survivor, &b));
240        if keep.is_same(&drop) {
241            // Already one vertex through earlier collapses; the edge is a
242            // loop on it and goes.
243            reshape.remove(&edge);
244            count += 1;
245            continue;
246        }
247        survivor.insert(drop.node(), keep.clone());
248        reshape.remove(&edge);
249        count += 1;
250    }
251    // The survivor stands where it stood, and the curves that ended at each
252    // vertex it absorbs still end there: it widens to reach every one, as
253    // far as the absorbed vertex stood plus that vertex's own tolerance.
254    // Merged without it, the neighbours of a collapsed edge stop the
255    // collapsed length short of their vertex and the wire gapes.
256    let placed = |model: &Model, v: &Shape| -> OgeomResult<Option<(ogeom_math::Point, f64)>> {
257        let Some(data) = model.node(v).and_then(|n| n.data().as_vertex()) else {
258            return Ok(None);
259        };
260        let (point, own) = (data.point, data.tolerance.get());
261        Ok(Some((v.transform(model.datums())?.apply(point), own)))
262    };
263    for vertex in explore_unique(model, shape, ShapeType::Vertex)? {
264        let to = root(&survivor, &vertex);
265        if !to.is_same(&vertex) {
266            if let (Some((from, reach)), Some((at, _))) =
267                (placed(model, &vertex)?, placed(model, &to)?)
268            {
269                let need = from.distance(at) + reach;
270                model.widen(&to, ogeom_core::Tolerance::new(need.max(tol.confusion()))?)?;
271            }
272            reshape.replace(&vertex, to);
273        }
274    }
275    if count > 0 && reshape.is_empty() {
276        ogeom_bail!(Construction, "a collapse staged nothing");
277    }
278    Ok(count)
279}