ogeom_algo/mesh_solid.rs
1//! A B-rep solid from a triangle mesh.
2//!
3//! A mesh already says which triangles meet along which edges: once its
4//! repeated vertices are welded, two triangles that share two vertices
5//! share an edge. So the topology is built from that connectivity
6//! directly (one vertex per mesh vertex, one edge per mesh edge), and no
7//! geometric sewing search is needed, which is what keeps a mesh of
8//! hundreds of thousands of triangles cheap to convert.
9//!
10//! Adjacent triangles that lie in one plane merge into one planar face,
11//! bounded by the region's outer loop and its holes, and a run of boundary
12//! segments along one straight line between the same two faces becomes
13//! one edge: a cube's twelve triangles become six faces and twelve edges,
14//! and the faces take fillets and chamfers as a modelled box's do.
15//!
16//! Curved regions are recognized: triangles across which the surface
17//! turns smoothly are grown into regions for as long as their vertices lie
18//! on one cylinder, cone, sphere or torus, verified at the stated
19//! tolerance, and the region is rebuilt on that surface. Its boundary with
20//! each neighbour is placed on the surface exactly (a parallel circle or
21//! a ruling of it), so a meshed bore comes back as a cylinder between two
22//! circles, and a band all the way round its axis gets a seam as a
23//! modelled one has. A sphere or torus with no boundary is one face, a
24//! sphere bounded by one circle a cap, and a band round a torus's tube
25//! runs between two of its meridians. A region whose boundary is no such
26//! curve, or that nothing canonical fits, stays faceted.
27//!
28//! Windings are made consistent across each connected piece and turned
29//! outward. A mesh that does not close (a hole, an edge three triangles
30//! share, a piece that cannot be oriented) does not fail: it comes back
31//! as open shells, with the report saying why.
32
33use ogeom_core::{FastMap, FastSet, OgeomResult, Tolerances, ogeom_bail};
34use ogeom_math::{Plane, Point};
35use ogeom_topo::{Model, Shape, ShapeType, TShapeId, Triangulation, explore_unique};
36
37use crate::recognize::Canonical;
38
39mod astray;
40mod bodies;
41mod builder;
42mod caches;
43mod checks;
44mod frames;
45mod holed;
46mod planner;
47mod regions;
48mod rounds;
49mod seams;
50mod segment;
51mod snap;
52mod steps;
53mod weld;
54
55pub use steps::{
56 FallbackReason, FitConstraints, MeshRegion, MeshRegions, RegionFallback, RegionId,
57 RegionRefusal, SurfaceKind,
58};
59
60use astray::astray_faces;
61pub(crate) use astray::distance_to_triangle;
62use bodies::{assemble, body_culprits};
63use builder::Builder;
64use caches::{AreaCache, ImageCache, SnapCache};
65use checks::{
66 Regrouped, absorb_facets, any_turned_in, backwards_facets, broken_faces, collapsed_beside,
67 crossed_boundaries, crossed_seams, crossing_faces, failing_faces, folded_seams, give_back,
68 overlapping_faces, planes_off_their_vertices, spanned_faces, unbuilt_culprits, unmatched_faces,
69};
70use planner::{NOISE_FLOOR, Planner, Replan};
71use regions::{flat_noise, split_disconnected};
72use segment::{coplanar_groups, one_each, plane_of, segment, widen_where_pressed};
73use weld::{
74 Adjacency, Piece, by_position, diagonal, has_area, inside, lowest_first, orient, unfold,
75 weld_points,
76};
77
78/// How [`solid_from_mesh`] builds.
79#[derive(Debug, Clone, Copy, PartialEq)]
80pub struct MeshSolidOptions {
81 /// Merge adjacent coplanar triangles into one planar face, and collinear
82 /// boundary segments into one edge. Without it every triangle is a face
83 /// and every mesh edge an edge.
84 pub merge_coplanar: bool,
85 /// The angle, in radians, below which two triangles' normals count as
86 /// parallel for merging.
87 pub coplanar_angle: f64,
88 /// How far a triangle's corner may sit off a face's plane, or a
89 /// merged edge's intermediate vertex off its line, and still be merged.
90 /// `None` takes the largest of twice the mesh's `quantum`, a millionth
91 /// of its bounding box's diagonal, one and a half times how far its
92 /// vertices stand off the flat faces they lie on (measured across
93 /// nearly coplanar edges), and the weld distance. With `recognize`, a
94 /// distance given below two and a half times that scatter (or twice
95 /// the `quantum`) is raised to it, and the report says so: no surface
96 /// fitted to the vertices holds them closer. The
97 /// vertices and edges of a merged face widen their tolerances to cover
98 /// what they stand off it.
99 pub coplanar_distance: Option<f64>,
100 /// How far the mesh's own encoding may have moved a vertex: the
101 /// rounding of its coordinates (see
102 /// [`single_precision_quantum`] for a mesh held in `f32`, and the STL
103 /// reader's for a file). A vertex that far off its surface is on it;
104 /// `None` knows nothing of the encoding.
105 pub quantum: Option<f64>,
106 /// Vertices closer than this are welded into one before building; an
107 /// STL repeats every vertex once per triangle that uses it. `None` is
108 /// the confusion tolerance.
109 pub weld: Option<f64>,
110 /// Recognize curved regions as cylinders, cones, spheres and tori, at
111 /// the coplanar distance. Needs `merge_coplanar`.
112 pub recognize: bool,
113 /// The angle, in radians, from which a turn between two triangles is a
114 /// crease (an edge of the model) rather than the surface curving on:
115 /// thirty degrees by default. A mesh drawn coarser than this round a
116 /// curve reads as facets.
117 pub crease: f64,
118 /// Keep every mesh vertex a vertex of the solid: collinear boundary
119 /// segments stay edges of their own. A solid built without recognition
120 /// to be refined later ([`refine_solid`]) keeps the samples that
121 /// recognition reads, where merged edges along a curved stretch's
122 /// facets would have dropped all but their ends.
123 pub keep_vertices: bool,
124 /// After the canonical surfaces, rebuild a smooth region none of them
125 /// fits as an extrusion of a fitted profile (its normals all square to
126 /// one direction) or a surface of revolution (its normals all meeting
127 /// one axis), verified at every vertex. Needs `recognize`.
128 pub sweeps: bool,
129 /// After the sweeps, rebuild a smooth region nothing else fits as one
130 /// fitted B-spline patch, where the region is one disk bounded by one
131 /// loop and the patch verifies: every vertex within the coplanar
132 /// distance of it, every triangle's interior within that and its own
133 /// sag, both ways, and the patch's normal regular and agreeing with
134 /// every triangle's. Its chart is a nearly fitting canonical surface's
135 /// where one fits within ten times the distance, and otherwise the
136 /// mean-value map of the region onto a square. Curved regions the
137 /// smooth area encloses (meeting nothing outside it except across
138 /// creases), as a fine mesh of a free-form surface is cut into, are
139 /// tried with the smooth regions they join as one region first, and
140 /// kept apart where that patch does not verify. A region that is not a
141 /// disk, is too narrow to hold a patch across it, or whose patch does
142 /// not verify stays faceted and is counted
143 /// ([`MeshSolidReport::patches_not_disk`],
144 /// [`MeshSolidReport::patches_narrow`],
145 /// [`MeshSolidReport::patches_unverified`]). A patch whose seams cannot
146 /// be built falls back to facets as any recognized face does. Needs
147 /// `recognize`.
148 pub patches: bool,
149}
150
151impl Default for MeshSolidOptions {
152 fn default() -> Self {
153 Self {
154 merge_coplanar: true,
155 coplanar_angle: 1e-3,
156 coplanar_distance: None,
157 quantum: None,
158 weld: None,
159 recognize: true,
160 crease: core::f64::consts::FRAC_PI_6,
161 keep_vertices: false,
162 sweeps: true,
163 patches: true,
164 }
165 }
166}
167
168/// How far single-precision storage may have moved a mesh's vertices: half
169/// the spacing of `f32` values at its largest coordinate on each axis, over
170/// the three axes at once. The quantum of a mesh that went through `f32`
171/// (a render mesh, a binary STL), for [`MeshSolidOptions::quantum`].
172#[must_use]
173pub fn single_precision_quantum(mesh: &Triangulation) -> f64 {
174 let largest = mesh
175 .positions
176 .iter()
177 .flat_map(|p| [p.x.abs(), p.y.abs(), p.z.abs()])
178 .fold(0.0_f64, f64::max);
179 #[allow(
180 clippy::cast_possible_truncation,
181 reason = "the magnitude is an f32 coordinate's"
182 )]
183 let x = (largest as f32).max(f32::MIN_POSITIVE);
184 let step = f64::from(f32::from_bits(x.to_bits() + 1) - x);
185 step * 3.0_f64.sqrt() / 2.0
186}
187
188/// What [`solid_from_mesh`] did, and where the mesh does not close.
189#[derive(Debug, Clone, Default, PartialEq, Eq)]
190pub struct MeshSolidReport {
191 /// Triangles built from, after the dropped ones.
192 pub triangles: usize,
193 /// Faces built: the triangles, or their coplanar groups and recognized
194 /// regions.
195 pub faces: usize,
196 /// Faces built on a recognized curved surface.
197 pub curved_faces: usize,
198 /// Regions whose face could not be built on their surface, and which
199 /// were faceted instead: curved regions, and the few planar ones whose
200 /// face failed or whose plane does not hold their vertices, their
201 /// triangles gathered again.
202 pub curved_faceted: usize,
203 /// Those regions by name, each with why it was faceted, in the order
204 /// they fell back: as many as [`MeshSolidReport::curved_faceted`]
205 /// counts. The names are the regions' in
206 /// [`MeshRegions`], whose [`MeshRegions::find`] names them alike for
207 /// the same mesh and options.
208 pub fallbacks: Vec<RegionFallback>,
209 /// Mesh vertices that welded onto another.
210 pub vertices_welded: usize,
211 /// Triangles dropped for having no area.
212 pub degenerate_dropped: usize,
213 /// Triangles dropped for repeating another's three vertices.
214 pub duplicates_dropped: usize,
215 /// Triangles whose winding was reversed to agree with their neighbours
216 /// and face outward.
217 pub windings_flipped: usize,
218 /// Mesh edges only one triangle uses: the rims of holes.
219 pub edges_used_once: usize,
220 /// Mesh edges three or more triangles use: non-manifold.
221 pub edges_used_more: usize,
222 /// Shared edges whose windings cannot be made to agree, in a piece that
223 /// is not orientable.
224 pub orientation_conflicts: usize,
225 /// Connected pieces, each one shell.
226 pub shells: usize,
227 /// Whether recognition was withdrawn from a body because the solid it
228 /// built did not hold that body's mesh volume, and the body was built
229 /// faceted instead.
230 pub recognition_withdrawn: bool,
231 /// Whether the coplanar distance asked for was below what the mesh's
232 /// own scatter allows recognition to hold to, and was raised to that
233 /// ([`MeshSolid::coplanar_distance`] is the one used).
234 pub coplanar_distance_raised: bool,
235 /// Faces built on a fitted B-spline patch, among the curved faces.
236 pub patch_faces: usize,
237 /// Of those, the ones whose chart is the mean-value map onto a square
238 /// rather than a nearly fitting canonical surface's.
239 pub patch_charts_mapped: usize,
240 /// Smooth regions nothing else fitted that were left faceted because
241 /// they are not one disk bounded by one loop (a region with a hole, or
242 /// one that touches itself).
243 pub patches_not_disk: usize,
244 /// Smooth regions nothing else fitted that were left faceted because
245 /// too few of their vertices lie inside their boundary for a patch to be
246 /// held across them (a row or two of facets along a seam).
247 pub patches_narrow: usize,
248 /// Smooth regions nothing else fitted whose fitted patch failed its
249 /// verification, left faceted.
250 pub patches_unverified: usize,
251 /// Free edges of curved faces (a run of the mesh's boundary with one
252 /// face and nothing across it) that no parallel or ruling of the face
253 /// holds, built as a curve fitted onto the face through the feet of
254 /// the run's points, its tolerance measured against every vertex of the
255 /// run. The others are parallels and rulings, placed exactly.
256 pub free_edges_fitted: usize,
257}
258
259/// The result of [`solid_from_mesh`].
260#[derive(Debug, Clone)]
261pub struct MeshSolid {
262 /// A solid when the mesh closes (a compound of solids when it is
263 /// several disjoint closed pieces), and otherwise a shell, or a
264 /// compound of shells.
265 pub shape: Shape,
266 /// Whether every piece closed and became a solid.
267 pub closed: bool,
268 /// The coplanar distance the faces were built to: the one asked for,
269 /// or the default, as widened where the recognized surfaces pressed
270 /// against it or raised to the mesh's scatter (see
271 /// [`MeshSolidReport::coplanar_distance_raised`]). Where the build put
272 /// back every region the default found, it is the default.
273 pub coplanar_distance: f64,
274 /// What was built, and where the mesh does not close.
275 pub report: MeshSolidReport,
276}
277
278/// Seal the cracks flat slivers leave when they are dropped.
279///
280/// A triangle with three distinct corners and no area has one corner on the
281/// segment between the other two. In a closed mesh the triangle across that
282/// long side fills the other side of it, and the two short sides are shared
283/// with the neighbours beyond: dropped alone, the sliver leaves all three
284/// used once. The triangle across the long side is split at the middle
285/// corner instead, so each short side is shared again. Slivers against
286/// slivers resolve as their neighbours are split, round by round.
287fn split_across_slivers(points: &[Point], triangles: &mut Vec<[u32; 3]>, slivers: &[[u32; 3]]) {
288 let mut pending: Vec<[u32; 3]> = slivers
289 .iter()
290 .map(|&[a, b, c]| {
291 // The middle corner is the one opposite the longest side.
292 let long = |x: u32, y: u32| points[x as usize].distance(points[y as usize]);
293 let sides = [
294 (long(b, c), a, [b, c]),
295 (long(c, a), b, [c, a]),
296 (long(a, b), c, [a, b]),
297 ];
298 let (_, middle, [p, q]) =
299 sides.into_iter().fold(
300 sides[0],
301 |best, side| if side.0 > best.0 { side } else { best },
302 );
303 [p, middle, q]
304 })
305 .collect();
306 // The triangles on each undirected edge, by index, lowest first: the
307 // triangle across a sliver's long side is found from its edge, not by
308 // scanning the mesh, and the lowest index is the one a scan would meet
309 // first.
310 let key = |x: u32, y: u32| (x.min(y), x.max(y));
311 let mut on_edge: ogeom_core::FastMap<(u32, u32), Vec<usize>> = ogeom_core::FastMap::default();
312 for (i, t) in triangles.iter().enumerate() {
313 for k in 0..3 {
314 on_edge
315 .entry(key(t[k], t[(k + 1) % 3]))
316 .or_default()
317 .push(i);
318 }
319 }
320 let unlink =
321 |on_edge: &mut ogeom_core::FastMap<(u32, u32), Vec<usize>>, t: [u32; 3], i: usize| {
322 for k in 0..3 {
323 if let Some(list) = on_edge.get_mut(&key(t[k], t[(k + 1) % 3]))
324 && let Ok(at) = list.binary_search(&i)
325 {
326 list.remove(at);
327 }
328 }
329 };
330 let link =
331 |on_edge: &mut ogeom_core::FastMap<(u32, u32), Vec<usize>>, t: [u32; 3], i: usize| {
332 for k in 0..3 {
333 let list = on_edge.entry(key(t[k], t[(k + 1) % 3])).or_default();
334 if let Err(at) = list.binary_search(&i) {
335 list.insert(at, i);
336 }
337 }
338 };
339 loop {
340 let mut progress = false;
341 let mut left = Vec::new();
342 for [p, middle, q] in pending {
343 let across = on_edge.get(&key(p, q)).and_then(|list| {
344 list.iter()
345 .copied()
346 .find(|&i| !triangles[i].contains(&middle))
347 });
348 let Some(index) = across else {
349 left.push([p, middle, q]);
350 continue;
351 };
352 let t = triangles[index];
353 let Some(k) = (0..3).find(|&k| {
354 let (x, y) = (t[k], t[(k + 1) % 3]);
355 (x == p && y == q) || (x == q && y == p)
356 }) else {
357 continue;
358 };
359 let (x, y, z) = (t[k], t[(k + 1) % 3], t[(k + 2) % 3]);
360 unlink(&mut on_edge, t, index);
361 triangles[index] = [x, middle, z];
362 link(&mut on_edge, triangles[index], index);
363 triangles.push([middle, y, z]);
364 link(&mut on_edge, [middle, y, z], triangles.len() - 1);
365 progress = true;
366 }
367 pending = left;
368 if pending.is_empty() || !progress {
369 break;
370 }
371 }
372}
373
374/// Rebuild a solid's faceted stretches on the surfaces they approximate.
375///
376/// The second of two steps: [`solid_from_mesh`] without recognition keeps
377/// a mesh as it is, every flat stretch one face and every curved one its
378/// facets; this finds the cylinders, cones, spheres and tori among the
379/// facets of such a solid (or of any solid) and builds them anew as
380/// `solid_from_mesh` with recognition would. The solid is triangulated,
381/// its flat faces exactly and any curved ones to a chord of a
382/// ten-thousandth of its size, and the triangles read as a mesh. The first
383/// step reads best with [`MeshSolidOptions::keep_vertices`], so the
384/// solid's edges keep the mesh's samples along curved stretches; pass its
385/// `quantum` in `options` where the mesh came from a file.
386///
387/// # Errors
388///
389/// As [`solid_from_mesh`], and where the solid cannot be triangulated.
390pub fn refine_solid(
391 model: &mut Model,
392 shape: &Shape,
393 options: &MeshSolidOptions,
394 tol: Tolerances,
395) -> OgeomResult<MeshSolid> {
396 let bounds = crate::tight_bounds(model, shape, tol)?;
397 let size = match (bounds.low(), bounds.high()) {
398 (Some(lo), Some(hi)) => lo.distance(hi),
399 _ => ogeom_bail!(Construction, "the shape has no extent to refine"),
400 };
401 let chord = (size * 1e-4).max(tol.confusion() * 10.0);
402 let mesh = ogeom_mesh::triangulate(
403 model,
404 shape,
405 ogeom_mesh::Deflection::with_chord(chord)?,
406 tol,
407 )?;
408 let options = MeshSolidOptions {
409 recognize: true,
410 keep_vertices: false,
411 ..*options
412 };
413 solid_from_mesh(model, &mesh, &options, tol)
414}
415
416/// How near its distance a recognized fit must come for the distance, not
417/// the surface, to be what bounds it.
418const PRESSED: f64 = 0.7;
419
420/// Build a B-rep from a triangle mesh.
421///
422/// See the module documentation for the construction. A closed piece
423/// inside another becomes a void of the solid around it. It is
424/// [`MeshRegions::find`] followed by [`MeshRegions::build`], with no region
425/// changed between them.
426///
427/// # Errors
428///
429/// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if the
430/// mesh has no triangle with area, an index names a vertex that is not
431/// there, or an option is not finite and positive.
432pub fn solid_from_mesh(
433 model: &mut Model,
434 mesh: &Triangulation,
435 options: &MeshSolidOptions,
436 tol: Tolerances,
437) -> OgeomResult<MeshSolid> {
438 MeshRegions::find(mesh, options, tol)?.build(model)
439}
440
441/// A mesh welded, cleaned and oriented, its pieces found, and its triangles
442/// gathered into regions: what a solid is built from.
443#[derive(Clone)]
444struct Found {
445 points: Vec<Point>,
446 /// The welded vertex each of the input mesh's vertices became.
447 remap: Vec<u32>,
448 triangles: Vec<[u32; 3]>,
449 adjacency: Adjacency,
450 pieces: Vec<Piece>,
451 /// How many closed pieces each piece lies inside.
452 depth: Vec<usize>,
453 all_closed: bool,
454 /// The coplanar distance the regions were found to.
455 flat: f64,
456 /// The default distance, where the regions were found at a wider one.
457 tight: f64,
458 /// The regions found at the default, where they were found again at a
459 /// wider distance.
460 first: Option<Box<Groups>>,
461 groups: Groups,
462 report: MeshSolidReport,
463}
464
465/// Weld, clean and orient the mesh, and gather its triangles into regions.
466fn find(mesh: &Triangulation, options: &MeshSolidOptions, tol: Tolerances) -> OgeomResult<Found> {
467 find_in(mesh, options, true, tol)
468}
469
470/// [`find`], reading the mesh in an order taken from its geometry where
471/// `canonical`, else as it lists its points and triangles.
472fn find_in(
473 mesh: &Triangulation,
474 options: &MeshSolidOptions,
475 canonical: bool,
476 tol: Tolerances,
477) -> OgeomResult<Found> {
478 let weld = options.weld.unwrap_or_else(|| tol.confusion());
479 if !(weld.is_finite() && weld > 0.0) {
480 ogeom_bail!(
481 Construction,
482 "the weld distance {weld} is not finite and positive"
483 );
484 }
485 if !(options.coplanar_angle.is_finite() && options.coplanar_angle >= 0.0) {
486 ogeom_bail!(Construction, "the coplanar angle is not finite");
487 }
488 if let Some(distance) = options.coplanar_distance
489 && !(distance.is_finite() && distance > 0.0)
490 {
491 ogeom_bail!(
492 Construction,
493 "the coplanar distance {distance} is not finite and positive"
494 );
495 }
496 if let Some(quantum) = options.quantum
497 && !(quantum.is_finite() && quantum >= 0.0)
498 {
499 ogeom_bail!(
500 Construction,
501 "the quantum {quantum} is not finite and non-negative"
502 );
503 }
504 // A crease turns by more than nothing and no more than a half turn: at
505 // zero every turn is a crease, and nothing turns further than back on
506 // itself.
507 if !(options.crease.is_finite()
508 && options.crease > 0.0
509 && options.crease <= core::f64::consts::PI)
510 {
511 ogeom_bail!(
512 Construction,
513 "the crease angle {} is not a turn between nothing and a half turn",
514 options.crease
515 );
516 }
517 let count = mesh.positions.len();
518 if mesh
519 .triangles
520 .iter()
521 .flatten()
522 .any(|&v| v as usize >= count)
523 {
524 ogeom_bail!(
525 Construction,
526 "a triangle names a vertex past the {count} the mesh has"
527 );
528 }
529 let mut report = MeshSolidReport::default();
530
531 let (points, remap) = weld_points(&mesh.positions, weld);
532 report.vertices_welded = count - points.len();
533 // Everything after reads the mesh in an order taken from its geometry
534 // (points by position, triangles by their corners), so the regions
535 // found do not depend on how the mesh lists its triangles.
536 let (points, remap) = if canonical {
537 by_position(points, remap)
538 } else {
539 (points, remap)
540 };
541 let mut listed: Vec<[u32; 3]> = mesh
542 .triangles
543 .iter()
544 .map(|t| t.map(|v| remap[v as usize]))
545 .collect();
546 if canonical {
547 for t in &mut listed {
548 *t = lowest_first(*t);
549 }
550 listed.sort_unstable();
551 }
552 let mut triangles = Vec::with_capacity(listed.len());
553 let mut seen: FastMap<[u32; 3], ()> =
554 FastMap::with_capacity_and_hasher(listed.len(), Default::default());
555 let mut flat_slivers: Vec<[u32; 3]> = Vec::new();
556 for [a, b, c] in listed {
557 if a == b || b == c || c == a || !has_area(&points, [a, b, c], weld) {
558 report.degenerate_dropped += 1;
559 if a != b && b != c && c != a {
560 flat_slivers.push([a, b, c]);
561 }
562 continue;
563 }
564 let mut key = [a, b, c];
565 key.sort_unstable();
566 if seen.insert(key, ()).is_some() {
567 report.duplicates_dropped += 1;
568 continue;
569 }
570 triangles.push([a, b, c]);
571 }
572 if triangles.is_empty() {
573 ogeom_bail!(Construction, "the mesh has no triangle with area");
574 }
575 split_across_slivers(&points, &mut triangles, &flat_slivers);
576 report.triangles = triangles.len();
577
578 // Orient each piece consistently, then outward where it closes.
579 let adjacency = Adjacency::new(&triangles);
580 report.edges_used_once = adjacency.used_once;
581 report.edges_used_more = adjacency.used_more;
582 let pieces = orient(&points, &mut triangles, &adjacency, &mut report);
583 // A closed piece nested an odd number of pieces deep bounds a void of
584 // the one around it, and faces into the void, out of the material.
585 let all_closed = pieces.iter().all(|p| p.closed);
586 let depth: Vec<usize> = if all_closed {
587 (0..pieces.len())
588 .map(|i| {
589 (0..pieces.len())
590 .filter(|&j| j != i && inside(&points, &triangles, &pieces[j], &pieces[i]))
591 .count()
592 })
593 .collect()
594 } else {
595 vec![0; pieces.len()]
596 };
597 for (piece, d) in pieces.iter().zip(&depth) {
598 if d % 2 == 1 {
599 for &t in &piece.triangles {
600 triangles[t as usize].swap(1, 2);
601 report.windings_flipped += 1;
602 }
603 }
604 }
605 // Flipping renumbers every triangle's edges; the twins are found again.
606 let mut adjacency = Adjacency::new(&triangles);
607 report.shells = pieces.len();
608 for _ in 0..3 {
609 if unfold(&points, &mut triangles, &adjacency) == 0 {
610 break;
611 }
612 adjacency = Adjacency::new(&triangles);
613 }
614
615 let diagonal = diagonal(&points);
616 let noise = flat_noise(&points, &triangles, &adjacency);
617 let quantum = options.quantum.unwrap_or(0.0);
618 let scatter = (2.0 * quantum).max(1.5 * noise);
619 let mut flat = options
620 .coplanar_distance
621 .unwrap_or_else(|| (1e-6 * diagonal).max(scatter))
622 .max(weld);
623 // A distance asked below what the mesh's own scatter allows cannot be
624 // met: no surface fitted to the vertices holds them closer, regions
625 // break into fragments, and the seams between the fragments open. A
626 // seam's points lie within the distance of two surfaces each fitted
627 // to vertices that scatter by the noise, and the noise is measured at
628 // its ninetieth percentile, so recognition holds to at least
629 // [`NOISE_FLOOR`] times it, and says so.
630 if options.recognize && options.coplanar_distance.is_some() {
631 // A mesh every coordinate of which single precision holds was
632 // stored in it, and scatters by its quantum whether or not the
633 // facets the noise is read from show it: a strip along an axis
634 // lies flat whatever the rounding did.
635 #[allow(
636 clippy::cast_possible_truncation,
637 reason = "the test is the round trip"
638 )]
639 let single = options.quantum.is_none()
640 && points
641 .iter()
642 .all(|p| [p.x, p.y, p.z].iter().all(|&c| f64::from(c as f32) == c));
643 let stored = if single {
644 let largest = points
645 .iter()
646 .flat_map(|p| [p.x.abs(), p.y.abs(), p.z.abs()])
647 .fold(0.0_f64, f64::max);
648 #[allow(
649 clippy::cast_possible_truncation,
650 reason = "an f32 coordinate's magnitude"
651 )]
652 let x = (largest as f32).max(f32::MIN_POSITIVE);
653 f64::from(f32::from_bits(x.to_bits() + 1) - x) * 3.0_f64.sqrt() / 2.0
654 } else {
655 quantum
656 };
657 let floor = (2.0 * stored).max(NOISE_FLOOR * noise);
658 if flat < floor {
659 flat = floor;
660 report.coplanar_distance_raised = true;
661 }
662 }
663 let mut groups = segment(&points, &triangles, &adjacency, options, flat, tol)?;
664 // The default distance is what single precision resolves, and some
665 // exporters place their vertices a few times farther off their own
666 // surfaces than that. The recognized surfaces say so: where their fits
667 // press against the distance, it is the distance that stops them, and
668 // their rims' last triangles stay facets. Unless the caller chose the
669 // distance, it widens while that holds, twice at most. Where the mesh's
670 // own scatter (its noise or its rounding) set the distance, that
671 // scatter is everywhere, and so is the widening. Otherwise it widens for
672 // the smooth patches the pressing fits lie in alone: a fit that runs on
673 // tangentially into its neighbours does so within its patch, and
674 // elsewhere a wider distance only lets fits spread over facets of other
675 // surfaces and merges facets of surfaces nothing recognizes into planes.
676 // Each curved region found wider keeps the regions the default found
677 // among its triangles, which the build puts back where it cannot place
678 // the wider one.
679 let found_at = flat;
680 let mut first = None;
681 if options.coplanar_distance.is_none() && options.recognize {
682 let narrow = groups.clone();
683 let everywhere = scatter > 1e-6 * diagonal;
684 for _ in 0..2 {
685 let pressing: Vec<usize> = groups
686 .carriers
687 .iter()
688 .enumerate()
689 .filter_map(|(g, c)| match c {
690 Carrier::Curved(curved) if curved.fitted > flat * PRESSED => Some(g),
691 _ => None,
692 })
693 .collect();
694 if pressing.is_empty() {
695 break;
696 }
697 flat *= 2.0;
698 let wide = segment(&points, &triangles, &adjacency, options, flat, tol)?;
699 widen_where_pressed(
700 &points,
701 &triangles,
702 &adjacency,
703 options,
704 &mut groups,
705 &wide,
706 &narrow,
707 if everywhere { None } else { Some(&pressing) },
708 flat,
709 );
710 }
711 if flat > found_at {
712 first = Some(Box::new(narrow));
713 }
714 }
715 Ok(Found {
716 points,
717 remap,
718 triangles,
719 adjacency,
720 pieces,
721 depth,
722 all_closed,
723 flat,
724 tight: found_at,
725 first,
726 groups,
727 report,
728 })
729}
730
731/// The solid built from found regions.
732///
733/// A region whose carrier is gone and still holds triangles gives them to
734/// planar faces first, as the planar pass gathers them. A facet holding a
735/// triangle marked in `protected` (one per triangle, or empty for none) is
736/// never given to a curved neighbour. A region found at a wider distance
737/// than the default is tried again as the default found it before it is
738/// faceted: the build starts over from the regions found, with it put
739/// back.
740fn build(
741 model: &mut Model,
742 found: &Found,
743 options: &MeshSolidOptions,
744 protected: &[bool],
745 tol: Tolerances,
746) -> OgeomResult<MeshSolid> {
747 let (triangles, adjacency) = (&found.triangles, &found.adjacency);
748 let mut start = found.groups.clone();
749 let mut flat = found.flat;
750 let mut first = found.first.clone();
751 // Face meshes kept across every build from these regions, and whether
752 // each body's volume held on the last build that measured it: most
753 // faces recur unchanged from one build to the next.
754 let kept = crate::mass::VolumeKept::default();
755 let mut held: Vec<bool> = Vec::new();
756 loop {
757 let narrowing = match build_from(
758 model, found, &start, flat, options, protected, &kept, &mut held, tol,
759 )? {
760 Ok(solid) => return Ok(solid),
761 Err(Unbuilt::Narrow(narrowing)) => narrowing,
762 Err(Unbuilt::Split(facets)) => {
763 let split = found.split_at_middles(&facets);
764 let protected: Vec<bool> = if protected.is_empty() {
765 Vec::new()
766 } else {
767 let mut grown = protected.to_vec();
768 for &t in &facets {
769 grown.extend([protected[t]; 2]);
770 }
771 grown
772 };
773 return build(model, &split, options, &protected, tol);
774 }
775 };
776 for g in narrowing {
777 narrow(&mut start, triangles, g);
778 }
779 // With every region put back, the regions are the ones the default
780 // found.
781 if start.narrower.is_empty()
782 && let Some(regions) = first.take()
783 {
784 start = *regions;
785 } else {
786 split_disconnected(triangles, adjacency, &mut start);
787 coplanar_groups(
788 &found.points,
789 triangles,
790 adjacency,
791 options,
792 found.tight,
793 &mut start,
794 tol,
795 )?;
796 }
797 flat = start
798 .narrower
799 .values()
800 .map(|n| n.distance)
801 .fold(found.tight, f64::max);
802 }
803}
804
805/// [`build`] from regions `start`, to the coplanar distance `flat`: the
806/// solid, or the regions found at a wider distance than the default that
807/// it would facet.
808#[allow(clippy::too_many_arguments, reason = "the build's inputs")]
809fn build_from(
810 model: &mut Model,
811 found: &Found,
812 start: &Groups,
813 flat: f64,
814 options: &MeshSolidOptions,
815 protected: &[bool],
816 kept: &crate::mass::VolumeKept,
817 held: &mut Vec<bool>,
818 tol: Tolerances,
819) -> OgeomResult<Result<MeshSolid, Unbuilt>> {
820 let (points, triangles, adjacency) = (&found.points, &found.triangles, &found.adjacency);
821 let (pieces, depth, all_closed) = (&found.pieces, &found.depth, found.all_closed);
822 let mut groups = start.clone();
823 let mut report = found.report.clone();
824 // A plane that does not hold its region's vertices (one a caller put
825 // there) can place none of its boundary; its triangles are gathered
826 // again.
827 let off = planes_off_their_vertices(points, triangles, &groups, flat);
828 let mut released = !off.is_empty();
829 for g in off {
830 withdraw(
831 &mut groups,
832 &mut report,
833 g,
834 FallbackReason::BoundaryNotPlaced,
835 );
836 }
837 for t in 0..triangles.len() {
838 if matches!(groups.carriers.get(groups.of[t]), Some(Carrier::Gone)) {
839 groups.of[t] = usize::MAX;
840 released = true;
841 }
842 }
843 if released {
844 if options.merge_coplanar {
845 coplanar_groups(
846 points,
847 triangles,
848 adjacency,
849 options,
850 flat,
851 &mut groups,
852 tol,
853 )?;
854 } else {
855 one_each(points, triangles, &mut groups, tol)?;
856 }
857 }
858 // Facets left between curved faces go to them first. Whether that
859 // leaves every face facing out is known only once the solid is built;
860 // where one faces into the material, the conversion is made again
861 // without them.
862 let unabsorbed = (groups.clone(), report.clone());
863 let mut absorbed = if options.recognize {
864 absorb_facets(points, triangles, adjacency, &mut groups, protected, flat)
865 } else {
866 FastMap::default()
867 };
868 let mut absorbing = !absorbed.is_empty();
869 let mut regrouped = Regrouped::default();
870 let snaps = std::sync::Mutex::new(SnapCache::default());
871 let images = std::sync::Mutex::new(ImageCache::default());
872 // Areas kept across the builds: most faces recur unchanged.
873 let areas = AreaCache::default();
874 let shape = 'attempt: loop {
875 // Plan until every curved face's boundary is exact, faceting the ones
876 // whose boundary is not; then build, and facet any recognized face that
877 // reaches past the triangles it replaces (a boundary placed on the wrong
878 // turn of its surface closes a face of the wrong extent) and build again.
879 let mut pinned: ogeom_core::FastSet<u32> = ogeom_core::FastSet::default();
880 let mut straight: ogeom_core::FastSet<(u32, u32)> = ogeom_core::FastSet::default();
881 // What stood before any seam was threaded straight: a straightened
882 // build with a face collapsed beside a threaded seam is set aside for
883 // it. A face turned into the material beside one is an overlap like
884 // any other, and the curved faces beside it are faceted.
885 let mut unthreaded: Option<(Groups, MeshSolidReport)> = None;
886 let mut threading_refused = false;
887 let shape = 'build: loop {
888 let fans = groups.fans_by_group(triangles, adjacency);
889 let planner = Planner {
890 points,
891 triangles,
892 adjacency,
893 groups: &groups,
894 merge: options.merge_coplanar && !options.keep_vertices,
895 pinned: &pinned,
896 straight: &straight,
897 crease: options.crease,
898 flat,
899 tol,
900 snaps: &snaps,
901 images: &images,
902 fans: &fans,
903 };
904 // A step that fails on the built faces withdraws the faces that
905 // cannot be meshed or measured on their own (or, where none is
906 // found, every recognized one), and the error stands only where
907 // nothing is left to withdraw.
908 let mut step_error = None;
909 macro_rules! or_withdraw {
910 ($label:lifetime, $built:expr, $step:expr) => {
911 match $step {
912 Ok(value) => value,
913 Err(error) => {
914 step_error = Some(error);
915 break $label(
916 failing_faces(model, &groups, $built, tol),
917 FallbackReason::BuildFailed,
918 );
919 }
920 }
921 };
922 }
923 let failed = match planner.plan() {
924 Err(error) => {
925 step_error = Some(error);
926 (
927 failing_faces(model, &groups, &[], tol),
928 FallbackReason::BuildFailed,
929 )
930 }
931 Ok(Err(Replan::Pin(vertices))) => {
932 pinned.extend(vertices);
933 continue;
934 }
935 Ok(Err(Replan::Fan(facets))) => {
936 // A facet sharing its plane with another is taken out
937 // into a plane of its own, which it is then alone in.
938 for &t in &facets {
939 let g = groups.of[t];
940 if (0..triangles.len()).any(|u| u != t && groups.of[u] == g) {
941 groups.of[t] = groups.carriers.len();
942 groups.carriers.push(groups.carriers[g].clone());
943 }
944 }
945 groups.fans.extend(facets);
946 continue;
947 }
948 Ok(Err(Replan::Facet(failed))) => (failed, FallbackReason::BoundaryNotPlaced),
949 Ok(Ok(plan)) => 'checks: {
950 model.begin_operation();
951 let (built, unbuilt) = Builder {
952 model,
953 points,
954 triangles,
955 groups: &groups,
956 plan: &plan,
957 fans: &fans,
958 tol,
959 }
960 .build();
961 if !unbuilt.is_empty() {
962 break 'checks (
963 unbuilt_culprits(&unbuilt, &groups, adjacency, &fans),
964 FallbackReason::BuildFailed,
965 );
966 }
967 let astray = or_withdraw!(
968 'checks,
969 &built,
970 astray_faces(model, points, triangles, &groups, &built, flat, kept.meshes(), tol)
971 );
972 if astray.is_empty() {
973 let (shape, bodies) = or_withdraw!(
974 'checks,
975 &built,
976 assemble(
977 model, points, triangles, pieces, depth, all_closed, &groups,
978 &built,
979 )
980 );
981 let (mut culprits, mut reason) = if options.recognize && all_closed {
982 or_withdraw!(
983 'checks,
984 &built,
985 body_culprits(
986 model,
987 points,
988 triangles,
989 &groups,
990 &built,
991 &bodies,
992 flat,
993 kept,
994 held,
995 tol,
996 &mut report,
997 )
998 )
999 } else {
1000 (Vec::new(), FallbackReason::TurnedIn)
1001 };
1002 if culprits.is_empty() && options.recognize && !threading_refused {
1003 let crossed = or_withdraw!(
1004 'checks,
1005 &built,
1006 crossed_seams(
1007 model, &shape, triangles, adjacency, &groups, &built,
1008 (kept.meshes(), &areas), tol,
1009 )
1010 );
1011 let before = straight.len();
1012 if before == 0 && !crossed.is_empty() {
1013 unthreaded = Some((groups.clone(), report.clone()));
1014 }
1015 straight.extend(crossed);
1016 if straight.len() > before {
1017 continue 'build;
1018 }
1019 if let Some((was, then)) = unthreaded.take()
1020 && or_withdraw!(
1021 'checks,
1022 &built,
1023 collapsed_beside(
1024 model, triangles, &groups, &built, &straight, &areas,
1025 tol,
1026 )
1027 )
1028 {
1029 groups = was;
1030 report = then;
1031 straight.clear();
1032 threading_refused = true;
1033 continue 'build;
1034 }
1035 }
1036 if culprits.is_empty() && options.recognize {
1037 let backwards = or_withdraw!(
1038 'checks,
1039 &built,
1040 backwards_facets(
1041 model, points, triangles, adjacency, &groups, &built, &fans,
1042 tol,
1043 )
1044 );
1045 if !backwards.is_empty() {
1046 groups.fans.extend(backwards);
1047 continue 'build;
1048 }
1049 }
1050 if culprits.is_empty() && options.recognize {
1051 let fanned;
1052 (culprits, fanned) = or_withdraw!(
1053 'checks,
1054 &built,
1055 folded_seams(model, triangles, adjacency, &groups, &built, tol)
1056 );
1057 // A facet folding a curved face beside it is
1058 // built as a fan before that face is faceted.
1059 if !fanned.is_empty() {
1060 groups.fans.extend(fanned);
1061 continue 'build;
1062 }
1063 reason = FallbackReason::FoldedSeam;
1064 }
1065 if culprits.is_empty() && options.recognize {
1066 let fanned;
1067 (culprits, fanned) = or_withdraw!(
1068 'checks,
1069 &built,
1070 crossed_boundaries(
1071 model, triangles, adjacency, &groups, &built, ®rouped, tol,
1072 )
1073 );
1074 // A facet whose seam with a curved face crosses
1075 // its other side short of their corner is built
1076 // as a fan before that face is faceted.
1077 if !fanned.is_empty() {
1078 groups.fans.extend(fanned);
1079 continue 'build;
1080 }
1081 reason = FallbackReason::FoldedSeam;
1082 }
1083 if culprits.is_empty() && options.recognize {
1084 let (turned, alone, covering);
1085 (culprits, turned, alone, covering) = or_withdraw!(
1086 'checks,
1087 &built,
1088 overlapping_faces(model, &shape, adjacency, &groups, &built, tol)
1089 );
1090 reason = FallbackReason::Overlaps;
1091 // A facet turned in beside one curved face is
1092 // built as a fan before that face is faceted.
1093 let fanned: Vec<usize> = turned
1094 .iter()
1095 .filter(|&&g| !fans.contains_key(&g))
1096 .filter_map(|&g| {
1097 let t = groups.of.iter().position(|&o| o == g)?;
1098 groups.fan_at(t, triangles, adjacency).map(|_| t)
1099 })
1100 .collect();
1101 if !fanned.is_empty() {
1102 groups.fans.extend(fanned);
1103 continue 'build;
1104 }
1105 // A curved face that took facets in can run
1106 // over the planar faces near it, which then face
1107 // into it: every such face across a turned-in
1108 // one gives its facets back before the curved
1109 // face beside it is blamed.
1110 let covered: Vec<usize> = covering
1111 .into_iter()
1112 .filter(|g| absorbed.contains_key(g))
1113 .collect();
1114 if !covered.is_empty() {
1115 for g in covered {
1116 if let Some(record) = absorbed.remove(&g) {
1117 give_back(&mut groups, g, record);
1118 }
1119 }
1120 continue 'build;
1121 }
1122 // A planar face turned in with no recognized
1123 // face near it is gathered again, then built a
1124 // face per triangle, while that is left to try.
1125 if culprits.is_empty() {
1126 culprits = alone
1127 .into_iter()
1128 .filter(|&g| regrouped.can_withdraw(&groups, g))
1129 .collect();
1130 reason = FallbackReason::TurnedIn;
1131 }
1132 }
1133 if culprits.is_empty() && options.recognize {
1134 culprits = or_withdraw!(
1135 'checks,
1136 &built,
1137 unmatched_faces(model, &shape, &groups, &built, flat, kept.meshes(), tol)
1138 );
1139 reason = FallbackReason::MeshesOpen;
1140 }
1141 if culprits.is_empty() && options.recognize {
1142 culprits = or_withdraw!(
1143 'checks,
1144 &built,
1145 broken_faces(model, &groups, &built, ®rouped, tol)
1146 );
1147 reason = FallbackReason::BoundaryOpen;
1148 }
1149 if culprits.is_empty() && options.recognize {
1150 let stuck;
1151 (culprits, stuck) = or_withdraw!(
1152 'checks,
1153 &built,
1154 crossing_faces(
1155 model, &shape, &groups, &built, ®rouped, kept.meshes(), tol,
1156 )
1157 );
1158 reason = FallbackReason::Crosses;
1159 if culprits.is_empty()
1160 && let Some(at) = stuck
1161 {
1162 ogeom_bail!(
1163 Construction,
1164 "the mesh passes through itself at ({:.6}, {:.6}, {:.6}): \
1165 its own triangles cross there, so no solid built on them \
1166 bounds a single region",
1167 at.x,
1168 at.y,
1169 at.z
1170 );
1171 }
1172 }
1173 if culprits.is_empty() && options.recognize {
1174 culprits = or_withdraw!(
1175 'checks,
1176 &built,
1177 spanned_faces(model, &shape, &groups, &built, ®rouped, tol)
1178 );
1179 reason = FallbackReason::SpansShort;
1180 }
1181 if culprits.is_empty() {
1182 report.faces = built.iter().flatten().count();
1183 report.curved_faces = groups
1184 .carriers
1185 .iter()
1186 .zip(&built)
1187 .filter(|(c, b)| matches!(c, Carrier::Curved(_)) && b.is_some())
1188 .count();
1189 let patches: Vec<&Curved> = groups
1190 .carriers
1191 .iter()
1192 .zip(&built)
1193 .filter_map(|(c, b)| match (c, b) {
1194 (Carrier::Curved(curved), Some(_))
1195 if curved.patch.is_some() =>
1196 {
1197 Some(curved)
1198 }
1199 _ => None,
1200 })
1201 .collect();
1202 report.patch_faces = patches.len();
1203 report.patch_charts_mapped =
1204 patches.iter().filter(|c| c.patch == Some(true)).count();
1205 report.patches_not_disk = groups.refused.not_disk;
1206 report.patches_narrow = groups.refused.narrow;
1207 report.patches_unverified = groups.refused.unverified;
1208 report.free_edges_fitted = plan.free_fitted;
1209 // A facet alone beside a curved face whose seam
1210 // stands off it, as built, farther than a snapped
1211 // curve may (a chord the plan took within the
1212 // reach, widened where the build placed its
1213 // ends) is built as a fan onto that face, as one
1214 // the plan saw wide is.
1215 if options.recognize {
1216 let reach = flat * planner::REACH;
1217 let fanned: Vec<usize> = (0..triangles.len())
1218 .filter(|t| !groups.fans.contains(t))
1219 .filter(|&t| {
1220 groups.fan_at(t, triangles, adjacency).is_some_and(|fan| {
1221 fan.seams().any(|(_, c)| {
1222 seam_built_wider(
1223 model,
1224 &built,
1225 groups.of[t],
1226 c,
1227 reach,
1228 )
1229 })
1230 })
1231 })
1232 .collect();
1233 if !fanned.is_empty() {
1234 groups.fans.extend(fanned);
1235 continue 'build;
1236 }
1237 // A facet alone with a curved face across
1238 // every side is no fan's: where a seam of it
1239 // stands past the reach, the conversion is
1240 // made again with the facet split at its
1241 // middle into three, each a facet with one
1242 // curved side.
1243 let enclosed: Vec<usize> = (0..triangles.len())
1244 .filter(|&t| {
1245 enclosed_wide(model, &built, &groups, adjacency, t, reach)
1246 })
1247 .collect();
1248 if !enclosed.is_empty() {
1249 return Ok(Err(Unbuilt::Split(enclosed)));
1250 }
1251 }
1252 break 'build shape;
1253 }
1254 (culprits, reason)
1255 } else {
1256 // A facet all but tangent to a curved face beside
1257 // it meets the face's surface along a seam that can
1258 // lie well past the facet, and the face then reaches
1259 // past its triangles: each facet beside such a face
1260 // that can be a fan is built as one onto it before
1261 // the face is faceted.
1262 let fanned: Vec<usize> = (0..triangles.len())
1263 .filter(|&t| !fans.contains_key(&groups.of[t]))
1264 .filter(|&t| {
1265 groups.fan_at(t, triangles, adjacency).is_some_and(|fan| {
1266 fan.seams().any(|(_, c)| astray.contains(&c))
1267 })
1268 })
1269 .collect();
1270 if !fanned.is_empty() {
1271 groups.fans.extend(fanned);
1272 continue 'build;
1273 }
1274 (astray, FallbackReason::ReachesPast)
1275 }
1276 }
1277 };
1278 let (failed, reason) = failed;
1279 // A region that took facets gives them back and is tried again
1280 // without them before it is faceted.
1281 let returned: Vec<usize> = failed
1282 .iter()
1283 .copied()
1284 .filter(|g| absorbed.contains_key(g))
1285 .collect();
1286 if !returned.is_empty() {
1287 for g in returned {
1288 if let Some(record) = absorbed.remove(&g) {
1289 give_back(&mut groups, g, record);
1290 }
1291 }
1292 continue;
1293 }
1294 let narrowing: Vec<usize> = failed
1295 .iter()
1296 .copied()
1297 .filter(|g| groups.narrower.contains_key(g))
1298 .collect();
1299 if !narrowing.is_empty() {
1300 return Ok(Err(Unbuilt::Narrow(narrowing)));
1301 }
1302 let mut alone = Vec::new();
1303 let mut withdrawn = false;
1304 for g in failed {
1305 if !regrouped.can_withdraw(&groups, g) {
1306 continue;
1307 }
1308 if matches!(groups.carriers[g], Carrier::Plane(_)) {
1309 alone.extend(regrouped.withdraw_plane(&groups, g));
1310 }
1311 withdraw(&mut groups, &mut report, g, reason);
1312 withdrawn = true;
1313 }
1314 if !withdrawn {
1315 if let Some(error) = step_error {
1316 return Err(error);
1317 }
1318 ogeom_bail!(Construction, "a facet of the mesh could not be built");
1319 }
1320 for t in alone {
1321 groups.of[t] = groups.carriers.len();
1322 groups
1323 .carriers
1324 .push(Carrier::Plane(plane_of(points, triangles[t], tol)?));
1325 }
1326 coplanar_groups(
1327 points,
1328 triangles,
1329 adjacency,
1330 options,
1331 flat,
1332 &mut groups,
1333 tol,
1334 )?;
1335 };
1336 if absorbing && any_turned_in(model, &shape, tol)? {
1337 absorbing = false;
1338 absorbed.clear();
1339 (groups, report) = unabsorbed.clone();
1340 continue 'attempt;
1341 }
1342 break shape;
1343 };
1344 crate::pcurve_gap::state_pcurve_gaps(model, &shape, tol)?;
1345 Ok(Ok(MeshSolid {
1346 shape,
1347 closed: all_closed,
1348 coplanar_distance: flat,
1349 report,
1350 }))
1351}
1352
1353/// Facet region `g`: its face is not built on its surface, its triangles
1354/// are left for the planar faces to gather, and the report names it.
1355fn withdraw(groups: &mut Groups, report: &mut MeshSolidReport, g: usize, reason: FallbackReason) {
1356 groups.carriers[g] = Carrier::Gone;
1357 report.curved_faceted += 1;
1358 report.fallbacks.push(RegionFallback {
1359 region: RegionId(g),
1360 reason,
1361 });
1362 for of in &mut groups.of {
1363 if *of == g {
1364 *of = usize::MAX;
1365 }
1366 }
1367}
1368
1369/// Put back in place of region `g`, found at a wider distance, the curved
1370/// regions the default found within it: each takes back its triangles,
1371/// from `g`, from a planar face (gathered again round it) or from another
1372/// curved region found wider, and what `g` held beyond them is left for
1373/// the planar faces. A curved region left holding no triangle is gone.
1374/// Nothing changes where `g` was not found wider.
1375fn narrow(groups: &mut Groups, triangles: &[[u32; 3]], g: usize) {
1376 let Some(record) = groups.narrower.remove(&g) else {
1377 return;
1378 };
1379 groups.carriers[g] = Carrier::Gone;
1380 let mut touched = vec![g];
1381 for (_, held) in &record.regions {
1382 for &t in held {
1383 let p = groups.of[t];
1384 if matches!(groups.carriers.get(p), Some(Carrier::Plane(_))) {
1385 groups.carriers[p] = Carrier::Gone;
1386 }
1387 }
1388 }
1389 for of in &mut groups.of {
1390 if *of == g || matches!(groups.carriers.get(*of), Some(Carrier::Gone)) {
1391 *of = usize::MAX;
1392 }
1393 }
1394 for (k, (carrier, held)) in record.regions.into_iter().enumerate() {
1395 let at = if k == 0 {
1396 groups.carriers[g] = carrier;
1397 g
1398 } else {
1399 groups.carriers.push(carrier);
1400 groups.carriers.len() - 1
1401 };
1402 touched.push(at);
1403 for t in held {
1404 let p = groups.of[t];
1405 if p == usize::MAX || groups.narrower.contains_key(&p) {
1406 touched.push(p);
1407 groups.of[t] = at;
1408 }
1409 }
1410 }
1411 touched.sort_unstable();
1412 touched.dedup();
1413 for c in touched {
1414 if c == usize::MAX {
1415 continue;
1416 }
1417 let mut vertices: Vec<u32> = groups
1418 .of
1419 .iter()
1420 .enumerate()
1421 .filter(|&(_, &o)| o == c)
1422 .flat_map(|(t, _)| triangles[t])
1423 .collect();
1424 vertices.sort_unstable();
1425 vertices.dedup();
1426 if vertices.is_empty() {
1427 // A default region already taken back whole through another
1428 // region found wider, or a region found wider whose triangles
1429 // all went back so: no face is built on it.
1430 if matches!(groups.carriers[c], Carrier::Curved(_)) {
1431 groups.carriers[c] = Carrier::Gone;
1432 groups.narrower.remove(&c);
1433 }
1434 } else if let Carrier::Curved(curved) = &mut groups.carriers[c] {
1435 curved.vertices = vertices;
1436 }
1437 }
1438}
1439
1440/// What a face is built on.
1441#[derive(Debug, Clone)]
1442enum Carrier {
1443 /// A plane, its normal outward.
1444 Plane(Plane),
1445 /// A surface recognition decided the region is.
1446 Curved(Curved),
1447 /// A region demoted to faceted, whose triangles went to other faces.
1448 Gone,
1449}
1450
1451#[derive(Debug, Clone)]
1452struct Curved {
1453 shape: Canonical,
1454 /// How far the region's vertices stand off it.
1455 deviation: f64,
1456 /// How far they stood off the region's own fit, before a shared axis
1457 /// or a sphere's frame moved it: how hard the fit pressed against the
1458 /// distance it was grown within.
1459 fitted: f64,
1460 /// The chart point every pcurve on it is unwrapped around, so they all
1461 /// read on one branch.
1462 centre: (f64, f64),
1463 /// Whether the region runs all the way round its axis, and so needs a
1464 /// seam across it.
1465 wraps: bool,
1466 /// Whether it runs all the way round a torus's tube, and so needs a
1467 /// seam along it.
1468 wraps_v: bool,
1469 /// Whether the frame was set from the region's own boundary: a sphere
1470 /// bounded by circles in parallel planes turns about their normal, so
1471 /// they are its latitudes.
1472 fixed: bool,
1473 /// The region's mesh vertices.
1474 vertices: Vec<u32>,
1475 /// For a fitted patch, whether its chart is the mean-value map.
1476 patch: Option<bool>,
1477}
1478
1479/// How a curved face's boundary lies in its chart.
1480#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1481enum Layout {
1482 /// A patch inside one branch of the chart.
1483 Open,
1484 /// A band between two full circles, joined by a seam: round the axis,
1485 /// or round a torus's tube.
1486 Band { round_tube: bool },
1487 /// A sphere's cap: one latitude circle, a seam to the pole, and the
1488 /// pole; or a cone closing at its apex: one rim circle, a ruling to the
1489 /// apex, and the apex.
1490 Cap,
1491 /// A sphere's cap with holes: one rim of any shape going round the
1492 /// axis, a seam from a vertex of it to the pole, straight in the chart
1493 /// and clear of the holes, the pole, and the holes as inner wires.
1494 HoledCap,
1495 /// The whole surface, with no boundary of its own.
1496 Whole,
1497 /// Round the axis (or round a torus's tube) between two rims of any
1498 /// shape, with holes: a seam joins a vertex of each rim, straight in
1499 /// the chart and clear of the holes.
1500 Wrapped,
1501 /// A sphere or a torus whole but for holes, its seams and poles clear
1502 /// of them: the whole surface's face, the holes its inner wires.
1503 Holed,
1504 /// A torus whole but for holes that between them leave no parallel
1505 /// (or no meridian) free: its seam that way runs through some of
1506 /// them (see [`Thread`]).
1507 Threaded,
1508}
1509
1510/// The seam of a torus whose holes leave no parallel (or no meridian)
1511/// free, read in a working chart whose first angle is the one the seam
1512/// goes round (the axis's, or the tube's where `round_tube`). The seam is a
1513/// chain: from a point on the free circle the other way, straight in the
1514/// chart to a hole's vertex furthest back along it, round the hole to its
1515/// vertex furthest on, straight to the next hole, and so on back to the
1516/// start. Each straight piece is a seam edge, met once from either side.
1517/// Each hole it passes through is split at those two vertices: one arc
1518/// bounds the face along the chain, the other a whole turn across.
1519#[derive(Debug, Clone)]
1520struct Thread {
1521 round_tube: bool,
1522 /// The free circle's first angle, where the chain starts and ends.
1523 start: (f64, f64),
1524 /// The holes the chain passes through, in order: the ring, the mesh
1525 /// vertices it enters and leaves at and where those stand in the
1526 /// working chart.
1527 stops: Vec<Stop>,
1528}
1529
1530/// One hole a [`Thread`] passes through.
1531#[derive(Debug, Clone, Copy)]
1532struct Stop {
1533 ring: usize,
1534 enter: u32,
1535 leave: u32,
1536 enter_at: (f64, f64),
1537 leave_at: (f64, f64),
1538}
1539
1540/// Triangles gathered into faces: which face each triangle is in, and what
1541/// each face is built on.
1542#[derive(Clone)]
1543struct Groups {
1544 of: Vec<usize>,
1545 carriers: Vec<Carrier>,
1546 /// The smooth regions refused a patch, by why.
1547 refused: PatchRefusals,
1548 /// Triangles each built as a fan (see [`Fan`]) rather than flat, while
1549 /// they still stand alone in their group beside one curved face.
1550 fans: std::collections::BTreeSet<usize>,
1551 /// The curved regions found at a wider distance than the default, each
1552 /// with what the default found among its triangles.
1553 narrower: FastMap<usize, Narrower>,
1554}
1555
1556/// What the default distance found among the triangles of a region found
1557/// at a wider one.
1558#[derive(Clone)]
1559struct Narrower {
1560 /// The distance the region was found at.
1561 distance: f64,
1562 /// The curved regions found at the default holding any of its
1563 /// triangles, each with all of its own.
1564 regions: Vec<(Carrier, Vec<usize>)>,
1565}
1566
1567/// A planar facet of one triangle beside one curved face, built as the
1568/// ruled surface from its far corner to its seam lifted onto the curved
1569/// surface. Where a facet meets a curved face all but tangentially (a chord
1570/// of a neighbour left faceted), the plane through it meets the curved
1571/// surface along a curve that bulges past the facet's third corner, or not
1572/// along the seam at all; a seam threaded straight instead trims the curved
1573/// face short of it by the seam's sag. The fan shares the lifted seam with
1574/// the curved face and its two straight sides with the faces beside it,
1575/// exactly, and closes at its apex on an edge with no length.
1576///
1577/// A facet with two sides on curved faces and the third on a planar one is
1578/// a wedge: built as the ruled surface between its two seams, each lifted
1579/// onto its curved surface, from the corner they share (where it closes on
1580/// an edge with no length) to its third side, the ruling at their far ends.
1581#[derive(Debug, Clone, Copy)]
1582struct Fan {
1583 /// The triangle's corner off the seam.
1584 apex: u32,
1585 /// The seam's ends, in the triangle's winding.
1586 seam: (u32, u32),
1587 /// The curved face across the seam.
1588 curved: usize,
1589 /// For a wedge, the curved face across its second seam, from the
1590 /// seam's second end to the apex.
1591 across: Option<usize>,
1592}
1593
1594impl Fan {
1595 /// Its seams, each by its ends and the curved face across it.
1596 fn seams(&self) -> impl Iterator<Item = ((u32, u32), usize)> + use<> {
1597 let second = self.across.map(|c| ((self.seam.1, self.apex), c));
1598 core::iter::once((self.seam, self.curved)).chain(second)
1599 }
1600
1601 /// The curved face across the seam between `p` and `q`, either way.
1602 fn seam_between(&self, p: u32, q: u32) -> Option<usize> {
1603 self.seams()
1604 .find(|&((a, b), _)| (a, b) == (p, q) || (b, a) == (p, q))
1605 .map(|(_, c)| c)
1606 }
1607}
1608
1609/// Whether the built faces of groups `facet` and `curved` share an edge
1610/// standing wider than `reach`.
1611fn seam_built_wider(
1612 model: &Model,
1613 built: &[Option<Shape>],
1614 facet: usize,
1615 curved: usize,
1616 reach: f64,
1617) -> bool {
1618 let (Some(Some(face)), Some(Some(other))) = (built.get(facet), built.get(curved)) else {
1619 return false;
1620 };
1621 let edges = |face: &Shape| explore_unique(model, face, ShapeType::Edge).unwrap_or_default();
1622 let theirs: FastSet<TShapeId> = edges(other).iter().map(Shape::node).collect();
1623 edges(face).iter().any(|edge| {
1624 theirs.contains(&edge.node())
1625 && model
1626 .node(edge)
1627 .and_then(|n| n.data().as_edge())
1628 .is_some_and(|d| d.tolerance.get() > reach)
1629 })
1630}
1631
1632/// Why [`build_from`] did not build: the regions found at a wider distance
1633/// to put back as the default found them, or the facets to split at their
1634/// middles before the conversion is made again.
1635enum Unbuilt {
1636 Narrow(Vec<usize>),
1637 Split(Vec<usize>),
1638}
1639
1640/// Whether triangle `t` is a facet alone in its plane with a curved face
1641/// across every side, and a built edge it shares with one of them stands
1642/// wider than `reach`.
1643fn enclosed_wide(
1644 model: &Model,
1645 built: &[Option<Shape>],
1646 groups: &Groups,
1647 adjacency: &Adjacency,
1648 t: usize,
1649 reach: f64,
1650) -> bool {
1651 let g = groups.of[t];
1652 if !matches!(groups.carriers.get(g), Some(Carrier::Plane(_))) {
1653 return false;
1654 }
1655 let mut across = Vec::with_capacity(3);
1656 for &slot in &adjacency.twin[3 * t..3 * t + 3] {
1657 let Some(twin) = slot else {
1658 return false;
1659 };
1660 let o = groups.of[twin / 3];
1661 if o == g || !matches!(groups.carriers.get(o), Some(Carrier::Curved(_))) {
1662 return false;
1663 }
1664 across.push(o);
1665 }
1666 across
1667 .into_iter()
1668 .any(|c| seam_built_wider(model, built, g, c, reach))
1669}
1670
1671impl Found {
1672 /// The same mesh with each of `facets` split at its middle into three
1673 /// triangles, wound as it was, each in a plane of its own on the
1674 /// facet's plane; among the regions found at the default distance, the
1675 /// three stay in the facet's region.
1676 fn split_at_middles(&self, facets: &[usize]) -> Self {
1677 let mut found = self.clone();
1678 for &t in facets {
1679 let [a, b, c] = found.triangles[t];
1680 let corners = [a, b, c].map(|v| found.points[v as usize].to_vector());
1681 #[allow(
1682 clippy::cast_possible_truncation,
1683 reason = "a mesh's vertex count fits its u32 indices"
1684 )]
1685 let middle = found.points.len() as u32;
1686 found.points.push(Point::from_vector(
1687 (corners[0] + corners[1] + corners[2]) / 3.0,
1688 ));
1689 let first = found.triangles.len();
1690 found.triangles[t] = [a, b, middle];
1691 found.triangles.push([b, c, middle]);
1692 found.triangles.push([c, a, middle]);
1693 for added in [first, first + 1] {
1694 let g = found.groups.of[t];
1695 found.groups.of.push(found.groups.carriers.len());
1696 let carrier = found.groups.carriers[g].clone();
1697 found.groups.carriers.push(carrier);
1698 if let Some(regions) = found.first.as_deref_mut() {
1699 let held = regions.of[t];
1700 regions.of.push(held);
1701 }
1702 #[allow(
1703 clippy::cast_possible_truncation,
1704 reason = "a mesh's triangle count fits its u32 indices"
1705 )]
1706 let added = added as u32;
1707 #[allow(
1708 clippy::cast_possible_truncation,
1709 reason = "a mesh's triangle count fits its u32 indices"
1710 )]
1711 let held = t as u32;
1712 // The piece's box holds: the middle lies within the facet.
1713 if let Some(piece) = found
1714 .pieces
1715 .iter_mut()
1716 .find(|piece| piece.triangles.contains(&held))
1717 {
1718 piece.triangles.push(added);
1719 }
1720 }
1721 }
1722 found.adjacency = Adjacency::new(&found.triangles);
1723 found
1724 }
1725}
1726
1727impl Groups {
1728 /// The fan a triangle would be built as: alone in its planar group, one
1729 /// side against a curved face and the other two against planar ones, or
1730 /// a wedge, two sides against curved faces and the third against a
1731 /// planar one.
1732 fn fan_at(&self, t: usize, triangles: &[[u32; 3]], adjacency: &Adjacency) -> Option<Fan> {
1733 self.fan_apart(t, triangles, adjacency, false)
1734 }
1735
1736 /// [`Groups::fan_at`], or with `apart` the fan the triangle would be
1737 /// once taken out of its group into a plane of its own: the rest of
1738 /// its group is then a planar neighbour like any other.
1739 fn fan_apart(
1740 &self,
1741 t: usize,
1742 triangles: &[[u32; 3]],
1743 adjacency: &Adjacency,
1744 apart: bool,
1745 ) -> Option<Fan> {
1746 let g = *self.of.get(t)?;
1747 if !matches!(self.carriers.get(g), Some(Carrier::Plane(_))) {
1748 return None;
1749 }
1750 let mut sides = [None; 3];
1751 for (k, side) in sides.iter_mut().enumerate() {
1752 let o = self.of[adjacency.twin[3 * t + k]? / 3];
1753 match self.carriers.get(o) {
1754 _ if o == g && apart => {}
1755 _ if o == g => return None,
1756 Some(Carrier::Curved(_)) => *side = Some(o),
1757 Some(Carrier::Plane(_)) => {}
1758 _ => return None,
1759 }
1760 }
1761 // The seam is the curved side; a wedge's, the curved side whose
1762 // successor in the winding is curved too.
1763 let k = match sides.iter().flatten().count() {
1764 1 => sides.iter().position(Option::is_some)?,
1765 2 => (0..3).find(|&k| sides[k].is_some() && sides[(k + 1) % 3].is_some())?,
1766 _ => return None,
1767 };
1768 let tri = triangles[t];
1769 Some(Fan {
1770 apex: tri[(k + 2) % 3],
1771 seam: (tri[k], tri[(k + 1) % 3]),
1772 curved: sides[k]?,
1773 across: sides[(k + 1) % 3],
1774 })
1775 }
1776
1777 /// The fans standing, by group.
1778 fn fans_by_group(&self, triangles: &[[u32; 3]], adjacency: &Adjacency) -> FastMap<usize, Fan> {
1779 self.fans
1780 .iter()
1781 .filter_map(|&t| Some((self.of[t], self.fan_at(t, triangles, adjacency)?)))
1782 .collect()
1783 }
1784}
1785
1786/// How many smooth regions were refused a patch, by why.
1787#[derive(Debug, Clone, Copy, Default)]
1788struct PatchRefusals {
1789 not_disk: usize,
1790 narrow: usize,
1791 unverified: usize,
1792}
1793
1794#[cfg(test)]
1795#[allow(clippy::unwrap_used)]
1796mod narrow_tests {
1797 use super::*;
1798
1799 fn cylinder(vertices: Vec<u32>) -> Carrier {
1800 let tol = Tolerances::millimetres();
1801 let shape = Canonical::Cylinder(
1802 ogeom_math::Cylinder::new(ogeom_math::Frame::WORLD, 1.0, tol).unwrap(),
1803 );
1804 Carrier::Curved(Curved {
1805 shape,
1806 deviation: 0.0,
1807 fitted: 0.0,
1808 centre: (0.0, 0.0),
1809 wraps: false,
1810 wraps_v: false,
1811 fixed: false,
1812 vertices,
1813 patch: None,
1814 })
1815 }
1816
1817 /// One region the default distance found, split between two regions
1818 /// found wider: putting both back gives it back once, whole, and leaves
1819 /// no curved region holding no triangle for the planner to facet.
1820 #[test]
1821 fn a_default_region_split_between_two_wider_ones_comes_back_once() {
1822 let triangles = [[0, 1, 2], [1, 3, 2], [2, 3, 4], [3, 5, 4]];
1823 let default = (cylinder(vec![0, 1, 2, 3, 4, 5]), vec![0, 1, 2, 3]);
1824 let record = || Narrower {
1825 distance: 2.0,
1826 regions: vec![default.clone()],
1827 };
1828 let mut groups = Groups {
1829 of: vec![0, 0, 1, 1],
1830 carriers: vec![cylinder(vec![0, 1, 2, 3]), cylinder(vec![2, 3, 4, 5])],
1831 refused: PatchRefusals::default(),
1832 fans: std::collections::BTreeSet::new(),
1833 narrower: [(0, record()), (1, record())].into_iter().collect(),
1834 };
1835 narrow(&mut groups, &triangles, 0);
1836 narrow(&mut groups, &triangles, 1);
1837 assert_eq!(groups.of, vec![0; 4]);
1838 assert!(groups.narrower.is_empty());
1839 for (c, carrier) in groups.carriers.iter().enumerate() {
1840 if let Carrier::Curved(curved) = carrier {
1841 assert!(
1842 groups.of.contains(&c),
1843 "curved region {c} holds no triangle"
1844 );
1845 assert!(!curved.vertices.is_empty());
1846 }
1847 }
1848 }
1849}