Skip to main content

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, &regrouped, 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, &regrouped, 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, &regrouped, 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, &regrouped, 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}