1use 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#[derive(Debug, Clone)]
42pub struct FixReport {
43 pub before: Diagnosis,
45 pub after: Diagnosis,
48 pub wires_reordered: usize,
50 pub edges_collapsed: usize,
52 pub edges_trimmed: usize,
54 pub sewn: Option<(usize, usize)>,
56 pub tolerances_reduced: usize,
58 pub tolerances_widened: usize,
61}
62
63#[derive(Debug, Clone)]
65pub struct Fixed {
66 pub shape: Shape,
68 pub history: History,
70 pub report: FixReport,
72}
73
74const TRIM_CAP: f64 = 1e7;
77
78pub 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 let mut reshape = Reshape::new();
94 let wires_reordered = reorder_wires(model, ¤t, &mut reshape, tol)?;
95 let edges_collapsed = collapse_small_edges(model, ¤t, &mut reshape, tol)?;
96 if !reshape.is_empty() {
97 let built = reshape.apply(model, ¤t)?;
98 history = history.then(&built.history);
99 current = built.shape;
100 }
101
102 let mut edges_trimmed = 0;
104 for face in explore_unique(model, ¤t, ShapeType::Face)? {
105 let trims = fix_face_pcurves(model, &face, tol.confusion() * TRIM_CAP, tol)?;
106 edges_trimmed += trims.fitted;
107 }
108
109 let mut sewn = None;
111 let kind = model.kind_of(¤t)?;
112 if matches!(kind, ShapeType::Compound | ShapeType::Shell) {
113 let faces = explore_unique(model, ¤t, ShapeType::Face)?;
114 if !faces.is_empty() {
115 let result = sew(model, &faces, tol)?;
116 sewn = Some((result.joined, result.free_edges.len()));
117 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(¤t, rebuilt.clone());
127 history = history.then(&step);
128 current = rebuilt;
129 }
130 }
131 }
132
133 let tolerances_reduced = reduce_tolerances(model, ¤t, tol)?;
134 let tolerances_widened = ogeom_algo::restore_containment(model, ¤t)?;
139 let after = check(model, ¤t, 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
156fn 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 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
182fn 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
198fn 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(¤t.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 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 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 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}