1use 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#[derive(Debug, Clone)]
33pub struct SmallFaces {
34 pub built: Built,
36 pub spots: usize,
38 pub strips: usize,
40}
41
42pub 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 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 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 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(¤t.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 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 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 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 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 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 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 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
272pub 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
314fn 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
341fn 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}