Skip to main content

ogeom_algo/mesh_solid/
steps.rs

1//! The mesh conversion's steps, between finding the regions and building
2//! the solid.
3
4use core::fmt;
5
6use ogeom_core::{OgeomResult, Tolerances};
7use ogeom_math::{Axis, Cylinder, Frame, Plane, Point, Sphere, Torus, Vector};
8use ogeom_topo::{Model, Triangulation};
9
10use super::frames::{align_one, hole_frame, slit_band};
11use super::planner::sags_as_the_surface;
12use super::regions::{region_patch, sphere_axis, swept_claim};
13use super::rounds::{plane_normals, tangent_blends, tangent_rounds};
14use super::segment::{axis_frame, unit_normal};
15use super::snap::plane_through;
16use super::weld::from_to;
17use super::{Carrier, Curved, Found, MeshSolid, MeshSolidOptions, MeshSolidReport};
18use crate::recognize::{self, Canonical, recognize_curved, worst_deviation};
19use crate::recognize_patch::Refused;
20
21/// The regions of a mesh, found and not yet built.
22///
23/// [`MeshRegions::find`] welds, cleans and orients the mesh and gathers its
24/// triangles into regions, each on the surface recognition found for it;
25/// [`MeshRegions::build`] places the seams and corners and builds the
26/// faces and the solid. [`solid_from_mesh`](super::solid_from_mesh) is the
27/// one followed by the other. Between them a region can be merged with a
28/// neighbour ([`MeshRegions::merge`]), split along a path of mesh vertices
29/// ([`MeshRegions::split`]), or given a surface of a chosen kind
30/// ([`MeshRegions::fit`]), and the build that follows goes through the
31/// same seams, corners, faces and checks as an automatic one: a region
32/// corrected to what recognition would have found builds what recognition
33/// would have built.
34///
35/// Every surface a step puts on a region is verified as recognition's are:
36/// every vertex of the region within the coplanar distance of it, and
37/// every curved surface's triangles no farther off it in their middles
38/// than their own sag. A step that cannot be taken is refused by name
39/// ([`RegionRefusal`]), the regions unchanged.
40///
41/// A region a step left on a surface goes through the passes recognition
42/// runs over its own regions: a round meeting two planes across smooth
43/// edges is put on the cylinder tangent to both, and a round, ball or
44/// torus among fillets on the surface its supports fix (see
45/// [`MeshRegions::fit`] for the fits that keep what they were given). A
46/// planar region of one or two triangles a step made is built as its own
47/// face, never given to a curved neighbour as the build gives the facets
48/// recognition leaves. A curved region the build cannot place falls back
49/// to facets and is named in [`MeshSolidReport::fallbacks`], with why.
50///
51/// Indices name the mesh as the regions read it: [`MeshRegions::points`]
52/// and [`MeshRegions::triangles`], welded, cleaned and oriented, not the
53/// mesh handed to [`MeshRegions::find`] ([`MeshRegions::vertex_of`] maps
54/// its vertices).
55#[derive(Clone)]
56pub struct MeshRegions {
57    found: Found,
58    options: MeshSolidOptions,
59    tol: Tolerances,
60    /// The regions a step made or changed, by index.
61    touched: Vec<usize>,
62}
63
64/// Names one region of a [`MeshRegions`]. A merge keeps the first region's
65/// name and retires the second's; a split keeps the name for one piece and
66/// gives the other a new one.
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
68pub struct RegionId(pub(super) usize);
69
70/// A curved region the build faceted, and why: see
71/// [`MeshSolidReport::fallbacks`].
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
73pub struct RegionFallback {
74    /// The region.
75    pub region: RegionId,
76    /// Why its face was not built on its surface.
77    pub reason: FallbackReason,
78}
79
80/// Why a curved region was built as facets instead of on its surface.
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
82#[non_exhaustive]
83pub enum FallbackReason {
84    /// Its seams or corners could not be placed on its surface and its
85    /// neighbours' within their tolerance.
86    BoundaryNotPlaced,
87    /// Its face, built, reached past the triangles it replaces: its
88    /// boundary closed on the wrong part of the surface.
89    ReachesPast,
90    /// Its face, built, faced into the material, against its triangles.
91    TurnedIn,
92    /// The body it is part of did not hold its mesh's volume, within what
93    /// its curved faces may add over their facets.
94    VolumeOff,
95    /// One of its seams folded back in its chart, deeper than the seam's
96    /// tolerance.
97    FoldedSeam,
98    /// Its face overlapped a face beside it.
99    Overlaps,
100    /// Its face's own mesh did not meet its neighbours': the solid would
101    /// not tessellate closed.
102    MeshesOpen,
103    /// Building its face, or meshing or measuring it once built, failed.
104    BuildFailed,
105    /// Its face's boundary did not close: a wire's edges did not meet end
106    /// to end, or did not close in its surface's chart.
107    BoundaryOpen,
108    /// Its face, built, passed through another face of the solid or
109    /// through itself, away from the edges they share.
110    Crosses,
111    /// Its face, built, had a pcurve standing farther off an edge than
112    /// the edge is long.
113    SpansShort,
114}
115
116impl fmt::Display for FallbackReason {
117    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118        f.write_str(match self {
119            Self::BoundaryNotPlaced => "its boundary could not be placed on its surface",
120            Self::ReachesPast => "its face reached past its triangles",
121            Self::TurnedIn => "its face faced into the material",
122            Self::VolumeOff => "its body did not hold the mesh's volume",
123            Self::FoldedSeam => "a seam of it folded in its chart",
124            Self::Overlaps => "its face overlapped a neighbour",
125            Self::MeshesOpen => "its face did not mesh closed with its neighbours",
126            Self::BuildFailed => "building or meshing its face failed",
127            Self::BoundaryOpen => "its face's boundary did not close",
128            Self::Crosses => "its face passed through another face",
129            Self::SpansShort => {
130                "a pcurve of its face stood off an edge farther than the edge is long"
131            }
132        })
133    }
134}
135
136impl RegionId {
137    /// The region's index among every region the mesh has had, retired
138    /// ones included.
139    #[must_use]
140    pub const fn index(self) -> usize {
141        self.0
142    }
143}
144
145/// One region as found: its triangles, the surface they are built on, and
146/// the regions beside it.
147#[derive(Debug, Clone, PartialEq)]
148pub struct MeshRegion {
149    /// Its name.
150    pub id: RegionId,
151    /// Its triangles, indices into [`MeshRegions::triangles`], ascending.
152    pub triangles: Vec<usize>,
153    /// The surface its face is built on: a plane, a recognized curved
154    /// surface, or a fitted sweep or patch ([`Canonical::Swept`]). `None`
155    /// for a region with no surface, whose triangles are built as planar
156    /// facets.
157    pub surface: Option<Canonical>,
158    /// The worst distance from any of its vertices to the surface,
159    /// measured; `None` with no surface.
160    pub deviation: Option<f64>,
161    /// The regions sharing a mesh edge with it, ascending.
162    pub neighbours: Vec<RegionId>,
163}
164
165/// A kind of surface to fit to a region with [`MeshRegions::fit`].
166#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
167pub enum SurfaceKind {
168    /// A plane.
169    Plane,
170    /// A cylinder.
171    Cylinder,
172    /// A cone.
173    Cone,
174    /// A sphere.
175    Sphere,
176    /// A torus.
177    Torus,
178    /// A fitted B-spline patch, as the automatic conversion fits a smooth
179    /// region nothing else fits ([`MeshSolidOptions::patches`]): over a
180    /// region that is one disk bounded by one loop, verified at every
181    /// vertex and across every triangle. It holds no axis or radius.
182    Patch,
183}
184
185/// What a fit holds fixed.
186#[derive(Debug, Clone, Copy, PartialEq, Default)]
187pub struct FitConstraints {
188    /// The surface's axis, as a line (its sense is free): a cylinder's, a
189    /// cone's or a torus's. A plane and a sphere have none to fix.
190    pub axis: Option<Axis>,
191    /// A cylinder's or a sphere's radius, or a torus's tube (minor)
192    /// radius. A plane has none, and a cone's changes along it.
193    pub radius: Option<f64>,
194}
195
196/// Why a step on the regions was refused. The regions are unchanged.
197#[derive(Debug, Clone, PartialEq)]
198pub enum RegionRefusal {
199    /// No region has this name, or it holds no triangle (a merge retired
200    /// it).
201    NoSuchRegion(RegionId),
202    /// A region cannot be merged with itself.
203    SameRegion(RegionId),
204    /// The two regions share no mesh edge.
205    NotAdjacent(RegionId, RegionId),
206    /// A path needs at least two vertices.
207    PathTooShort,
208    /// Two consecutive vertices of the path are not the ends of an edge of
209    /// the region's triangles.
210    NotAnEdgeOfTheRegion(u32, u32),
211    /// Cut along the path, the region falls into this many pieces, not two.
212    DoesNotCut {
213        /// How many pieces the path leaves.
214        pieces: usize,
215    },
216    /// The kind of surface has no such thing to hold fixed.
217    ConstraintDoesNotApply {
218        /// The kind asked for.
219        kind: SurfaceKind,
220        /// The constraint, `"axis"` or `"radius"`.
221        constraint: &'static str,
222    },
223    /// A fixed radius that is not a finite positive distance.
224    InvalidRadius(f64),
225    /// The region has too few vertices to determine the kind.
226    TooFewVertices {
227        /// The kind asked for.
228        kind: SurfaceKind,
229        /// How many it needs.
230        needed: usize,
231        /// How many the region has.
232        found: usize,
233    },
234    /// No surface of the kind could be fitted to the vertices at all.
235    NoFit(SurfaceKind),
236    /// The fitted surface misses a vertex by more than the coplanar
237    /// distance.
238    DoesNotVerify {
239        /// The kind asked for.
240        kind: SurfaceKind,
241        /// The worst distance from a vertex to it.
242        deviation: f64,
243        /// The coplanar distance it had to be within.
244        distance: f64,
245    },
246    /// A triangle's middle stands off the fitted surface by more than its
247    /// sag allows: its corners are on the surface and it spans off it.
248    TriangleOffTheSurface {
249        /// The kind asked for.
250        kind: SurfaceKind,
251        /// The triangle, an index into [`MeshRegions::triangles`].
252        triangle: usize,
253    },
254    /// A patch needs a region that is one disk bounded by one loop; this
255    /// one has a hole or touches itself.
256    NotADisk,
257    /// Too few of the region's vertices lie inside its boundary to hold a
258    /// patch across it.
259    TooNarrowForAPatch,
260    /// No patch fitted to the region passed the verification: within the
261    /// coplanar distance of every vertex and across every triangle, its
262    /// normal agreeing with theirs.
263    PatchDoesNotVerify,
264}
265
266impl fmt::Display for RegionRefusal {
267    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
268        match self {
269            Self::NoSuchRegion(id) => write!(f, "no region {} with triangles", id.0),
270            Self::SameRegion(id) => write!(f, "region {} cannot be merged with itself", id.0),
271            Self::NotAdjacent(a, b) => {
272                write!(f, "regions {} and {} share no mesh edge", a.0, b.0)
273            }
274            Self::PathTooShort => write!(f, "a path needs at least two vertices"),
275            Self::NotAnEdgeOfTheRegion(a, b) => {
276                write!(
277                    f,
278                    "vertices {a} and {b} are not joined by an edge of the region"
279                )
280            }
281            Self::DoesNotCut { pieces } => write!(
282                f,
283                "the path leaves the region in {pieces} piece(s), not two"
284            ),
285            Self::ConstraintDoesNotApply { kind, constraint } => {
286                write!(f, "a {kind:?} has no {constraint} to fix")
287            }
288            Self::InvalidRadius(r) => write!(f, "the radius {r} is not a positive distance"),
289            Self::TooFewVertices {
290                kind,
291                needed,
292                found,
293            } => write!(
294                f,
295                "a {kind:?} needs {needed} vertices and the region has {found}"
296            ),
297            Self::NoFit(kind) => write!(f, "no {kind:?} fits the region's vertices"),
298            Self::DoesNotVerify {
299                kind,
300                deviation,
301                distance,
302            } => write!(
303                f,
304                "the fitted {kind:?} misses a vertex by {deviation:e}, past the distance {distance:e}"
305            ),
306            Self::TriangleOffTheSurface { kind, triangle } => write!(
307                f,
308                "triangle {triangle} spans off the fitted {kind:?} by more than its sag"
309            ),
310            Self::NotADisk => write!(f, "the region is not one disk, as a patch needs"),
311            Self::TooNarrowForAPatch => {
312                write!(f, "too few vertices lie inside the region to hold a patch")
313            }
314            Self::PatchDoesNotVerify => write!(f, "no patch fitted to the region verifies"),
315        }
316    }
317}
318
319impl std::error::Error for RegionRefusal {}
320
321impl fmt::Debug for MeshRegions {
322    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
323        f.debug_struct("MeshRegions")
324            .field("triangles", &self.found.triangles.len())
325            .field("regions", &self.ids().len())
326            .field("distance", &self.found.flat)
327            .finish_non_exhaustive()
328    }
329}
330
331impl MeshRegions {
332    /// Weld, clean and orient the mesh, and gather its triangles into
333    /// regions as [`solid_from_mesh`](super::solid_from_mesh) does before it builds.
334    ///
335    /// # Errors
336    ///
337    /// As [`solid_from_mesh`](super::solid_from_mesh).
338    pub fn find(
339        mesh: &Triangulation,
340        options: &MeshSolidOptions,
341        tol: Tolerances,
342    ) -> OgeomResult<Self> {
343        Ok(Self {
344            found: super::find(mesh, options, tol)?,
345            options: *options,
346            tol,
347            touched: Vec::new(),
348        })
349    }
350
351    /// [`MeshRegions::find`], reading the mesh as it lists its points and
352    /// triangles rather than in the order of their positions: how a test
353    /// reaches a region set that listing grows. Present only with the
354    /// `test-hooks` feature, which nothing but the workspace's own tests
355    /// enables.
356    ///
357    /// # Errors
358    ///
359    /// As [`MeshRegions::find`].
360    #[cfg(feature = "test-hooks")]
361    #[doc(hidden)]
362    pub fn find_as_listed(
363        mesh: &Triangulation,
364        options: &MeshSolidOptions,
365        tol: Tolerances,
366    ) -> OgeomResult<Self> {
367        Ok(Self {
368            found: super::find_in(mesh, options, false, tol)?,
369            options: *options,
370            tol,
371            touched: Vec::new(),
372        })
373    }
374
375    /// Build the solid from the regions as they stand, through the same
376    /// seams, corners, faces and checks as
377    /// [`solid_from_mesh`](super::solid_from_mesh). A region with no
378    /// surface is built as planar facets; a curved region whose boundary
379    /// cannot be placed on its surface falls back to facets, and the
380    /// report names it and why ([`MeshSolidReport::fallbacks`]): a region
381    /// a step put on a surface and named there did not hold. A planar
382    /// region of one or two triangles a step made stays a face of its own.
383    ///
384    /// # Errors
385    ///
386    /// As [`solid_from_mesh`](super::solid_from_mesh).
387    pub fn build(&self, model: &mut Model) -> OgeomResult<MeshSolid> {
388        let protected: Vec<bool> = if self.touched.is_empty() {
389            Vec::new()
390        } else {
391            let mut by_region = vec![false; self.found.groups.carriers.len()];
392            for &g in &self.touched {
393                by_region[g] = true;
394            }
395            self.found
396                .groups
397                .of
398                .iter()
399                .map(|&g| by_region.get(g).copied().unwrap_or(false))
400                .collect()
401        };
402        super::build(model, &self.found, &self.options, &protected, self.tol)
403    }
404
405    /// The mesh's vertices as the regions read them, welded and numbered
406    /// in the order of their positions.
407    #[must_use]
408    pub fn points(&self) -> &[Point] {
409        &self.found.points
410    }
411
412    /// The mesh's triangles as the regions read them: without the ones of
413    /// no area or repeated, wound consistently and outward, and listed by
414    /// their corners' positions rather than as the input listed them.
415    #[must_use]
416    pub fn triangles(&self) -> &[[u32; 3]] {
417        &self.found.triangles
418    }
419
420    /// The vertex of [`MeshRegions::points`] the input mesh's vertex
421    /// `input` was welded into; `None` past the input's vertices.
422    #[must_use]
423    pub fn vertex_of(&self, input: usize) -> Option<u32> {
424        self.found.remap.get(input).copied()
425    }
426
427    /// The coplanar distance the regions were found to, and every surface
428    /// put on a region is verified at.
429    #[must_use]
430    pub fn distance(&self) -> f64 {
431        self.found.flat
432    }
433
434    /// What finding the regions measured of the mesh: its welds, dropped
435    /// triangles, windings and closure. The build's report starts from it.
436    #[must_use]
437    pub fn report(&self) -> &MeshSolidReport {
438        &self.found.report
439    }
440
441    /// The region holding a triangle.
442    #[must_use]
443    pub fn region_of(&self, triangle: usize) -> Option<RegionId> {
444        self.found
445            .groups
446            .of
447            .get(triangle)
448            .filter(|&&g| g < self.found.groups.carriers.len())
449            .map(|&g| RegionId(g))
450    }
451
452    /// The names of the regions holding triangles, ascending.
453    #[must_use]
454    pub fn ids(&self) -> Vec<RegionId> {
455        let mut held = vec![false; self.found.groups.carriers.len()];
456        for &g in &self.found.groups.of {
457            if let Some(h) = held.get_mut(g) {
458                *h = true;
459            }
460        }
461        held.iter()
462            .enumerate()
463            .filter(|(_, h)| **h)
464            .map(|(g, _)| RegionId(g))
465            .collect()
466    }
467
468    /// Every region holding triangles, in the order of their names.
469    #[must_use]
470    pub fn regions(&self) -> Vec<MeshRegion> {
471        let members = self.members_all();
472        members
473            .into_iter()
474            .enumerate()
475            .filter(|(_, m)| !m.is_empty())
476            .map(|(g, m)| self.describe(g, m))
477            .collect()
478    }
479
480    /// One region, or `None` where no region of that name holds triangles.
481    #[must_use]
482    pub fn region(&self, id: RegionId) -> Option<MeshRegion> {
483        let members = self.members(id.0);
484        (!members.is_empty()).then(|| self.describe(id.0, members))
485    }
486
487    /// Merge region `b` into region `a`, which must share a mesh edge.
488    ///
489    /// The merged region takes the surface recognition finds for its
490    /// triangles as a whole (a plane where they all lie within the
491    /// distance of one, otherwise a cylinder, cone, sphere, torus or sweep
492    /// where one verifies); where none does, the surface of the larger of
493    /// the two where it holds the other's vertices (unless that is a patch,
494    /// fitted to the larger alone); then, with
495    /// [`MeshSolidOptions::patches`], a patch fitted to the whole where it
496    /// is one disk and the patch verifies; and otherwise none, which
497    /// builds as facets. `b` is retired and the merged region keeps the
498    /// name `a`.
499    ///
500    /// # Errors
501    ///
502    /// [`RegionRefusal::NoSuchRegion`], [`RegionRefusal::SameRegion`] or
503    /// [`RegionRefusal::NotAdjacent`].
504    pub fn merge(&mut self, a: RegionId, b: RegionId) -> Result<RegionId, RegionRefusal> {
505        let first = self.held(a)?;
506        let second = self.held(b)?;
507        if a == b {
508            return Err(RegionRefusal::SameRegion(a));
509        }
510        let groups = &self.found.groups;
511        let adjacent = first.iter().any(|&t| {
512            (3 * t..3 * t + 3)
513                .any(|h| self.found.adjacency.twin[h].is_some_and(|o| groups.of[o / 3] == b.0))
514        });
515        if !adjacent {
516            return Err(RegionRefusal::NotAdjacent(a, b));
517        }
518        let mut union: Vec<usize> = first.iter().chain(&second).copied().collect();
519        union.sort_unstable();
520        let carrier = self.refitted(&union, || {
521            let (large, small) = if first.len() >= second.len() {
522                (a.0, &second)
523            } else {
524                (b.0, &first)
525            };
526            match &self.found.groups.carriers[large] {
527                Carrier::Curved(c) if c.patch.is_some() => Carrier::Gone,
528                carrier => self.kept(carrier, small, &union),
529            }
530        });
531        self.found.groups.narrower.clear();
532        for &t in &union {
533            self.found.groups.of[t] = a.0;
534        }
535        self.found.groups.carriers[b.0] = Carrier::Gone;
536        self.found.groups.carriers[a.0] = carrier;
537        self.touched.retain(|&g| g != b.0);
538        self.lay_out(a.0, false, true);
539        Ok(a)
540    }
541
542    /// Split a region in two along a path of mesh vertices, consecutive
543    /// ones joined by edges of the region's triangles: a path across the
544    /// region from boundary to boundary, or a closed one (its last vertex
545    /// its first) round part of it.
546    ///
547    /// Each piece takes the surface recognition finds for it, as a merged
548    /// region does, and otherwise keeps the region's own (which holds it,
549    /// as it held the whole); a piece of a region with no surface takes a
550    /// patch as a merged region does. The piece holding the region's first
551    /// triangle keeps the name; the other is named anew.
552    ///
553    /// # Errors
554    ///
555    /// [`RegionRefusal::NoSuchRegion`], [`RegionRefusal::PathTooShort`],
556    /// [`RegionRefusal::NotAnEdgeOfTheRegion`] or
557    /// [`RegionRefusal::DoesNotCut`] where the path leaves the region in
558    /// one piece or in more than two.
559    pub fn split(
560        &mut self,
561        id: RegionId,
562        path: &[u32],
563    ) -> Result<(RegionId, RegionId), RegionRefusal> {
564        let members = self.held(id)?;
565        if path.len() < 2 {
566            return Err(RegionRefusal::PathTooShort);
567        }
568        let triangles = &self.found.triangles;
569        let mut cut: ogeom_core::FastSet<(u32, u32)> = ogeom_core::FastSet::default();
570        for pair in path.windows(2) {
571            let (u, v) = (pair[0], pair[1]);
572            let on = members.iter().any(|&t| {
573                (3 * t..3 * t + 3).any(|h| {
574                    let (a, b) = from_to(triangles, h);
575                    (a, b) == (u, v) || (a, b) == (v, u)
576                })
577            });
578            if !on {
579                return Err(RegionRefusal::NotAnEdgeOfTheRegion(u, v));
580            }
581            cut.insert((u.min(v), u.max(v)));
582        }
583        let mut piece = vec![usize::MAX; triangles.len()];
584        let mut pieces = 0_usize;
585        for &start in &members {
586            if piece[start] != usize::MAX {
587                continue;
588            }
589            piece[start] = pieces;
590            let mut stack = vec![start];
591            while let Some(t) = stack.pop() {
592                for h in 3 * t..3 * t + 3 {
593                    let (a, b) = from_to(triangles, h);
594                    if cut.contains(&(a.min(b), a.max(b))) {
595                        continue;
596                    }
597                    if let Some(twin) = self.found.adjacency.twin[h] {
598                        let o = twin / 3;
599                        if self.found.groups.of[o] == id.0 && piece[o] == usize::MAX {
600                            piece[o] = pieces;
601                            stack.push(o);
602                        }
603                    }
604                }
605            }
606            pieces += 1;
607        }
608        if pieces != 2 {
609            return Err(RegionRefusal::DoesNotCut { pieces });
610        }
611        let (kept, other): (Vec<usize>, Vec<usize>) = members.iter().partition(|&&t| piece[t] == 0);
612        let parent = self.found.groups.carriers[id.0].clone();
613        let carriers =
614            [&kept, &other].map(|part| self.refitted(part, || self.kept(&parent, part, part)));
615        let [first, second] = carriers;
616        self.found.groups.narrower.clear();
617        let new = self.found.groups.carriers.len();
618        self.found.groups.carriers.push(second);
619        self.found.groups.carriers[id.0] = first;
620        for &t in &other {
621            self.found.groups.of[t] = new;
622        }
623        self.lay_out(id.0, false, true);
624        self.lay_out(new, false, true);
625        Ok((id, RegionId(new)))
626    }
627
628    /// Fit a surface of the kind to the region, holding what `constraints`
629    /// fix, and put the region on it where it verifies: every vertex of
630    /// the region within the coplanar distance, and every triangle's middle
631    /// no farther off it than its sag. Returns the worst distance from a
632    /// vertex to the surface.
633    ///
634    /// The fit is least squares on the vertices' distances to the surface.
635    /// With an axis fixed it is the surface about that line (a cylinder's
636    /// radius the vertices' mean distance from it, a torus's spine from
637    /// the circle its profile traces); with a radius fixed the rest moves
638    /// about it. An axis fixed is kept as given; otherwise the axis is set
639    /// square to a plane of the solid it all but is square to, and onto a
640    /// coaxial region's, where the surface still verifies there, as
641    /// recognition does. A fit holding nothing then goes through the
642    /// tangent passes as recognition's regions do (a round between two
643    /// planes put on the cylinder tangent to both, a fillet on the surface
644    /// its supports fix), where the surface they give still verifies; a fit
645    /// holding an axis or a radius keeps the surface it was given.
646    ///
647    /// A [`SurfaceKind::Patch`] is fitted over the region's triangles by
648    /// the automatic conversion's own patch fit and verification, whatever
649    /// [`MeshSolidOptions::patches`] says, and is kept as fitted.
650    ///
651    /// # Errors
652    ///
653    /// [`RegionRefusal::NoSuchRegion`],
654    /// [`RegionRefusal::ConstraintDoesNotApply`],
655    /// [`RegionRefusal::InvalidRadius`], [`RegionRefusal::TooFewVertices`],
656    /// [`RegionRefusal::NoFit`], [`RegionRefusal::DoesNotVerify`] or
657    /// [`RegionRefusal::TriangleOffTheSurface`]; for a patch,
658    /// [`RegionRefusal::NotADisk`], [`RegionRefusal::TooNarrowForAPatch`]
659    /// or [`RegionRefusal::PatchDoesNotVerify`].
660    pub fn fit(
661        &mut self,
662        id: RegionId,
663        kind: SurfaceKind,
664        constraints: &FitConstraints,
665    ) -> Result<f64, RegionRefusal> {
666        let members = self.held(id)?;
667        applies(kind, constraints, self.tol)?;
668        if kind == SurfaceKind::Patch {
669            let claim = self.patch(&members).map_err(|refused| match refused {
670                Refused::NotDisk => RegionRefusal::NotADisk,
671                Refused::Narrow => RegionRefusal::TooNarrowForAPatch,
672                Refused::Unverified => RegionRefusal::PatchDoesNotVerify,
673            })?;
674            let deviation = claim.deviation;
675            self.found.groups.narrower.clear();
676            self.found.groups.carriers[id.0] = Carrier::Curved(claim);
677            self.lay_out(id.0, false, false);
678            return Ok(deviation);
679        }
680        let vertices = self.vertices(&members);
681        let (pts, nrm) = self.samples(&vertices, &members);
682        let shape = fitted(kind, constraints, &pts, &nrm, self.found.flat, self.tol)?;
683        let flat = self.found.flat;
684        let deviation = worst_deviation(&shape, &pts);
685        if deviation.is_nan() || deviation > flat {
686            return Err(RegionRefusal::DoesNotVerify {
687                kind,
688                deviation,
689                distance: flat,
690            });
691        }
692        if kind != SurfaceKind::Plane
693            && let Some(&t) = members.iter().find(|&&t| !self.sags(&shape, t))
694        {
695            return Err(RegionRefusal::TriangleOffTheSurface { kind, triangle: t });
696        }
697        let carrier = match shape {
698            Canonical::Plane(plane) => Carrier::Plane(self.outward(plane, &members)),
699            shape => Carrier::Curved(curved(shape, deviation, vertices)),
700        };
701        self.found.groups.narrower.clear();
702        self.found.groups.carriers[id.0] = carrier;
703        let free = constraints.axis.is_none() && constraints.radius.is_none();
704        self.lay_out(id.0, constraints.axis.is_some(), free);
705        // Set onto a shared axis, the surface is measured again there.
706        Ok(match &self.found.groups.carriers[id.0] {
707            Carrier::Curved(c) => c.deviation,
708            _ => deviation,
709        })
710    }
711
712    /// Put `surface` on a region as given, unverified, keeping how its face
713    /// is laid out: a plane on a planar region, or a curved surface of the
714    /// kind the region is on. What the build makes of a surface its
715    /// vertices do not lie on is what its fallbacks catch; this is how a
716    /// test hands it one. Present only with the `test-hooks` feature, which
717    /// nothing but the workspace's own tests enables.
718    ///
719    /// # Errors
720    ///
721    /// [`RegionRefusal::NoSuchRegion`], or [`RegionRefusal::NoFit`] where
722    /// the surface is not of the region's kind.
723    #[cfg(feature = "test-hooks")]
724    #[doc(hidden)]
725    pub fn put_surface_unverified(
726        &mut self,
727        id: RegionId,
728        surface: Canonical,
729    ) -> Result<(), RegionRefusal> {
730        self.held(id)?;
731        self.found.groups.narrower.clear();
732        let carrier = &mut self.found.groups.carriers[id.0];
733        match (carrier, surface) {
734            (Carrier::Plane(plane), Canonical::Plane(given)) => *plane = given,
735            (Carrier::Curved(curved), given)
736                if core::mem::discriminant(&curved.shape) == core::mem::discriminant(&given) =>
737            {
738                curved.shape = given;
739            }
740            (_, given) => {
741                return Err(RegionRefusal::NoFit(match given {
742                    Canonical::Plane(_) => SurfaceKind::Plane,
743                    Canonical::Cylinder(_) => SurfaceKind::Cylinder,
744                    Canonical::Cone(_) => SurfaceKind::Cone,
745                    Canonical::Sphere(_) => SurfaceKind::Sphere,
746                    Canonical::Torus(_) => SurfaceKind::Torus,
747                    Canonical::Swept(_) => SurfaceKind::Patch,
748                }));
749            }
750        }
751        Ok(())
752    }
753
754    /// Each region's triangles, by region.
755    fn members_all(&self) -> Vec<Vec<usize>> {
756        let mut members = vec![Vec::new(); self.found.groups.carriers.len()];
757        for (t, &g) in self.found.groups.of.iter().enumerate() {
758            if let Some(m) = members.get_mut(g) {
759                m.push(t);
760            }
761        }
762        members
763    }
764
765    fn members(&self, g: usize) -> Vec<usize> {
766        (0..self.found.triangles.len())
767            .filter(|&t| self.found.groups.of[t] == g)
768            .collect()
769    }
770
771    /// A region's triangles, refused where it holds none.
772    fn held(&self, id: RegionId) -> Result<Vec<usize>, RegionRefusal> {
773        let members = self.members(id.0);
774        if members.is_empty() {
775            return Err(RegionRefusal::NoSuchRegion(id));
776        }
777        Ok(members)
778    }
779
780    fn vertices(&self, members: &[usize]) -> Vec<u32> {
781        let mut vertices: Vec<u32> = members
782            .iter()
783            .flat_map(|&t| self.found.triangles[t])
784            .collect();
785        vertices.sort_unstable();
786        vertices.dedup();
787        vertices
788    }
789
790    fn points_of(&self, vertices: &[u32]) -> Vec<Point> {
791        vertices
792            .iter()
793            .map(|&v| self.found.points[v as usize])
794            .collect()
795    }
796
797    /// The vertices with normals averaged over the region's triangles.
798    fn samples(&self, vertices: &[u32], members: &[usize]) -> (Vec<Point>, Vec<Vector>) {
799        let mut sum: ogeom_core::FastMap<u32, Vector> = ogeom_core::FastMap::default();
800        for &t in members {
801            let n = unit_normal(&self.found.points, self.found.triangles[t]);
802            for &v in &self.found.triangles[t] {
803                *sum.entry(v).or_insert(Vector::ZERO) += n;
804            }
805        }
806        let nrm = vertices
807            .iter()
808            .map(|v| {
809                let s = sum.get(v).copied().unwrap_or(Vector::Z);
810                let m = s.magnitude();
811                if m > 0.0 { s / m } else { Vector::Z }
812            })
813            .collect();
814        (self.points_of(vertices), nrm)
815    }
816
817    /// Whether a triangle sags as the surface does.
818    fn sags(&self, shape: &Canonical, t: usize) -> bool {
819        let corners = self.found.triangles[t].map(|v| self.found.points[v as usize]);
820        sags_as_the_surface(shape, corners, self.found.flat)
821    }
822
823    /// A plane turned to face the way the region's triangles do.
824    fn outward(&self, plane: Plane, members: &[usize]) -> Plane {
825        let facing = members.iter().fold(Vector::ZERO, |s, &t| {
826            s + unit_normal(&self.found.points, self.found.triangles[t])
827        });
828        let frame = plane.frame();
829        if facing.dot(frame.z().vector()) >= 0.0 {
830            plane
831        } else {
832            Plane::new(Frame::about(frame.origin(), -frame.z()))
833        }
834    }
835
836    /// The surface recognition finds for a set of triangles taken whole: a
837    /// plane, a canonical curved surface, or (with the sweeps on) a sweep.
838    fn recognized(&self, members: &[usize]) -> Option<Carrier> {
839        let flat = self.found.flat;
840        let vertices = self.vertices(members);
841        let (pts, nrm) = self.samples(&vertices, members);
842        if pts.len() >= 3 && recognize::is_flat(&pts, flat, self.tol) {
843            let (centre, normal) = plane_through(&pts, self.tol)?;
844            let plane = Plane::new(Frame::about(centre, normal));
845            return Some(Carrier::Plane(self.outward(plane, members)));
846        }
847        if !(self.options.recognize && self.options.merge_coplanar) {
848            return None;
849        }
850        let stride = members.len().div_ceil(100).max(1);
851        let chords: Vec<(Point, Point)> = members
852            .iter()
853            .step_by(stride)
854            .flat_map(|&t| {
855                let [a, b, c] = self.found.triangles[t].map(|v| self.found.points[v as usize]);
856                [(a, b), (b, c), (c, a)]
857            })
858            .collect();
859        if let Some(found) = recognize_curved(&pts, &nrm, &chords, flat, self.tol)
860            && members.iter().all(|&t| self.sags(&found.surface, t))
861        {
862            return Some(Carrier::Curved(curved(
863                found.surface,
864                found.deviation,
865                vertices,
866            )));
867        }
868        if self.options.sweeps {
869            let normals: Vec<Vector> = self
870                .found
871                .triangles
872                .iter()
873                .map(|t| unit_normal(&self.found.points, *t))
874                .collect();
875            return swept_claim(
876                &self.found.points,
877                &self.found.triangles,
878                &normals,
879                members,
880                flat,
881                self.tol,
882            )
883            .map(Carrier::Curved);
884        }
885        None
886    }
887
888    /// The surface recognition finds for a set of triangles taken whole;
889    /// else the one `kept` gives; else, with the patches on, a patch over
890    /// them; else none.
891    fn refitted(&self, members: &[usize], kept: impl FnOnce() -> Carrier) -> Carrier {
892        if let Some(carrier) = self.recognized(members) {
893            return carrier;
894        }
895        let carrier = kept();
896        if !matches!(carrier, Carrier::Gone) {
897            return carrier;
898        }
899        if self.options.recognize && self.options.merge_coplanar && self.options.patches {
900            return self.patch(members).map_or(Carrier::Gone, Carrier::Curved);
901        }
902        Carrier::Gone
903    }
904
905    /// A patch fitted to a set of triangles and verified, as the automatic
906    /// conversion fits one to a smooth region, the faces across its
907    /// boundary read from the regions as they stand.
908    fn patch(&self, members: &[usize]) -> Result<Curved, Refused> {
909        let f = &self.found;
910        region_patch(
911            &f.points,
912            &f.triangles,
913            &f.adjacency,
914            &f.groups,
915            members,
916            f.flat,
917            self.tol,
918        )
919    }
920
921    /// A carrier kept for a region's new triangles where it holds the
922    /// vertices of `added` within the distance, measured over `all`; gone
923    /// otherwise.
924    fn kept(&self, carrier: &Carrier, added: &[usize], all: &[usize]) -> Carrier {
925        let flat = self.found.flat;
926        let pts = self.points_of(&self.vertices(added));
927        match carrier {
928            Carrier::Plane(plane) => {
929                if pts
930                    .iter()
931                    .all(|p| plane.signed_distance_to(*p).abs() <= flat)
932                {
933                    Carrier::Plane(*plane)
934                } else {
935                    Carrier::Gone
936                }
937            }
938            Carrier::Curved(c) => {
939                if worst_deviation(&c.shape, &pts) > flat {
940                    return Carrier::Gone;
941                }
942                let vertices = self.vertices(all);
943                let deviation = worst_deviation(&c.shape, &self.points_of(&vertices));
944                Carrier::Curved(Curved {
945                    deviation,
946                    vertices,
947                    ..c.clone()
948                })
949            }
950            Carrier::Gone => Carrier::Gone,
951        }
952    }
953
954    /// Lay a curved region out on its surface as recognition lays its own:
955    /// a sphere turned about the normal its boundary circles share, with
956    /// `tangent` a round between two planes put on the cylinder tangent to
957    /// both, the axis squared to a plane and set onto a coaxial region's
958    /// unless it was fixed, with `tangent` a fillet put on the surface its
959    /// supports fix, the chart's branch and whether the region wraps, and a
960    /// closed surface's seams set clear of its holes. The region is marked
961    /// as one a step touched. A patch takes no tangent pass: it is what is
962    /// left where no surface of theirs fits.
963    fn lay_out(&mut self, g: usize, axis_fixed: bool, tangent: bool) {
964        if !self.touched.contains(&g) {
965            self.touched.push(g);
966        }
967        let cos_crease = self.options.crease.cos();
968        let f = &mut self.found;
969        let tol = self.tol;
970        sphere_axis(
971            &f.points,
972            &f.triangles,
973            &f.adjacency,
974            &mut f.groups,
975            g,
976            f.flat,
977            tol,
978        );
979        let tangent =
980            tangent && matches!(&f.groups.carriers[g], Carrier::Curved(c) if c.patch.is_none());
981        if tangent {
982            let planes = f.groups.clone();
983            tangent_rounds(
984                &f.points,
985                &f.triangles,
986                &f.adjacency,
987                &mut f.groups,
988                &planes,
989                cos_crease,
990                f.flat,
991                Some(g),
992                tol,
993            );
994        }
995        let Carrier::Curved(mut curved) = f.groups.carriers[g].clone() else {
996            return;
997        };
998        let (mut leaders, normals) = if axis_fixed {
999            (Vec::new(), Vec::new())
1000        } else {
1001            let mut others: Vec<(usize, usize, Frame)> = f
1002                .groups
1003                .carriers
1004                .iter()
1005                .enumerate()
1006                .filter(|&(o, _)| o != g)
1007                .filter_map(|(_, c)| match c {
1008                    Carrier::Curved(c) => {
1009                        let rank = match c.shape {
1010                            Canonical::Cylinder(_) => 0,
1011                            Canonical::Cone(_) => 1,
1012                            Canonical::Torus(_) => 2,
1013                            _ => return None,
1014                        };
1015                        Some((rank, usize::MAX - c.vertices.len(), axis_frame(&c.shape)?))
1016                    }
1017                    _ => None,
1018                })
1019                .collect();
1020            others.sort_by_key(|&(rank, size, _)| (rank, size));
1021            (
1022                others.into_iter().map(|(_, _, frame)| frame).collect(),
1023                plane_normals(&f.groups),
1024            )
1025        };
1026        align_one(&f.points, &mut curved, &mut leaders, &normals, f.flat, tol);
1027        f.groups.carriers[g] = Carrier::Curved(curved);
1028        if tangent {
1029            let planes = f.groups.clone();
1030            tangent_blends(
1031                &f.points,
1032                &f.triangles,
1033                &f.adjacency,
1034                &mut f.groups,
1035                &planes,
1036                cos_crease,
1037                f.flat,
1038                Some(g),
1039                tol,
1040            );
1041        }
1042        hole_frame(&f.points, &f.triangles, &f.adjacency, &mut f.groups, g, tol);
1043        slit_band(&f.points, &f.triangles, &f.adjacency, &mut f.groups, g, tol);
1044    }
1045
1046    fn describe(&self, g: usize, triangles: Vec<usize>) -> MeshRegion {
1047        let groups = &self.found.groups;
1048        let mut neighbours: Vec<RegionId> = triangles
1049            .iter()
1050            .flat_map(|&t| {
1051                (3 * t..3 * t + 3).filter_map(|h| self.found.adjacency.twin[h].map(|o| o / 3))
1052            })
1053            .map(|o| groups.of[o])
1054            .filter(|&o| o != g && o < groups.carriers.len())
1055            .map(RegionId)
1056            .collect();
1057        neighbours.sort_unstable();
1058        neighbours.dedup();
1059        let (surface, deviation) = match &groups.carriers[g] {
1060            Carrier::Plane(plane) => {
1061                let pts = self.points_of(&self.vertices(&triangles));
1062                let deviation = pts
1063                    .iter()
1064                    .map(|p| plane.signed_distance_to(*p).abs())
1065                    .fold(0.0_f64, f64::max);
1066                (Some(Canonical::Plane(*plane)), Some(deviation))
1067            }
1068            Carrier::Curved(c) => (Some(c.shape.clone()), Some(c.deviation)),
1069            Carrier::Gone => (None, None),
1070        };
1071        MeshRegion {
1072            id: RegionId(g),
1073            triangles,
1074            surface,
1075            deviation,
1076            neighbours,
1077        }
1078    }
1079}
1080
1081/// A region on a curved surface, not yet laid out.
1082fn curved(shape: Canonical, deviation: f64, vertices: Vec<u32>) -> Curved {
1083    Curved {
1084        shape,
1085        deviation,
1086        fitted: deviation,
1087        centre: (0.0, 0.0),
1088        wraps: false,
1089        wraps_v: false,
1090        fixed: false,
1091        vertices,
1092        patch: None,
1093    }
1094}
1095
1096/// The fewest vertices each kind is fitted to: half as many again as its
1097/// unknowns, as recognition asks.
1098/// A patch counts the vertices inside its boundary instead.
1099const fn floor(kind: SurfaceKind) -> usize {
1100    match kind {
1101        SurfaceKind::Plane => 3,
1102        SurfaceKind::Sphere => 6,
1103        SurfaceKind::Cylinder => 8,
1104        SurfaceKind::Cone => 9,
1105        SurfaceKind::Torus => 11,
1106        SurfaceKind::Patch => 0,
1107    }
1108}
1109
1110/// Whether the kind has what the constraints fix, and a fixed radius is a
1111/// distance.
1112fn applies(
1113    kind: SurfaceKind,
1114    constraints: &FitConstraints,
1115    tol: Tolerances,
1116) -> Result<(), RegionRefusal> {
1117    let refuse = |constraint| Err(RegionRefusal::ConstraintDoesNotApply { kind, constraint });
1118    match kind {
1119        SurfaceKind::Plane | SurfaceKind::Sphere | SurfaceKind::Patch
1120            if constraints.axis.is_some() =>
1121        {
1122            return refuse("axis");
1123        }
1124        SurfaceKind::Plane | SurfaceKind::Cone | SurfaceKind::Patch
1125            if constraints.radius.is_some() =>
1126        {
1127            return refuse("radius");
1128        }
1129        _ => {}
1130    }
1131    if let Some(r) = constraints.radius
1132        && !(r.is_finite() && r > tol.confusion())
1133    {
1134        return Err(RegionRefusal::InvalidRadius(r));
1135    }
1136    Ok(())
1137}
1138
1139/// A canonical surface of the kind fitted to the points, holding the
1140/// constraints, which [`applies`] has checked.
1141fn fitted(
1142    kind: SurfaceKind,
1143    constraints: &FitConstraints,
1144    pts: &[Point],
1145    nrm: &[Vector],
1146    flat: f64,
1147    tol: Tolerances,
1148) -> Result<Canonical, RegionRefusal> {
1149    let needed = floor(kind);
1150    if pts.len() < needed {
1151        return Err(RegionRefusal::TooFewVertices {
1152            kind,
1153            needed,
1154            found: pts.len(),
1155        });
1156    }
1157    let none = RegionRefusal::NoFit(kind);
1158    let free = |seed: Option<Canonical>| -> Result<Canonical, RegionRefusal> {
1159        let seed = seed.ok_or_else(|| none.clone())?;
1160        Ok(recognize::refine(seed.clone(), pts, flat, tol).unwrap_or(seed))
1161    };
1162    let (axis, radius) = (constraints.axis, constraints.radius);
1163    let centroid = Point::from_vector(
1164        pts.iter().fold(Vector::ZERO, |s, p| s + p.to_vector()) / count(pts.len()),
1165    );
1166    match kind {
1167        // Fitted over the triangles, not the points alone.
1168        SurfaceKind::Patch => Err(none),
1169        SurfaceKind::Plane => {
1170            let (centre, normal) = plane_through(pts, tol).ok_or(none)?;
1171            Ok(Canonical::Plane(Plane::new(Frame::about(centre, normal))))
1172        }
1173        SurfaceKind::Sphere => {
1174            let seed = free(recognize::fit_sphere(pts, tol))?;
1175            let Some(r) = radius else {
1176                return Ok(seed);
1177            };
1178            let Canonical::Sphere(s) = seed else {
1179                return Err(none);
1180            };
1181            let c = s.centre();
1182            let y = solve(vec![c.x, c.y, c.z], pts, &|y, p| {
1183                p.distance(Point::new(y[0], y[1], y[2])) - r
1184            });
1185            Sphere::centred(Point::new(y[0], y[1], y[2]), r, tol)
1186                .map(Canonical::Sphere)
1187                .map_err(|_| none)
1188        }
1189        SurfaceKind::Cylinder => match (axis, radius) {
1190            (Some(axis), r) => {
1191                let origin = on_line(axis, centroid);
1192                let r = r.unwrap_or_else(|| {
1193                    pts.iter()
1194                        .map(|p| distance_from_line(axis, *p))
1195                        .sum::<f64>()
1196                        / count(pts.len())
1197                });
1198                Cylinder::new(Frame::about(origin, axis.direction), r, tol)
1199                    .map(Canonical::Cylinder)
1200                    .map_err(|_| none)
1201            }
1202            (None, Some(r)) => {
1203                let seed = free(recognize::fit_cylinder(pts, nrm, tol))?;
1204                let Canonical::Cylinder(c) = seed else {
1205                    return Err(none);
1206                };
1207                let (o, d) = (c.frame().origin(), c.frame().z().vector());
1208                let like = Canonical::Cylinder(c);
1209                let y = solve(vec![o.x, o.y, o.z, d.x, d.y, d.z], pts, &|y, p| {
1210                    recognize::residual(&like, &[y[0], y[1], y[2], y[3], y[4], y[5], r], p)
1211                });
1212                recognize::rebuild(&like, &[y[0], y[1], y[2], y[3], y[4], y[5], r], tol).ok_or(none)
1213            }
1214            (None, None) => free(recognize::fit_cylinder(pts, nrm, tol)),
1215        },
1216        SurfaceKind::Cone => {
1217            let Some(axis) = axis else {
1218                return free(recognize::fit_cone(pts, nrm, tol));
1219            };
1220            let Some(Canonical::Cone(seed)) =
1221                recognize::ruled_about(pts, axis.location, axis.direction, 0.0, tol)
1222            else {
1223                return Err(none);
1224            };
1225            let (o, d) = (seed.frame().origin(), seed.frame().z().vector());
1226            let like = Canonical::Cone(seed);
1227            let x = |y: &[f64]| [o.x, o.y, o.z, d.x, d.y, d.z, y[0], y[1]];
1228            let y = solve(
1229                vec![seed.reference_radius(), seed.half_angle()],
1230                pts,
1231                &|y, p| recognize::residual(&like, &x(y), p),
1232            );
1233            recognize::rebuild(&like, &x(&y), tol).ok_or(none)
1234        }
1235        SurfaceKind::Torus => match axis {
1236            Some(axis) => {
1237                let d = axis.direction.vector();
1238                let base = on_line(axis, centroid);
1239                let (major, height, minor) =
1240                    recognize::profile_circle(pts, base, axis).ok_or(none.clone())?;
1241                let like = Canonical::Torus(
1242                    Torus::new(
1243                        Frame::about(base + d * height, axis.direction),
1244                        major,
1245                        radius.unwrap_or(minor),
1246                        tol,
1247                    )
1248                    .map_err(|_| none.clone())?,
1249                );
1250                let x = |y: &[f64]| {
1251                    let o = base + d * y[0];
1252                    [
1253                        o.x,
1254                        o.y,
1255                        o.z,
1256                        d.x,
1257                        d.y,
1258                        d.z,
1259                        y[1],
1260                        radius.unwrap_or_else(|| y[2]),
1261                    ]
1262                };
1263                let y0 = match radius {
1264                    Some(_) => vec![height, major],
1265                    None => vec![height, major, minor],
1266                };
1267                let y = solve(y0, pts, &|y, p| recognize::residual(&like, &x(y), p));
1268                recognize::rebuild(&like, &x(&y), tol).ok_or(none)
1269            }
1270            None => {
1271                let seed = free(recognize::fit_torus(pts, nrm, tol))?;
1272                let Some(r) = radius else {
1273                    return Ok(seed);
1274                };
1275                let Canonical::Torus(t) = seed else {
1276                    return Err(none);
1277                };
1278                let (o, d) = (t.frame().origin(), t.frame().z().vector());
1279                let like = Canonical::Torus(t);
1280                let x = |y: &[f64]| [y[0], y[1], y[2], y[3], y[4], y[5], y[6], r];
1281                let y = solve(
1282                    vec![o.x, o.y, o.z, d.x, d.y, d.z, t.major_radius()],
1283                    pts,
1284                    &|y, p| recognize::residual(&like, &x(y), p),
1285                );
1286                recognize::rebuild(&like, &x(&y), tol).ok_or(none)
1287            }
1288        },
1289    }
1290}
1291
1292#[allow(
1293    clippy::cast_precision_loss,
1294    reason = "vertex counts are far below 2^52"
1295)]
1296fn count(n: usize) -> f64 {
1297    n.max(1) as f64
1298}
1299
1300/// The point of the axis nearest `p`.
1301fn on_line(axis: Axis, p: Point) -> Point {
1302    let d = axis.direction.vector();
1303    axis.location + d * (p - axis.location).dot(d)
1304}
1305
1306fn distance_from_line(axis: Axis, p: Point) -> f64 {
1307    let d = axis.direction.vector();
1308    let w = p - axis.location;
1309    (w - d * w.dot(d)).magnitude()
1310}
1311
1312/// Least squares on a residual over the points, from `y`: Gauss-Newton
1313/// steps through the Jacobian's singular value decomposition, the
1314/// directions the points do not determine left where they are, each step
1315/// halved until it lowers the cost. Fitted on an even subsample of a large
1316/// set, as recognition fits.
1317fn solve(mut y: Vec<f64>, points: &[Point], residual: &dyn Fn(&[f64], Point) -> f64) -> Vec<f64> {
1318    const FIT_SAMPLES: usize = 1500;
1319    let stride = points.len().div_ceil(FIT_SAMPLES).max(1);
1320    let points: Vec<Point> = points.iter().step_by(stride).copied().collect();
1321    let n = y.len();
1322    let cost = |y: &[f64]| -> f64 { points.iter().map(|p| residual(y, *p).powi(2)).sum() };
1323    let scale = points
1324        .iter()
1325        .map(|p| p.distance(points[0]))
1326        .fold(f64::MIN_POSITIVE, f64::max);
1327    let mut current = cost(&y);
1328    for _ in 0..60 {
1329        let steps: Vec<f64> = y.iter().map(|v| 1e-7 * v.abs().max(scale)).collect();
1330        let mut jacobian = nalgebra::DMatrix::<f64>::zeros(points.len(), n);
1331        let mut r = nalgebra::DVector::<f64>::zeros(points.len());
1332        for (i, p) in points.iter().enumerate() {
1333            r[i] = residual(&y, *p);
1334            for k in 0..n {
1335                let mut up = y.clone();
1336                let mut down = y.clone();
1337                up[k] += steps[k];
1338                down[k] -= steps[k];
1339                jacobian[(i, k)] = (residual(&up, *p) - residual(&down, *p)) / (2.0 * steps[k]);
1340            }
1341        }
1342        let svd = jacobian.svd(true, true);
1343        let largest = svd.singular_values.max();
1344        if largest.is_nan() || largest <= 0.0 {
1345            break;
1346        }
1347        let Ok(step) = svd.solve(&(-r), largest * 1e-8) else {
1348            break;
1349        };
1350        let mut scale_step = 1.0;
1351        let mut gained = 0.0;
1352        for _ in 0..20 {
1353            let trial: Vec<f64> = y
1354                .iter()
1355                .zip(step.iter())
1356                .map(|(a, b)| b.mul_add(scale_step, *a))
1357                .collect();
1358            let c = cost(&trial);
1359            if c.is_finite() && c < current {
1360                gained = current - c;
1361                y = trial;
1362                current = c;
1363                break;
1364            }
1365            scale_step *= 0.5;
1366        }
1367        if gained <= current * 1e-12 {
1368            break;
1369        }
1370    }
1371    y
1372}